@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.
@@ -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
- function camelToSnake(s) {
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;
@@ -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
  }
@@ -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
- function camelToSnake(s) {
21
- return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
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
- if (!allowedOperators.includes(c.operator.toUpperCase())) {
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
- const allRelationMetas = [
120
- { meta: (0, BelongsTo_1.getBelongsToMeta)(this.model), type: 'BelongsTo' },
121
- { meta: (0, HasMany_1.getHasManyMeta)(this.model), type: 'HasMany' },
122
- { meta: (0, HasOne_1.getHasOneMeta)(this.model), type: 'HasOne' }
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
- const relationAlias = `t${index + 2}`;
127
- let relationInfo = null;
128
- for (const { meta, type } of allRelationMetas) {
129
- if (meta) {
130
- for (const [prop, opts] of meta.entries()) {
131
- if (opts.model() === includeModel) {
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
- if (relationInfo) {
141
- const relatedTable = (0, Table_1.getTableName)(includeModel);
142
- const relatedColumnsMeta = (0, Column_1.getColumnMeta)(includeModel);
143
- if (!relatedTable || !relatedColumnsMeta)
144
- return;
145
- for (const prop of relatedColumnsMeta.keys()) {
146
- const colName = this.getColumnName(prop, relatedColumnsMeta);
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
- else { // HasOne or HasMany
155
- const foreignKey = relationInfo.opts.foreignKey;
156
- const foreignKeyCol = this.getColumnName(foreignKey, relatedColumnsMeta);
157
- joinClause += ` LEFT JOIN \`${relatedTable}\` AS ${relationAlias} ON ${baseAlias}.id = ${relationAlias}.\`${foreignKeyCol}\``;
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
- return this.mapRowsToEntities(rows);
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
- for (const { meta, type } of allRelationMetas) {
214
- if (meta) {
215
- for (const [relationProp, opts] of meta.entries()) {
216
- const prefix = `${relationProp}__`;
217
- if (row[`${prefix}id`] === null)
218
- continue;
219
- const relatedModelCtor = opts.model();
220
- const relatedObj = new relatedModelCtor();
221
- const relatedColsMeta = (0, Column_1.getColumnMeta)(relatedModelCtor);
222
- for (const [prop] of relatedColsMeta.entries()) {
223
- relatedObj[prop] = row[`${prefix}${prop}`];
224
- }
225
- if (type === 'HasMany') {
226
- if (!mainEntity[relationProp]) {
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
  }
@@ -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;
@@ -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
- function camelToSnake(s) {
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(table, columns, indexes, foreignKeys) {
68
- return __awaiter(this, void 0, void 0, function* () {
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(table, desired, indexes, existing, primaryKey, foreignKeys, existingConstraints) {
106
- return __awaiter(this, void 0, void 0, function* () {
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 (type === Number && opts.decimal) {
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.PersistenceException(`Property '${prop}' is not a mapped column on model '${modelName}'. It cannot be used in queries.`, null);
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
  }
@@ -5,6 +5,7 @@ export type ColumnOptions = {
5
5
  adapter?: new () => ColumnAdapter;
6
6
  unique?: boolean;
7
7
  limit?: number;
8
+ text?: boolean;
8
9
  type?: any;
9
10
  index?: boolean;
10
11
  decimal?: boolean | [number, number];
package/dist/index.d.ts CHANGED
@@ -11,3 +11,5 @@ export * from "./decorators/HasMany";
11
11
  export * from "./decorators/HasOne";
12
12
  export * from "./decorators/BelongsTo";
13
13
  export * from "./PersistenceException";
14
+ export * from "./util";
15
+ export * from "./Security";
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.8.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.