@aetherframework/database 1.1.0 → 1.1.2
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/examples/mysql-test-pressure.js +1530 -0
- package/examples/test-direct.js +116 -0
- package/examples/transaction_example.js +127 -0
- package/package.json +3 -1
- package/src/DatabaseManager.js +565 -0
- package/src/core/ConnectionManager.js +351 -0
- package/src/core/DatabaseFactory.js +188 -0
- package/src/core/MongoQueryBuilder.js +576 -0
- package/src/core/PluginManager.js +968 -0
- package/src/core/QueryBuilder.js +4394 -0
- package/src/core/TransactionManager.js +40 -0
- package/src/drivers/clickhouse-driver.js +272 -0
- package/src/drivers/index.js +273 -0
- package/src/drivers/mongodb-driver.js +87 -0
- package/src/drivers/mssql-driver.js +117 -0
- package/src/drivers/mysql-driver.js +169 -0
- package/src/drivers/oracle-driver.js +101 -0
- package/src/drivers/postgres-driver.js +234 -0
- package/src/drivers/redis-driver.js +52 -0
- package/src/drivers/sqlite-driver.js +67 -0
- package/src/middleware/connection-pool.js +455 -0
- package/src/middleware/performance-monitor.js +652 -0
- package/src/middleware/query-cache.js +500 -0
- package/src/middleware/query-logger.js +262 -0
- package/src/plugins/AuditPlugin.js +447 -0
- package/src/plugins/BasePlugin.js +418 -0
- package/src/plugins/BatchOperationPlugin.js +165 -0
- package/src/plugins/CachePlugin.js +407 -0
- package/src/plugins/CtePlugin.js +523 -0
- package/src/plugins/DistributedPlugin.js +543 -0
- package/src/plugins/EncryptionPlugin.js +211 -0
- package/src/plugins/FullTextSearchPlugin.js +164 -0
- package/src/plugins/GeospatialPlugin.js +219 -0
- package/src/plugins/GraphQLPlugin.js +162 -0
- package/src/plugins/HookPlugin.js +211 -0
- package/src/plugins/JsonPlugin.js +366 -0
- package/src/plugins/OptimisticLockPlugin.js +374 -0
- package/src/plugins/PerformancePlugin.js +175 -0
- package/src/plugins/ResiliencePlugin.js +114 -0
- package/src/plugins/ShardingPlugin.js +227 -0
- package/src/plugins/SoftDeletePlugin.js +258 -0
- package/src/plugins/SyncPlugin.js +373 -0
- package/src/plugins/VersioningPlugin.js +314 -0
- package/src/plugins/WindowFunctionPlugin.js +343 -0
- package/src/utils/config-loader.js +632 -0
- package/src/utils/error-handler.js +724 -0
- package/src/utils/migration-runner.js +1066 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/database/plugin/EncryptionPlugin
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
import { BasePlugin } from './BasePlugin.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Encryption Plugin - Provides data encryption/decryption functionality
|
|
12
|
+
* Supports AES-256-GCM encryption for sensitive fields
|
|
13
|
+
*/
|
|
14
|
+
export class EncryptionPlugin extends BasePlugin {
|
|
15
|
+
constructor(queryBuilder) {
|
|
16
|
+
super(queryBuilder);
|
|
17
|
+
this.encryptionKey = null;
|
|
18
|
+
this.encryptedFields = new Set();
|
|
19
|
+
this.encryptionAlgorithm = 'aes-256-gcm';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_registerMethods() {
|
|
23
|
+
// Register encryption methods to QueryBuilder
|
|
24
|
+
this.queryBuilder.setEncryptionKey = this.setEncryptionKey.bind(this);
|
|
25
|
+
this.queryBuilder.encryptField = this.encryptField.bind(this);
|
|
26
|
+
this.queryBuilder.encryptData = this.encryptData.bind(this);
|
|
27
|
+
this.queryBuilder.decryptData = this.decryptData.bind(this);
|
|
28
|
+
this.queryBuilder.isEncryptionEnabled = this.isEncryptionEnabled.bind(this);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Set encryption key
|
|
33
|
+
* @param {string|Buffer} key - Encryption key (32 bytes for AES-256)
|
|
34
|
+
* @returns {QueryBuilder} Query builder instance
|
|
35
|
+
*/
|
|
36
|
+
setEncryptionKey(key) {
|
|
37
|
+
if (typeof key === 'string') {
|
|
38
|
+
// Ensure key is 32 bytes for AES-256
|
|
39
|
+
const keyBuffer = Buffer.from(key, 'utf8');
|
|
40
|
+
if (keyBuffer.length !== 32) {
|
|
41
|
+
throw new Error('Encryption key must be 32 bytes for AES-256');
|
|
42
|
+
}
|
|
43
|
+
this.encryptionKey = keyBuffer;
|
|
44
|
+
} else if (Buffer.isBuffer(key)) {
|
|
45
|
+
if (key.length !== 32) {
|
|
46
|
+
throw new Error('Encryption key must be 32 bytes for AES-256');
|
|
47
|
+
}
|
|
48
|
+
this.encryptionKey = key;
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error('Encryption key must be a string or Buffer');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Add encryption hooks
|
|
54
|
+
this._addEncryptionHooks();
|
|
55
|
+
return this.queryBuilder;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mark field for encryption
|
|
60
|
+
* @param {string} fieldName - Field name to encrypt
|
|
61
|
+
* @returns {QueryBuilder} Query builder instance
|
|
62
|
+
*/
|
|
63
|
+
encryptField(fieldName) {
|
|
64
|
+
this.encryptedFields.add(fieldName);
|
|
65
|
+
return this.queryBuilder;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Encrypt data before insertion/update
|
|
70
|
+
* @param {Object} data - Data to encrypt
|
|
71
|
+
* @returns {Object} Encrypted data
|
|
72
|
+
*/
|
|
73
|
+
encryptData(data) {
|
|
74
|
+
if (!this.encryptionKey || this.encryptedFields.size === 0) {
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const encrypted = { ...data };
|
|
79
|
+
|
|
80
|
+
for (const field of this.encryptedFields) {
|
|
81
|
+
if (encrypted[field] !== undefined && encrypted[field] !== null) {
|
|
82
|
+
try {
|
|
83
|
+
const iv = crypto.randomBytes(16);
|
|
84
|
+
const cipher = crypto.createCipheriv(
|
|
85
|
+
this.encryptionAlgorithm,
|
|
86
|
+
this.encryptionKey,
|
|
87
|
+
iv
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
let encryptedText = cipher.update(
|
|
91
|
+
String(encrypted[field]),
|
|
92
|
+
'utf8',
|
|
93
|
+
'hex'
|
|
94
|
+
);
|
|
95
|
+
encryptedText += cipher.final('hex');
|
|
96
|
+
const authTag = cipher.getAuthTag();
|
|
97
|
+
|
|
98
|
+
// Store format: iv:authTag:encryptedText
|
|
99
|
+
encrypted[field] =
|
|
100
|
+
`${iv.toString('hex')}:${authTag.toString('hex')}:${encryptedText}`;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(`Failed to encrypt field ${field}:`, error.message);
|
|
103
|
+
throw new Error(`Encryption failed for field ${field}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return encrypted;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Decrypt data after retrieval
|
|
113
|
+
* @param {Object} data - Data to decrypt
|
|
114
|
+
* @returns {Object} Decrypted data
|
|
115
|
+
*/
|
|
116
|
+
decryptData(data) {
|
|
117
|
+
if (!this.encryptionKey || this.encryptedFields.size === 0) {
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const decrypted = { ...data };
|
|
122
|
+
|
|
123
|
+
for (const field of this.encryptedFields) {
|
|
124
|
+
if (decrypted[field] && decrypted[field].includes(':')) {
|
|
125
|
+
try {
|
|
126
|
+
const [ivHex, authTagHex, encryptedText] = decrypted[field].split(':');
|
|
127
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
128
|
+
const authTag = Buffer.from(authTagHex, 'hex');
|
|
129
|
+
|
|
130
|
+
const decipher = crypto.createDecipheriv(
|
|
131
|
+
this.encryptionAlgorithm,
|
|
132
|
+
this.encryptionKey,
|
|
133
|
+
iv
|
|
134
|
+
);
|
|
135
|
+
decipher.setAuthTag(authTag);
|
|
136
|
+
|
|
137
|
+
let decryptedText = decipher.update(encryptedText, 'hex', 'utf8');
|
|
138
|
+
decryptedText += decipher.final('utf8');
|
|
139
|
+
|
|
140
|
+
decrypted[field] = decryptedText;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn(`Failed to decrypt field ${field}:`, error.message);
|
|
143
|
+
decrypted[field] = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return decrypted;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if encryption is enabled
|
|
153
|
+
* @returns {boolean} True if encryption is enabled
|
|
154
|
+
*/
|
|
155
|
+
isEncryptionEnabled() {
|
|
156
|
+
return this.encryptionKey !== null && this.encryptedFields.size > 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Add encryption hooks to QueryBuilder
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
_addEncryptionHooks() {
|
|
164
|
+
// Hook for encrypting data before insert/update
|
|
165
|
+
this.queryBuilder.addHook('beforeInsert', async (data) => {
|
|
166
|
+
return this.encryptData(data);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
this.queryBuilder.addHook('beforeUpdate', async (data) => {
|
|
170
|
+
return this.encryptData(data);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Hook for decrypting data after select
|
|
174
|
+
this.queryBuilder.addHook('afterSelect', async (result) => {
|
|
175
|
+
if (Array.isArray(result)) {
|
|
176
|
+
return result.map(row => this.decryptData(row));
|
|
177
|
+
} else if (result && typeof result === 'object') {
|
|
178
|
+
return this.decryptData(result);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Generate encryption key
|
|
186
|
+
* @param {number} length - Key length in bytes (default: 32)
|
|
187
|
+
* @returns {Buffer} Generated key
|
|
188
|
+
*/
|
|
189
|
+
static generateKey(length = 32) {
|
|
190
|
+
return crypto.randomBytes(length);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get plugin metadata
|
|
195
|
+
* @returns {Object} Plugin metadata
|
|
196
|
+
*/
|
|
197
|
+
getMetadata() {
|
|
198
|
+
return {
|
|
199
|
+
name: 'EncryptionPlugin',
|
|
200
|
+
version: '1.0.0',
|
|
201
|
+
description: 'Provides AES-256-GCM encryption for sensitive data fields',
|
|
202
|
+
dependencies: ['crypto'],
|
|
203
|
+
features: [
|
|
204
|
+
'Field-level encryption',
|
|
205
|
+
'AES-256-GCM algorithm',
|
|
206
|
+
'Automatic encryption/decryption hooks',
|
|
207
|
+
'Key management'
|
|
208
|
+
]
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/database/plugin/FullTextSearchPlugin
|
|
6
|
+
*/
|
|
7
|
+
import { BasePlugin } from "./BasePlugin.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 全文搜索插件 - 封装不同数据库的全文搜索语法
|
|
11
|
+
*/
|
|
12
|
+
export class FullTextSearchPlugin extends BasePlugin {
|
|
13
|
+
constructor(queryBuilder) {
|
|
14
|
+
super(queryBuilder);
|
|
15
|
+
this.pluginName = "FullTextSearchPlugin";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_registerMethods() {
|
|
19
|
+
// 注册全文搜索方法到 QueryBuilder
|
|
20
|
+
this.queryBuilder.fullTextSearch = this.fullTextSearch.bind(this);
|
|
21
|
+
this.queryBuilder.orderByRelevance = this.orderByRelevance.bind(this);
|
|
22
|
+
this.queryBuilder.matchAgainst = this.matchAgainst.bind(this);
|
|
23
|
+
this.queryBuilder.fullTextBoolean = this.fullTextBoolean.bind(this);
|
|
24
|
+
this.queryBuilder.fullTextQueryExpansion = this.fullTextQueryExpansion.bind(this);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 全文搜索
|
|
29
|
+
* @param {string|Array} columns - 要搜索的列
|
|
30
|
+
* @param {string} searchTerm - 搜索词
|
|
31
|
+
* @param {string} mode - 搜索模式 (natural, boolean)
|
|
32
|
+
* @returns {QueryBuilder} QueryBuilder 实例
|
|
33
|
+
*/
|
|
34
|
+
fullTextSearch(columns, searchTerm, mode = "natural") {
|
|
35
|
+
this.queryBuilder.query.fullTextSearch = {
|
|
36
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
37
|
+
searchTerm,
|
|
38
|
+
mode: mode.toLowerCase(),
|
|
39
|
+
};
|
|
40
|
+
return this.queryBuilder;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 按相关性排序(全文搜索)
|
|
45
|
+
* @param {string|Array} columns - 用于相关性的列
|
|
46
|
+
* @param {string} searchTerm - 搜索词
|
|
47
|
+
* @returns {QueryBuilder} QueryBuilder 实例
|
|
48
|
+
*/
|
|
49
|
+
orderByRelevance(columns, searchTerm) {
|
|
50
|
+
this.queryBuilder.query.orderByRelevance = { columns, searchTerm };
|
|
51
|
+
return this.queryBuilder;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 使用 MATCH AGAINST 进行全文搜索
|
|
56
|
+
* @param {string|Array} columns - 要搜索的列
|
|
57
|
+
* @param {string} searchTerm - 搜索词
|
|
58
|
+
* @param {string} mode - 搜索模式 (natural, boolean, query expansion)
|
|
59
|
+
* @param {boolean} withQueryExpansion - 是否使用查询扩展
|
|
60
|
+
* @returns {QueryBuilder} QueryBuilder 实例
|
|
61
|
+
*/
|
|
62
|
+
matchAgainst(columns, searchTerm, mode = "natural", withQueryExpansion = false) {
|
|
63
|
+
const columnList = Array.isArray(columns)
|
|
64
|
+
? columns.map((col) => this.queryBuilder.wrapColumn(col)).join(", ")
|
|
65
|
+
: this.queryBuilder.wrapColumn(columns);
|
|
66
|
+
|
|
67
|
+
let modeClause = "";
|
|
68
|
+
if (mode === "boolean") {
|
|
69
|
+
modeClause = "IN BOOLEAN MODE";
|
|
70
|
+
} else if (mode === "query expansion" || withQueryExpansion) {
|
|
71
|
+
modeClause = "WITH QUERY EXPANSION";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.queryBuilder.query.where.push({
|
|
75
|
+
type: "fulltext",
|
|
76
|
+
raw: `MATCH(${columnList}) AGAINST(? ${modeClause})`,
|
|
77
|
+
boolean: "and",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.queryBuilder.bindings.push(searchTerm);
|
|
81
|
+
return this.queryBuilder;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 布尔模式全文搜索
|
|
86
|
+
* @param {string|Array} columns - 要搜索的列
|
|
87
|
+
* @param {string} searchTerm - 带有布尔运算符的搜索词
|
|
88
|
+
* @returns {QueryBuilder} QueryBuilder 实例
|
|
89
|
+
*/
|
|
90
|
+
fullTextBoolean(columns, searchTerm) {
|
|
91
|
+
return this.matchAgainst(columns, searchTerm, "boolean");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 查询扩展全文搜索
|
|
96
|
+
* @param {string|Array} columns - 要搜索的列
|
|
97
|
+
* @param {string} searchTerm - 搜索词
|
|
98
|
+
* @returns {QueryBuilder} QueryBuilder 实例
|
|
99
|
+
*/
|
|
100
|
+
fullTextQueryExpansion(columns, searchTerm) {
|
|
101
|
+
return this.matchAgainst(columns, searchTerm, "query expansion", true);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 构建全文搜索 SQL
|
|
106
|
+
* @param {Object} fullTextSearch - 全文搜索配置
|
|
107
|
+
* @returns {string} SQL 片段
|
|
108
|
+
*/
|
|
109
|
+
buildFullTextSearchSQL(fullTextSearch) {
|
|
110
|
+
const { columns, searchTerm, mode } = fullTextSearch;
|
|
111
|
+
const columnList = Array.isArray(columns)
|
|
112
|
+
? columns.map((col) => this.queryBuilder.wrapColumn(col)).join(", ")
|
|
113
|
+
: this.queryBuilder.wrapColumn(columns);
|
|
114
|
+
|
|
115
|
+
if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
|
|
116
|
+
let modeClause = "";
|
|
117
|
+
if (mode === "boolean") {
|
|
118
|
+
modeClause = "IN BOOLEAN MODE";
|
|
119
|
+
} else if (mode === "query expansion") {
|
|
120
|
+
modeClause = "WITH QUERY EXPANSION";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return `MATCH(${columnList}) AGAINST(? ${modeClause})`;
|
|
124
|
+
} else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
|
|
125
|
+
const searchVector = Array.isArray(columns)
|
|
126
|
+
? columns.map((col) => `to_tsvector(${col})`).join(" || ")
|
|
127
|
+
: `to_tsvector(${this.queryBuilder.wrapColumn(columns)})`;
|
|
128
|
+
|
|
129
|
+
return `${searchVector} @@ plainto_tsquery(?)`;
|
|
130
|
+
} else {
|
|
131
|
+
// 对于不支持全文搜索的数据库,使用 LIKE 模式
|
|
132
|
+
const columnArray = Array.isArray(columns) ? columns : [columns];
|
|
133
|
+
const likeConditions = columnArray.map((col) => {
|
|
134
|
+
return `${this.queryBuilder.wrapColumn(col)} LIKE ?`;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return `(${likeConditions.join(" OR ")})`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 构建相关性排序 SQL
|
|
143
|
+
* @param {Object} orderByRelevance - 相关性排序配置
|
|
144
|
+
* @returns {string} SQL 片段
|
|
145
|
+
*/
|
|
146
|
+
buildOrderByRelevanceSQL(orderByRelevance) {
|
|
147
|
+
const { columns, searchTerm } = orderByRelevance;
|
|
148
|
+
const columnList = Array.isArray(columns)
|
|
149
|
+
? columns.map((col) => this.queryBuilder.wrapColumn(col)).join(", ")
|
|
150
|
+
: this.queryBuilder.wrapColumn(columns);
|
|
151
|
+
|
|
152
|
+
if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
|
|
153
|
+
return `MATCH(${columnList}) AGAINST(?) DESC`;
|
|
154
|
+
} else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
|
|
155
|
+
const searchVector = Array.isArray(columns)
|
|
156
|
+
? columns.map((col) => `to_tsvector(${col})`).join(" || ")
|
|
157
|
+
: `to_tsvector(${this.queryBuilder.wrapColumn(columns)})`;
|
|
158
|
+
|
|
159
|
+
return `ts_rank(${searchVector}, plainto_tsquery(?)) DESC`;
|
|
160
|
+
} else {
|
|
161
|
+
return null; // 不支持相关性排序
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/database/plugin/GeospatialPlugin
|
|
6
|
+
*/
|
|
7
|
+
import { BasePlugin } from "./BasePlugin.js";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export class GeospatialPlugin extends BasePlugin {
|
|
11
|
+
constructor(queryBuilder) {
|
|
12
|
+
super(queryBuilder);
|
|
13
|
+
this.pluginName = "GeospatialPlugin";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_registerMethods() {
|
|
17
|
+
// 注册地理空间查询方法到 QueryBuilder
|
|
18
|
+
this.queryBuilder.whereDistance = this.whereDistance.bind(this);
|
|
19
|
+
this.queryBuilder.whereWithin = this.whereWithin.bind(this);
|
|
20
|
+
this.queryBuilder.whereIntersects = this.whereIntersects.bind(this);
|
|
21
|
+
this.queryBuilder.orderByDistance = this.orderByDistance.bind(this);
|
|
22
|
+
this.queryBuilder.selectDistance = this.selectDistance.bind(this);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* WHERE 距离条件(适用于 MySQL/PostgreSQL)
|
|
27
|
+
* @param {string} column - 几何列名
|
|
28
|
+
* @param {Object} point - 点坐标 {latitude, longitude} 或 {x, y}
|
|
29
|
+
* @param {number} distance - 距离(米)
|
|
30
|
+
* @param {string} operator - 比较运算符 (<, <=, >, >=, =)
|
|
31
|
+
* @returns {QueryBuilder} QueryBuilder 实例
|
|
32
|
+
*/
|
|
33
|
+
whereDistance(column, point, distance, operator = "<") {
|
|
34
|
+
const { latitude, longitude, x, y } = point;
|
|
35
|
+
const lat = latitude !== undefined ? latitude : y;
|
|
36
|
+
const lng = longitude !== undefined ? longitude : x;
|
|
37
|
+
|
|
38
|
+
if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
|
|
39
|
+
// MySQL 空间扩展
|
|
40
|
+
this.queryBuilder.whereRaw(
|
|
41
|
+
`ST_Distance_Sphere(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?)) ${operator} ?`,
|
|
42
|
+
[`POINT(${lng} ${lat})`, distance]
|
|
43
|
+
);
|
|
44
|
+
} else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
|
|
45
|
+
// PostgreSQL PostGIS
|
|
46
|
+
this.queryBuilder.whereRaw(
|
|
47
|
+
`ST_Distance(${this.queryBuilder.wrapColumn(column)}::geography, ST_MakePoint(?, ?)::geography) ${operator} ?`,
|
|
48
|
+
[lng, lat, distance]
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return this.queryBuilder;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* WHERE 在多边形内条件
|
|
57
|
+
* @param {string} column - 几何列名
|
|
58
|
+
* @param {Array} polygon - 点数组 [{latitude, longitude}, ...]
|
|
59
|
+
* @returns {QueryBuilder} QueryBuilder 实例
|
|
60
|
+
*/
|
|
61
|
+
whereWithin(column, polygon) {
|
|
62
|
+
const points = polygon
|
|
63
|
+
.map((p) => {
|
|
64
|
+
const { latitude, longitude, x, y } = p;
|
|
65
|
+
const lat = latitude !== undefined ? latitude : y;
|
|
66
|
+
const lng = longitude !== undefined ? longitude : x;
|
|
67
|
+
return `${lng} ${lat}`;
|
|
68
|
+
})
|
|
69
|
+
.join(", ");
|
|
70
|
+
|
|
71
|
+
const polygonWkt = `POLYGON((${points}))`;
|
|
72
|
+
|
|
73
|
+
if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
|
|
74
|
+
this.queryBuilder.whereRaw(
|
|
75
|
+
`ST_Within(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?))`,
|
|
76
|
+
[polygonWkt]
|
|
77
|
+
);
|
|
78
|
+
} else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
|
|
79
|
+
this.queryBuilder.whereRaw(
|
|
80
|
+
`ST_Within(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?, 4326))`,
|
|
81
|
+
[polygonWkt]
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return this.queryBuilder;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* WHERE 与几何图形相交条件
|
|
90
|
+
* @param {string} column - 几何列名
|
|
91
|
+
* @param {Object} geometry - 几何对象
|
|
92
|
+
* @returns {QueryBuilder} QueryBuilder 实例
|
|
93
|
+
*/
|
|
94
|
+
whereIntersects(column, geometry) {
|
|
95
|
+
let geometryWkt;
|
|
96
|
+
|
|
97
|
+
if (geometry.type === "point") {
|
|
98
|
+
const { latitude, longitude, x, y } = geometry;
|
|
99
|
+
const lat = latitude !== undefined ? latitude : y;
|
|
100
|
+
const lng = longitude !== undefined ? longitude : x;
|
|
101
|
+
geometryWkt = `POINT(${lng} ${lat})`;
|
|
102
|
+
} else if (geometry.type === "polygon") {
|
|
103
|
+
const points = geometry.coordinates
|
|
104
|
+
.map((p) => {
|
|
105
|
+
const { latitude, longitude, x, y } = p;
|
|
106
|
+
const lat = latitude !== undefined ? latitude : y;
|
|
107
|
+
const lng = longitude !== undefined ? longitude : x;
|
|
108
|
+
return `${lng} ${lat}`;
|
|
109
|
+
})
|
|
110
|
+
.join(", ");
|
|
111
|
+
geometryWkt = `POLYGON((${points}))`;
|
|
112
|
+
} else if (geometry.type === "linestring") {
|
|
113
|
+
const points = geometry.coordinates
|
|
114
|
+
.map((p) => {
|
|
115
|
+
const { latitude, longitude, x, y } = p;
|
|
116
|
+
const lat = latitude !== undefined ? latitude : y;
|
|
117
|
+
const lng = longitude !== undefined ? longitude : x;
|
|
118
|
+
return `${lng} ${lat}`;
|
|
119
|
+
})
|
|
120
|
+
.join(", ");
|
|
121
|
+
geometryWkt = `LINESTRING(${points})`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (geometryWkt) {
|
|
125
|
+
if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
|
|
126
|
+
this.queryBuilder.whereRaw(
|
|
127
|
+
`ST_Intersects(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?))`,
|
|
128
|
+
[geometryWkt]
|
|
129
|
+
);
|
|
130
|
+
} else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
|
|
131
|
+
this.queryBuilder.whereRaw(
|
|
132
|
+
`ST_Intersects(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?, 4326))`,
|
|
133
|
+
[geometryWkt]
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return this.queryBuilder;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 按距离排序
|
|
143
|
+
* @param {string} column - 几何列名
|
|
144
|
+
* @param {Object} point - 点坐标
|
|
145
|
+
* @param {string} direction - 排序方向 (asc, desc)
|
|
146
|
+
* @returns {QueryBuilder} QueryBuilder 实例
|
|
147
|
+
*/
|
|
148
|
+
orderByDistance(column, point, direction = "asc") {
|
|
149
|
+
const { latitude, longitude, x, y } = point;
|
|
150
|
+
const lat = latitude !== undefined ? latitude : y;
|
|
151
|
+
const lng = longitude !== undefined ? longitude : x;
|
|
152
|
+
|
|
153
|
+
if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
|
|
154
|
+
this.queryBuilder.orderByRaw(
|
|
155
|
+
`ST_Distance_Sphere(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?)) ${direction.toUpperCase()}`,
|
|
156
|
+
[`POINT(${lng} ${lat})`]
|
|
157
|
+
);
|
|
158
|
+
} else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
|
|
159
|
+
this.queryBuilder.orderByRaw(
|
|
160
|
+
`ST_Distance(${this.queryBuilder.wrapColumn(column)}::geography, ST_MakePoint(?, ?)::geography) ${direction.toUpperCase()}`,
|
|
161
|
+
[lng, lat]
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.queryBuilder;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 在 SELECT 中计算距离
|
|
170
|
+
* @param {string} column - 几何列名
|
|
171
|
+
* @param {Object} point - 点坐标
|
|
172
|
+
* @param {string} alias - 列别名
|
|
173
|
+
* @returns {QueryBuilder} QueryBuilder 实例
|
|
174
|
+
*/
|
|
175
|
+
selectDistance(column, point, alias = "distance") {
|
|
176
|
+
const { latitude, longitude, x, y } = point;
|
|
177
|
+
const lat = latitude !== undefined ? latitude : y;
|
|
178
|
+
const lng = longitude !== undefined ? longitude : x;
|
|
179
|
+
|
|
180
|
+
if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
|
|
181
|
+
this.queryBuilder.selectRaw(
|
|
182
|
+
`ST_Distance_Sphere(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?)) as ${alias}`,
|
|
183
|
+
[`POINT(${lng} ${lat})`]
|
|
184
|
+
);
|
|
185
|
+
} else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
|
|
186
|
+
this.queryBuilder.selectRaw(
|
|
187
|
+
`ST_Distance(${this.queryBuilder.wrapColumn(column)}::geography, ST_MakePoint(?, ?)::geography) as ${alias}`,
|
|
188
|
+
[lng, lat]
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return this.queryBuilder;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 获取支持的地理空间函数
|
|
197
|
+
* @returns {Array} 支持的函数列表
|
|
198
|
+
*/
|
|
199
|
+
getSupportedFunctions() {
|
|
200
|
+
const functions = {
|
|
201
|
+
mysql: ["ST_Distance_Sphere", "ST_Within", "ST_Intersects", "ST_GeomFromText"],
|
|
202
|
+
mariadb: ["ST_Distance_Sphere", "ST_Within", "ST_Intersects", "ST_GeomFromText"],
|
|
203
|
+
postgresql: ["ST_Distance", "ST_Within", "ST_Intersects", "ST_MakePoint", "ST_GeomFromText"],
|
|
204
|
+
postgres: ["ST_Distance", "ST_Within", "ST_Intersects", "ST_MakePoint", "ST_GeomFromText"],
|
|
205
|
+
pg: ["ST_Distance", "ST_Within", "ST_Intersects", "ST_MakePoint", "ST_GeomFromText"],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return functions[this.queryBuilder.dialect] || [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 检查数据库是否支持地理空间查询
|
|
213
|
+
* @returns {boolean} 是否支持
|
|
214
|
+
*/
|
|
215
|
+
isGeospatialSupported() {
|
|
216
|
+
const supportedDialects = ["mysql", "mariadb", "postgresql", "postgres", "pg"];
|
|
217
|
+
return supportedDialects.includes(this.queryBuilder.dialect);
|
|
218
|
+
}
|
|
219
|
+
}
|