monsqlize 1.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 +2474 -0
- package/LICENSE +21 -0
- package/README.md +1368 -0
- package/index.d.ts +1052 -0
- package/lib/cache.js +491 -0
- package/lib/common/cursor.js +58 -0
- package/lib/common/docs-urls.js +72 -0
- package/lib/common/index-options.js +222 -0
- package/lib/common/log.js +60 -0
- package/lib/common/namespace.js +21 -0
- package/lib/common/normalize.js +33 -0
- package/lib/common/page-result.js +42 -0
- package/lib/common/runner.js +56 -0
- package/lib/common/server-features.js +231 -0
- package/lib/common/shape-builders.js +26 -0
- package/lib/common/validation.js +49 -0
- package/lib/connect.js +76 -0
- package/lib/constants.js +54 -0
- package/lib/count-queue.js +187 -0
- package/lib/distributed-cache-invalidator.js +259 -0
- package/lib/errors.js +157 -0
- package/lib/index.js +352 -0
- package/lib/logger.js +224 -0
- package/lib/model/examples/test.js +114 -0
- package/lib/mongodb/common/accessor-helpers.js +44 -0
- package/lib/mongodb/common/agg-pipeline.js +32 -0
- package/lib/mongodb/common/iid.js +27 -0
- package/lib/mongodb/common/lexicographic-expr.js +52 -0
- package/lib/mongodb/common/shape.js +31 -0
- package/lib/mongodb/common/sort.js +38 -0
- package/lib/mongodb/common/transaction-aware.js +24 -0
- package/lib/mongodb/connect.js +178 -0
- package/lib/mongodb/index.js +458 -0
- package/lib/mongodb/management/admin-ops.js +199 -0
- package/lib/mongodb/management/bookmark-ops.js +166 -0
- package/lib/mongodb/management/cache-ops.js +49 -0
- package/lib/mongodb/management/collection-ops.js +386 -0
- package/lib/mongodb/management/database-ops.js +201 -0
- package/lib/mongodb/management/index-ops.js +474 -0
- package/lib/mongodb/management/index.js +16 -0
- package/lib/mongodb/management/namespace.js +30 -0
- package/lib/mongodb/management/validation-ops.js +267 -0
- package/lib/mongodb/queries/aggregate.js +133 -0
- package/lib/mongodb/queries/chain.js +623 -0
- package/lib/mongodb/queries/count.js +88 -0
- package/lib/mongodb/queries/distinct.js +68 -0
- package/lib/mongodb/queries/find-and-count.js +183 -0
- package/lib/mongodb/queries/find-by-ids.js +235 -0
- package/lib/mongodb/queries/find-one-by-id.js +170 -0
- package/lib/mongodb/queries/find-one.js +61 -0
- package/lib/mongodb/queries/find-page.js +565 -0
- package/lib/mongodb/queries/find.js +161 -0
- package/lib/mongodb/queries/index.js +49 -0
- package/lib/mongodb/writes/delete-many.js +181 -0
- package/lib/mongodb/writes/delete-one.js +173 -0
- package/lib/mongodb/writes/find-one-and-delete.js +193 -0
- package/lib/mongodb/writes/find-one-and-replace.js +222 -0
- package/lib/mongodb/writes/find-one-and-update.js +223 -0
- package/lib/mongodb/writes/increment-one.js +243 -0
- package/lib/mongodb/writes/index.js +41 -0
- package/lib/mongodb/writes/insert-batch.js +498 -0
- package/lib/mongodb/writes/insert-many.js +218 -0
- package/lib/mongodb/writes/insert-one.js +171 -0
- package/lib/mongodb/writes/replace-one.js +199 -0
- package/lib/mongodb/writes/result-handler.js +236 -0
- package/lib/mongodb/writes/update-many.js +205 -0
- package/lib/mongodb/writes/update-one.js +207 -0
- package/lib/mongodb/writes/upsert-one.js +190 -0
- package/lib/multi-level-cache.js +189 -0
- package/lib/operators.js +330 -0
- package/lib/redis-cache-adapter.js +237 -0
- package/lib/transaction/CacheLockManager.js +161 -0
- package/lib/transaction/DistributedCacheLockManager.js +239 -0
- package/lib/transaction/Transaction.js +314 -0
- package/lib/transaction/TransactionManager.js +266 -0
- package/lib/transaction/index.js +10 -0
- package/package.json +111 -0
package/lib/logger.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认日志记录器工具类
|
|
3
|
+
* 提供标准的日志记录功能,支持不同级别的日志输出
|
|
4
|
+
*
|
|
5
|
+
* v2.0 新增功能:
|
|
6
|
+
* - traceId 支持(用于分布式追踪)
|
|
7
|
+
* - 结构化日志输出(JSON 格式)
|
|
8
|
+
* - 上下文信息(数据库、集合、操作等)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 生成唯一的 traceId
|
|
15
|
+
* @returns {string} 16 字符的唯一 ID
|
|
16
|
+
*/
|
|
17
|
+
function generateTraceId() {
|
|
18
|
+
return crypto.randomBytes(8).toString('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 异步本地存储(用于在异步调用链中传递 traceId)
|
|
23
|
+
*/
|
|
24
|
+
let AsyncLocalStorage;
|
|
25
|
+
try {
|
|
26
|
+
AsyncLocalStorage = require('async_hooks').AsyncLocalStorage;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Node.js < 12.17.0 不支持 AsyncLocalStorage
|
|
29
|
+
AsyncLocalStorage = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const traceStorage = AsyncLocalStorage ? new AsyncLocalStorage() : null;
|
|
33
|
+
|
|
34
|
+
module.exports = class Logger {
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 创建日志记录器实例
|
|
38
|
+
* @param {Object} [customLogger] - 自定义日志记录器
|
|
39
|
+
* @param {Function} customLogger.debug - debug级别日志方法
|
|
40
|
+
* @param {Function} customLogger.info - info级别日志方法
|
|
41
|
+
* @param {Function} customLogger.warn - warn级别日志方法
|
|
42
|
+
* @param {Function} customLogger.error - error级别日志方法
|
|
43
|
+
* @param {Object} [options] - 日志选项
|
|
44
|
+
* @param {boolean} [options.structured=false] - 是否使用结构化日志(JSON)
|
|
45
|
+
* @param {boolean} [options.enableTraceId=false] - 是否启用 traceId
|
|
46
|
+
* @returns {Object} 日志记录器对象
|
|
47
|
+
*/
|
|
48
|
+
static create(customLogger, options = {}) {
|
|
49
|
+
if (customLogger && this.isValidLogger(customLogger)) {
|
|
50
|
+
// 如果提供自定义 logger,包装它以支持新特性
|
|
51
|
+
return this.wrapLogger(customLogger, options);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return this.createDefaultLogger(options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 验证自定义日志记录器是否有效
|
|
59
|
+
* @param {Object} logger - 待验证的日志记录器
|
|
60
|
+
* @returns {boolean} 是否为有效的日志记录器
|
|
61
|
+
*/
|
|
62
|
+
static isValidLogger(logger) {
|
|
63
|
+
const requiredMethods = ['debug', 'info', 'warn', 'error'];
|
|
64
|
+
return requiredMethods.every(method =>
|
|
65
|
+
typeof logger[method] === 'function'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 包装自定义 logger 以支持新特性
|
|
71
|
+
* @param {Object} baseLogger - 基础 logger
|
|
72
|
+
* @param {Object} options - 选项
|
|
73
|
+
* @returns {Object} 包装后的 logger
|
|
74
|
+
*/
|
|
75
|
+
static wrapLogger(baseLogger, options = {}) {
|
|
76
|
+
const { structured = false, enableTraceId = false } = options;
|
|
77
|
+
|
|
78
|
+
const wrap = (level) => (msg, ...args) => {
|
|
79
|
+
const logData = this._prepareLogData(level, msg, args, { structured, enableTraceId });
|
|
80
|
+
|
|
81
|
+
if (structured) {
|
|
82
|
+
baseLogger[level](JSON.stringify(logData));
|
|
83
|
+
} else {
|
|
84
|
+
const prefix = enableTraceId && logData.traceId ? `[${logData.traceId}] ` : '';
|
|
85
|
+
baseLogger[level](`${prefix}${msg}`, ...args);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
debug: wrap('debug'),
|
|
91
|
+
info: wrap('info'),
|
|
92
|
+
warn: wrap('warn'),
|
|
93
|
+
error: wrap('error'),
|
|
94
|
+
// 暴露 traceId 管理方法
|
|
95
|
+
withTraceId: enableTraceId ? this.withTraceId : undefined,
|
|
96
|
+
getTraceId: enableTraceId ? this.getTraceId : undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 准备日志数据
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
static _prepareLogData(level, msg, args, options) {
|
|
105
|
+
const logData = {
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
level: level.toUpperCase(),
|
|
108
|
+
message: msg,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// 添加 traceId
|
|
112
|
+
if (options.enableTraceId) {
|
|
113
|
+
const traceId = this.getTraceId();
|
|
114
|
+
if (traceId) logData.traceId = traceId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 处理额外参数(上下文信息)
|
|
118
|
+
if (args.length > 0) {
|
|
119
|
+
const context = args[0];
|
|
120
|
+
if (context && typeof context === 'object') {
|
|
121
|
+
logData.context = context;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return logData;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 创建默认的控制台日志记录器
|
|
130
|
+
* @param {Object} [options] - 日志选项
|
|
131
|
+
* @returns {Object} 默认日志记录器对象
|
|
132
|
+
*/
|
|
133
|
+
static createDefaultLogger(options = {}) {
|
|
134
|
+
const { structured = false, enableTraceId = false } = options;
|
|
135
|
+
|
|
136
|
+
const createLogFn = (level, consoleFn) => {
|
|
137
|
+
return (msg, ...args) => {
|
|
138
|
+
const logData = this._prepareLogData(level, msg, args, { structured, enableTraceId });
|
|
139
|
+
|
|
140
|
+
if (structured) {
|
|
141
|
+
consoleFn(JSON.stringify(logData));
|
|
142
|
+
} else {
|
|
143
|
+
const prefix = enableTraceId && logData.traceId ? `[${logData.traceId}] ` : '';
|
|
144
|
+
consoleFn(`[${level.toUpperCase()}] ${prefix}${msg}`, ...args);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
debug: createLogFn('debug', console.debug),
|
|
151
|
+
info: createLogFn('info', console.log),
|
|
152
|
+
warn: createLogFn('warn', console.warn),
|
|
153
|
+
error: createLogFn('error', console.error),
|
|
154
|
+
// 暴露 traceId 管理方法
|
|
155
|
+
withTraceId: enableTraceId ? this.withTraceId.bind(this) : undefined,
|
|
156
|
+
getTraceId: enableTraceId ? this.getTraceId.bind(this) : undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 创建带时间戳的日志记录器
|
|
162
|
+
* @param {Object} [customLogger] - 自定义日志记录器
|
|
163
|
+
* @returns {Object} 带时间戳的日志记录器
|
|
164
|
+
*/
|
|
165
|
+
static createWithTimestamp(customLogger) {
|
|
166
|
+
const baseLogger = this.create(customLogger);
|
|
167
|
+
const getTimestamp = () => new Date().toISOString();
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
debug: (msg, ...args) => baseLogger.debug(`${getTimestamp()} ${msg}`, ...args),
|
|
171
|
+
info: (msg, ...args) => baseLogger.info(`${getTimestamp()} ${msg}`, ...args),
|
|
172
|
+
warn: (msg, ...args) => baseLogger.warn(`${getTimestamp()} ${msg}`, ...args),
|
|
173
|
+
error: (msg, ...args) => baseLogger.error(`${getTimestamp()} ${msg}`, ...args)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 创建静默日志记录器(不输出任何内容)
|
|
179
|
+
* @returns {Object} 静默日志记录器
|
|
180
|
+
*/
|
|
181
|
+
static createSilent() {
|
|
182
|
+
const noop = () => {};
|
|
183
|
+
return {
|
|
184
|
+
debug: noop,
|
|
185
|
+
info: noop,
|
|
186
|
+
warn: noop,
|
|
187
|
+
error: noop
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 在指定的 traceId 上下文中运行函数
|
|
193
|
+
* @param {Function} fn - 要执行的函数
|
|
194
|
+
* @param {string} [traceId] - 可选的 traceId,不提供则自动生成
|
|
195
|
+
* @returns {*} 函数执行结果
|
|
196
|
+
*/
|
|
197
|
+
static withTraceId(fn, traceId = null) {
|
|
198
|
+
if (!traceStorage) {
|
|
199
|
+
// 不支持 AsyncLocalStorage,直接执行
|
|
200
|
+
return fn();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const id = traceId || generateTraceId();
|
|
204
|
+
return traceStorage.run(id, fn);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 获取当前上下文的 traceId
|
|
209
|
+
* @returns {string|null} traceId 或 null
|
|
210
|
+
*/
|
|
211
|
+
static getTraceId() {
|
|
212
|
+
if (!traceStorage) return null;
|
|
213
|
+
return traceStorage.getStore() || null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 生成新的 traceId
|
|
218
|
+
* @returns {string} 新的 traceId
|
|
219
|
+
*/
|
|
220
|
+
static generateTraceId() {
|
|
221
|
+
return generateTraceId();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
|
|
5
|
+
// 枚举配置
|
|
6
|
+
enums: {
|
|
7
|
+
role: ['admin', 'user'],
|
|
8
|
+
status: ['active', 'inactive']
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
// 定义model
|
|
12
|
+
schema:(sc)=>{
|
|
13
|
+
return {
|
|
14
|
+
username: sc.string().min(3).max(30).required(),
|
|
15
|
+
password: sc.string().pattern(/^[a-zA-Z0-9]{6,30}$/).required(),
|
|
16
|
+
age: sc.number().integer().min(0).default(18), // 默认值
|
|
17
|
+
role: sc.string().valid('admin', 'user').default('user'),
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// 自定义方法
|
|
22
|
+
methods: (model)=>{
|
|
23
|
+
return {
|
|
24
|
+
// 实例方法
|
|
25
|
+
instance: {
|
|
26
|
+
checkPassword(password) {
|
|
27
|
+
return this.password === password;
|
|
28
|
+
},
|
|
29
|
+
async getPosts() {
|
|
30
|
+
// return await model.find({ userId: this._id });
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
// 静态方法
|
|
34
|
+
static: {
|
|
35
|
+
findByName(name) {
|
|
36
|
+
return this.find({ username: name });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// 支持操作前、后处理
|
|
43
|
+
hooks:(model)=>{
|
|
44
|
+
return {
|
|
45
|
+
find: {
|
|
46
|
+
before:(ctx,options)=>{},
|
|
47
|
+
after:(ctx,docs,result)=>{},
|
|
48
|
+
},
|
|
49
|
+
insert:{
|
|
50
|
+
before:async (ctx,docs)=>{
|
|
51
|
+
// ctx.session = await model.startTransaction(); // ctx 里传递事务对象
|
|
52
|
+
// return ctx.data;
|
|
53
|
+
},
|
|
54
|
+
after:async (ctx,docs,result)=>{
|
|
55
|
+
// await ctx.session.commitTransaction();
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
update:{
|
|
59
|
+
before:(ctx,options)=>{},
|
|
60
|
+
after:(ctx,result)=>{},
|
|
61
|
+
},
|
|
62
|
+
delete:{
|
|
63
|
+
before:(ctx,options)=>{},
|
|
64
|
+
after:(ctx,result)=>{},
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// 创建索引
|
|
70
|
+
indexes: [
|
|
71
|
+
{ key: { username: 1 }, unique: true }, // 唯一索引
|
|
72
|
+
{ key: { age: -1 } }, // 普通索引,降序
|
|
73
|
+
],
|
|
74
|
+
|
|
75
|
+
// 关系
|
|
76
|
+
relations: {
|
|
77
|
+
posts: {
|
|
78
|
+
type: 'hasMany', // 一对多
|
|
79
|
+
target: 'Post', // 目标模型
|
|
80
|
+
foreignKey: 'userId', // 外键字段(存在哪张表里)
|
|
81
|
+
localKey: '_id', // 本表对应字段
|
|
82
|
+
as: 'posts', // 实例访问属性 user.posts
|
|
83
|
+
cascade: false // 是否级联删除/更新
|
|
84
|
+
},
|
|
85
|
+
profile: {
|
|
86
|
+
type: 'hasOne', // 一对一
|
|
87
|
+
target: 'Profile',
|
|
88
|
+
foreignKey: 'userId',
|
|
89
|
+
localKey: '_id',
|
|
90
|
+
as: 'profile', // 实例访问属性 user.profile
|
|
91
|
+
cascade: true, // 删除用户时级联删除 profile
|
|
92
|
+
required: false // 是否必须关联
|
|
93
|
+
},
|
|
94
|
+
roles: { // 多对多
|
|
95
|
+
type: 'manyToMany',
|
|
96
|
+
target: 'Role',
|
|
97
|
+
through: 'UserRole', // 中间表
|
|
98
|
+
foreignKey: 'userId',
|
|
99
|
+
otherKey: 'roleId',
|
|
100
|
+
as: 'roles'
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// 选项
|
|
105
|
+
options: {
|
|
106
|
+
timestamps: true, // 自动维护 createdAt / updatedAt
|
|
107
|
+
softDelete: true, // 启用 deletedAt 替代物理删除,deletedAt = null → 正常数据,deletedAt = Date → 已删除
|
|
108
|
+
sync: true, // 启用 index 索引自动同步
|
|
109
|
+
version: true, // 自动维护 __v 版本号
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 自动索引 / 慢查询统计 → 全局 ORM 层统一管理
|
|
113
|
+
// 恢复软删除 → 全局方法注入到每个启用 softDelete 的模型
|
|
114
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Mongo 适配器辅助工具:慢查询日志整形器(脱敏)和缓存键构建器
|
|
2
|
+
// - mongoSlowLogShaper: 通过组合通用的元信息与 Mongo 的结构,生成安全的慢查询日志附加字段
|
|
3
|
+
// - mongoKeyBuilder: 当 op==='findPage' 时,注入 pipelineHash(对 pipeline 进行 stableStringify 后再做 sha256)
|
|
4
|
+
//并在键值材料中省略原始 pipeline,以保持键的简短和稳定
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const CacheFactory = require('../../cache');
|
|
9
|
+
const { buildCommonLogExtra } = require('../../common/shape-builders');
|
|
10
|
+
const { shapeQuery, shapeProjection, shapeSort } = require('./shape');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mongo 专属:慢日志去敏形状构造器
|
|
14
|
+
* @param {object} options - 原始调用选项(仅读取形状/标记,不含具体值)
|
|
15
|
+
* @returns {object} 去敏后的形状对象(仅字段集合/标记位)
|
|
16
|
+
*/
|
|
17
|
+
function mongoSlowLogShaper(options) {
|
|
18
|
+
const extra = buildCommonLogExtra(options);
|
|
19
|
+
if (options?.query) extra.queryShape = shapeQuery(options.query);
|
|
20
|
+
if (options?.projection) extra.projectionShape = shapeProjection(options.projection);
|
|
21
|
+
if (options?.sort) extra.sortShape = shapeSort(options.sort);
|
|
22
|
+
return extra;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Mongo 专属:缓存键构造器
|
|
27
|
+
* 仅在 findPage 时:对 pipeline 做稳定串行化并 sha256 → pipelineHash;
|
|
28
|
+
* 同时从参与键的 options 中去除原始 pipeline,避免键过长与不稳定。
|
|
29
|
+
* @param {string} op
|
|
30
|
+
* @param {object} options
|
|
31
|
+
* @returns {object} 用于参与缓存键构造的 options 视图
|
|
32
|
+
*/
|
|
33
|
+
function mongoKeyBuilder(op, options) {
|
|
34
|
+
const opts = options || {};
|
|
35
|
+
if (op !== 'findPage') return opts;
|
|
36
|
+
const pipelineHash = crypto
|
|
37
|
+
.createHash('sha256')
|
|
38
|
+
.update(CacheFactory.stableStringify(opts.pipeline || []))
|
|
39
|
+
.digest('hex');
|
|
40
|
+
const { pipeline, ...rest } = opts;
|
|
41
|
+
return { ...rest, pipelineHash };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { mongoSlowLogShaper, mongoKeyBuilder };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mongo 聚合分页管道构造器(方案 A:先分页后联表)
|
|
3
|
+
* 流程:$match(query) → $match($expr-游标比较) → $sort(sort) → $limit(limit+1) → 页内 $lookup →
|
|
4
|
+
* 若为 before 方向,再 $sort(反转) 恢复视觉顺序。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { buildLexiExpr } = require('./lexicographic-expr');
|
|
8
|
+
const { reverseSort } = require('./sort');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 构造方案 A 的聚合管道
|
|
12
|
+
* @param {object} params
|
|
13
|
+
* @param {object} [params.query]
|
|
14
|
+
* @param {Record<string,1|-1>} params.sort - 查询阶段使用的排序(before 方向已反转)
|
|
15
|
+
* @param {number} params.limit - 页大小
|
|
16
|
+
* @param {{a:object,s:object}|null} [params.cursor] - 解析后的游标对象
|
|
17
|
+
* @param {'after'|'before'|null} [params.direction]
|
|
18
|
+
* @param {object[]} [params.lookupPipeline] - 页内联表等追加管道
|
|
19
|
+
* @returns {object[]} 聚合管道数组
|
|
20
|
+
*/
|
|
21
|
+
function buildPagePipelineA({ query = {}, sort, limit, cursor, direction, lookupPipeline = [] }) {
|
|
22
|
+
const pipeline = [];
|
|
23
|
+
if (query && Object.keys(query).length) pipeline.push({ $match: query });
|
|
24
|
+
if (cursor) pipeline.push({ $match: { $expr: buildLexiExpr(sort, cursor.a) } });
|
|
25
|
+
pipeline.push({ $sort: sort });
|
|
26
|
+
pipeline.push({ $limit: limit + 1 });
|
|
27
|
+
if (lookupPipeline && lookupPipeline.length) pipeline.push(...lookupPipeline);
|
|
28
|
+
if (direction === 'before') pipeline.push({ $sort: reverseSort(sort) });
|
|
29
|
+
return pipeline;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { buildPagePipelineA };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mongo 实例指纹(iid)生成
|
|
3
|
+
* 基于 uri/db 生成稳定短标识,用于缓存命名空间等
|
|
4
|
+
*/
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
function genInstanceId(databaseName, uri, explicitId) {
|
|
8
|
+
if (explicitId) return String(explicitId);
|
|
9
|
+
const safeDb = String(databaseName || '');
|
|
10
|
+
try {
|
|
11
|
+
const u = new URL(String(uri || ''));
|
|
12
|
+
const proto = (u.protocol || '').replace(':', '');
|
|
13
|
+
const host = u.hostname || '';
|
|
14
|
+
const port = u.port || (proto === 'mongodb+srv' ? 'srv' : '27017');
|
|
15
|
+
const rs = u.searchParams.get('replicaSet') || '';
|
|
16
|
+
const authSource = u.searchParams.get('authSource') || '';
|
|
17
|
+
const tls = u.searchParams.get('tls') || u.searchParams.get('ssl') || '';
|
|
18
|
+
const safe = `${proto}://${host}:${port}/${safeDb}?rs=${rs}&auth=${authSource}&tls=${tls}`;
|
|
19
|
+
const h = crypto.createHash('sha1').update(safe).digest('base64url').slice(0, 12);
|
|
20
|
+
return `mdb:${h}`;
|
|
21
|
+
} catch (_) {
|
|
22
|
+
const h = crypto.createHash('sha1').update(String(uri || '') + '|' + safeDb).digest('base64url').slice(0, 12);
|
|
23
|
+
return `mdb:${h}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { genInstanceId };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mongo 专属:构造 $expr 的词典序比较表达式(lexicographic compare)
|
|
3
|
+
* 场景:按复合排序键(含 `_id` 兜底)进行游标分页的"锚点之后/之前"过滤。
|
|
4
|
+
* 例:sort={k1:1,k2:-1,_id:1}, anchor={k1:...,k2:...,_id:...}
|
|
5
|
+
* 生成:{$or:[ {$gt:['$k1',a.k1]}, {$and:[{$eq:['$k1',a.k1]},{$lt:['$k2',a.k2]}]}, ...]}
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { ObjectId } = require('mongodb');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 转换锚点值,处理特殊类型
|
|
12
|
+
* @param {any} value - 锚点值
|
|
13
|
+
* @param {string} fieldName - 字段名
|
|
14
|
+
* @returns {any} 转换后的值
|
|
15
|
+
*/
|
|
16
|
+
function convertAnchorValue(value, fieldName) {
|
|
17
|
+
// 如果是 _id 字段且值是字符串,转换为 ObjectId
|
|
18
|
+
if (fieldName === '_id' && typeof value === 'string' && /^[0-9a-fA-F]{24}$/.test(value)) {
|
|
19
|
+
return new ObjectId(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 如果值是 ISO 日期字符串,转换为 Date 对象
|
|
23
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/.test(value)) {
|
|
24
|
+
return new Date(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 构造 $expr 词典序比较
|
|
32
|
+
* @param {Record<string,1|-1>} sort - 稳定排序(含 `_id`)
|
|
33
|
+
* @param {Record<string,any>} anchor - 锚点值对象
|
|
34
|
+
* @returns {object} Mongo $expr 表达式对象
|
|
35
|
+
*/
|
|
36
|
+
function buildLexiExpr(sort, anchor) {
|
|
37
|
+
const keys = Object.keys(sort || {});
|
|
38
|
+
const or = [];
|
|
39
|
+
for (let i = 0; i < keys.length; i++) {
|
|
40
|
+
const and = [];
|
|
41
|
+
for (let j = 0; j < i; j++) {
|
|
42
|
+
and.push({ $eq: [ `$${keys[j]}`, convertAnchorValue(anchor[keys[j]], keys[j]) ] });
|
|
43
|
+
}
|
|
44
|
+
const dir = sort[keys[i]];
|
|
45
|
+
const op = dir === 1 ? '$gt' : '$lt';
|
|
46
|
+
and.push({ [op]: [ `$${keys[i]}`, convertAnchorValue(anchor[keys[i]], keys[i]) ] });
|
|
47
|
+
or.push(and.length === 1 ? and[0] : { $and: and });
|
|
48
|
+
}
|
|
49
|
+
return { $or: or };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { buildLexiExpr };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mongo 去敏形状构造
|
|
3
|
+
* - shapeQuery: 仅输出字段名与运算符(以 '$' 代表),不含具体值
|
|
4
|
+
* - shapeProjection: 将数组投影转为字段名数组,或对象投影的键名数组
|
|
5
|
+
* - shapeSort: 仅输出排序键与 1/-1
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function shapeQuery(input, maxKeys = 30, maxDepth = 3) {
|
|
9
|
+
const walk = (v, depth) => {
|
|
10
|
+
if (depth > maxDepth || v == null || typeof v !== 'object') return true; // 值用 true 表示
|
|
11
|
+
if (Array.isArray(v)) return v.length ? [walk(v[0], depth + 1)] : [];
|
|
12
|
+
const out = {};
|
|
13
|
+
let count = 0;
|
|
14
|
+
for (const k of Object.keys(v)) {
|
|
15
|
+
out[k] = k.startsWith('$') ? '$' : walk(v[k], depth + 1);
|
|
16
|
+
if (++count >= maxKeys) { out.__truncated__ = true; break; }
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
};
|
|
20
|
+
return walk(input, 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function shapeProjection(p) {
|
|
24
|
+
return Array.isArray(p) ? p.slice(0, 30) : Object.keys(p || {}).slice(0, 30);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shapeSort(s) {
|
|
28
|
+
return Object.fromEntries(Object.entries(s || {}).slice(0, 30).map(([k, v]) => [k, v === -1 ? -1 : 1]));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { shapeQuery, shapeProjection, shapeSort };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mongo 专属:稳定排序与锚点选择
|
|
3
|
+
* - ensureStableSort:确保排序键(sort)末尾包含 `_id`(稳定排序兜底)。
|
|
4
|
+
* - reverseSort:将排序方向整体反转(1 ↔ -1),用于 before 方向的查询阶段。
|
|
5
|
+
* - pickAnchor:按排序键顺序从文档中提取锚点字段值对象(含 `_id`)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 确保排序对象包含稳定键 `_id`
|
|
10
|
+
* @param {Record<string, 1|-1>|undefined} sort - 排序对象
|
|
11
|
+
* @returns {Record<string, 1|-1>} 新的排序对象(若缺 `_id` 自动追加 `_id:1`)
|
|
12
|
+
*/
|
|
13
|
+
function ensureStableSort(sort) {
|
|
14
|
+
const s = { ...(sort || {}) };
|
|
15
|
+
if (!('_id' in s)) s._id = 1;
|
|
16
|
+
return s;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 反转排序方向(1 ↔ -1)
|
|
21
|
+
* @param {Record<string, 1|-1>} sort
|
|
22
|
+
* @returns {Record<string, 1|-1>}
|
|
23
|
+
*/
|
|
24
|
+
function reverseSort(sort) {
|
|
25
|
+
const r = {}; for (const k of Object.keys(sort || {})) r[k] = sort[k] === 1 ? -1 : 1; return r;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 提取锚点:按排序键顺序从文档取值(含 `_id`)
|
|
30
|
+
* @param {any} doc - 文档
|
|
31
|
+
* @param {Record<string, 1|-1>} sort - 稳定排序(含 `_id`)
|
|
32
|
+
* @returns {Record<string, any>} 锚点值对象 { k1: v1, k2: v2, _id: id }
|
|
33
|
+
*/
|
|
34
|
+
function pickAnchor(doc, sort) {
|
|
35
|
+
const a = {}; for (const k of Object.keys(sort || {})) a[k] = doc ? doc[k] : undefined; return a;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { ensureStableSort, reverseSort, pickAnchor };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检查操作是否在事务中执行
|
|
3
|
+
* @param {Object} options - 操作选项
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
6
|
+
function isInTransaction(options) {
|
|
7
|
+
return !!(options && options.session && options.session.inTransaction && options.session.inTransaction());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 从 session 中获取 Transaction 实例
|
|
12
|
+
* @param {Object} session - MongoDB ClientSession
|
|
13
|
+
* @returns {Transaction|null}
|
|
14
|
+
*/
|
|
15
|
+
function getTransactionFromSession(session) {
|
|
16
|
+
if (!session) return null;
|
|
17
|
+
return session.__monSQLizeTransaction || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
isInTransaction,
|
|
22
|
+
getTransactionFromSession
|
|
23
|
+
};
|
|
24
|
+
|