@aceitadev/adatabase 0.8.0 → 0.9.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/dist/ActiveRecord.js +4 -6
- package/dist/PersistenceException.d.ts +15 -0
- package/dist/PersistenceException.js +36 -1
- package/dist/QueryBuilder.d.ts +5 -0
- package/dist/QueryBuilder.js +130 -69
- package/dist/SchemaManager.d.ts +11 -1
- package/dist/SchemaManager.js +188 -20
- package/dist/Security.js +1 -1
- package/dist/adapters/MySQLAdapter.d.ts +2 -0
- package/dist/adapters/MySQLAdapter.js +28 -1
- package/dist/adapters/PostgresAdapter.d.ts +2 -0
- package/dist/adapters/PostgresAdapter.js +26 -1
- package/dist/decorators/Column.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
- package/README.md +0 -171
package/dist/ActiveRecord.js
CHANGED
|
@@ -15,9 +15,7 @@ const Column_1 = require("./decorators/Column");
|
|
|
15
15
|
const PersistenceException_1 = require("./PersistenceException");
|
|
16
16
|
const QueryBuilder_1 = require("./QueryBuilder");
|
|
17
17
|
const Table_1 = require("./decorators/Table");
|
|
18
|
-
|
|
19
|
-
return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
20
|
-
}
|
|
18
|
+
const util_1 = require("./util");
|
|
21
19
|
class ActiveRecord {
|
|
22
20
|
static getIdField(ctor) {
|
|
23
21
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -55,7 +53,7 @@ class ActiveRecord {
|
|
|
55
53
|
if (!this.hasOwnProperty(prop))
|
|
56
54
|
continue;
|
|
57
55
|
const value = this[prop];
|
|
58
|
-
const colName = (opts === null || opts === void 0 ? void 0 : opts.name) ? opts.name : camelToSnake(prop);
|
|
56
|
+
const colName = (opts === null || opts === void 0 ? void 0 : opts.name) ? opts.name : (0, util_1.camelToSnake)(prop);
|
|
59
57
|
colEntries.push({ colName, value });
|
|
60
58
|
}
|
|
61
59
|
const idValue = this[idField];
|
|
@@ -74,7 +72,7 @@ ${table}
|
|
|
74
72
|
else {
|
|
75
73
|
const setClause = colEntries.map(c => `\`${c.colName}\` = ?`).join(", ");
|
|
76
74
|
const params = [...colEntries.map(c => c.value === undefined ? null : c.value), idValue];
|
|
77
|
-
const idColName = camelToSnake(idField);
|
|
75
|
+
const idColName = (0, util_1.camelToSnake)(idField);
|
|
78
76
|
const sql = `UPDATE
|
|
79
77
|
${table}
|
|
80
78
|
SET ${setClause} WHERE
|
|
@@ -110,7 +108,7 @@ ${idColName}
|
|
|
110
108
|
return;
|
|
111
109
|
const conn = tx !== null && tx !== void 0 ? tx : yield (0, Database_1.getConnection)();
|
|
112
110
|
try {
|
|
113
|
-
const idColName = camelToSnake(idField);
|
|
111
|
+
const idColName = (0, util_1.camelToSnake)(idField);
|
|
114
112
|
const sql = `DELETE FROM \`${table}\` WHERE \`${idColName}\` = ?`;
|
|
115
113
|
yield (0, Database_1.execute)(sql, [idValue], conn);
|
|
116
114
|
}
|
|
@@ -2,3 +2,18 @@ export declare class PersistenceException extends Error {
|
|
|
2
2
|
cause?: (Error | null) | undefined;
|
|
3
3
|
constructor(message: string, cause?: (Error | null) | undefined);
|
|
4
4
|
}
|
|
5
|
+
export declare class InvalidColumnException extends PersistenceException {
|
|
6
|
+
constructor(column: string, model: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class DuplicateEntryException extends PersistenceException {
|
|
9
|
+
constructor(message: string, cause?: Error | null);
|
|
10
|
+
}
|
|
11
|
+
export declare class ForeignKeyException extends PersistenceException {
|
|
12
|
+
constructor(message: string, cause?: Error | null);
|
|
13
|
+
}
|
|
14
|
+
export declare class ConnectionException extends PersistenceException {
|
|
15
|
+
constructor(message: string, cause?: Error | null);
|
|
16
|
+
}
|
|
17
|
+
export declare class TimeoutException extends PersistenceException {
|
|
18
|
+
constructor(message: string, cause?: Error | null);
|
|
19
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PersistenceException = void 0;
|
|
3
|
+
exports.TimeoutException = exports.ConnectionException = exports.ForeignKeyException = exports.DuplicateEntryException = exports.InvalidColumnException = exports.PersistenceException = void 0;
|
|
4
4
|
class PersistenceException extends Error {
|
|
5
5
|
constructor(message, cause) {
|
|
6
6
|
var _a, _b;
|
|
@@ -14,3 +14,38 @@ class PersistenceException extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
exports.PersistenceException = PersistenceException;
|
|
17
|
+
class InvalidColumnException extends PersistenceException {
|
|
18
|
+
constructor(column, model) {
|
|
19
|
+
super(`Property '${column}' is not a mapped column on model '${model}'.`, null);
|
|
20
|
+
Object.setPrototypeOf(this, InvalidColumnException.prototype);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.InvalidColumnException = InvalidColumnException;
|
|
24
|
+
class DuplicateEntryException extends PersistenceException {
|
|
25
|
+
constructor(message, cause) {
|
|
26
|
+
super(`Duplicate entry violation: ${message}`, cause);
|
|
27
|
+
Object.setPrototypeOf(this, DuplicateEntryException.prototype);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.DuplicateEntryException = DuplicateEntryException;
|
|
31
|
+
class ForeignKeyException extends PersistenceException {
|
|
32
|
+
constructor(message, cause) {
|
|
33
|
+
super(`Foreign key constraint violation: ${message}`, cause);
|
|
34
|
+
Object.setPrototypeOf(this, ForeignKeyException.prototype);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.ForeignKeyException = ForeignKeyException;
|
|
38
|
+
class ConnectionException extends PersistenceException {
|
|
39
|
+
constructor(message, cause) {
|
|
40
|
+
super(`Database connection error: ${message}`, cause);
|
|
41
|
+
Object.setPrototypeOf(this, ConnectionException.prototype);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.ConnectionException = ConnectionException;
|
|
45
|
+
class TimeoutException extends PersistenceException {
|
|
46
|
+
constructor(message, cause) {
|
|
47
|
+
super(`Query timeout: ${message}`, cause);
|
|
48
|
+
Object.setPrototypeOf(this, TimeoutException.prototype);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.TimeoutException = TimeoutException;
|
package/dist/QueryBuilder.d.ts
CHANGED
|
@@ -18,6 +18,11 @@ export declare class QueryBuilder<T extends ActiveRecord> {
|
|
|
18
18
|
first(): Promise<T | null>;
|
|
19
19
|
count(): Promise<number>;
|
|
20
20
|
get(): Promise<T[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Loads HasMany relations using separate queries with WHERE IN
|
|
23
|
+
* This avoids the cartesian product problem of JOINs
|
|
24
|
+
*/
|
|
25
|
+
private loadHasManyRelations;
|
|
21
26
|
private getColumnName;
|
|
22
27
|
private mapRowsToEntities;
|
|
23
28
|
}
|
package/dist/QueryBuilder.js
CHANGED
|
@@ -17,9 +17,8 @@ const PersistenceException_1 = require("./PersistenceException");
|
|
|
17
17
|
const BelongsTo_1 = require("./decorators/BelongsTo");
|
|
18
18
|
const HasMany_1 = require("./decorators/HasMany");
|
|
19
19
|
const HasOne_1 = require("./decorators/HasOne");
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
20
|
+
const util_1 = require("./util");
|
|
21
|
+
const Security_1 = require("./Security");
|
|
23
22
|
class QueryBuilder {
|
|
24
23
|
constructor(model) {
|
|
25
24
|
this.model = model;
|
|
@@ -37,10 +36,14 @@ class QueryBuilder {
|
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
38
|
where(field, operator, value) {
|
|
39
|
+
// Validate operator at call time to fail fast
|
|
40
|
+
(0, Security_1.validateOperator)(operator);
|
|
40
41
|
this.whereClauses.push({ field: field, operator, value, booleanOp: "AND" });
|
|
41
42
|
return this;
|
|
42
43
|
}
|
|
43
44
|
include(...models) {
|
|
45
|
+
// HasMany relations are loaded via separate optimized queries (WHERE IN)
|
|
46
|
+
// BelongsTo and HasOne use efficient JOINs
|
|
44
47
|
this._includes.push(...models);
|
|
45
48
|
return this;
|
|
46
49
|
}
|
|
@@ -73,16 +76,13 @@ class QueryBuilder {
|
|
|
73
76
|
if (!columnsMeta) {
|
|
74
77
|
throw new PersistenceException_1.PersistenceException("Model has no @Column decorators", null);
|
|
75
78
|
}
|
|
76
|
-
const allowedOperators = ['=', '!=', '<>', '>', '<', '>=', '<=', 'LIKE', 'IN', 'IS NULL', 'IS NOT NULL'];
|
|
77
79
|
const params = [];
|
|
78
80
|
const baseAlias = 't1';
|
|
79
81
|
let sql = `SELECT COUNT(*) as count FROM \`${table}\` AS ${baseAlias}`;
|
|
80
82
|
if (this.whereClauses.length > 0) {
|
|
81
83
|
sql += " WHERE ";
|
|
82
84
|
this.whereClauses.forEach((c, i) => {
|
|
83
|
-
|
|
84
|
-
throw new Error(`Invalid operator used: ${c.operator}`);
|
|
85
|
-
}
|
|
85
|
+
// Operator is already validated in where() method
|
|
86
86
|
if (i > 0) {
|
|
87
87
|
sql += ` ${c.booleanOp} `;
|
|
88
88
|
}
|
|
@@ -116,48 +116,67 @@ class QueryBuilder {
|
|
|
116
116
|
const baseAlias = 't1';
|
|
117
117
|
let selectFields = `${baseAlias}.*`;
|
|
118
118
|
let joinClause = '';
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
// Separate relations by type - HasMany will use separate queries
|
|
120
|
+
const belongsToMeta = (0, BelongsTo_1.getBelongsToMeta)(this.model);
|
|
121
|
+
const hasOneMeta = (0, HasOne_1.getHasOneMeta)(this.model);
|
|
122
|
+
const hasManyMeta = (0, HasMany_1.getHasManyMeta)(this.model);
|
|
123
|
+
// Track which includes are HasMany (will be loaded separately)
|
|
124
|
+
const hasManyIncludes = [];
|
|
125
|
+
const joinIncludes = [];
|
|
124
126
|
if (this._includes.length > 0) {
|
|
125
127
|
this._includes.forEach((includeModel, index) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
relationInfo = { prop, opts, type };
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
128
|
+
// Check if this is a HasMany relation
|
|
129
|
+
if (hasManyMeta) {
|
|
130
|
+
for (const [prop, opts] of hasManyMeta.entries()) {
|
|
131
|
+
if (opts.model() === includeModel) {
|
|
132
|
+
hasManyIncludes.push({ model: includeModel, prop, opts });
|
|
133
|
+
return; // Don't add to JOIN
|
|
135
134
|
}
|
|
136
135
|
}
|
|
137
|
-
if (relationInfo)
|
|
138
|
-
break;
|
|
139
136
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
selectFields += `, ${relationAlias}.\`${colName}\` AS \`${relationInfo.prop}__${prop}\``;
|
|
148
|
-
}
|
|
149
|
-
if (relationInfo.type === 'BelongsTo') {
|
|
150
|
-
const foreignKey = relationInfo.opts.foreignKey;
|
|
151
|
-
const foreignKeyCol = this.getColumnName(foreignKey, columnsMeta);
|
|
152
|
-
joinClause += ` LEFT JOIN \`${relatedTable}\` AS ${relationAlias} ON ${baseAlias}.\`${foreignKeyCol}\` = ${relationAlias}.id`;
|
|
137
|
+
// Check BelongsTo - use JOIN
|
|
138
|
+
if (belongsToMeta) {
|
|
139
|
+
for (const [prop, opts] of belongsToMeta.entries()) {
|
|
140
|
+
if (opts.model() === includeModel) {
|
|
141
|
+
joinIncludes.push({ model: includeModel, prop, opts, type: 'BelongsTo' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
153
144
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
145
|
+
}
|
|
146
|
+
// Check HasOne - use JOIN
|
|
147
|
+
if (hasOneMeta) {
|
|
148
|
+
for (const [prop, opts] of hasOneMeta.entries()) {
|
|
149
|
+
if (opts.model() === includeModel) {
|
|
150
|
+
joinIncludes.push({ model: includeModel, prop, opts, type: 'HasOne' });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
158
153
|
}
|
|
159
154
|
}
|
|
160
155
|
});
|
|
156
|
+
// Build JOINs only for BelongsTo and HasOne
|
|
157
|
+
joinIncludes.forEach((relation, index) => {
|
|
158
|
+
const relationAlias = `t${index + 2}`;
|
|
159
|
+
const relatedTable = (0, Table_1.getTableName)(relation.model);
|
|
160
|
+
const relatedColumnsMeta = (0, Column_1.getColumnMeta)(relation.model);
|
|
161
|
+
if (!relatedTable || !relatedColumnsMeta)
|
|
162
|
+
return;
|
|
163
|
+
// Add select fields for this relation
|
|
164
|
+
for (const prop of relatedColumnsMeta.keys()) {
|
|
165
|
+
const colName = this.getColumnName(prop, relatedColumnsMeta);
|
|
166
|
+
selectFields += `, ${relationAlias}.\`${colName}\` AS \`${relation.prop}__${prop}\``;
|
|
167
|
+
}
|
|
168
|
+
// Build JOIN clause
|
|
169
|
+
if (relation.type === 'BelongsTo') {
|
|
170
|
+
const foreignKey = relation.opts.foreignKey;
|
|
171
|
+
const foreignKeyCol = this.getColumnName(foreignKey, columnsMeta);
|
|
172
|
+
joinClause += ` LEFT JOIN \`${relatedTable}\` AS ${relationAlias} ON ${baseAlias}.\`${foreignKeyCol}\` = ${relationAlias}.id`;
|
|
173
|
+
}
|
|
174
|
+
else { // HasOne
|
|
175
|
+
const foreignKey = relation.opts.foreignKey;
|
|
176
|
+
const foreignKeyCol = this.getColumnName(foreignKey, relatedColumnsMeta);
|
|
177
|
+
joinClause += ` LEFT JOIN \`${relatedTable}\` AS ${relationAlias} ON ${baseAlias}.id = ${relationAlias}.\`${foreignKeyCol}\``;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
161
180
|
}
|
|
162
181
|
let sql = `SELECT ${selectFields} FROM \`${table}\` AS ${baseAlias}${joinClause}`;
|
|
163
182
|
if (this.whereClauses.length) {
|
|
@@ -181,22 +200,70 @@ class QueryBuilder {
|
|
|
181
200
|
sql += ` OFFSET ${this._offset}`;
|
|
182
201
|
}
|
|
183
202
|
sql += ";";
|
|
203
|
+
// Execute main query
|
|
184
204
|
const rows = yield (0, Database_1.query)(sql, params);
|
|
185
|
-
|
|
205
|
+
const entities = this.mapRowsToEntities(rows, joinIncludes);
|
|
206
|
+
// Load HasMany relations with separate optimized queries
|
|
207
|
+
if (hasManyIncludes.length > 0 && entities.length > 0) {
|
|
208
|
+
yield this.loadHasManyRelations(entities, hasManyIncludes);
|
|
209
|
+
}
|
|
210
|
+
return entities;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Loads HasMany relations using separate queries with WHERE IN
|
|
215
|
+
* This avoids the cartesian product problem of JOINs
|
|
216
|
+
*/
|
|
217
|
+
loadHasManyRelations(entities, hasManyIncludes) {
|
|
218
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
219
|
+
// Get primary key column name
|
|
220
|
+
const columnsMeta = (0, Column_1.getColumnMeta)(this.model);
|
|
221
|
+
const pkColName = this.getColumnName(this.primaryKey, columnsMeta);
|
|
222
|
+
// Collect all entity IDs
|
|
223
|
+
const entityIds = entities.map(e => e[this.primaryKey]);
|
|
224
|
+
if (entityIds.length === 0)
|
|
225
|
+
return;
|
|
226
|
+
for (const relation of hasManyIncludes) {
|
|
227
|
+
const relatedTable = (0, Table_1.getTableName)(relation.model);
|
|
228
|
+
const relatedColumnsMeta = (0, Column_1.getColumnMeta)(relation.model);
|
|
229
|
+
if (!relatedTable || !relatedColumnsMeta)
|
|
230
|
+
continue;
|
|
231
|
+
// Get foreign key column name in the related table
|
|
232
|
+
const foreignKeyCol = this.getColumnName(relation.opts.foreignKey, relatedColumnsMeta);
|
|
233
|
+
// Build optimized query with WHERE IN
|
|
234
|
+
const placeholders = entityIds.map(() => '?').join(', ');
|
|
235
|
+
const sql = `SELECT * FROM \`${relatedTable}\` WHERE \`${foreignKeyCol}\` IN (${placeholders});`;
|
|
236
|
+
const relatedRows = yield (0, Database_1.query)(sql, entityIds);
|
|
237
|
+
// Initialize arrays for each entity
|
|
238
|
+
for (const entity of entities) {
|
|
239
|
+
entity[relation.prop] = [];
|
|
240
|
+
}
|
|
241
|
+
// Map related rows to their parent entities
|
|
242
|
+
const entityMap = new Map(entities.map(e => [e[this.primaryKey], e]));
|
|
243
|
+
for (const row of relatedRows) {
|
|
244
|
+
const parentId = row[foreignKeyCol];
|
|
245
|
+
const parentEntity = entityMap.get(parentId);
|
|
246
|
+
if (parentEntity) {
|
|
247
|
+
const relatedObj = new relation.model();
|
|
248
|
+
for (const [prop] of relatedColumnsMeta.entries()) {
|
|
249
|
+
const colName = this.getColumnName(prop, relatedColumnsMeta);
|
|
250
|
+
if (row.hasOwnProperty(colName)) {
|
|
251
|
+
relatedObj[prop] = row[colName];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
parentEntity[relation.prop].push(relatedObj);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
186
258
|
});
|
|
187
259
|
}
|
|
188
260
|
getColumnName(prop, meta) {
|
|
189
261
|
const opts = meta.get(prop);
|
|
190
|
-
return (opts === null || opts === void 0 ? void 0 : opts.name) ? opts.name : camelToSnake(prop);
|
|
262
|
+
return (opts === null || opts === void 0 ? void 0 : opts.name) ? opts.name : (0, util_1.camelToSnake)(prop);
|
|
191
263
|
}
|
|
192
|
-
mapRowsToEntities(rows) {
|
|
264
|
+
mapRowsToEntities(rows, joinIncludes = []) {
|
|
193
265
|
const mainEntityMap = new Map();
|
|
194
266
|
const columnsMeta = (0, Column_1.getColumnMeta)(this.model);
|
|
195
|
-
const allRelationMetas = [
|
|
196
|
-
{ meta: (0, BelongsTo_1.getBelongsToMeta)(this.model), type: 'BelongsTo' },
|
|
197
|
-
{ meta: (0, HasMany_1.getHasManyMeta)(this.model), type: 'HasMany' },
|
|
198
|
-
{ meta: (0, HasOne_1.getHasOneMeta)(this.model), type: 'HasOne' }
|
|
199
|
-
];
|
|
200
267
|
for (const row of rows) {
|
|
201
268
|
const pkValue = row[this.primaryKey];
|
|
202
269
|
if (!mainEntityMap.has(pkValue)) {
|
|
@@ -210,28 +277,22 @@ class QueryBuilder {
|
|
|
210
277
|
mainEntityMap.set(pkValue, obj);
|
|
211
278
|
}
|
|
212
279
|
const mainEntity = mainEntityMap.get(pkValue);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
mainEntity[relationProp] = [];
|
|
228
|
-
}
|
|
229
|
-
mainEntity[relationProp].push(relatedObj);
|
|
230
|
-
}
|
|
231
|
-
else { // BelongsTo or HasOne
|
|
232
|
-
mainEntity[relationProp] = relatedObj;
|
|
233
|
-
}
|
|
280
|
+
// Only process BelongsTo and HasOne relations (from JOIN)
|
|
281
|
+
// HasMany is now handled separately by loadHasManyRelations()
|
|
282
|
+
for (const relation of joinIncludes) {
|
|
283
|
+
const prefix = `${relation.prop}__`;
|
|
284
|
+
if (row[`${prefix}id`] === null)
|
|
285
|
+
continue;
|
|
286
|
+
const relatedModelCtor = relation.opts.model();
|
|
287
|
+
const relatedColsMeta = (0, Column_1.getColumnMeta)(relatedModelCtor);
|
|
288
|
+
// For BelongsTo/HasOne, only set if not already set
|
|
289
|
+
// (no deduplication needed since 1:1 relationship)
|
|
290
|
+
if (mainEntity[relation.prop] === undefined) {
|
|
291
|
+
const relatedObj = new relatedModelCtor();
|
|
292
|
+
for (const [prop] of relatedColsMeta.entries()) {
|
|
293
|
+
relatedObj[prop] = row[`${prefix}${prop}`];
|
|
234
294
|
}
|
|
295
|
+
mainEntity[relation.prop] = relatedObj;
|
|
235
296
|
}
|
|
236
297
|
}
|
|
237
298
|
}
|
package/dist/SchemaManager.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
export interface MigrationOptions {
|
|
2
|
+
/** If true, logs the SQL that would be executed without actually running it */
|
|
3
|
+
dryRun?: boolean;
|
|
4
|
+
/** If true, enables the two-phase column drop system */
|
|
5
|
+
enableDropWarnings?: boolean;
|
|
6
|
+
}
|
|
1
7
|
export declare class SchemaManager {
|
|
2
8
|
private models;
|
|
3
9
|
private changes;
|
|
10
|
+
private pendingSql;
|
|
11
|
+
private pendingDropWarnings;
|
|
4
12
|
constructor(models: Function[]);
|
|
5
|
-
migrate(): Promise<void>;
|
|
13
|
+
migrate(options?: MigrationOptions): Promise<void>;
|
|
14
|
+
private printDryRunSummary;
|
|
15
|
+
private printDropWarnings;
|
|
6
16
|
private printMigrationSummary;
|
|
7
17
|
private createTable;
|
|
8
18
|
private updateTable;
|
package/dist/SchemaManager.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
36
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
37
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -15,17 +48,22 @@ const Column_1 = require("./decorators/Column");
|
|
|
15
48
|
const Nullable_1 = require("./decorators/Nullable");
|
|
16
49
|
const BelongsTo_1 = require("./decorators/BelongsTo");
|
|
17
50
|
const Database_1 = require("./Database");
|
|
18
|
-
|
|
19
|
-
return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
20
|
-
}
|
|
51
|
+
const util_1 = require("./util");
|
|
21
52
|
class SchemaManager {
|
|
22
53
|
constructor(models) {
|
|
23
54
|
this.changes = new Map();
|
|
55
|
+
this.pendingSql = [];
|
|
56
|
+
this.pendingDropWarnings = [];
|
|
24
57
|
this.models = models;
|
|
25
58
|
}
|
|
26
|
-
migrate() {
|
|
59
|
+
migrate(options) {
|
|
27
60
|
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
var _a, _b;
|
|
62
|
+
const dryRun = (_a = options === null || options === void 0 ? void 0 : options.dryRun) !== null && _a !== void 0 ? _a : false;
|
|
63
|
+
const enableDropWarnings = (_b = options === null || options === void 0 ? void 0 : options.enableDropWarnings) !== null && _b !== void 0 ? _b : false;
|
|
28
64
|
this.changes.clear();
|
|
65
|
+
this.pendingSql = [];
|
|
66
|
+
this.pendingDropWarnings = [];
|
|
29
67
|
for (const model of this.models) {
|
|
30
68
|
const table = (0, Table_1.getTableName)(model);
|
|
31
69
|
if (!table)
|
|
@@ -33,16 +71,52 @@ class SchemaManager {
|
|
|
33
71
|
const { columns, indexes, primaryKey, foreignKeys } = this.getSchemaFromModel(model);
|
|
34
72
|
const existing = yield this.getExistingColumns(table);
|
|
35
73
|
if (Object.keys(existing).length === 0) {
|
|
36
|
-
yield this.createTable(table, columns, indexes, foreignKeys);
|
|
74
|
+
yield this.createTable(table, columns, indexes, foreignKeys, dryRun);
|
|
37
75
|
}
|
|
38
76
|
else {
|
|
39
77
|
const existingConstraints = yield this.getExistingConstraints(table);
|
|
40
|
-
yield this.updateTable(table, columns, indexes, existing, primaryKey, foreignKeys, existingConstraints);
|
|
78
|
+
yield this.updateTable(table, columns, indexes, existing, primaryKey, foreignKeys, existingConstraints, dryRun, enableDropWarnings);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (dryRun) {
|
|
82
|
+
this.printDryRunSummary();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.printMigrationSummary();
|
|
86
|
+
if (this.pendingDropWarnings.length > 0) {
|
|
87
|
+
this.printDropWarnings();
|
|
41
88
|
}
|
|
42
89
|
}
|
|
43
|
-
this.printMigrationSummary();
|
|
44
90
|
});
|
|
45
91
|
}
|
|
92
|
+
printDryRunSummary() {
|
|
93
|
+
console.log("\n╔══════════════════════════════════════════════════════════════╗");
|
|
94
|
+
console.log("║ DRY RUN MODE ║");
|
|
95
|
+
console.log("║ No changes were made to the database ║");
|
|
96
|
+
console.log("╚══════════════════════════════════════════════════════════════╝\n");
|
|
97
|
+
if (this.pendingSql.length === 0) {
|
|
98
|
+
console.log("✓ No schema changes detected.\n");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
console.log("The following SQL statements would be executed:\n");
|
|
102
|
+
this.pendingSql.forEach((sql, i) => {
|
|
103
|
+
console.log(` ${i + 1}. ${sql}\n`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
printDropWarnings() {
|
|
107
|
+
console.log("\n╔══════════════════════════════════════════════════════════════╗");
|
|
108
|
+
console.log("║ ⚠️ WARNING: COLUMNS SCHEDULED FOR REMOVAL ⚠️ ║");
|
|
109
|
+
console.log("╚══════════════════════════════════════════════════════════════╝\n");
|
|
110
|
+
console.log("\x1b[33m"); // Yellow color
|
|
111
|
+
console.log("The following columns exist in the database but NOT in your models.");
|
|
112
|
+
console.log("They are scheduled for removal on the NEXT migration run.\n");
|
|
113
|
+
for (const warning of this.pendingDropWarnings) {
|
|
114
|
+
console.log(` • Table: ${warning.table}, Column: ${warning.column}`);
|
|
115
|
+
}
|
|
116
|
+
console.log("\n To prevent removal, add these columns back to your models.");
|
|
117
|
+
console.log(" To confirm removal, run the migration again.\n");
|
|
118
|
+
console.log("\x1b[0m"); // Reset color
|
|
119
|
+
}
|
|
46
120
|
printMigrationSummary() {
|
|
47
121
|
if (this.changes.size === 0) {
|
|
48
122
|
return;
|
|
@@ -64,8 +138,8 @@ class SchemaManager {
|
|
|
64
138
|
});
|
|
65
139
|
});
|
|
66
140
|
}
|
|
67
|
-
createTable(
|
|
68
|
-
return __awaiter(this,
|
|
141
|
+
createTable(table_1, columns_1, indexes_1, foreignKeys_1) {
|
|
142
|
+
return __awaiter(this, arguments, void 0, function* (table, columns, indexes, foreignKeys, dryRun = false) {
|
|
69
143
|
const adapter = (0, Database_1.getAdapter)();
|
|
70
144
|
const colsSql = Object.entries(columns).map(([k, v]) => `\`${k}\` ${v}`).join(", ");
|
|
71
145
|
let indexSql = "";
|
|
@@ -77,6 +151,13 @@ class SchemaManager {
|
|
|
77
151
|
fkSql = ", " + foreignKeys.map(fk => `CONSTRAINT \`${fk.constraintName}\` FOREIGN KEY (\`${fk.column}\`) REFERENCES \`${fk.referenceTable}\` (\`${fk.referenceColumn}\`)`).join(", ");
|
|
78
152
|
}
|
|
79
153
|
const sql = `CREATE TABLE \`${table}\` (${colsSql}${indexSql}${fkSql});`;
|
|
154
|
+
if (dryRun) {
|
|
155
|
+
this.pendingSql.push(sql);
|
|
156
|
+
const columnChanges = Object.keys(columns).map(col => `+ ${col} (would be added)`);
|
|
157
|
+
const fkChanges = foreignKeys.map(fk => `+ FK ${fk.constraintName} (would be added)`);
|
|
158
|
+
this.changes.set(table, [...columnChanges, ...fkChanges]);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
80
161
|
const conn = yield (0, Database_1.getConnection)();
|
|
81
162
|
try {
|
|
82
163
|
yield (0, Database_1.execute)(sql, [], conn);
|
|
@@ -91,6 +172,10 @@ class SchemaManager {
|
|
|
91
172
|
for (const col of indexes) {
|
|
92
173
|
const indexName = `idx_${table}_${col}`;
|
|
93
174
|
const indexCreationSql = `CREATE INDEX \`${indexName}\` ON \`${table}\` (\`${col}\`);`;
|
|
175
|
+
if (dryRun) {
|
|
176
|
+
this.pendingSql.push(indexCreationSql);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
94
179
|
const indexConn = yield (0, Database_1.getConnection)();
|
|
95
180
|
try {
|
|
96
181
|
yield (0, Database_1.execute)(indexCreationSql, [], indexConn);
|
|
@@ -102,9 +187,64 @@ class SchemaManager {
|
|
|
102
187
|
}
|
|
103
188
|
});
|
|
104
189
|
}
|
|
105
|
-
updateTable(
|
|
106
|
-
return __awaiter(this,
|
|
190
|
+
updateTable(table_1, desired_1, indexes_1, existing_1, primaryKey_1, foreignKeys_1, existingConstraints_1) {
|
|
191
|
+
return __awaiter(this, arguments, void 0, function* (table, desired, indexes, existing, primaryKey, foreignKeys, existingConstraints, dryRun = false, enableDropWarnings = false) {
|
|
107
192
|
const tableChanges = [];
|
|
193
|
+
// Improved normalize function for better MySQL/PostgreSQL type comparison
|
|
194
|
+
const normalize = (t) => {
|
|
195
|
+
let normalized = t.toLowerCase().replace(/\s/g, '');
|
|
196
|
+
// PostgreSQL types to MySQL equivalents
|
|
197
|
+
normalized = normalized.replace('character varying', 'varchar');
|
|
198
|
+
normalized = normalized.replace('int4', 'integer');
|
|
199
|
+
normalized = normalized.replace('int8', 'bigint');
|
|
200
|
+
normalized = normalized.replace('float8', 'double');
|
|
201
|
+
normalized = normalized.replace('float4', 'real');
|
|
202
|
+
normalized = normalized.replace('bool', 'boolean');
|
|
203
|
+
// Remove length from integer types for comparison
|
|
204
|
+
normalized = normalized.replace(/^(int|integer|tinyint|smallint|mediumint|bigint)\(\d+\)/, '$1');
|
|
205
|
+
// Normalize int/integer
|
|
206
|
+
if (normalized === 'int')
|
|
207
|
+
normalized = 'integer';
|
|
208
|
+
// Handle SERIAL -> INTEGER for comparison
|
|
209
|
+
if (normalized.includes('serial'))
|
|
210
|
+
normalized = normalized.replace('serial', 'integer');
|
|
211
|
+
return normalized;
|
|
212
|
+
};
|
|
213
|
+
if (dryRun) {
|
|
214
|
+
// In dryRun mode, just collect SQL statements without executing
|
|
215
|
+
for (const [col, type] of Object.entries(desired)) {
|
|
216
|
+
if (col === primaryKey)
|
|
217
|
+
continue;
|
|
218
|
+
if (!existing.hasOwnProperty(col)) {
|
|
219
|
+
const sql = `ALTER TABLE \`${table}\` ADD COLUMN \`${col}\` ${type};`;
|
|
220
|
+
this.pendingSql.push(sql);
|
|
221
|
+
tableChanges.push(`+ ${col} (would be added)`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const existingType = normalize(existing[col]);
|
|
225
|
+
const desiredType = normalize(type.split(' ')[0]);
|
|
226
|
+
if (existingType !== desiredType) {
|
|
227
|
+
const sql = `ALTER TABLE \`${table}\` MODIFY COLUMN \`${col}\` ${type};`;
|
|
228
|
+
this.pendingSql.push(sql);
|
|
229
|
+
tableChanges.push(`~ ${col} (would be changed: ${existing[col].toUpperCase()} → ${type.split(' ')[0].toUpperCase()})`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Check for foreign keys
|
|
234
|
+
if (foreignKeys && foreignKeys.length > 0) {
|
|
235
|
+
for (const fk of foreignKeys) {
|
|
236
|
+
if (!existingConstraints.includes(fk.constraintName)) {
|
|
237
|
+
const sql = `ALTER TABLE \`${table}\` ADD CONSTRAINT \`${fk.constraintName}\` FOREIGN KEY (\`${fk.column}\`) REFERENCES \`${fk.referenceTable}\` (\`${fk.referenceColumn}\`);`;
|
|
238
|
+
this.pendingSql.push(sql);
|
|
239
|
+
tableChanges.push(`+ FK ${fk.constraintName} (would be added)`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (tableChanges.length > 0) {
|
|
244
|
+
this.changes.set(table, tableChanges);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
108
248
|
const conn = yield (0, Database_1.getConnection)();
|
|
109
249
|
try {
|
|
110
250
|
for (const [col, type] of Object.entries(desired)) {
|
|
@@ -117,11 +257,6 @@ class SchemaManager {
|
|
|
117
257
|
tableChanges.push(`+ ${col} (added)`);
|
|
118
258
|
}
|
|
119
259
|
else {
|
|
120
|
-
const normalize = (t) => {
|
|
121
|
-
let normalized = t.toLowerCase().replace(/\s/g, '').replace('character varying', 'varchar');
|
|
122
|
-
normalized = normalized.replace(/^(int|integer|tinyint|smallint|mediumint|bigint)\(\d+\)/, '$1');
|
|
123
|
-
return normalized;
|
|
124
|
-
};
|
|
125
260
|
const existingType = normalize(existing[col]);
|
|
126
261
|
const desiredType = normalize(type.split(' ')[0]);
|
|
127
262
|
if (existingType !== desiredType) {
|
|
@@ -141,6 +276,36 @@ class SchemaManager {
|
|
|
141
276
|
}
|
|
142
277
|
}
|
|
143
278
|
}
|
|
279
|
+
// Check for columns that exist in DB but not in model (potential DROP candidates)
|
|
280
|
+
if (enableDropWarnings) {
|
|
281
|
+
const desiredColumns = new Set(Object.keys(desired));
|
|
282
|
+
for (const existingCol of Object.keys(existing)) {
|
|
283
|
+
if (existingCol === primaryKey)
|
|
284
|
+
continue;
|
|
285
|
+
if (!desiredColumns.has(existingCol)) {
|
|
286
|
+
// Check if this column was already warned about
|
|
287
|
+
const warningFile = `.adatabase_drop_warning_${table}_${existingCol}`;
|
|
288
|
+
const fs = yield Promise.resolve().then(() => __importStar(require('fs'))).then(m => m.promises);
|
|
289
|
+
try {
|
|
290
|
+
yield fs.access(warningFile);
|
|
291
|
+
// Warning file exists - this is the second run, execute DROP
|
|
292
|
+
const sql = `ALTER TABLE \`${table}\` DROP COLUMN \`${existingCol}\`;`;
|
|
293
|
+
yield (0, Database_1.execute)(sql, [], conn);
|
|
294
|
+
tableChanges.push(`- ${existingCol} (REMOVED)`);
|
|
295
|
+
yield fs.unlink(warningFile);
|
|
296
|
+
}
|
|
297
|
+
catch (_a) {
|
|
298
|
+
// Warning file doesn't exist - create it and warn user
|
|
299
|
+
yield fs.writeFile(warningFile, `Column ${existingCol} scheduled for removal from table ${table}.\nCreated: ${new Date().toISOString()}`);
|
|
300
|
+
this.pendingDropWarnings.push({
|
|
301
|
+
table,
|
|
302
|
+
column: existingCol,
|
|
303
|
+
warningFile
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
144
309
|
if (tableChanges.length > 0) {
|
|
145
310
|
this.changes.set(table, tableChanges);
|
|
146
311
|
}
|
|
@@ -201,7 +366,7 @@ class SchemaManager {
|
|
|
201
366
|
return { columns, indexes, primaryKey, foreignKeys };
|
|
202
367
|
}
|
|
203
368
|
for (const [prop, opts] of colMeta.entries()) {
|
|
204
|
-
const colName = (opts === null || opts === void 0 ? void 0 : opts.name) ? opts.name : camelToSnake(prop);
|
|
369
|
+
const colName = (opts === null || opts === void 0 ? void 0 : opts.name) ? opts.name : (0, util_1.camelToSnake)(prop);
|
|
205
370
|
if (opts.index) {
|
|
206
371
|
indexes.push(colName);
|
|
207
372
|
}
|
|
@@ -221,7 +386,10 @@ class SchemaManager {
|
|
|
221
386
|
continue;
|
|
222
387
|
}
|
|
223
388
|
let sqlType = this.getSqlTypeForClass(type);
|
|
224
|
-
if (
|
|
389
|
+
if (opts === null || opts === void 0 ? void 0 : opts.text) {
|
|
390
|
+
sqlType = "TEXT";
|
|
391
|
+
}
|
|
392
|
+
else if (type === Number && opts.decimal) {
|
|
225
393
|
if (Array.isArray(opts.decimal) && opts.decimal.length === 2) {
|
|
226
394
|
sqlType = `DECIMAL(${opts.decimal[0]},${opts.decimal[1]})`;
|
|
227
395
|
}
|
|
@@ -258,14 +426,14 @@ class SchemaManager {
|
|
|
258
426
|
continue;
|
|
259
427
|
// Find the column name for the foreign key property
|
|
260
428
|
const colOpts = colMeta.get(fkProp);
|
|
261
|
-
const fkColName = (colOpts === null || colOpts === void 0 ? void 0 : colOpts.name) ? colOpts.name : camelToSnake(fkProp);
|
|
429
|
+
const fkColName = (colOpts === null || colOpts === void 0 ? void 0 : colOpts.name) ? colOpts.name : (0, util_1.camelToSnake)(fkProp);
|
|
262
430
|
// Find primary key of target model
|
|
263
431
|
let targetPk = 'id';
|
|
264
432
|
const targetColMeta = (0, Column_1.getColumnMeta)(targetModel);
|
|
265
433
|
if (targetColMeta) {
|
|
266
434
|
for (const [tProp, tOpts] of targetColMeta.entries()) {
|
|
267
435
|
if (tOpts.id) {
|
|
268
|
-
targetPk = tOpts.name ? tOpts.name : camelToSnake(tProp);
|
|
436
|
+
targetPk = tOpts.name ? tOpts.name : (0, util_1.camelToSnake)(tProp);
|
|
269
437
|
break;
|
|
270
438
|
}
|
|
271
439
|
}
|
package/dist/Security.js
CHANGED
|
@@ -17,7 +17,7 @@ function validateOperator(operator) {
|
|
|
17
17
|
}
|
|
18
18
|
function getAndValidateColumnName(prop, meta, modelName) {
|
|
19
19
|
if (!meta.has(prop)) {
|
|
20
|
-
throw new PersistenceException_1.
|
|
20
|
+
throw new PersistenceException_1.InvalidColumnException(prop, modelName);
|
|
21
21
|
}
|
|
22
22
|
const opts = meta.get(prop);
|
|
23
23
|
return (opts === null || opts === void 0 ? void 0 : opts.name) ? opts.name : (0, util_1.camelToSnake)(prop);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { IDatabaseAdapter, GenericConnection } from './IDatabaseAdapter';
|
|
2
|
+
import { PersistenceException } from '../PersistenceException';
|
|
3
|
+
export declare function mapMySQLError(error: any): PersistenceException;
|
|
2
4
|
export declare class MySQLAdapter implements IDatabaseAdapter {
|
|
3
5
|
readonly type = "mysql";
|
|
4
6
|
private pool;
|
|
@@ -13,7 +13,34 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.MySQLAdapter = void 0;
|
|
16
|
+
exports.mapMySQLError = mapMySQLError;
|
|
16
17
|
const promise_1 = __importDefault(require("mysql2/promise"));
|
|
18
|
+
const PersistenceException_1 = require("../PersistenceException");
|
|
19
|
+
function sanitizeError(error) {
|
|
20
|
+
const message = error.message || String(error);
|
|
21
|
+
// Remove connection strings and passwords from error messages
|
|
22
|
+
return message.replace(/password=([^\s&;]+)/gi, 'password=***')
|
|
23
|
+
.replace(/:[^:@]+@/g, ':***@')
|
|
24
|
+
.replace(/host=[^\s&;]+/gi, 'host=***');
|
|
25
|
+
}
|
|
26
|
+
function mapMySQLError(error) {
|
|
27
|
+
const code = error.code || '';
|
|
28
|
+
const safeMessage = sanitizeError(error);
|
|
29
|
+
if (code === 'ER_DUP_ENTRY') {
|
|
30
|
+
return new PersistenceException_1.DuplicateEntryException(safeMessage, error);
|
|
31
|
+
}
|
|
32
|
+
if (code === 'ER_NO_REFERENCED_ROW' || code === 'ER_NO_REFERENCED_ROW_2' ||
|
|
33
|
+
code === 'ER_ROW_IS_REFERENCED' || code === 'ER_ROW_IS_REFERENCED_2') {
|
|
34
|
+
return new PersistenceException_1.ForeignKeyException(safeMessage, error);
|
|
35
|
+
}
|
|
36
|
+
if (code === 'ECONNREFUSED' || code === 'PROTOCOL_CONNECTION_LOST' || code === 'ENOTFOUND') {
|
|
37
|
+
return new PersistenceException_1.ConnectionException(safeMessage, error);
|
|
38
|
+
}
|
|
39
|
+
if (code === 'ETIMEDOUT' || code === 'ER_LOCK_WAIT_TIMEOUT') {
|
|
40
|
+
return new PersistenceException_1.TimeoutException(safeMessage, error);
|
|
41
|
+
}
|
|
42
|
+
return new PersistenceException_1.PersistenceException(safeMessage, error);
|
|
43
|
+
}
|
|
17
44
|
class MySQLAdapter {
|
|
18
45
|
constructor() {
|
|
19
46
|
this.type = 'mysql';
|
|
@@ -30,7 +57,7 @@ class MySQLAdapter {
|
|
|
30
57
|
console.log("[aDatabase] MySQL connection pool initialized.");
|
|
31
58
|
}
|
|
32
59
|
catch (error) {
|
|
33
|
-
console.error("[aDatabase] MySQL connection pool initialization failed:", error);
|
|
60
|
+
console.error("[aDatabase] MySQL connection pool initialization failed:", sanitizeError(error));
|
|
34
61
|
process.exit(1);
|
|
35
62
|
}
|
|
36
63
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { IDatabaseAdapter, GenericConnection } from './IDatabaseAdapter';
|
|
2
|
+
import { PersistenceException } from '../PersistenceException';
|
|
3
|
+
export declare function mapPostgresError(error: any): PersistenceException;
|
|
2
4
|
export declare class PostgresAdapter implements IDatabaseAdapter {
|
|
3
5
|
readonly type = "postgres";
|
|
4
6
|
private pool;
|
|
@@ -10,7 +10,32 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.PostgresAdapter = void 0;
|
|
13
|
+
exports.mapPostgresError = mapPostgresError;
|
|
13
14
|
const pg_1 = require("pg");
|
|
15
|
+
const PersistenceException_1 = require("../PersistenceException");
|
|
16
|
+
function sanitizeError(error) {
|
|
17
|
+
const message = error.message || String(error);
|
|
18
|
+
return message.replace(/password=([^\s&;]+)/gi, 'password=***')
|
|
19
|
+
.replace(/:[^:@]+@/g, ':***@')
|
|
20
|
+
.replace(/host=[^\s&;]+/gi, 'host=***');
|
|
21
|
+
}
|
|
22
|
+
function mapPostgresError(error) {
|
|
23
|
+
const code = error.code || '';
|
|
24
|
+
const safeMessage = sanitizeError(error);
|
|
25
|
+
if (code === '23505') { // unique_violation
|
|
26
|
+
return new PersistenceException_1.DuplicateEntryException(safeMessage, error);
|
|
27
|
+
}
|
|
28
|
+
if (code === '23503') { // foreign_key_violation
|
|
29
|
+
return new PersistenceException_1.ForeignKeyException(safeMessage, error);
|
|
30
|
+
}
|
|
31
|
+
if (code === 'ECONNREFUSED' || code === '08006' || code === '08001') {
|
|
32
|
+
return new PersistenceException_1.ConnectionException(safeMessage, error);
|
|
33
|
+
}
|
|
34
|
+
if (code === '57014' || code === '57P01') { // query_canceled or admin_shutdown
|
|
35
|
+
return new PersistenceException_1.TimeoutException(safeMessage, error);
|
|
36
|
+
}
|
|
37
|
+
return new PersistenceException_1.PersistenceException(safeMessage, error);
|
|
38
|
+
}
|
|
14
39
|
class PostgresAdapter {
|
|
15
40
|
constructor() {
|
|
16
41
|
this.type = 'postgres';
|
|
@@ -28,7 +53,7 @@ class PostgresAdapter {
|
|
|
28
53
|
console.log("[aDatabase] PostgreSQL connection pool initialized.");
|
|
29
54
|
}
|
|
30
55
|
catch (error) {
|
|
31
|
-
console.error("[aDatabase] PostgreSQL connection pool initialization failed:", error);
|
|
56
|
+
console.error("[aDatabase] PostgreSQL connection pool initialization failed:", sanitizeError(error));
|
|
32
57
|
process.exit(1);
|
|
33
58
|
}
|
|
34
59
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -27,3 +27,5 @@ __exportStar(require("./decorators/HasMany"), exports);
|
|
|
27
27
|
__exportStar(require("./decorators/HasOne"), exports);
|
|
28
28
|
__exportStar(require("./decorators/BelongsTo"), exports);
|
|
29
29
|
__exportStar(require("./PersistenceException"), exports);
|
|
30
|
+
__exportStar(require("./util"), exports);
|
|
31
|
+
__exportStar(require("./Security"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aceitadev/adatabase",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Uma biblioteca para facilitar a interação com bancos de dados MySQL e PostgreSQL em projetos TypeScript/Node.js.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/README.md
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
# aDatabase ORM
|
|
2
|
-
|
|
3
|
-
aDatabase is a lightweight and easy-to-use ORM (Object-Relational Mapping) for TypeScript, inspired by the Active Record pattern. It allows you to interact with your database using object-oriented syntax, abstracting away the need to write raw SQL queries for most common operations.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
* **Active Record Pattern**: Models represent database tables and instances represent rows.
|
|
8
|
-
* **Decorator-based Schema**: Define your database schema using simple decorators.
|
|
9
|
-
* **Multi-database Support**: Supports both MySQL and PostgreSQL.
|
|
10
|
-
* **Query Builder**: A fluent API for building complex queries.
|
|
11
|
-
* **Schema Migration**: Keep your database schema in sync with your models.
|
|
12
|
-
* **Relationships**: Supports `HasOne`, `HasMany`, and `BelongsTo` relationships.
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm install @aceitadev/adatabase
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Configuration
|
|
21
|
-
|
|
22
|
-
First, you need to initialize the database connection. This should be done once when your application starts.
|
|
23
|
-
|
|
24
|
-
```typescript
|
|
25
|
-
import { init } from '@aceitadev/adatabase';
|
|
26
|
-
|
|
27
|
-
init('mysql', {
|
|
28
|
-
host: 'localhost',
|
|
29
|
-
user: 'root',
|
|
30
|
-
password: 'password',
|
|
31
|
-
database: 'my_database'
|
|
32
|
-
});
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Supported database types are `'mysql'` and `'postgres'`.
|
|
36
|
-
|
|
37
|
-
## Defining Models
|
|
38
|
-
|
|
39
|
-
Models are defined as classes that extend `ActiveRecord`. You use decorators to map the class and its properties to a database table and its columns.
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
import { ActiveRecord, Table, Id, Column } from '@aceitadev/adatabase';
|
|
43
|
-
|
|
44
|
-
@Table('users')
|
|
45
|
-
export class User extends ActiveRecord {
|
|
46
|
-
@Id()
|
|
47
|
-
id: number;
|
|
48
|
-
|
|
49
|
-
@Column({ type: String })
|
|
50
|
-
name: string;
|
|
51
|
-
|
|
52
|
-
@Column({ type: String, unique: true })
|
|
53
|
-
email: string;
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## CRUD Operations
|
|
58
|
-
|
|
59
|
-
### Creating and Saving Records
|
|
60
|
-
|
|
61
|
-
To create a new record, instantiate a model and call the `save()` method.
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
const user = new User();
|
|
65
|
-
user.name = 'John Doe';
|
|
66
|
-
user.email = 'john.doe@example.com';
|
|
67
|
-
await user.save();
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Finding Records
|
|
71
|
-
|
|
72
|
-
Use the static `find()` method to get a `QueryBuilder` instance.
|
|
73
|
-
|
|
74
|
-
**Find all users:**
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
const users = await User.find().get();
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
**Find a single user by ID:**
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
const user = await User.find().where('id', '=', 1).first();
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
**Find users with a specific condition:**
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
const users = await User.find().where('name', 'LIKE', 'John%').get();
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Updating Records
|
|
93
|
-
|
|
94
|
-
To update a record, modify its properties and call `save()` again.
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
const user = await User.find().where('id', '=', 1).first();
|
|
98
|
-
if (user) {
|
|
99
|
-
user.name = 'Jane Doe';
|
|
100
|
-
await user.save();
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Deleting Records
|
|
105
|
-
|
|
106
|
-
To delete a record, call the `delete()` method on a model instance.
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
const user = await User.find().where('id', '=', 1).first();
|
|
110
|
-
if (user) {
|
|
111
|
-
await user.delete();
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## Relationships
|
|
116
|
-
|
|
117
|
-
You can define relationships between your models using the `@BelongsTo`, `@HasMany`, and `@HasOne` decorators.
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
import { ActiveRecord, Table, Id, Column, BelongsTo, HasMany } from '@aceitadev/adatabase';
|
|
121
|
-
|
|
122
|
-
@Table('posts')
|
|
123
|
-
export class Post extends ActiveRecord {
|
|
124
|
-
@Id()
|
|
125
|
-
id: number;
|
|
126
|
-
|
|
127
|
-
@Column({ type: String })
|
|
128
|
-
title: string;
|
|
129
|
-
|
|
130
|
-
@Column({ type: Number })
|
|
131
|
-
userId: number;
|
|
132
|
-
|
|
133
|
-
@BelongsTo(() => User, { foreignKey: 'userId' })
|
|
134
|
-
user: User;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
@Table('users')
|
|
138
|
-
export class User extends ActiveRecord {
|
|
139
|
-
@Id()
|
|
140
|
-
id: number;
|
|
141
|
-
|
|
142
|
-
@Column({ type: String })
|
|
143
|
-
name: string;
|
|
144
|
-
|
|
145
|
-
@HasMany(() => Post, { foreignKey: 'userId' })
|
|
146
|
-
posts: Post[];
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
To eager-load relationships, use the `include()` method in the `QueryBuilder`.
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
const userWithPosts = await User.find().where('id', '=', 1).include(Post).first();
|
|
154
|
-
console.log(userWithPosts.posts);
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## Schema Migration
|
|
158
|
-
|
|
159
|
-
The `SchemaManager` helps you keep your database schema in sync with your models.
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
import { SchemaManager } from '@aceitadev/adatabase';
|
|
163
|
-
import { User, Post } from './models'; // Import your models
|
|
164
|
-
|
|
165
|
-
const schemaManager = new SchemaManager([User, Post]);
|
|
166
|
-
await schemaManager.migrate();
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
When you run `migrate()`, the `SchemaManager` will:
|
|
170
|
-
* Create tables that don't exist.
|
|
171
|
-
* Add columns that are missing from existing tables.
|