@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,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/database/plugin/GrapthQLPlugin
|
|
6
|
+
*/
|
|
7
|
+
import { BasePlugin } from "./BasePlugin.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GraphQL Plugin - Provides GraphQL-style field selection and relation loading
|
|
11
|
+
*/
|
|
12
|
+
export class GraphQLPlugin extends BasePlugin {
|
|
13
|
+
constructor(queryBuilder) {
|
|
14
|
+
super(queryBuilder);
|
|
15
|
+
this.pluginName = "GraphQLPlugin";
|
|
16
|
+
this.graphqlFields = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_registerMethods() {
|
|
20
|
+
// Register GraphQL-style methods to QueryBuilder
|
|
21
|
+
this.queryBuilder.selectFields = this.selectFields.bind(this);
|
|
22
|
+
this.queryBuilder.extractGraphQLFields = this.extractGraphQLFields.bind(this);
|
|
23
|
+
this.queryBuilder.with = this.with.bind(this);
|
|
24
|
+
this.queryBuilder.executeWithRelations = this.executeWithRelations.bind(this);
|
|
25
|
+
this.queryBuilder.loadRelation = this.loadRelation.bind(this);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* GraphQL-style field selection
|
|
30
|
+
* @param {string|Array|Object} fields - Fields to select
|
|
31
|
+
* @returns {QueryBuilder} Query builder instance
|
|
32
|
+
*/
|
|
33
|
+
selectFields(fields) {
|
|
34
|
+
if (typeof fields === "string") {
|
|
35
|
+
// Comma-separated string
|
|
36
|
+
this.queryBuilder.query.columns = fields.split(",").map((f) => f.trim());
|
|
37
|
+
} else if (Array.isArray(fields)) {
|
|
38
|
+
// Array of fields
|
|
39
|
+
this.queryBuilder.query.columns = fields;
|
|
40
|
+
} else if (typeof fields === "object") {
|
|
41
|
+
// GraphQL-style object with nested fields
|
|
42
|
+
this.graphqlFields = fields;
|
|
43
|
+
this.queryBuilder.query.columns = this.extractGraphQLFields(fields);
|
|
44
|
+
}
|
|
45
|
+
return this.queryBuilder;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extract fields from GraphQL-style selection
|
|
50
|
+
* @param {Object} fields - GraphQL fields object
|
|
51
|
+
* @param {string} prefix - Field prefix
|
|
52
|
+
* @returns {Array} Extracted fields
|
|
53
|
+
*/
|
|
54
|
+
extractGraphQLFields(fields, prefix = "") {
|
|
55
|
+
const extracted = [];
|
|
56
|
+
|
|
57
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
58
|
+
if (value === true || value === 1) {
|
|
59
|
+
// Simple field
|
|
60
|
+
extracted.push(prefix ? `${prefix}.${key}` : key);
|
|
61
|
+
} else if (typeof value === "object") {
|
|
62
|
+
// Nested field or sub-query
|
|
63
|
+
if (value.fields) {
|
|
64
|
+
// Sub-query with fields
|
|
65
|
+
const subFields = this.extractGraphQLFields(value.fields, key);
|
|
66
|
+
extracted.push(...subFields);
|
|
67
|
+
} else {
|
|
68
|
+
// Nested object
|
|
69
|
+
const nestedFields = this.extractGraphQLFields(value, key);
|
|
70
|
+
extracted.push(...nestedFields);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return extracted;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Include related data (eager loading)
|
|
80
|
+
* @param {string|Object} relations - Relations to include
|
|
81
|
+
* @returns {QueryBuilder} Query builder instance
|
|
82
|
+
*/
|
|
83
|
+
with(relations) {
|
|
84
|
+
if (typeof relations === "string") {
|
|
85
|
+
this.queryBuilder.query.with = [relations];
|
|
86
|
+
} else if (Array.isArray(relations)) {
|
|
87
|
+
this.queryBuilder.query.with = relations;
|
|
88
|
+
} else if (typeof relations === "object") {
|
|
89
|
+
this.queryBuilder.query.with = Object.keys(relations);
|
|
90
|
+
this.graphqlFields = relations;
|
|
91
|
+
}
|
|
92
|
+
return this.queryBuilder;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Execute query with GraphQL-style field selection
|
|
97
|
+
* @returns {Promise<Object>} Query result with nested relations
|
|
98
|
+
*/
|
|
99
|
+
async executeWithRelations() {
|
|
100
|
+
const result = await this.queryBuilder.execute();
|
|
101
|
+
|
|
102
|
+
if (!this.queryBuilder.query.with || !result.rows || result.rows.length === 0) {
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Load relations for each row
|
|
107
|
+
for (const relation of this.queryBuilder.query.with) {
|
|
108
|
+
await this.loadRelation(result.rows, relation);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Load relation for rows
|
|
116
|
+
* @param {Array} rows - Parent rows
|
|
117
|
+
* @param {string} relation - Relation name
|
|
118
|
+
* @returns {Promise<void>}
|
|
119
|
+
*/
|
|
120
|
+
async loadRelation(rows, relation) {
|
|
121
|
+
const relationConfig = this.graphqlFields?.[relation];
|
|
122
|
+
if (!relationConfig) return;
|
|
123
|
+
|
|
124
|
+
// Extract parent IDs
|
|
125
|
+
const parentIds = rows.map((row) => row.id).filter((id) => id);
|
|
126
|
+
if (parentIds.length === 0) return;
|
|
127
|
+
|
|
128
|
+
// Build relation query
|
|
129
|
+
const relationQuery = new this.queryBuilder.constructor(
|
|
130
|
+
relation,
|
|
131
|
+
this.queryBuilder.connection,
|
|
132
|
+
this.queryBuilder.dialect,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Apply GraphQL field selection if specified
|
|
136
|
+
if (relationConfig.fields) {
|
|
137
|
+
relationQuery.selectFields(relationConfig.fields);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Add WHERE condition for relation
|
|
141
|
+
const foreignKey =
|
|
142
|
+
relationConfig.foreignKey || `${this.queryBuilder.tableName.slice(0, -1)}_id`;
|
|
143
|
+
const relatedRows = await relationQuery
|
|
144
|
+
.whereIn(foreignKey, parentIds)
|
|
145
|
+
.get();
|
|
146
|
+
|
|
147
|
+
// Group related rows by foreign key
|
|
148
|
+
const relatedByParent = {};
|
|
149
|
+
relatedRows.forEach((row) => {
|
|
150
|
+
const parentId = row[foreignKey];
|
|
151
|
+
if (!relatedByParent[parentId]) {
|
|
152
|
+
relatedByParent[parentId] = [];
|
|
153
|
+
}
|
|
154
|
+
relatedByParent[parentId].push(row);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Attach related rows to parent rows
|
|
158
|
+
rows.forEach((row) => {
|
|
159
|
+
row[relation] = relatedByParent[row.id] || [];
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -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/HoolPlugin
|
|
6
|
+
*/
|
|
7
|
+
import { BasePlugin } from './BasePlugin.js';
|
|
8
|
+
|
|
9
|
+
export class HookPlugin extends BasePlugin {
|
|
10
|
+
constructor(queryBuilder) {
|
|
11
|
+
super(queryBuilder);
|
|
12
|
+
this.hooks = {};
|
|
13
|
+
this.validationRules = {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register plugin methods to QueryBuilder
|
|
18
|
+
* @protected
|
|
19
|
+
*/
|
|
20
|
+
_registerMethods() {
|
|
21
|
+
// Register hook management methods
|
|
22
|
+
this.queryBuilder.addHook = this.addHook.bind(this);
|
|
23
|
+
this.queryBuilder.triggerHook = this.triggerHook.bind(this);
|
|
24
|
+
this.queryBuilder.setValidationRules = this.setValidationRules.bind(this);
|
|
25
|
+
this.queryBuilder.validateData = this.validateData.bind(this);
|
|
26
|
+
|
|
27
|
+
// Register specific hook shortcuts
|
|
28
|
+
this.queryBuilder.beforeInsert = (callback) => this.addHook('beforeInsert', callback);
|
|
29
|
+
this.queryBuilder.afterInsert = (callback) => this.addHook('afterInsert', callback);
|
|
30
|
+
this.queryBuilder.beforeUpdate = (callback) => this.addHook('beforeUpdate', callback);
|
|
31
|
+
this.queryBuilder.afterUpdate = (callback) => this.addHook('afterUpdate', callback);
|
|
32
|
+
this.queryBuilder.beforeDelete = (callback) => this.addHook('beforeDelete', callback);
|
|
33
|
+
this.queryBuilder.afterDelete = (callback) => this.addHook('afterDelete', callback);
|
|
34
|
+
this.queryBuilder.beforeSelect = (callback) => this.addHook('beforeSelect', callback);
|
|
35
|
+
this.queryBuilder.afterSelect = (callback) => this.addHook('afterSelect', callback);
|
|
36
|
+
this.queryBuilder.onBeforeInsert = (callback) => this.addHook('beforeInsert', callback);
|
|
37
|
+
this.queryBuilder.onAfterInsert = (callback) => this.addHook('afterInsert', callback);
|
|
38
|
+
this.queryBuilder.onBeforeUpdate = (callback) => this.addHook('beforeUpdate', callback);
|
|
39
|
+
this.queryBuilder.onAfterUpdate = (callback) => this.addHook('afterUpdate', callback);
|
|
40
|
+
this.queryBuilder.onBeforeDelete = (callback) => this.addHook('beforeDelete', callback);
|
|
41
|
+
this.queryBuilder.onAfterDelete = (callback) => this.addHook('afterDelete', callback);
|
|
42
|
+
this.queryBuilder.onBeforeSelect = (callback) => this.addHook('beforeSelect', callback);
|
|
43
|
+
this.queryBuilder.onAfterSelect = (callback) => this.addHook('afterSelect', callback);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Add a hook for specific event
|
|
48
|
+
* @param {string} event - Hook event name
|
|
49
|
+
* @param {Function} callback - Hook callback function
|
|
50
|
+
* @returns {QueryBuilder} Query builder instance
|
|
51
|
+
*/
|
|
52
|
+
addHook(event, callback) {
|
|
53
|
+
if (!this.hooks[event]) {
|
|
54
|
+
this.hooks[event] = [];
|
|
55
|
+
}
|
|
56
|
+
this.hooks[event].push(callback);
|
|
57
|
+
return this.queryBuilder;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Trigger hooks for specific event
|
|
62
|
+
* @param {string} event - Hook event name
|
|
63
|
+
* @param {*} data - Data to pass to hooks
|
|
64
|
+
* @returns {Promise<void>}
|
|
65
|
+
*/
|
|
66
|
+
async triggerHook(event, data) {
|
|
67
|
+
if (!this.hooks[event]) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const hook of this.hooks[event]) {
|
|
72
|
+
const result = await hook(data, this.queryBuilder);
|
|
73
|
+
if (result === false) {
|
|
74
|
+
throw new Error(`Hook ${event} returned false, operation cancelled`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set validation rules for data
|
|
81
|
+
* @param {Object} rules - Validation rules object
|
|
82
|
+
* @returns {QueryBuilder} Query builder instance
|
|
83
|
+
*/
|
|
84
|
+
setValidationRules(rules) {
|
|
85
|
+
this.validationRules = rules;
|
|
86
|
+
return this.queryBuilder;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate data against rules
|
|
91
|
+
* @param {Object} data - Data to validate
|
|
92
|
+
* @returns {Array} Array of validation errors
|
|
93
|
+
*/
|
|
94
|
+
validateData(data) {
|
|
95
|
+
const errors = [];
|
|
96
|
+
|
|
97
|
+
for (const [field, rule] of Object.entries(this.validationRules)) {
|
|
98
|
+
const value = data[field];
|
|
99
|
+
|
|
100
|
+
// Check required field
|
|
101
|
+
if (rule.required && (value === undefined || value === null || value === '')) {
|
|
102
|
+
errors.push(`${field} is required`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Skip validation if value is not provided and not required
|
|
107
|
+
if (value === undefined || value === null) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Type validation
|
|
112
|
+
if (rule.type) {
|
|
113
|
+
const typeCheck = this._validateType(value, rule.type);
|
|
114
|
+
if (!typeCheck.valid) {
|
|
115
|
+
errors.push(`${field} must be ${rule.type}, got ${typeCheck.actual}`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Minimum value validation
|
|
121
|
+
if (rule.min !== undefined) {
|
|
122
|
+
if (typeof value === 'number' && value < rule.min) {
|
|
123
|
+
errors.push(`${field} must be at least ${rule.min}`);
|
|
124
|
+
} else if (typeof value === 'string' && value.length < rule.min) {
|
|
125
|
+
errors.push(`${field} must be at least ${rule.min} characters`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Maximum value validation
|
|
130
|
+
if (rule.max !== undefined) {
|
|
131
|
+
if (typeof value === 'number' && value > rule.max) {
|
|
132
|
+
errors.push(`${field} must be at most ${rule.max}`);
|
|
133
|
+
} else if (typeof value === 'string' && value.length > rule.max) {
|
|
134
|
+
errors.push(`${field} must be at most ${rule.max} characters`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Pattern validation
|
|
139
|
+
if (rule.pattern && !rule.pattern.test(value)) {
|
|
140
|
+
errors.push(`${field} format is invalid`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Enum validation
|
|
144
|
+
if (rule.enum && !rule.enum.includes(value)) {
|
|
145
|
+
errors.push(`${field} must be one of: ${rule.enum.join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Custom validation function
|
|
149
|
+
if (rule.validate && typeof rule.validate === 'function') {
|
|
150
|
+
const customResult = rule.validate(value, data);
|
|
151
|
+
if (customResult !== true) {
|
|
152
|
+
errors.push(`${field}: ${customResult}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return errors;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validate data type
|
|
162
|
+
* @private
|
|
163
|
+
*/
|
|
164
|
+
_validateType(value, expectedType) {
|
|
165
|
+
const actualType = typeof value;
|
|
166
|
+
|
|
167
|
+
// Handle special cases
|
|
168
|
+
if (expectedType === 'array' && Array.isArray(value)) {
|
|
169
|
+
return { valid: true, actual: 'array' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (expectedType === 'object' && value !== null && !Array.isArray(value) && actualType === 'object') {
|
|
173
|
+
return { valid: true, actual: 'object' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (expectedType === 'integer' && Number.isInteger(value)) {
|
|
177
|
+
return { valid: true, actual: 'integer' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (expectedType === 'float' && typeof value === 'number' && !Number.isInteger(value)) {
|
|
181
|
+
return { valid: true, actual: 'float' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (expectedType === 'date' && value instanceof Date) {
|
|
185
|
+
return { valid: true, actual: 'date' };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (expectedType === 'email' && typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
189
|
+
return { valid: true, actual: 'email' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (expectedType === 'url' && typeof value === 'string' && /^https?:\/\/[^\s$.?#].[^\s]*$/.test(value)) {
|
|
193
|
+
return { valid: true, actual: 'url' };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { valid: actualType === expectedType, actual: actualType };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get plugin metadata
|
|
201
|
+
* @returns {Object} Plugin metadata
|
|
202
|
+
*/
|
|
203
|
+
getMetadata() {
|
|
204
|
+
return {
|
|
205
|
+
...super.getMetadata(),
|
|
206
|
+
description: 'Hook and validation plugin for QueryBuilder',
|
|
207
|
+
hooks: Object.keys(this.hooks),
|
|
208
|
+
validationRules: Object.keys(this.validationRules)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|