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
package/lib/model/index.js
DELETED
|
@@ -1,1265 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Model 层 - 基于 collection 的增强封装
|
|
3
|
-
*
|
|
4
|
-
* 核心功能:
|
|
5
|
-
* 1. Schema 定义与验证(集成 schema-dsl)
|
|
6
|
-
* 2. 自定义方法扩展(instance + static)
|
|
7
|
-
* 3. 生命周期钩子(before/after)
|
|
8
|
-
* 4. 索引管理(自动创建)
|
|
9
|
-
* 5. 关系定义(hasOne/hasMany/belongsTo)
|
|
10
|
-
*
|
|
11
|
-
* @module lib/model
|
|
12
|
-
* @version 1.0.3
|
|
13
|
-
* @since 1.0.3
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// ========== 依赖导入 ==========
|
|
17
|
-
let schemaDsl;
|
|
18
|
-
try {
|
|
19
|
-
schemaDsl = require("schema-dsl");
|
|
20
|
-
} catch (err) {
|
|
21
|
-
// schema-dsl 未安装时的友好提示
|
|
22
|
-
const installHint = `
|
|
23
|
-
╔════════════════════════════════════════════════════════════════╗
|
|
24
|
-
║ ⚠️ schema-dsl 未安装 ║
|
|
25
|
-
║ ║
|
|
26
|
-
║ Model 功能需要 schema-dsl 包支持 ║
|
|
27
|
-
║ ║
|
|
28
|
-
║ 安装方式: ║
|
|
29
|
-
║ 1. npm link(开发): ║
|
|
30
|
-
║ cd path/to/schema-dsl && npm link ║
|
|
31
|
-
║ cd path/to/monSQLize && npm link schema-dsl ║
|
|
32
|
-
║ ║
|
|
33
|
-
║ 2. npm 安装(生产): ║
|
|
34
|
-
║ npm install schema-dsl ║
|
|
35
|
-
╚════════════════════════════════════════════════════════════════╝
|
|
36
|
-
`;
|
|
37
|
-
console.error(installHint);
|
|
38
|
-
throw new Error(
|
|
39
|
-
"schema-dsl is required for Model functionality. Please install it first.",
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const { dsl, validate } = schemaDsl;
|
|
44
|
-
|
|
45
|
-
// ========== relations + populate 支持 ==========
|
|
46
|
-
const RelationManager = require("./features/relations");
|
|
47
|
-
const { PopulateBuilder, PopulateProxy } = require("./features/populate");
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Model 基类
|
|
51
|
-
* 提供全局注册和实例化能力
|
|
52
|
-
*/
|
|
53
|
-
class Model {
|
|
54
|
-
/**
|
|
55
|
-
* 全局 Model 注册表
|
|
56
|
-
* @private
|
|
57
|
-
* @type {Map<string, Object>}
|
|
58
|
-
*/
|
|
59
|
-
static _registry = new Map();
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 已被 redefine 的 collectionName 集合(用于缓存失效通知)
|
|
63
|
-
* @private
|
|
64
|
-
* @type {Set<string>}
|
|
65
|
-
* @since 1.2.1
|
|
66
|
-
*/
|
|
67
|
-
static _redefinedNames = new Set();
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* 定义并注册 Model
|
|
71
|
-
*
|
|
72
|
-
* @param {string} collectionName - 集合名称
|
|
73
|
-
* @param {Object} definition - Model 定义对象
|
|
74
|
-
* @param {Object} [definition.enums] - 枚举配置(可选)
|
|
75
|
-
* @param {Function|Object} definition.schema - Schema 定义(必需)
|
|
76
|
-
* @param {Function} [definition.methods] - 自定义方法(可选)
|
|
77
|
-
* @param {Function} [definition.hooks] - 生命周期钩子(可选)
|
|
78
|
-
* @param {Array} [definition.indexes] - 索引定义(可选)
|
|
79
|
-
* @param {Object} [definition.relations] - 关系定义(可选)
|
|
80
|
-
*
|
|
81
|
-
* @throws {Error} 集合名称无效
|
|
82
|
-
* @throws {Error} schema 未定义
|
|
83
|
-
* @throws {Error} Model 已存在
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* Model.define('users', {
|
|
87
|
-
* enums: {
|
|
88
|
-
* role: 'admin|user'
|
|
89
|
-
* },
|
|
90
|
-
* schema: function(dsl) {
|
|
91
|
-
* return dsl({
|
|
92
|
-
* username: 'string:3-32!',
|
|
93
|
-
* role: this.enums.role.default('user')
|
|
94
|
-
* });
|
|
95
|
-
* }
|
|
96
|
-
* });
|
|
97
|
-
*/
|
|
98
|
-
static define(collectionName, definition) {
|
|
99
|
-
try {
|
|
100
|
-
// ========== 参数验证 ==========
|
|
101
|
-
if (
|
|
102
|
-
!collectionName ||
|
|
103
|
-
typeof collectionName !== "string" ||
|
|
104
|
-
collectionName.trim() === ""
|
|
105
|
-
) {
|
|
106
|
-
const err = new Error("Collection name must be a non-empty string.");
|
|
107
|
-
err.code = "INVALID_COLLECTION_NAME";
|
|
108
|
-
throw err;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 检查特殊字符(MongoDB 集合名不允许包含 $, ., 空格, null 字符等)
|
|
112
|
-
const invalidChars = /[$.\s\x00]/;
|
|
113
|
-
if (invalidChars.test(collectionName)) {
|
|
114
|
-
const err = new Error(
|
|
115
|
-
"Invalid collection name: contains special characters ($, ., space, or null character).",
|
|
116
|
-
);
|
|
117
|
-
err.code = "INVALID_COLLECTION_NAME";
|
|
118
|
-
throw err;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!definition || typeof definition !== "object") {
|
|
122
|
-
const err = new Error("Model definition must be an object.");
|
|
123
|
-
err.code = "INVALID_MODEL_DEFINITION";
|
|
124
|
-
throw err;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!definition.schema) {
|
|
128
|
-
const err = new Error(
|
|
129
|
-
"Model definition must include a schema property.",
|
|
130
|
-
);
|
|
131
|
-
err.code = "MISSING_SCHEMA";
|
|
132
|
-
throw err;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
typeof definition.schema !== "function" &&
|
|
137
|
-
typeof definition.schema !== "object"
|
|
138
|
-
) {
|
|
139
|
-
const err = new Error("Schema must be a function or object.");
|
|
140
|
-
err.code = "INVALID_SCHEMA_TYPE";
|
|
141
|
-
throw err;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ========== 检查重复注册 ==========
|
|
145
|
-
if (this._registry.has(collectionName)) {
|
|
146
|
-
const err = new Error(`Model '${collectionName}' is already defined.`);
|
|
147
|
-
err.code = "MODEL_ALREADY_EXISTS";
|
|
148
|
-
throw err;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ========== 验证 relations 配置 ==========
|
|
152
|
-
if (definition.relations && typeof definition.relations === "object") {
|
|
153
|
-
for (const [name, config] of Object.entries(definition.relations)) {
|
|
154
|
-
this._validateRelationConfig(name, config);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ========== 验证 options 配置 ==========
|
|
159
|
-
if (definition.options) {
|
|
160
|
-
this._validateOptions(definition.options);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ========== 验证 connection 配置(v1.2.2+)==========
|
|
164
|
-
if (definition.connection) {
|
|
165
|
-
this._validateConnection(definition.connection);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ========== 解析 timestamps 配置 ==========
|
|
169
|
-
const timestampsConfig = this._parseTimestampsConfig(
|
|
170
|
-
definition.options?.timestamps,
|
|
171
|
-
);
|
|
172
|
-
if (timestampsConfig) {
|
|
173
|
-
// 保存到内部 hooks 配置
|
|
174
|
-
if (!definition._internalHooks) {
|
|
175
|
-
definition._internalHooks = {};
|
|
176
|
-
}
|
|
177
|
-
definition._internalHooks.timestamps = timestampsConfig;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ========== 注册 Model ==========
|
|
181
|
-
this._registry.set(collectionName, {
|
|
182
|
-
collectionName,
|
|
183
|
-
definition,
|
|
184
|
-
});
|
|
185
|
-
} catch (err) {
|
|
186
|
-
// 统一错误处理
|
|
187
|
-
if (!err.code) {
|
|
188
|
-
err.code = "MODEL_DEFINE_ERROR";
|
|
189
|
-
}
|
|
190
|
-
throw err;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* 验证关系配置
|
|
196
|
-
* @private
|
|
197
|
-
* @param {string} name - 关系名称
|
|
198
|
-
* @param {Object} config - 关系配置
|
|
199
|
-
* @throws {Error} 配置不合法时抛出错误
|
|
200
|
-
*/
|
|
201
|
-
static _validateRelationConfig(name, config) {
|
|
202
|
-
// 验证必需字段
|
|
203
|
-
const required = ["from", "localField", "foreignField"];
|
|
204
|
-
for (const field of required) {
|
|
205
|
-
if (!config[field]) {
|
|
206
|
-
throw new Error(`relations 配置缺少必需字段: ${field}`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// 验证 from 必须是字符串(集合名)
|
|
211
|
-
if (typeof config.from !== "string") {
|
|
212
|
-
throw new Error("relations.from 必须是字符串(集合名称)");
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 验证 localField 和 foreignField 必须是字符串
|
|
216
|
-
if (typeof config.localField !== "string") {
|
|
217
|
-
throw new Error("relations.localField 必须是字符串");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (typeof config.foreignField !== "string") {
|
|
221
|
-
throw new Error("relations.foreignField 必须是字符串");
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// 验证 single 必须是布尔值(如果提供)
|
|
225
|
-
if (config.single !== undefined && typeof config.single !== "boolean") {
|
|
226
|
-
throw new Error("relations.single 必须是布尔值");
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* 验证 connection 配置
|
|
232
|
-
*
|
|
233
|
-
* @private
|
|
234
|
-
* @param {Object} connection - connection 配置对象
|
|
235
|
-
* @throws {Error} 配置不合法时抛出错误
|
|
236
|
-
*
|
|
237
|
-
* @since v1.2.2
|
|
238
|
-
*/
|
|
239
|
-
static _validateConnection(connection) {
|
|
240
|
-
if (!connection || typeof connection !== 'object') {
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (connection.pool !== undefined) {
|
|
245
|
-
if (typeof connection.pool !== 'string' || connection.pool.trim() === '') {
|
|
246
|
-
const err = new Error('connection.pool must be a non-empty string');
|
|
247
|
-
err.code = 'INVALID_MODEL_DEFINITION';
|
|
248
|
-
throw err;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (connection.database !== undefined) {
|
|
253
|
-
if (typeof connection.database !== 'string' || connection.database.trim() === '') {
|
|
254
|
-
const err = new Error('connection.database must be a non-empty string');
|
|
255
|
-
err.code = 'INVALID_MODEL_DEFINITION';
|
|
256
|
-
throw err;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* 验证 options 配置
|
|
263
|
-
* @private
|
|
264
|
-
* @param {Object} options - options 配置对象
|
|
265
|
-
* @throws {Error} 配置不合法时抛出错误
|
|
266
|
-
*/
|
|
267
|
-
static _validateOptions(options) {
|
|
268
|
-
if (!options || typeof options !== "object") {
|
|
269
|
-
return; // options 是可选的
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// 验证 timestamps
|
|
273
|
-
if (options.timestamps !== undefined) {
|
|
274
|
-
if (
|
|
275
|
-
typeof options.timestamps !== "boolean" &&
|
|
276
|
-
typeof options.timestamps !== "object"
|
|
277
|
-
) {
|
|
278
|
-
throw new Error("options.timestamps must be boolean or object");
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
typeof options.timestamps === "object" &&
|
|
283
|
-
options.timestamps !== null
|
|
284
|
-
) {
|
|
285
|
-
const validKeys = ["createdAt", "updatedAt"];
|
|
286
|
-
const invalidKeys = Object.keys(options.timestamps).filter(
|
|
287
|
-
(k) => !validKeys.includes(k),
|
|
288
|
-
);
|
|
289
|
-
if (invalidKeys.length > 0) {
|
|
290
|
-
throw new Error(
|
|
291
|
-
`Invalid timestamps keys: ${invalidKeys.join(", ")}. Valid keys are: ${validKeys.join(", ")}`,
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// 验证 softDelete
|
|
298
|
-
if (options.softDelete !== undefined) {
|
|
299
|
-
if (
|
|
300
|
-
typeof options.softDelete !== "boolean" &&
|
|
301
|
-
typeof options.softDelete !== "object"
|
|
302
|
-
) {
|
|
303
|
-
throw new Error("options.softDelete must be boolean or object");
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// 验证 version
|
|
308
|
-
if (options.version !== undefined) {
|
|
309
|
-
if (
|
|
310
|
-
typeof options.version !== "boolean" &&
|
|
311
|
-
typeof options.version !== "object"
|
|
312
|
-
) {
|
|
313
|
-
throw new Error("options.version must be boolean or object");
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// 验证 validate
|
|
318
|
-
if (options.validate !== undefined) {
|
|
319
|
-
if (typeof options.validate !== "boolean") {
|
|
320
|
-
throw new Error("options.validate must be boolean");
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* 解析 timestamps 配置
|
|
327
|
-
*
|
|
328
|
-
* @private
|
|
329
|
-
* @param {boolean|Object} config - timestamps 配置
|
|
330
|
-
* @returns {Object|null} 解析后的配置对象,包含 createdAt 和 updatedAt 字段名
|
|
331
|
-
*
|
|
332
|
-
* @example
|
|
333
|
-
* _parseTimestampsConfig(true)
|
|
334
|
-
* // => { createdAt: 'createdAt', updatedAt: 'updatedAt' }
|
|
335
|
-
*
|
|
336
|
-
* _parseTimestampsConfig({ createdAt: 'created_time', updatedAt: false })
|
|
337
|
-
* // => { createdAt: 'created_time' }
|
|
338
|
-
*/
|
|
339
|
-
static _parseTimestampsConfig(config) {
|
|
340
|
-
if (!config) return null;
|
|
341
|
-
|
|
342
|
-
// 简单模式:timestamps: true
|
|
343
|
-
if (config === true) {
|
|
344
|
-
return {
|
|
345
|
-
createdAt: "createdAt",
|
|
346
|
-
updatedAt: "updatedAt",
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// 对象模式
|
|
351
|
-
if (typeof config === "object") {
|
|
352
|
-
const result = {};
|
|
353
|
-
|
|
354
|
-
// createdAt 配置
|
|
355
|
-
if (config.createdAt === true) {
|
|
356
|
-
result.createdAt = "createdAt";
|
|
357
|
-
} else if (typeof config.createdAt === "string") {
|
|
358
|
-
result.createdAt = config.createdAt;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// updatedAt 配置(默认启用)
|
|
362
|
-
if (config.updatedAt !== false) {
|
|
363
|
-
if (config.updatedAt === true || config.updatedAt === undefined) {
|
|
364
|
-
result.updatedAt = "updatedAt";
|
|
365
|
-
} else if (typeof config.updatedAt === "string") {
|
|
366
|
-
result.updatedAt = config.updatedAt;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return Object.keys(result).length > 0 ? result : null;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* 获取已注册的 Model 定义
|
|
378
|
-
*
|
|
379
|
-
* @param {string} collectionName - 集合名称
|
|
380
|
-
* @returns {Object|undefined} Model 定义对象
|
|
381
|
-
*
|
|
382
|
-
* @example
|
|
383
|
-
* const userModelDef = Model.get('users');
|
|
384
|
-
*/
|
|
385
|
-
static get(collectionName) {
|
|
386
|
-
return this._registry.get(collectionName);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* 检查 Model 是否已注册
|
|
391
|
-
*
|
|
392
|
-
* @param {string} collectionName - 集合名称
|
|
393
|
-
* @returns {boolean}
|
|
394
|
-
*
|
|
395
|
-
* @example
|
|
396
|
-
* if (Model.has('users')) {
|
|
397
|
-
* // Model 已注册
|
|
398
|
-
* }
|
|
399
|
-
*/
|
|
400
|
-
static has(collectionName) {
|
|
401
|
-
return this._registry.has(collectionName);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* 获取所有已注册的 Model 名称
|
|
406
|
-
*
|
|
407
|
-
* @returns {string[]} Model 名称数组
|
|
408
|
-
*
|
|
409
|
-
* @example
|
|
410
|
-
* const modelNames = Model.list();
|
|
411
|
-
* // ['users', 'posts', 'comments']
|
|
412
|
-
*/
|
|
413
|
-
static list() {
|
|
414
|
-
return Array.from(this._registry.keys());
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* 注销已注册的 Model 定义
|
|
419
|
-
*
|
|
420
|
-
* 从全局注册表中移除指定的 Model 定义。
|
|
421
|
-
* 已实例化的 ModelInstance 不受影响,仅影响后续通过 msq.model() 获取的新实例。
|
|
422
|
-
*
|
|
423
|
-
* 主要用途:
|
|
424
|
-
* - 开发模式下的 Model 热重载(配合 Model.define() 实现替换)
|
|
425
|
-
* - 测试中清理单个 Model(比 _clear() 更精确)
|
|
426
|
-
*
|
|
427
|
-
* @param {string} collectionName - 要注销的集合名称
|
|
428
|
-
* @returns {boolean} 如果成功移除返回 true,如果 Model 不存在返回 false
|
|
429
|
-
*
|
|
430
|
-
* @example
|
|
431
|
-
* // 热重载场景
|
|
432
|
-
* Model.undefine('users'); // 移除旧定义
|
|
433
|
-
* Model.define('users', newDef); // 注册新定义
|
|
434
|
-
*
|
|
435
|
-
* @example
|
|
436
|
-
* // 检查返回值
|
|
437
|
-
* const removed = Model.undefine('users');
|
|
438
|
-
* console.log(removed); // true(已存在)或 false(不存在)
|
|
439
|
-
*
|
|
440
|
-
* @since 1.1.7
|
|
441
|
-
*/
|
|
442
|
-
static undefine(collectionName) {
|
|
443
|
-
// 标记需要缓存失效(MonSQLize.model() 检查此标记)
|
|
444
|
-
this._redefinedNames.add(collectionName);
|
|
445
|
-
return this._registry.delete(collectionName);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* 重新定义已注册的 Model
|
|
450
|
-
*
|
|
451
|
-
* 等效于 undefine() + define() 的组合操作。
|
|
452
|
-
* 如果 Model 不存在,行为等同于 define()(首次注册)。
|
|
453
|
-
*
|
|
454
|
-
* 注意:如果新的 definition 校验失败(define() 抛错),旧定义将被移除。
|
|
455
|
-
* 调用方如需回滚,应在 catch 中自行重新 define() 旧定义。
|
|
456
|
-
*
|
|
457
|
-
* 主要用途:
|
|
458
|
-
* - 开发模式下的 Model 热重载(一步完成替换)
|
|
459
|
-
* - _loadModels() reload 模式的内部支撑
|
|
460
|
-
*
|
|
461
|
-
* @param {string} collectionName - 集合名称
|
|
462
|
-
* @param {Object} definition - 新的 Model 定义对象
|
|
463
|
-
* @returns {void}
|
|
464
|
-
* @throws {Error} 参数验证失败时抛出(与 define() 相同的校验逻辑)
|
|
465
|
-
*
|
|
466
|
-
* @example
|
|
467
|
-
* // 替换已有 Model 定义
|
|
468
|
-
* Model.redefine('users', {
|
|
469
|
-
* schema: (dsl) => dsl({ username: 'string!', email: 'string!' })
|
|
470
|
-
* });
|
|
471
|
-
*
|
|
472
|
-
* @example
|
|
473
|
-
* // 首次定义(等同于 define)
|
|
474
|
-
* Model.redefine('posts', {
|
|
475
|
-
* schema: (dsl) => dsl({ title: 'string!' })
|
|
476
|
-
* });
|
|
477
|
-
*
|
|
478
|
-
* @since 1.1.7
|
|
479
|
-
*/
|
|
480
|
-
static redefine(collectionName, definition) {
|
|
481
|
-
this.undefine(collectionName);
|
|
482
|
-
this.define(collectionName, definition);
|
|
483
|
-
// 标记需要缓存失效(MonSQLize.model() 检查此标记)
|
|
484
|
-
this._redefinedNames.add(collectionName);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* 清空所有已注册的 Model(主要用于测试)
|
|
489
|
-
*
|
|
490
|
-
* @private
|
|
491
|
-
*/
|
|
492
|
-
static _clear() {
|
|
493
|
-
// 将所有已注册名标记为需要缓存失效,让 msq.model() 重建 ModelInstance
|
|
494
|
-
for (const name of this._registry.keys()) {
|
|
495
|
-
this._redefinedNames.add(name);
|
|
496
|
-
}
|
|
497
|
-
this._registry.clear();
|
|
498
|
-
// 注意:不清除 _redefinedNames,确保 msq 实例缓存能感知本次 clear
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* ModelInstance - Model 实例类
|
|
504
|
-
* 继承 collection 的所有方法,并扩展 Model 特性
|
|
505
|
-
*/
|
|
506
|
-
class ModelInstance {
|
|
507
|
-
/**
|
|
508
|
-
* 创建 ModelInstance 实例
|
|
509
|
-
*
|
|
510
|
-
* @param {Object} collection - monSQLize collection 对象
|
|
511
|
-
* @param {Object} definition - Model 定义对象
|
|
512
|
-
* @param {Object} msq - monSQLize 实例
|
|
513
|
-
*/
|
|
514
|
-
constructor(collection, definition, msq) {
|
|
515
|
-
this.collection = collection;
|
|
516
|
-
this.definition = definition;
|
|
517
|
-
this.msq = msq;
|
|
518
|
-
|
|
519
|
-
// ========== Schema 缓存优化 ==========
|
|
520
|
-
// 🚀 性能优化:编译 schema 并缓存,避免每次 validate 重新执行
|
|
521
|
-
this._schemaCache = null;
|
|
522
|
-
this._schemaError = null; // 🆕 记录schema错误,但不阻止实例化
|
|
523
|
-
|
|
524
|
-
if (typeof definition.schema === "function") {
|
|
525
|
-
try {
|
|
526
|
-
// 绑定 this 到 definition,支持访问 this.enums
|
|
527
|
-
this._schemaCache = definition.schema.call(definition, dsl);
|
|
528
|
-
} catch (err) {
|
|
529
|
-
// 🆕 schema 函数执行失败时,记录错误但不抛出
|
|
530
|
-
// 这样可以兼容MongoDB的无schema模式
|
|
531
|
-
this._schemaError = err;
|
|
532
|
-
this._schemaCache = null;
|
|
533
|
-
|
|
534
|
-
// 记录详细的警告日志
|
|
535
|
-
if (this.msq && this.msq.logger) {
|
|
536
|
-
this.msq.logger.warn(
|
|
537
|
-
`[Model] Schema function execution failed for collection '${collection.collectionName}': ${err.message}`,
|
|
538
|
-
{ originalError: err },
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
} else {
|
|
543
|
-
this._schemaCache = definition.schema;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// ========== 继承 collection 所有方法 ==========
|
|
547
|
-
// 将 collection 的所有方法代理到 ModelInstance
|
|
548
|
-
const collectionMethods = Object.getOwnPropertyNames(
|
|
549
|
-
Object.getPrototypeOf(collection),
|
|
550
|
-
)
|
|
551
|
-
.concat(Object.keys(collection))
|
|
552
|
-
.filter(
|
|
553
|
-
(key) => key !== "constructor" && typeof collection[key] === "function",
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
// 去重
|
|
557
|
-
const uniqueMethods = [...new Set(collectionMethods)];
|
|
558
|
-
|
|
559
|
-
// 🆕 需要支持 populate 的查询方法列表
|
|
560
|
-
const populateMethods = [
|
|
561
|
-
"find",
|
|
562
|
-
"findOne",
|
|
563
|
-
"findByIds",
|
|
564
|
-
"findOneById",
|
|
565
|
-
"findAndCount",
|
|
566
|
-
"findPage",
|
|
567
|
-
];
|
|
568
|
-
|
|
569
|
-
uniqueMethods.forEach((method) => {
|
|
570
|
-
if (!this[method]) {
|
|
571
|
-
// 🆕 支持 populate 的查询方法特殊处理
|
|
572
|
-
if (populateMethods.includes(method)) {
|
|
573
|
-
this[method] = (...args) => {
|
|
574
|
-
// 创建一个 Promise 来执行实际查询
|
|
575
|
-
const executeQuery = async () => {
|
|
576
|
-
// 🔧 Hook 拦截机制
|
|
577
|
-
const result = await this._interceptWithHooks(method, args);
|
|
578
|
-
|
|
579
|
-
// 🔧 实例方法注入
|
|
580
|
-
if (result) {
|
|
581
|
-
this._injectInstanceMethods(result);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return result;
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
// 执行查询并返回 PopulateProxy
|
|
588
|
-
const queryPromise = executeQuery();
|
|
589
|
-
|
|
590
|
-
// 判断返回类型
|
|
591
|
-
const singleDoc = method === "findOne" || method === "findOneById";
|
|
592
|
-
const isSpecialResult =
|
|
593
|
-
method === "findAndCount" || method === "findPage";
|
|
594
|
-
|
|
595
|
-
if (isSpecialResult) {
|
|
596
|
-
// findAndCount 和 findPage 返回特殊结构,需要特殊包装
|
|
597
|
-
return this._wrapWithSpecialPopulateProxy(queryPromise, method);
|
|
598
|
-
} else {
|
|
599
|
-
// 普通查询方法
|
|
600
|
-
return this._wrapWithPopulateProxyFromPromise(
|
|
601
|
-
queryPromise,
|
|
602
|
-
singleDoc,
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
};
|
|
606
|
-
} else {
|
|
607
|
-
// 其他方法保持原样
|
|
608
|
-
this[method] = async (...args) => {
|
|
609
|
-
// 🔧 incrementOne 特殊处理timestamps
|
|
610
|
-
if (
|
|
611
|
-
method === "incrementOne" &&
|
|
612
|
-
this.definition._internalHooks?.timestamps?.updatedAt
|
|
613
|
-
) {
|
|
614
|
-
// 调用 _applyTimestampsToIncrementOne 处理
|
|
615
|
-
args = this._applyTimestampsToIncrementOne(args);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// 🔧 Hook 拦截机制
|
|
619
|
-
const result = await this._interceptWithHooks(method, args);
|
|
620
|
-
|
|
621
|
-
// 🔧 实例方法注入:只在查询操作时注入(find/findOne/aggregate等)
|
|
622
|
-
const opType = this._getOperationType(method);
|
|
623
|
-
if (opType === "find" && result) {
|
|
624
|
-
this._injectInstanceMethods(result);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return result;
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
// ========== 扩展自定义方法 ==========
|
|
634
|
-
if (typeof definition.methods === "function") {
|
|
635
|
-
const customMethods = definition.methods(this);
|
|
636
|
-
|
|
637
|
-
// 1. instance 方法 - 保存引用,用于注入到查询结果
|
|
638
|
-
this._instanceMethods = customMethods.instance || {};
|
|
639
|
-
|
|
640
|
-
// 2. static 方法 - 挂载到 ModelInstance 本身
|
|
641
|
-
if (customMethods.static && typeof customMethods.static === "object") {
|
|
642
|
-
Object.keys(customMethods.static).forEach((methodName) => {
|
|
643
|
-
if (typeof customMethods.static[methodName] === "function") {
|
|
644
|
-
// 挂载到 this(ModelInstance 实例)
|
|
645
|
-
this[methodName] = customMethods.static[methodName].bind(this);
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// 3. 警告未识别的键(帮助用户发现错误)
|
|
651
|
-
if (customMethods && typeof customMethods === "object") {
|
|
652
|
-
const validKeys = ["instance", "static"];
|
|
653
|
-
const unknownKeys = Object.keys(customMethods).filter(
|
|
654
|
-
(k) => !validKeys.includes(k),
|
|
655
|
-
);
|
|
656
|
-
if (unknownKeys.length > 0 && this.msq && this.msq.logger) {
|
|
657
|
-
this.msq.logger.warn(
|
|
658
|
-
`[Model] methods 只支持 'instance' 和 'static' 两个分组。` +
|
|
659
|
-
`发现未识别的键: ${unknownKeys.join(", ")}`,
|
|
660
|
-
);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
} else {
|
|
664
|
-
this._instanceMethods = {};
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// ========== 初始化 indexes 数组 ==========
|
|
668
|
-
// 注意:必须在 setupSoftDelete 之前初始化,因为 softDelete 可能添加 TTL 索引
|
|
669
|
-
this.indexes = definition.indexes || [];
|
|
670
|
-
|
|
671
|
-
// ========== 关系定义管理 ==========
|
|
672
|
-
this._relations = new RelationManager(this);
|
|
673
|
-
|
|
674
|
-
// 注册 relations
|
|
675
|
-
if (definition.relations && typeof definition.relations === "object") {
|
|
676
|
-
for (const [name, config] of Object.entries(definition.relations)) {
|
|
677
|
-
this._relations.define(name, config);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// ========== 虚拟字段功能 ==========
|
|
682
|
-
const { setupVirtuals } = require("./features/virtuals");
|
|
683
|
-
setupVirtuals(this, definition.virtuals);
|
|
684
|
-
|
|
685
|
-
// ========== 默认值功能 ==========
|
|
686
|
-
const { setupDefaults } = require("./features/defaults");
|
|
687
|
-
setupDefaults(this, definition.defaults);
|
|
688
|
-
|
|
689
|
-
// ========== 软删除功能 ==========
|
|
690
|
-
const { setupSoftDelete } = require("./features/soft-delete");
|
|
691
|
-
setupSoftDelete(this, definition.options?.softDelete);
|
|
692
|
-
|
|
693
|
-
// ========== 乐观锁版本控制功能 ==========
|
|
694
|
-
const { setupVersion } = require("./features/version");
|
|
695
|
-
setupVersion(this, definition.options?.version);
|
|
696
|
-
|
|
697
|
-
// ========== 自动验证配置 ==========
|
|
698
|
-
// 🔴 默认启用验证(除非明确设置为 false)
|
|
699
|
-
this._autoValidate = definition.options?.validate !== false;
|
|
700
|
-
|
|
701
|
-
// ========== 自动创建索引 ==========
|
|
702
|
-
if (Array.isArray(this.indexes) && this.indexes.length > 0) {
|
|
703
|
-
// 延迟执行,避免阻塞初始化
|
|
704
|
-
setImmediate(() => {
|
|
705
|
-
this._createIndexes().catch((err) => {
|
|
706
|
-
// 索引创建失败仅记录警告,不中断流程
|
|
707
|
-
if (this.msq && this.msq.logger) {
|
|
708
|
-
this.msq.logger.warn(
|
|
709
|
-
`[Model] Failed to create indexes for ${this.collection.collectionName}:`,
|
|
710
|
-
err.message,
|
|
711
|
-
);
|
|
712
|
-
}
|
|
713
|
-
});
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* 将实例方法注入到文档对象
|
|
720
|
-
*
|
|
721
|
-
* @private
|
|
722
|
-
* @param {Object|Array} result - 查询返回的结果(文档对象或数组)
|
|
723
|
-
*/
|
|
724
|
-
_injectInstanceMethods(result) {
|
|
725
|
-
if (
|
|
726
|
-
!this._instanceMethods ||
|
|
727
|
-
Object.keys(this._instanceMethods).length === 0
|
|
728
|
-
) {
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// 处理数组结果(find 返回的)
|
|
733
|
-
if (Array.isArray(result)) {
|
|
734
|
-
result.forEach((doc) => {
|
|
735
|
-
if (doc && typeof doc === "object") {
|
|
736
|
-
this._injectToDocument(doc);
|
|
737
|
-
}
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
// 处理单个文档(findOne 返回的)
|
|
741
|
-
else if (result && typeof result === "object" && !Buffer.isBuffer(result)) {
|
|
742
|
-
this._injectToDocument(result);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
/**
|
|
747
|
-
* 将实例方法注入到单个文档对象
|
|
748
|
-
*
|
|
749
|
-
* @private
|
|
750
|
-
* @param {Object} doc - 文档对象
|
|
751
|
-
*/
|
|
752
|
-
_injectToDocument(doc) {
|
|
753
|
-
Object.keys(this._instanceMethods).forEach((methodName) => {
|
|
754
|
-
if (typeof this._instanceMethods[methodName] === "function") {
|
|
755
|
-
// 绑定 this 到文档对象
|
|
756
|
-
doc[methodName] = this._instanceMethods[methodName].bind(doc);
|
|
757
|
-
}
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* 包装查询结果为 PopulateProxy
|
|
763
|
-
*
|
|
764
|
-
* @private
|
|
765
|
-
* @param {Object|Array|null} result - 查询结果
|
|
766
|
-
* @param {boolean} singleDoc - 是否是单文档查询(findOne)
|
|
767
|
-
* @returns {PopulateProxy} PopulateProxy 实例
|
|
768
|
-
*/
|
|
769
|
-
_wrapWithPopulateProxy(result, singleDoc = false) {
|
|
770
|
-
// 如果结果为 null(findOne 未找到),仍然包装为 PopulateProxy
|
|
771
|
-
const docs =
|
|
772
|
-
result === null ? [] : Array.isArray(result) ? result : [result];
|
|
773
|
-
|
|
774
|
-
// 创建 PopulateBuilder
|
|
775
|
-
const builder = new PopulateBuilder(this, this.collection);
|
|
776
|
-
|
|
777
|
-
// 返回 PopulateProxy
|
|
778
|
-
return new PopulateProxy(docs, builder, singleDoc);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
/**
|
|
782
|
-
* 从 Promise 创建 PopulateProxy
|
|
783
|
-
*
|
|
784
|
-
* @private
|
|
785
|
-
* @param {Promise} queryPromise - 查询 Promise
|
|
786
|
-
* @param {boolean} singleDoc - 是否是单文档查询(findOne)
|
|
787
|
-
* @returns {PopulateProxy} PopulateProxy 实例
|
|
788
|
-
*/
|
|
789
|
-
_wrapWithPopulateProxyFromPromise(queryPromise, singleDoc = false) {
|
|
790
|
-
// 创建 PopulateBuilder
|
|
791
|
-
const builder = new PopulateBuilder(this, this.collection);
|
|
792
|
-
|
|
793
|
-
// 返回 PopulateProxy,传入 Promise 而不是实际数据
|
|
794
|
-
return new PopulateProxy(queryPromise, builder, singleDoc);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/**
|
|
798
|
-
* 从 Promise 创建特殊的 PopulateProxy(用于 findAndCount 和 findPage)
|
|
799
|
-
*
|
|
800
|
-
* @private
|
|
801
|
-
* @param {Promise} queryPromise - 查询 Promise
|
|
802
|
-
* @param {string} method - 方法名(findAndCount 或 findPage)
|
|
803
|
-
* @returns {PopulateProxy} PopulateProxy 实例
|
|
804
|
-
*/
|
|
805
|
-
_wrapWithSpecialPopulateProxy(queryPromise, method) {
|
|
806
|
-
// 创建 PopulateBuilder
|
|
807
|
-
const builder = new PopulateBuilder(this, this.collection);
|
|
808
|
-
|
|
809
|
-
// 返回特殊的 PopulateProxy
|
|
810
|
-
const { SpecialPopulateProxy } = require("./features/populate");
|
|
811
|
-
return new SpecialPopulateProxy(queryPromise, builder, method);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
/**
|
|
815
|
-
* Hook 拦截机制
|
|
816
|
-
* 在方法执行前后触发 before/after 钩子
|
|
817
|
-
*
|
|
818
|
-
* @private
|
|
819
|
-
* @param {string} method - 方法名
|
|
820
|
-
* @param {Array} args - 方法参数
|
|
821
|
-
* @returns {Promise<*>} 方法执行结果
|
|
822
|
-
*/
|
|
823
|
-
async _interceptWithHooks(method, args) {
|
|
824
|
-
const hooks =
|
|
825
|
-
typeof this.definition.hooks === "function"
|
|
826
|
-
? this.definition.hooks(this)
|
|
827
|
-
: {};
|
|
828
|
-
|
|
829
|
-
// 提取操作类型(find/insert/update/delete)
|
|
830
|
-
const opType = this._getOperationType(method);
|
|
831
|
-
const opHooks = hooks[opType] || {};
|
|
832
|
-
|
|
833
|
-
// 上下文对象(用于在 before/after 之间传递数据,如事务 session)
|
|
834
|
-
const ctx = {};
|
|
835
|
-
|
|
836
|
-
// ========== Before Hook ==========
|
|
837
|
-
if (typeof opHooks.before === "function") {
|
|
838
|
-
// 🔧 修复:before hook 错误必须中断操作
|
|
839
|
-
const modifiedArgs = await opHooks.before(ctx, ...args);
|
|
840
|
-
|
|
841
|
-
// 🔧 修复:正确应用 before hook 返回值
|
|
842
|
-
// 对于 insert 操作,第一个参数是待插入的文档
|
|
843
|
-
if (modifiedArgs !== undefined) {
|
|
844
|
-
if (opType === "insert") {
|
|
845
|
-
// insertOne/insertMany: args[0] 是文档/文档数组
|
|
846
|
-
args[0] = modifiedArgs;
|
|
847
|
-
} else if (Array.isArray(modifiedArgs)) {
|
|
848
|
-
// 其他操作:如果返回数组,替换整个 args
|
|
849
|
-
args = modifiedArgs;
|
|
850
|
-
} else {
|
|
851
|
-
// 单个返回值,替换第一个参数
|
|
852
|
-
args[0] = modifiedArgs;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
// ========== 自动应用默认值(仅 insert 操作)==========
|
|
858
|
-
if (opType === "insert" && this._defaults) {
|
|
859
|
-
args[0] = this._defaults.apply(args[0], ctx);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// ========== Schema 验证(仅 insert/update 操作)==========
|
|
863
|
-
if ((opType === "insert" || opType === "update") && this._schemaCache) {
|
|
864
|
-
// 检查是否跳过验证
|
|
865
|
-
const skipValidation =
|
|
866
|
-
args[args.length - 1] &&
|
|
867
|
-
typeof args[args.length - 1] === "object" &&
|
|
868
|
-
args[args.length - 1].skipValidation === true;
|
|
869
|
-
|
|
870
|
-
// 🔴 默认启用验证,除非显式跳过或配置为 false
|
|
871
|
-
const enableValidation =
|
|
872
|
-
!skipValidation &&
|
|
873
|
-
// 全局配置未明确禁用(默认为 true)
|
|
874
|
-
(this.definition.options?.validate !== false ||
|
|
875
|
-
// 单次操作启用
|
|
876
|
-
(args[args.length - 1] &&
|
|
877
|
-
typeof args[args.length - 1] === "object" &&
|
|
878
|
-
args[args.length - 1].validate === true));
|
|
879
|
-
|
|
880
|
-
if (enableValidation && typeof validate === "function") {
|
|
881
|
-
try {
|
|
882
|
-
if (opType === "insert") {
|
|
883
|
-
const docs = args[0];
|
|
884
|
-
const docsArray = Array.isArray(docs) ? docs : [docs];
|
|
885
|
-
|
|
886
|
-
for (let i = 0; i < docsArray.length; i++) {
|
|
887
|
-
const validationResult = validate(
|
|
888
|
-
this._schemaCache,
|
|
889
|
-
docsArray[i],
|
|
890
|
-
);
|
|
891
|
-
if (!validationResult.valid) {
|
|
892
|
-
// 格式化错误消息
|
|
893
|
-
const errors = validationResult.errors || [];
|
|
894
|
-
const errorMessages = errors
|
|
895
|
-
.map((err) => {
|
|
896
|
-
const field = err.field || err.path || "unknown";
|
|
897
|
-
const value = docsArray[i][field];
|
|
898
|
-
if (err.type === "type") {
|
|
899
|
-
return `Field '${field}': expected type '${err.expected}', got '${typeof value}'`;
|
|
900
|
-
} else if (err.type === "required") {
|
|
901
|
-
return `Field '${field}': required field is missing`;
|
|
902
|
-
} else {
|
|
903
|
-
return `Field '${field}': ${err.message || "validation failed"}`;
|
|
904
|
-
}
|
|
905
|
-
})
|
|
906
|
-
.join("; ");
|
|
907
|
-
|
|
908
|
-
const err = new Error(
|
|
909
|
-
`Schema validation failed${Array.isArray(docs) ? ` at index ${i}` : ""}: ${errorMessages}`,
|
|
910
|
-
);
|
|
911
|
-
err.code = "VALIDATION_ERROR";
|
|
912
|
-
err.errors = errors;
|
|
913
|
-
err.index = Array.isArray(docs) ? i : undefined;
|
|
914
|
-
throw err;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
} catch (err) {
|
|
919
|
-
// 如果是我们抛出的验证错误,直接抛出
|
|
920
|
-
if (err.code === "VALIDATION_ERROR") {
|
|
921
|
-
throw err;
|
|
922
|
-
}
|
|
923
|
-
// 如果 validate 函数不可用或执行失败,记录警告但继续
|
|
924
|
-
if (this.msq && this.msq.logger) {
|
|
925
|
-
this.msq.logger.warn(
|
|
926
|
-
"[Model] Schema validation skipped:",
|
|
927
|
-
err.message,
|
|
928
|
-
);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// ========== 自动注入时间戳(在用户 hook 之后执行)==========
|
|
935
|
-
if (this.definition._internalHooks?.timestamps) {
|
|
936
|
-
args = this._applyTimestamps(opType, method, args);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// ========== 执行原始方法 ==========
|
|
940
|
-
const result = await this.collection[method](...args);
|
|
941
|
-
|
|
942
|
-
// ========== After Hook ==========
|
|
943
|
-
if (typeof opHooks.after === "function") {
|
|
944
|
-
try {
|
|
945
|
-
const modifiedResult = await opHooks.after(ctx, result);
|
|
946
|
-
// 如果 after 返回值,使用修改后的结果
|
|
947
|
-
if (modifiedResult !== undefined) {
|
|
948
|
-
return modifiedResult;
|
|
949
|
-
}
|
|
950
|
-
} catch (err) {
|
|
951
|
-
// 🔧 修复:after hook 失败记录警告,但不影响操作结果
|
|
952
|
-
if (this.msq && this.msq.logger) {
|
|
953
|
-
this.msq.logger.warn(
|
|
954
|
-
`[Model] After hook failed for ${method}:`,
|
|
955
|
-
err.message,
|
|
956
|
-
);
|
|
957
|
-
}
|
|
958
|
-
// 不抛出错误,返回原始结果
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
return result;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
/**
|
|
966
|
-
* 应用时间戳到 incrementOne
|
|
967
|
-
*
|
|
968
|
-
* incrementOne 的参数: (filter, field, increment, options)
|
|
969
|
-
* 我们需要在底层的 findOneAndUpdate 调用中注入 $set.updatedAt
|
|
970
|
-
*
|
|
971
|
-
* @private
|
|
972
|
-
* @param {Array} args - incrementOne 参数数组
|
|
973
|
-
* @returns {Array} 修改后的参数
|
|
974
|
-
*/
|
|
975
|
-
_applyTimestampsToIncrementOne(args) {
|
|
976
|
-
const config = this.definition._internalHooks.timestamps;
|
|
977
|
-
const now = new Date();
|
|
978
|
-
|
|
979
|
-
// 找到 options 参数(可能在 args[2] 或 args[3])
|
|
980
|
-
let optionsIndex = -1;
|
|
981
|
-
if (args[3] && typeof args[3] === "object") {
|
|
982
|
-
optionsIndex = 3;
|
|
983
|
-
} else if (
|
|
984
|
-
args[2] &&
|
|
985
|
-
typeof args[2] === "object" &&
|
|
986
|
-
typeof args[2] !== "number"
|
|
987
|
-
) {
|
|
988
|
-
// args[2] 是对象且不是数字(increment)
|
|
989
|
-
optionsIndex = 2;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
// 创建或修改 options,添加 $set.updatedAt
|
|
993
|
-
if (optionsIndex === -1) {
|
|
994
|
-
// 没有 options,创建一个
|
|
995
|
-
args[3] = { $set: { [config.updatedAt]: now } };
|
|
996
|
-
} else {
|
|
997
|
-
// 有 options,合并 $set
|
|
998
|
-
const options = args[optionsIndex];
|
|
999
|
-
if (!options.$set) {
|
|
1000
|
-
options.$set = {};
|
|
1001
|
-
}
|
|
1002
|
-
options.$set[config.updatedAt] = now;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
return args;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
/**
|
|
1009
|
-
* 应用时间戳
|
|
1010
|
-
*
|
|
1011
|
-
* @private
|
|
1012
|
-
* @param {string} opType - 操作类型
|
|
1013
|
-
* @param {string} method - 方法名
|
|
1014
|
-
* @param {Array} args - 参数
|
|
1015
|
-
* @returns {Array} 修改后的参数
|
|
1016
|
-
*/
|
|
1017
|
-
_applyTimestamps(opType, method, args) {
|
|
1018
|
-
const config = this.definition._internalHooks.timestamps;
|
|
1019
|
-
const now = new Date();
|
|
1020
|
-
|
|
1021
|
-
if (opType === "insert") {
|
|
1022
|
-
// insertOne/insertMany
|
|
1023
|
-
const docs = args[0];
|
|
1024
|
-
|
|
1025
|
-
if (Array.isArray(docs)) {
|
|
1026
|
-
// insertMany
|
|
1027
|
-
args[0] = docs.map((doc) => {
|
|
1028
|
-
const newDoc = { ...doc };
|
|
1029
|
-
// 🔧 修复:只在用户未手动设置时添加时间戳
|
|
1030
|
-
if (config.createdAt && !doc[config.createdAt]) {
|
|
1031
|
-
newDoc[config.createdAt] = now;
|
|
1032
|
-
}
|
|
1033
|
-
if (config.updatedAt && !doc[config.updatedAt]) {
|
|
1034
|
-
newDoc[config.updatedAt] = now;
|
|
1035
|
-
}
|
|
1036
|
-
return newDoc;
|
|
1037
|
-
});
|
|
1038
|
-
} else {
|
|
1039
|
-
// insertOne
|
|
1040
|
-
const newDoc = { ...docs };
|
|
1041
|
-
// 🔧 修复:只在用户未手动设置时添加时间戳
|
|
1042
|
-
if (config.createdAt && !docs[config.createdAt]) {
|
|
1043
|
-
newDoc[config.createdAt] = now;
|
|
1044
|
-
}
|
|
1045
|
-
if (config.updatedAt && !docs[config.updatedAt]) {
|
|
1046
|
-
newDoc[config.updatedAt] = now;
|
|
1047
|
-
}
|
|
1048
|
-
args[0] = newDoc;
|
|
1049
|
-
}
|
|
1050
|
-
} else if (opType === "update") {
|
|
1051
|
-
// updateOne/updateMany/replaceOne/upsertOne/findOneAndUpdate/findOneAndReplace/incrementOne
|
|
1052
|
-
|
|
1053
|
-
if (method.startsWith("upsert")) {
|
|
1054
|
-
// 🔧 upsert 特殊处理:插入时添加 createdAt,更新时只更新 updatedAt
|
|
1055
|
-
const update = args[1] || {};
|
|
1056
|
-
|
|
1057
|
-
// $setOnInsert: 仅在插入新文档时执行
|
|
1058
|
-
if (config.createdAt) {
|
|
1059
|
-
if (!update.$setOnInsert) {
|
|
1060
|
-
update.$setOnInsert = {};
|
|
1061
|
-
}
|
|
1062
|
-
update.$setOnInsert[config.createdAt] = now;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// $set: 每次都执行(插入和更新都会设置 updatedAt)
|
|
1066
|
-
if (config.updatedAt) {
|
|
1067
|
-
if (!update.$set) {
|
|
1068
|
-
update.$set = {};
|
|
1069
|
-
}
|
|
1070
|
-
update.$set[config.updatedAt] = now;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
args[1] = update;
|
|
1074
|
-
} else if (method === "replaceOne" || method === "findOneAndReplace") {
|
|
1075
|
-
// replaceOne/findOneAndReplace: 直接在文档中添加(不能使用操作符)
|
|
1076
|
-
if (config.updatedAt) {
|
|
1077
|
-
const replacement = args[1] || {};
|
|
1078
|
-
// 🔧 修复:只在用户未手动设置时添加 updatedAt
|
|
1079
|
-
if (!replacement[config.updatedAt]) {
|
|
1080
|
-
replacement[config.updatedAt] = now;
|
|
1081
|
-
}
|
|
1082
|
-
args[1] = replacement;
|
|
1083
|
-
}
|
|
1084
|
-
} else if (method.startsWith("increment")) {
|
|
1085
|
-
// 🔧 incrementOne 特殊处理
|
|
1086
|
-
// incrementOne(filter, field, increment, options)
|
|
1087
|
-
// Model 层无法直接修改内部的 $inc 对象,但可以通过 options 传递时间戳更新
|
|
1088
|
-
// 注意:incrementOne 内部会调用 findOneAndUpdate,所以我们不在这里处理
|
|
1089
|
-
// 而是让 incrementOne 自己处理(需要修改 increment-one.js)
|
|
1090
|
-
// 暂时跳过
|
|
1091
|
-
} else if (config.updatedAt) {
|
|
1092
|
-
// 其他 update 操作(updateOne/updateMany/findOneAndUpdate):在 $set 中添加 updatedAt
|
|
1093
|
-
const update = args[1] || {};
|
|
1094
|
-
|
|
1095
|
-
if (!update.$set) {
|
|
1096
|
-
update.$set = {};
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
update.$set[config.updatedAt] = now;
|
|
1100
|
-
args[1] = update;
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
return args;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
/**
|
|
1108
|
-
* 提取操作类型
|
|
1109
|
-
*
|
|
1110
|
-
* @private
|
|
1111
|
-
* @param {string} method - 方法名
|
|
1112
|
-
* @returns {string} 操作类型(find/insert/update/delete)
|
|
1113
|
-
*/
|
|
1114
|
-
_getOperationType(method) {
|
|
1115
|
-
// findOneAnd* 方法需要特殊处理(优先判断)
|
|
1116
|
-
if (
|
|
1117
|
-
method === "findOneAndUpdate" ||
|
|
1118
|
-
method === "findOneAndReplace" ||
|
|
1119
|
-
method === "findOneAndDelete"
|
|
1120
|
-
) {
|
|
1121
|
-
if (method === "findOneAndDelete") {
|
|
1122
|
-
return "delete";
|
|
1123
|
-
}
|
|
1124
|
-
return "update";
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
if (
|
|
1128
|
-
method.startsWith("find") ||
|
|
1129
|
-
method === "aggregate" ||
|
|
1130
|
-
method === "count"
|
|
1131
|
-
) {
|
|
1132
|
-
return "find";
|
|
1133
|
-
}
|
|
1134
|
-
if (method.startsWith("insert")) {
|
|
1135
|
-
return "insert";
|
|
1136
|
-
}
|
|
1137
|
-
if (
|
|
1138
|
-
method.startsWith("update") ||
|
|
1139
|
-
method.startsWith("replace") ||
|
|
1140
|
-
method.startsWith("upsert") ||
|
|
1141
|
-
method.startsWith("increment")
|
|
1142
|
-
) {
|
|
1143
|
-
return "update";
|
|
1144
|
-
}
|
|
1145
|
-
if (method.startsWith("delete")) {
|
|
1146
|
-
return "delete";
|
|
1147
|
-
}
|
|
1148
|
-
return "unknown";
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
/**
|
|
1152
|
-
* 数据验证方法
|
|
1153
|
-
*
|
|
1154
|
-
* @param {Object} data - 待验证的数据
|
|
1155
|
-
* @param {Object} [options] - 验证选项
|
|
1156
|
-
* @param {string} [options.locale] - 语言(zh-CN/en-US等)
|
|
1157
|
-
* @returns {Object} 验证结果 { valid: boolean, errors: Array, data: Object }
|
|
1158
|
-
*
|
|
1159
|
-
* @example
|
|
1160
|
-
* const result = model.validate({ username: 'test' });
|
|
1161
|
-
* if (!result.valid) {
|
|
1162
|
-
* console.error(result.errors);
|
|
1163
|
-
* }
|
|
1164
|
-
*/
|
|
1165
|
-
validate(data, options = {}) {
|
|
1166
|
-
try {
|
|
1167
|
-
// 获取 schema(优先使用缓存)
|
|
1168
|
-
let schema = this._schemaCache;
|
|
1169
|
-
|
|
1170
|
-
// 如果缓存为空,重新编译
|
|
1171
|
-
if (!schema) {
|
|
1172
|
-
if (typeof this.definition.schema === "function") {
|
|
1173
|
-
schema = this.definition.schema.call(this.definition, dsl);
|
|
1174
|
-
} else {
|
|
1175
|
-
schema = this.definition.schema;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
// 执行验证
|
|
1180
|
-
const result = validate(schema, data);
|
|
1181
|
-
|
|
1182
|
-
// 返回统一格式
|
|
1183
|
-
return {
|
|
1184
|
-
valid: result.valid,
|
|
1185
|
-
errors: result.errors || [],
|
|
1186
|
-
data: result.data || data,
|
|
1187
|
-
};
|
|
1188
|
-
} catch (err) {
|
|
1189
|
-
// 验证过程失败
|
|
1190
|
-
return {
|
|
1191
|
-
valid: false,
|
|
1192
|
-
errors: [
|
|
1193
|
-
{
|
|
1194
|
-
field: "_schema",
|
|
1195
|
-
message: `Schema validation failed: ${err.message}`,
|
|
1196
|
-
code: "SCHEMA_ERROR",
|
|
1197
|
-
},
|
|
1198
|
-
],
|
|
1199
|
-
data,
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
/**
|
|
1205
|
-
* 自动创建索引
|
|
1206
|
-
*
|
|
1207
|
-
* @private
|
|
1208
|
-
* @returns {Promise<void>}
|
|
1209
|
-
*/
|
|
1210
|
-
async _createIndexes() {
|
|
1211
|
-
if (!Array.isArray(this.indexes) || this.indexes.length === 0) {
|
|
1212
|
-
return;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
try {
|
|
1216
|
-
// 使用 createIndexes 批量创建索引
|
|
1217
|
-
await this.collection.createIndexes(this.indexes);
|
|
1218
|
-
|
|
1219
|
-
if (this.msq && this.msq.logger) {
|
|
1220
|
-
this.msq.logger.info(
|
|
1221
|
-
`[Model] Created ${this.indexes.length} index(es) for ${this.collection.collectionName}`,
|
|
1222
|
-
);
|
|
1223
|
-
}
|
|
1224
|
-
} catch (err) {
|
|
1225
|
-
// 索引创建失败仅记录警告
|
|
1226
|
-
if (this.msq && this.msq.logger) {
|
|
1227
|
-
this.msq.logger.warn(
|
|
1228
|
-
`[Model] Failed to create indexes for ${this.collection.collectionName}:`,
|
|
1229
|
-
err.message,
|
|
1230
|
-
);
|
|
1231
|
-
}
|
|
1232
|
-
throw err;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
/**
|
|
1237
|
-
* 获取关系定义
|
|
1238
|
-
*
|
|
1239
|
-
* @returns {Object} 关系定义对象
|
|
1240
|
-
*
|
|
1241
|
-
* @example
|
|
1242
|
-
* const relations = model.getRelations();
|
|
1243
|
-
* // { posts: { type: 'hasMany', target: 'Post', ... } }
|
|
1244
|
-
*/
|
|
1245
|
-
getRelations() {
|
|
1246
|
-
return this.definition.relations || {};
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
/**
|
|
1250
|
-
* 获取 enums 配置
|
|
1251
|
-
*
|
|
1252
|
-
* @returns {Object} 枚举配置对象
|
|
1253
|
-
*
|
|
1254
|
-
* @example
|
|
1255
|
-
* const enums = model.getEnums();
|
|
1256
|
-
* // { role: 'admin|user', status: 'active|inactive' }
|
|
1257
|
-
*/
|
|
1258
|
-
getEnums() {
|
|
1259
|
-
return this.definition.enums || {};
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
// ========== 导出 ==========
|
|
1264
|
-
module.exports = Model;
|
|
1265
|
-
module.exports.ModelInstance = ModelInstance;
|