masterrecord 0.2.36 → 0.3.1

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.
Files changed (38) hide show
  1. package/.claude/settings.local.json +20 -1
  2. package/Entity/entityModel.js +6 -0
  3. package/Entity/entityTrackerModel.js +20 -3
  4. package/Entity/fieldTransformer.js +266 -0
  5. package/Migrations/migrationMySQLQuery.js +145 -1
  6. package/Migrations/migrationPostgresQuery.js +402 -0
  7. package/Migrations/migrationSQLiteQuery.js +145 -1
  8. package/Migrations/schema.js +131 -28
  9. package/QueryLanguage/queryMethods.js +193 -15
  10. package/QueryLanguage/queryParameters.js +136 -0
  11. package/QueryLanguage/queryScript.js +13 -4
  12. package/SQLLiteEngine.js +331 -20
  13. package/context.js +91 -14
  14. package/docs/INCLUDES_CLARIFICATION.md +202 -0
  15. package/docs/METHODS_REFERENCE.md +184 -0
  16. package/docs/MIGRATIONS_GUIDE.md +699 -0
  17. package/docs/POSTGRESQL_SETUP.md +415 -0
  18. package/examples/jsonArrayTransformer.js +215 -0
  19. package/mySQLEngine.js +273 -17
  20. package/package.json +3 -3
  21. package/postgresEngine.js +600 -483
  22. package/postgresSyncConnect.js +209 -0
  23. package/readme.md +1046 -416
  24. package/test/anyCommaStringTest.js +237 -0
  25. package/test/anyMethodTest.js +176 -0
  26. package/test/findByIdTest.js +227 -0
  27. package/test/includesFeatureTest.js +183 -0
  28. package/test/includesTransformTest.js +110 -0
  29. package/test/newMethodTest.js +330 -0
  30. package/test/newMethodUnitTest.js +320 -0
  31. package/test/parameterizedPlaceholderTest.js +159 -0
  32. package/test/postgresEngineTest.js +463 -0
  33. package/test/postgresIntegrationTest.js +381 -0
  34. package/test/securityTest.js +268 -0
  35. package/test/singleDollarPlaceholderTest.js +238 -0
  36. package/test/transformerTest.js +287 -0
  37. package/test/verifyFindById.js +169 -0
  38. package/test/verifyNewMethod.js +191 -0
package/postgresEngine.js CHANGED
@@ -1,618 +1,735 @@
1
- // Version 0.0.12
2
- var tools = require('masterrecord/Tools');
1
+ // Version 0.1.0 - Complete PostgreSQL implementation with pg 8.16.3
2
+ const tools = require('./Tools');
3
+ const FieldTransformer = require('./Entity/fieldTransformer');
4
+ const { Pool } = require('pg');
3
5
 
4
6
  class postgresEngine {
5
7
 
6
- unsupportedWords = ["order"]
8
+ constructor() {
9
+ this.pool = null;
10
+ this.db = null;
11
+ this.dbType = 'postgres';
12
+ this.unsupportedWords = ["order"];
13
+ }
14
+
15
+ /**
16
+ * Initialize PostgreSQL connection pool
17
+ * @param {Object} config - PostgreSQL connection config
18
+ */
19
+ async initialize(config) {
20
+ this.pool = new Pool({
21
+ host: config.host || 'localhost',
22
+ port: config.port || 5432,
23
+ database: config.database,
24
+ user: config.user,
25
+ password: config.password,
26
+ max: config.max || 20,
27
+ idleTimeoutMillis: config.idleTimeoutMillis || 30000,
28
+ connectionTimeoutMillis: config.connectionTimeoutMillis || 2000,
29
+ });
30
+
31
+ // Test connection
32
+ try {
33
+ const client = await this.pool.connect();
34
+ console.log('PostgreSQL connected successfully');
35
+ client.release();
36
+ } catch (err) {
37
+ console.error('PostgreSQL connection error:', err);
38
+ throw err;
39
+ }
40
+ }
7
41
 
8
- update(query){
9
- var sqlQuery = ` UPDATE [${query.tableName}]
10
- SET ${query.arg}
11
- WHERE [${query.tableName}].[${query.primaryKey}] = ${query.primaryKeyValue}` // primary key for that table =
12
- return this._run(sqlQuery);
42
+ /**
43
+ * UPDATE with parameterized query
44
+ */
45
+ async update(query) {
46
+ // Use parameterized query for security
47
+ // query.arg now contains {query, params} from _buildSQLEqualToParameterized
48
+ if (query.arg && typeof query.arg === 'object' && query.arg.query && query.arg.params) {
49
+ const sqlQuery = `UPDATE ${query.tableName} SET ${query.arg.query} WHERE ${query.tableName}.${query.primaryKey} = $${query.arg.params.length + 1}`;
50
+ // Add primaryKeyValue to params array
51
+ const params = [...query.arg.params, query.primaryKeyValue];
52
+ return await this._runWithParams(sqlQuery, params);
53
+ } else {
54
+ // Fallback for legacy support
55
+ const sqlQuery = `UPDATE ${query.tableName} SET ${query.arg} WHERE ${query.tableName}.${query.primaryKey} = $1`;
56
+ return await this._runWithParams(sqlQuery, [query.primaryKeyValue]);
57
+ }
13
58
  }
14
59
 
15
- delete(queryObject){
16
- var sqlObject = this._buildDeleteObject(queryObject);
17
- var sqlQuery = `DELETE FROM [${sqlObject.tableName}] WHERE [${sqlObject.tableName}].[${sqlObject.primaryKey}] = ${sqlObject.value}`;
18
- return this._execute(sqlQuery);
60
+ /**
61
+ * DELETE with parameterized query
62
+ */
63
+ async delete(queryObject) {
64
+ const sqlObject = this._buildDeleteObject(queryObject);
65
+ const sqlQuery = `DELETE FROM ${sqlObject.tableName} WHERE ${sqlObject.tableName}.${sqlObject.primaryKey} = $1`;
66
+ return await this._runWithParams(sqlQuery, [sqlObject.value]);
19
67
  }
20
68
 
21
- insert(queryObject){
22
- var sqlObject = this._buildSQLInsertObject(queryObject, queryObject.__entity);
23
- var query = `INSERT INTO [${sqlObject.tableName}] (${sqlObject.columns})
24
- VALUES (${sqlObject.values})`;
25
- var queryObj = this._run(query);
26
- var open = {
27
- "id": queryObj.lastInsertRowid
69
+ /**
70
+ * INSERT with parameterized query
71
+ * Postgres uses RETURNING to get the inserted ID
72
+ */
73
+ async insert(queryObject) {
74
+ const sqlObject = this._buildSQLInsertObjectParameterized(queryObject, queryObject.__entity);
75
+ if (sqlObject === -1) {
76
+ throw new Error('INSERT failed: No columns to insert');
77
+ }
78
+
79
+ // Get primary key name for RETURNING clause
80
+ const primaryKey = tools.getPrimaryKeyObject(queryObject.__entity);
81
+ const query = `INSERT INTO ${sqlObject.tableName} (${sqlObject.columns}) VALUES (${sqlObject.placeholders}) RETURNING ${primaryKey}`;
82
+
83
+ const result = await this._runWithParams(query, sqlObject.params);
84
+
85
+ return {
86
+ id: result.rows[0][primaryKey]
28
87
  };
29
- return open;
30
88
  }
31
89
 
32
- get(query, entity, context){
33
- var queryString = {};
90
+ /**
91
+ * SELECT single record
92
+ */
93
+ async get(query, entity, context) {
34
94
  try {
35
- if(query.raw){
36
- queryString.query = query.raw;
95
+ let queryString;
96
+ if (query.raw) {
97
+ queryString = { query: query.raw, params: [] };
98
+ } else if (typeof query === 'string') {
99
+ queryString = { query: query, params: [] };
100
+ } else {
101
+ queryString = this.buildQuery(query, entity, context);
37
102
  }
38
- else{
39
- if(typeof query === 'string'){
40
- queryString.query = query;
41
- }
42
- else{
43
- queryString = this.buildQuery(query, entity, context);
44
- }
45
- }
46
- if(queryString.query){
103
+
104
+ if (queryString.query) {
47
105
  console.log("SQL:", queryString.query);
48
- var queryReturn = this.db.prepare(queryString.query).get();
49
- return queryReturn;
106
+ console.log("Params:", queryString.params || []);
107
+ const result = await this._runWithParams(queryString.query, queryString.params || []);
108
+ return result.rows[0] || null;
50
109
  }
51
110
  return null;
52
111
  } catch (err) {
53
- console.error(err);
112
+ console.error('PostgreSQL get error:', err);
54
113
  return null;
55
114
  }
56
115
  }
57
116
 
58
- getCount(queryObject, entity, context){
59
- var query = queryObject.script;
60
- var queryString = {};
117
+ /**
118
+ * SELECT COUNT
119
+ */
120
+ async getCount(queryObject, entity, context) {
121
+ const query = queryObject.script;
61
122
  try {
62
- if(query.raw){
63
- queryString.query = query.raw;
64
- }
65
- else{
66
- if(query.count === undefined){
123
+ let queryString;
124
+ if (query.raw) {
125
+ queryString = { query: query.raw, params: [] };
126
+ } else {
127
+ if (query.count === undefined) {
67
128
  query.count = "none";
68
129
  }
69
- queryString.entity = this.getEntity(entity.__name, query.entityMap);
70
- queryString.query = `SELECT ${this.buildCount(query, entity)} ${this.buildFrom(query, entity)} ${this.buildWhere(query, entity)}`
130
+ const entityAlias = this.getEntity(entity.__name, query.entityMap);
131
+ queryString = {
132
+ query: `SELECT ${this.buildCount(query, entity)} ${this.buildFrom(query, entity)} ${this.buildWhere(query, entity)}`,
133
+ params: query.parameters ? query.parameters.getParams() : []
134
+ };
71
135
  }
72
- if(queryString.query){
73
- var queryCount = queryString.query
74
- console.log("SQL:", queryCount );
75
- var queryReturn = this.db.prepare(queryCount ).get();
76
- return queryReturn;
136
+
137
+ if (queryString.query) {
138
+ console.log("SQL:", queryString.query);
139
+ console.log("Params:", queryString.params);
140
+ const result = await this._runWithParams(queryString.query, queryString.params);
141
+ return result.rows[0] || null;
77
142
  }
78
143
  return null;
79
144
  } catch (err) {
80
- console.error(err);
145
+ console.error('PostgreSQL getCount error:', err);
81
146
  return null;
82
147
  }
83
148
  }
84
149
 
85
- all(query, entity, context){
86
- var selectQuery = {};
150
+ /**
151
+ * SELECT multiple records
152
+ */
153
+ async all(query, entity, context) {
87
154
  try {
88
- if(query.raw){
89
- selectQuery.query = query.raw;
90
- }
91
- else{
92
-
155
+ let selectQuery;
156
+ if (query.raw) {
157
+ selectQuery = { query: query.raw, params: [] };
158
+ } else {
93
159
  selectQuery = this.buildQuery(query, entity, context);
94
160
  }
95
- if(selectQuery.query){
161
+
162
+ if (selectQuery.query) {
96
163
  console.log("SQL:", selectQuery.query);
97
- var queryReturn = this.db.prepare(selectQuery.query).all();
98
- return queryReturn;
164
+ console.log("Params:", selectQuery.params || []);
165
+ const result = await this._runWithParams(selectQuery.query, selectQuery.params || []);
166
+ return result.rows || [];
99
167
  }
100
- return null;
168
+ return [];
101
169
  } catch (err) {
102
- console.error(err);
103
- return null;
170
+ console.error('PostgreSQL all error:', err);
171
+ return [];
104
172
  }
105
173
  }
106
174
 
107
- changeNullQuery(query){
108
- if(query.where){
109
- var whereClaus;
110
- whereClaus = query.where.expr.replace("=== null", "is null");
111
- if(whereClaus === query.where.expr){
112
- whereClaus = query.where.expr.replace("!= null", "is not null");
113
- }
114
- query.where.expr = whereClaus;
115
- }
116
-
175
+ /**
176
+ * Execute raw SQL with parameters
177
+ */
178
+ async exec(query, params = []) {
179
+ return await this._runWithParams(query, params);
117
180
  }
118
181
 
119
- buildCount(query, mainQuery){
120
- var entity = this.getEntity(query.parentName, query.entityMap);
121
- if(query.count){
122
- if(query.count !== "none"){
123
- return `COUNT(${entity}.${query.count.selectFields[0]})`
124
- }
125
- else{
126
- return `COUNT(*)`
127
- }
128
- }
129
- else{
130
- return ""
131
- }
132
- }
182
+ /**
183
+ * Build complete SELECT query with parameters
184
+ */
185
+ buildQuery(query, entity, context) {
186
+ const entityStr = this.getEntity(entity.__name, query.entityMap);
187
+ const params = query.parameters ? query.parameters.getParams() : [];
133
188
 
134
- buildQuery(query, entity, context, limit){
135
-
136
- var queryObject = {};
137
- queryObject.entity = this.getEntity(entity.__name, query.entityMap);
138
- queryObject.select = this.buildSelect(query, entity);
139
- queryObject.count = this.buildCount(query, entity);
140
- queryObject.from = this.buildFrom(query, entity);
141
- queryObject.include = this.buildInclude(query, entity, context, queryObject);
142
- queryObject.where = this.buildWhere(query, entity);
143
- queryObject.and = this.buildAnd(query, entity);
144
- queryObject.take = this.buildTake(query);
145
- queryObject.skip = this.buildSkip(query);
146
- queryObject.orderBy = this.buildOrderBy(query);
147
-
148
-
149
- var queryString = `${queryObject.select} ${queryObject.count} ${queryObject.from} ${queryObject.include} ${queryObject.where} ${queryObject.and} ${queryObject.orderBy} ${queryObject.take} ${queryObject.skip}`;
150
- return {
151
- query : queryString,
152
- entity : this.getEntity(entity.__name, query.entityMap)
153
- }
189
+ const sql = `SELECT ${this.buildSelectString(query, entity)} ${this.buildFrom(query, entity)} ${this.buildWhere(query, entity)} ${this.buildAnd(query, entity)} ${this.buildLimit(query)} ${this.buildSkip(query)} ${this.buildOrderBy(query)}`;
154
190
 
191
+ return {
192
+ query: sql,
193
+ params: params
194
+ };
155
195
  }
156
196
 
157
- buildOrderBy(query){
158
- // ORDER BY column1, column2, ... ASC|DESC;
159
- var $that = this;
160
- var orderByType = "ASC";
161
- var orderByEntity = query.orderBy;
162
- var strQuery = "";
163
- if(orderByEntity === false){
164
- orderByType = "DESC";
165
- orderByEntity = query.orderByDesc;
166
- }
167
- if(orderByEntity){
168
- var entity = this.getEntity(query.parentName, query.entityMap);
169
- var fieldList = "";
170
- for (const item in orderByEntity.selectFields) {
171
- fieldList += `${entity}.${orderByEntity.selectFields[item]}, `;
172
- };
173
- fieldList = fieldList.replace(/,\s*$/, "");
174
- strQuery = "ORDER BY";
175
- strQuery += ` ${fieldList} ${orderByType}`;
197
+ buildSelectString(query, entity) {
198
+ if (query.select) {
199
+ return query.select;
176
200
  }
177
- return strQuery;
201
+ return `${tools.convertEntityToSelectParameterString(entity)}`;
178
202
  }
179
203
 
180
- buildTake(query){
181
- if(query.take){
182
- return `LIMIT ${query.take}`
183
- }
184
- else{
185
- return "";
204
+ buildCount(query, entity) {
205
+ const entityStr = this.getEntity(entity.__name, query.entityMap);
206
+ if (query.count === "none") {
207
+ return `COUNT(${entityStr}.*)`;
186
208
  }
209
+ return `COUNT(${entityStr}.${query.count})`;
187
210
  }
188
211
 
189
- buildSkip(query){
190
- if(query.skip){
191
- return `OFFSET ${query.skip}`
192
- }
193
- else{
194
- return "";
195
- }
212
+ buildFrom(query, entity) {
213
+ return `FROM ${entity.__name}`;
196
214
  }
197
215
 
198
- buildAnd(query, mainQuery){
199
- // loop through the AND
200
- // loop update ther where .expr
201
- var andEntity = query.and;
202
- var strQuery = "";
203
- var $that = this;
204
- var str = "";
205
-
206
- if(andEntity){
207
- var entity = this.getEntity(query.parentName, query.entityMap);
208
- var andList = [];
209
- for (let entityPart in andEntity) { // loop through list of and's
210
- var itemEntity = andEntity[entityPart]; // get the entityANd
211
- for (let table in itemEntity[query.parentName]) { // find the main table
212
- var item = itemEntity[query.parentName][table];
216
+ /**
217
+ * Build AND clause with placeholder detection
218
+ */
219
+ buildAnd(query, mainQuery) {
220
+ const andEntity = query.and;
221
+ let strQuery = "";
222
+ const $that = this;
223
+
224
+ if (andEntity) {
225
+ const entity = this.getEntity(query.parentName, query.entityMap);
226
+ const andList = [];
227
+
228
+ for (let entityPart in andEntity) {
229
+ const itemEntity = andEntity[entityPart];
230
+ for (let table in itemEntity[query.parentName]) {
231
+ const item = itemEntity[query.parentName][table];
213
232
  for (let exp in item.expressions) {
214
- var field = tools.capitalizeFirstLetter(item.expressions[exp].field);
215
- if(mainQuery[field]){
216
- if(mainQuery[field].isNavigational){
217
- entity = $that.getEntity(field, query.entityMap);
218
- field = item.fields[1];
219
- }
233
+ let field = tools.capitalizeFirstLetter(item.expressions[exp].field);
234
+ let entityRef = entity;
235
+
236
+ if (mainQuery[field] && mainQuery[field].isNavigational) {
237
+ entityRef = $that.getEntity(field, query.entityMap);
238
+ field = item.fields[1];
220
239
  }
221
- if(item.expressions[exp].arg === "null"){
222
- if(item.expressions[exp].func === "="){
223
- item.expressions[exp].func = "is"
224
- }
225
- if(item.expressions[exp].func === "!="){
226
- item.expressions[exp].func = "is not"
227
- }
240
+
241
+ let func = item.expressions[exp].func;
242
+ const arg = item.expressions[exp].arg;
243
+
244
+ // Handle NULL
245
+ if (arg === "null") {
246
+ if (func === "=") func = "IS";
247
+ if (func === "!=") func = "IS NOT";
228
248
  }
229
- if(strQuery === ""){
230
- if(item.expressions[exp].arg === "null"){
231
- strQuery = `${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
232
- }else{
233
- strQuery = `${entity}.${field} ${item.expressions[exp].func} '${item.expressions[exp].arg}'`;
249
+
250
+ if (strQuery === "") {
251
+ if (arg === "null") {
252
+ strQuery = `${entityRef}.${field} ${func} ${arg}`;
253
+ } else {
254
+ // Check if arg is a parameterized placeholder
255
+ const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
256
+ if (isPlaceholder || func === "IN") {
257
+ strQuery = `${entityRef}.${field} ${func} ${arg}`;
258
+ } else {
259
+ strQuery = `${entityRef}.${field} ${func} '${arg}'`;
260
+ }
234
261
  }
235
- }
236
- else{
237
- if(item.expressions[exp].arg === "null"){
238
- strQuery = `${strQuery} and ${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
239
- }else{
240
- strQuery = `${strQuery} and ${entity}.${field} ${item.expressions[exp].func} '${item.expressions[exp].arg}'`;
262
+ } else {
263
+ if (arg === "null") {
264
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
265
+ } else {
266
+ // Check if arg is a parameterized placeholder
267
+ const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
268
+ if (isPlaceholder || func === "IN") {
269
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
270
+ } else {
271
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} '${arg}'`;
272
+ }
241
273
  }
242
-
243
274
  }
244
275
  }
245
276
  andList.push(strQuery);
246
277
  }
247
278
  }
279
+
280
+ if (andList.length > 0) {
281
+ return `AND ${andList.join(" AND ")}`;
282
+ }
248
283
  }
249
-
250
- if(andList.length > 0){
251
- str = `and ${andList.join(" and ")}`;
252
- }
253
- return str
284
+
285
+ return "";
254
286
  }
255
287
 
256
- buildWhere(query, mainQuery){
257
- var whereEntity = query.where;
288
+ /**
289
+ * Build WHERE clause with placeholder detection
290
+ */
291
+ buildWhere(query, mainQuery) {
292
+ const whereEntity = query.where;
293
+ let strQuery = "";
294
+ const $that = this;
258
295
 
259
- var strQuery = "";
260
- var $that = this;
261
- if(whereEntity){
262
- var entity = this.getEntity(query.parentName, query.entityMap);
296
+ if (whereEntity) {
297
+ const entity = this.getEntity(query.parentName, query.entityMap);
298
+ const item = whereEntity[query.parentName].query;
263
299
 
264
- var item = whereEntity[query.parentName].query;
265
300
  for (let exp in item.expressions) {
266
- var field = tools.capitalizeFirstLetter(item.expressions[exp].field);
267
- if(mainQuery[field]){
268
- if(mainQuery[field].isNavigational){
269
- entity = $that.getEntity(field, query.entityMap);
270
- field = item.fields[1];
271
- }
301
+ let field = tools.capitalizeFirstLetter(item.expressions[exp].field);
302
+ let entityRef = entity;
303
+
304
+ if (mainQuery[field] && mainQuery[field].isNavigational) {
305
+ entityRef = $that.getEntity(field, query.entityMap);
306
+ field = item.fields[1];
272
307
  }
273
- if(item.expressions[exp].arg === "null"){
274
- if(item.expressions[exp].func === "="){
275
- item.expressions[exp].func = "is"
276
- }
277
- if(item.expressions[exp].func === "!="){
278
- item.expressions[exp].func = "is not"
279
- }
308
+
309
+ let func = item.expressions[exp].func;
310
+ const arg = item.expressions[exp].arg;
311
+
312
+ // Handle NULL
313
+ if (arg === "null") {
314
+ if (func === "=") func = "IS";
315
+ if (func === "!=") func = "IS NOT";
280
316
  }
281
- if(strQuery === ""){
282
- if(item.expressions[exp].arg === "null"){
283
- strQuery = `WHERE ${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
284
- }else{
285
- if(item.expressions[exp].func === "IN"){
286
- strQuery = `WHERE ${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
287
- }
288
- else{
289
- strQuery = `WHERE ${entity}.${field} ${item.expressions[exp].func} '${item.expressions[exp].arg}'`;
317
+
318
+ if (strQuery === "") {
319
+ if (arg === "null") {
320
+ strQuery = `WHERE ${entityRef}.${field} ${func} ${arg}`;
321
+ } else if (func === "IN") {
322
+ strQuery = `WHERE ${entityRef}.${field} ${func} ${arg}`;
323
+ } else {
324
+ // Check if arg is a parameterized placeholder ($1, $2, etc.)
325
+ const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
326
+ if (isPlaceholder) {
327
+ strQuery = `WHERE ${entityRef}.${field} ${func} ${arg}`;
328
+ } else {
329
+ strQuery = `WHERE ${entityRef}.${field} ${func} '${arg}'`;
290
330
  }
291
331
  }
292
- }
293
- else{
294
- if(item.expressions[exp].arg === "null"){
295
- strQuery = `${strQuery} and ${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
296
- }else{
297
- strQuery = `${strQuery} and ${entity}.${field} ${item.expressions[exp].func} '${item.expressions[exp].arg}'`;
332
+ } else {
333
+ if (arg === "null") {
334
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
335
+ } else if (func === "IN") {
336
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
337
+ } else {
338
+ // Check if arg is a parameterized placeholder
339
+ const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
340
+ if (isPlaceholder) {
341
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
342
+ } else {
343
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} '${arg}'`;
344
+ }
298
345
  }
299
-
300
346
  }
301
347
  }
302
-
303
-
304
-
305
348
  }
349
+
306
350
  return strQuery;
307
351
  }
308
352
 
309
- buildInclude( query, entity, context){
310
- var includeQuery = "";
311
- for (let part in query.include) {
312
- var includeEntity = query.include[part];
313
- var $that = this;
314
- if(includeEntity){
315
- var parentObj = includeEntity[query.parentName];
316
- var currentContext = "";
317
- if(includeEntity.selectFields){
318
- currentContext = context[tools.capitalizeFirstLetter(includeEntity.selectFields[0])];
319
- }
320
-
321
- if(parentObj){
322
- parentObj.entityMap = query.entityMap;
323
- var foreignKey = $that.getForeignKey(entity.__name, currentContext.__entity);
324
- var mainPrimaryKey = $that.getPrimarykey(entity);
325
- var mainEntity = $that.getEntity(entity.__name, query.entityMap);
326
- if(currentContext.__entity[entity.__name].type === "hasManyThrough"){
327
- var foreignTable = tools.capitalizeFirstLetter(currentContext.__entity[entity.__name].foreignTable); //to uppercase letter
328
- foreignKey = $that.getPrimarykey(currentContext.__entity);
329
- mainPrimaryKey = context[foreignTable].__entity[currentContext.__entity.__name].foreignKey;
330
- var mainEntity = $that.getEntity(foreignTable,query.entityMap);
331
- }
332
- // add foreign key to select so that it picks it up
333
- if(parentObj.select){
334
- parentObj.select.selectFields.push(foreignKey);
335
- }else{
336
- parentObj.select = {};
337
- parentObj.select.selectFields = [];
338
- parentObj.select.selectFields.push(foreignKey);
339
- }
340
-
341
- var innerQuery = $that.buildQuery(parentObj, currentContext.__entity, context);
342
-
343
- includeQuery += `LEFT JOIN (${innerQuery.query}) AS ${innerQuery.entity} ON ${ mainEntity}.${mainPrimaryKey} = ${innerQuery.entity}.${foreignKey} `;
344
-
345
- }
346
- }
353
+ buildLimit(query) {
354
+ if (query.take) {
355
+ return `LIMIT ${query.take}`;
347
356
  }
348
- return includeQuery;
357
+ return "";
349
358
  }
350
359
 
351
- buildFrom(query, entity){
352
- var entityName = this.getEntity(entity.__name, query.entityMap);
353
- if(entityName ){
354
- return `FROM ${entity.__name } AS ${entityName}`;
360
+ buildSkip(query) {
361
+ if (query.skip) {
362
+ return `OFFSET ${query.skip}`;
355
363
  }
356
- else{ return "" }
364
+ return "";
357
365
  }
358
366
 
359
- buildSelect(query, entity){
360
- // this means that there is a select statement
361
- var select = "SELECT";
362
- var arr = "";
363
- var $that = this;
364
- if(query.select){
365
- for (const item in query.select.selectFields) {
366
- arr += `${$that.getEntity(entity.__name, query.entityMap)}.${query.select.selectFields[item]}, `;
367
- };
368
-
367
+ buildOrderBy(query) {
368
+ if (query.orderBy) {
369
+ const entityStr = this.getEntity(query.parentName, query.entityMap);
370
+ return `ORDER BY ${entityStr}.${query.orderBy} ASC`;
371
+ } else if (query.orderByDescending) {
372
+ const entityStr = this.getEntity(query.parentName, query.entityMap);
373
+ return `ORDER BY ${entityStr}.${query.orderByDescending} DESC`;
369
374
  }
370
- else{
371
- var entityList = this.getEntityList(entity);
372
- for (const item in entityList) {
373
- arr += `${$that.getEntity(entity.__name, query.entityMap)}.${entityList[item]}, `;
374
- };
375
- }
376
- arr = arr.replace(/,\s*$/, "");
377
- return `${select} ${arr} `;
375
+ return "";
378
376
  }
379
377
 
380
- getForeignKey(name, entity){
381
- if(entity && name){
382
- return entity[name].foreignKey;
378
+ getEntity(name, list) {
379
+ for (let i = 0; i < list.length; i++) {
380
+ if (list[i].name === name) {
381
+ return list[i].entity;
382
+ }
383
383
  }
384
- }
384
+ return name;
385
+ }
386
+
387
+ /**
388
+ * Build SQL SET clause with parameterized queries for UPDATE (PostgreSQL)
389
+ * Returns {query: "column1 = $1, column2 = $2", params: [value1, value2]}
390
+ */
391
+ _buildSQLEqualToParameterized(model) {
392
+ const $that = this;
393
+ const sqlParts = [];
394
+ const params = [];
395
+ const dirtyFields = model.__dirtyFields;
396
+ let paramIndex = 1;
397
+
398
+ for (let column in dirtyFields) {
399
+ const fieldName = dirtyFields[column];
400
+ const entityDef = model.__entity[fieldName];
401
+
402
+ // Check for required fields
403
+ if (entityDef && entityDef.nullable === false && entityDef.primary !== true) {
404
+ let persistedValue;
405
+ switch (entityDef.type) {
406
+ case "integer":
407
+ persistedValue = model["_" + fieldName];
408
+ break;
409
+ case "belongsTo":
410
+ persistedValue = model["_" + fieldName] !== undefined ? model["_" + fieldName] : model[fieldName];
411
+ break;
412
+ default:
413
+ persistedValue = model[fieldName];
414
+ }
415
+ const isEmptyString = (typeof persistedValue === 'string') && (persistedValue.trim() === '');
416
+ if (persistedValue === undefined || persistedValue === null || isEmptyString) {
417
+ throw new Error(`Entity ${model.__entity.__name} column ${fieldName} is a required Field`);
418
+ }
419
+ }
385
420
 
386
- getPrimarykey(entity){
387
- for (const item in entity) {
388
- if(entity[item].primary){
389
- if(entity[item].primary === true){
390
- return entity[item].name;
421
+ let type = model.__entity[dirtyFields[column]].type;
422
+ if (model.__entity[dirtyFields[column]].relationshipType === "belongsTo") {
423
+ type = "belongsTo";
424
+ }
425
+
426
+ switch (type) {
427
+ case "belongsTo":
428
+ const foreignKey = model.__entity[dirtyFields[column]].foreignKey;
429
+ let fkValue = model[dirtyFields[column]];
430
+ // Apply toDatabase transformer
431
+ try {
432
+ fkValue = FieldTransformer.toDatabase(fkValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
433
+ } catch (transformError) {
434
+ throw new Error(`UPDATE failed: ${transformError.message}`);
391
435
  }
392
- }
393
- };
394
- }
436
+ try {
437
+ fkValue = $that._validateAndCoerceFieldType(fkValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
438
+ } catch (typeError) {
439
+ throw new Error(`UPDATE failed: ${typeError.message}`);
440
+ }
441
+ fkValue = $that._convertValueForDatabase(fkValue, model.__entity[dirtyFields[column]].type);
442
+ const fore = `_${dirtyFields[column]}`;
443
+ sqlParts.push(`${foreignKey} = $${paramIndex++}`);
444
+ params.push(model[fore]);
445
+ break;
446
+
447
+ case "integer":
448
+ let intValue = model["_" + dirtyFields[column]];
449
+ // Apply toDatabase transformer
450
+ try {
451
+ intValue = FieldTransformer.toDatabase(intValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
452
+ } catch (transformError) {
453
+ throw new Error(`UPDATE failed: ${transformError.message}`);
454
+ }
455
+ try {
456
+ intValue = $that._validateAndCoerceFieldType(intValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
457
+ } catch (typeError) {
458
+ throw new Error(`UPDATE failed: ${typeError.message}`);
459
+ }
460
+ intValue = $that._convertValueForDatabase(intValue, model.__entity[dirtyFields[column]].type);
461
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
462
+ params.push(intValue);
463
+ break;
464
+
465
+ case "string":
466
+ let strValue = model[dirtyFields[column]];
467
+ // Apply toDatabase transformer
468
+ try {
469
+ strValue = FieldTransformer.toDatabase(strValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
470
+ } catch (transformError) {
471
+ throw new Error(`UPDATE failed: ${transformError.message}`);
472
+ }
473
+ try {
474
+ strValue = $that._validateAndCoerceFieldType(strValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
475
+ } catch (typeError) {
476
+ throw new Error(`UPDATE failed: ${typeError.message}`);
477
+ }
478
+ strValue = $that._convertValueForDatabase(strValue, model.__entity[dirtyFields[column]].type);
479
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
480
+ params.push(strValue);
481
+ break;
482
+
483
+ case "boolean":
484
+ let boolValue = model[dirtyFields[column]];
485
+ // Apply toDatabase transformer
486
+ try {
487
+ boolValue = FieldTransformer.toDatabase(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
488
+ } catch (transformError) {
489
+ throw new Error(`UPDATE failed: ${transformError.message}`);
490
+ }
491
+ try {
492
+ boolValue = $that._validateAndCoerceFieldType(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
493
+ } catch (typeError) {
494
+ throw new Error(`UPDATE failed: ${typeError.message}`);
495
+ }
496
+ boolValue = $that._convertValueForDatabase(boolValue, model.__entity[dirtyFields[column]].type);
497
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
498
+ params.push(boolValue);
499
+ break;
500
+
501
+ case "time":
502
+ let timeValue = model[dirtyFields[column]];
503
+ // Apply toDatabase transformer
504
+ try {
505
+ timeValue = FieldTransformer.toDatabase(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
506
+ } catch (transformError) {
507
+ throw new Error(`UPDATE failed: ${transformError.message}`);
508
+ }
509
+ try {
510
+ timeValue = $that._validateAndCoerceFieldType(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
511
+ } catch (typeError) {
512
+ throw new Error(`UPDATE failed: ${typeError.message}`);
513
+ }
514
+ timeValue = $that._convertValueForDatabase(timeValue, model.__entity[dirtyFields[column]].type);
515
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
516
+ params.push(timeValue);
517
+ break;
395
518
 
396
- getForeignTable(name, entity){
397
- if(entity && name){
398
- return entity[name].foreignTable;
399
- }
400
- }
519
+ case "hasMany":
520
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
521
+ params.push(model[dirtyFields[column]]);
522
+ break;
401
523
 
402
- getInclude(name, query){
403
- var include = query.include;
404
- if(include){
405
- for (let part in include) {
406
- if(tools.capitalizeFirstLetter(include[part].selectFields[0]) === name){
407
- return include[part];
408
- }
524
+ default:
525
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
526
+ params.push(model[dirtyFields[column]]);
409
527
  }
410
528
  }
411
- else{
412
- return "";
413
- }
414
- }
415
529
 
416
- getEntity(name, maps){
417
- for (let item in maps) {
418
- var map = maps[item];
419
- if(tools.capitalizeFirstLetter(name) === tools.capitalizeFirstLetter(map.name)){
420
- return map.entity
421
- }
422
- }
423
- return "";
424
- }
530
+ return sqlParts.length > 0 ? { query: sqlParts.join(', '), params: params } : -1;
531
+ }
532
+
533
+ /**
534
+ * Build parameterized INSERT object for PostgreSQL
535
+ * Uses $1, $2, $3... instead of ?
536
+ */
537
+ _buildSQLInsertObjectParameterized(fields, modelEntity) {
538
+ const $that = this;
539
+ const columnNames = [];
540
+ const params = [];
541
+ let paramIndex = 1;
542
+
543
+ for (const column in modelEntity) {
544
+ if (column.indexOf("__") === -1) {
545
+ let fieldColumn = fields[column];
546
+
547
+ if ((fieldColumn !== undefined && fieldColumn !== null) && typeof(fieldColumn) !== "object") {
548
+ // Apply toDatabase transformer
549
+ try {
550
+ fieldColumn = FieldTransformer.toDatabase(fieldColumn, modelEntity[column], modelEntity.__name, column);
551
+ } catch (transformError) {
552
+ throw new Error(`INSERT failed: ${transformError.message}`);
553
+ }
425
554
 
426
- // return a list of entity names and skip foreign keys and underscore.
427
- getEntityList(entity){
428
- var entitiesList = [];
429
- var $that = this;
430
- for (var ent in entity) {
431
- if(!ent.startsWith("_")){
432
- if(!entity[ent].foreignKey){
433
- if($that.chechUnsupportedWords(ent)){
434
- entitiesList.push(`'${ent}'`);
435
- }
436
- else{
437
- entitiesList.push(ent);
438
- }
555
+ // Validate and coerce type
556
+ try {
557
+ fieldColumn = $that._validateAndCoerceFieldType(fieldColumn, modelEntity[column], modelEntity.__name, column);
558
+ } catch (typeError) {
559
+ throw new Error(`INSERT failed: ${typeError.message}`);
439
560
  }
440
- else{
441
- if(entity[ent].type === 'belongsTo'){
442
- entitiesList.push(`${entity[ent].foreignKey}`);
443
- }
561
+
562
+ // Convert to database-specific format
563
+ fieldColumn = $that._convertValueForDatabase(fieldColumn, modelEntity[column].type);
564
+
565
+ // Skip auto-increment primary keys
566
+ if (modelEntity[column].auto !== true) {
567
+ columnNames.push(column);
568
+ params.push(fieldColumn);
444
569
  }
445
570
  }
446
571
  }
447
- return entitiesList
448
- }
572
+ }
449
573
 
450
- chechUnsupportedWords(word){
451
- for (var item in this.unsupportedWords) {
452
- var text = this.unsupportedWords[item];
453
- if(text === word){
454
- return true
455
- }
574
+ if (columnNames.length === 0) {
575
+ return -1;
456
576
  }
457
- return false;
458
- }
459
577
 
460
- startTransaction(){
461
- this.db.prepare('BEGIN').run();
462
- }
578
+ // Generate PostgreSQL placeholders: $1, $2, $3...
579
+ const placeholders = params.map((_, index) => `$${index + 1}`).join(', ');
463
580
 
464
- endTransaction(){
465
- this.db.prepare('COMMIT').run();
581
+ return {
582
+ tableName: modelEntity.__name,
583
+ columns: columnNames.join(', '),
584
+ placeholders: placeholders,
585
+ params: params
586
+ };
466
587
  }
467
588
 
468
- errorTransaction(){
469
- this.db.prepare('ROLLBACK').run();
589
+ _buildDeleteObject(queryObject) {
590
+ const primaryKey = tools.getPrimaryKeyObject(queryObject.__entity);
591
+ return {
592
+ tableName: queryObject.__entity.__name,
593
+ primaryKey: primaryKey,
594
+ value: queryObject[primaryKey]
595
+ };
470
596
  }
471
597
 
472
- _buildSQLEqualTo(model){
473
- var $that = this;
474
- var argument = null;
475
- var dirtyFields = model.__dirtyFields;
476
-
477
- for (var column in dirtyFields) {
478
-
479
- // TODO Boolean value is a string with a letter
480
- switch(model.__entity[dirtyFields[column]].type){
481
- case "integer" :
482
- //model.__entity[dirtyFields[column]].skipGetFunction = true;
483
- argument = argument === null ? `[${dirtyFields[column]}] = ${model[dirtyFields[column]]},` : `${argument} [${dirtyFields[column]}] = ${model[dirtyFields[column]]},`;
484
- //model.__entity[dirtyFields[column]].skipGetFunction = false;
485
- break;
486
- case "string" :
487
- argument = argument === null ? `[${dirtyFields[column]}] = '${$that._santizeSingleQuotes(model[dirtyFields[column]])}',` : `${argument} [${dirtyFields[column]}] = '${$that._santizeSingleQuotes(model[dirtyFields[column]])}',`;
488
- break;
489
- case "boolean" :
490
- var bool = "";
491
- if(model.__entity[dirtyFields[column]].valueConversion){
492
- bool = tools.convertBooleanToNumber(model[dirtyFields[column]]);
493
- }
494
- else{
495
- bool = model[dirtyFields[column]];
496
- }
497
- argument = argument === null ? `[${dirtyFields[column]}] = '${bool}',` : `${argument} [${dirtyFields[column]}] = ${bool},`;
498
- break;
499
- case "time" :
500
- argument = argument === null ? `[${dirtyFields[column]}] = '${model[dirtyFields[column]]}',` : `${argument} [${dirtyFields[column]}] = ${model[dirtyFields[column]]},`;
501
- break;
502
- case "belongsTo" :
503
- var fore = `_${dirtyFields[column]}`;
504
- argument = argument === null ? `[${model.__entity[dirtyFields[column]].foreignKey}] = '${model[fore]}',` : `${argument} [${model.__entity[dirtyFields[column]].foreignKey}] = '${model[fore]}',`;
505
- break;
506
- case "hasMany" :
507
- argument = argument === null ? `[${dirtyFields[column]}] = '${model[dirtyFields[column]]}',` : `${argument} [${dirtyFields[column]}] = '${model[dirtyFields[column]]}',`;
508
- break;
509
- default:
510
- argument = argument === null ? `[${dirtyFields[column]}] = '${model[dirtyFields[column]]}',` : `${argument} [${dirtyFields[column]}] = '${model[dirtyFields[column]]}',`;
598
+ /**
599
+ * Validate and coerce field type
600
+ */
601
+ _validateAndCoerceFieldType(value, fieldDef, entityName, fieldName) {
602
+ if (value === null || value === undefined) {
603
+ if (fieldDef.nullable === false || fieldDef.notNullable === true) {
604
+ throw new Error(`Field '${entityName}.${fieldName}' cannot be null`);
511
605
  }
606
+ return null;
512
607
  }
513
- return argument.replace(/,\s*$/, "");
514
- }
515
608
 
516
-
517
- _buildDeleteObject(currentModel){
518
- var primaryKey = currentModel.__Key === undefined ? tools.getPrimaryKeyObject(currentModel.__entity) : currentModel.__Key;
519
- var value = currentModel.__value === undefined ? currentModel[primaryKey] : currentModel.__value;
520
- var tableName = currentModel.__tableName === undefined ? currentModel.__entity.__name : currentModel.__tableName;
521
- return {tableName: tableName, primaryKey : primaryKey, value : value};
522
- }
609
+ const fieldType = fieldDef.type;
523
610
 
611
+ switch (fieldType) {
612
+ case 'string':
613
+ case 'text':
614
+ return String(value);
524
615
 
525
- // return columns and value strings
526
- _buildSQLInsertObject(fields, modelEntity){
527
- var $that = this;
528
- var columns = null;
529
- var values = null;
530
- for (var column in modelEntity) {
531
- // column1 = value1, column2 = value2, ...
532
- if(column.indexOf("__") === -1 ){
533
- // call the get method if avlable
534
- var fieldColumn = "";
535
- // check if get function is avaliable if so use that
536
- fieldColumn = fields[column];
537
-
538
- if((fieldColumn !== undefined && fieldColumn !== null && fieldColumn !== "" ) && typeof(fieldColumn) !== "object"){
539
- switch(modelEntity[column].type){
540
- case "belongsTo" :
541
- column = modelEntity[column].foreignKey === undefined ? column : modelEntity[column].foreignKey;
542
- break;
543
- case "string" :
544
- fieldColumn = `'${$that._santizeSingleQuotes(fields[column])}'`;
545
- break;
546
- case "time" :
547
- fieldColumn = fields[column];
548
- break;
549
- }
616
+ case 'integer':
617
+ case 'int':
618
+ const intVal = parseInt(value, 10);
619
+ if (isNaN(intVal)) {
620
+ throw new Error(`Field '${entityName}.${fieldName}' must be an integer, got: ${value}`);
621
+ }
622
+ return intVal;
623
+
624
+ case 'float':
625
+ case 'double':
626
+ case 'decimal':
627
+ const floatVal = parseFloat(value);
628
+ if (isNaN(floatVal)) {
629
+ throw new Error(`Field '${entityName}.${fieldName}' must be a number, got: ${value}`);
630
+ }
631
+ return floatVal;
550
632
 
551
- columns = columns === null ? `'${column}',` : `${columns} '${column}',`;
552
- values = values === null ? `${fieldColumn},` : `${values} ${fieldColumn},`;
633
+ case 'boolean':
634
+ case 'bool':
635
+ return Boolean(value);
553
636
 
637
+ case 'date':
638
+ case 'datetime':
639
+ case 'timestamp':
640
+ if (value instanceof Date) {
641
+ return value;
554
642
  }
555
- else{
556
- switch(modelEntity[column].type){
557
- case "belongsTo" :
558
- var fieldObject = tools.findTrackedObject(fields.__context.__trackedEntities, column );
559
- if( Object.keys(fieldObject).length > 0){
560
- var primaryKey = tools.getPrimaryKeyObject(fieldObject.__entity);
561
- fieldColumn = fieldObject[primaryKey];
562
- column = modelEntity[column].foreignKey;
563
- columns = columns === null ? `'${column}',` : `${columns} '${column}',`;
564
- values = values === null ? `${fieldColumn},` : `${values} ${fieldColumn},`;
565
- }else{
566
- console.log("Cannot find belings to relationship")
567
- }
568
-
569
- break;
570
- }
571
-
643
+ const dateVal = new Date(value);
644
+ if (isNaN(dateVal.getTime())) {
645
+ throw new Error(`Field '${entityName}.${fieldName}' must be a valid date, got: ${value}`);
572
646
  }
573
- }
574
- }
575
- return {tableName: modelEntity.__name, columns: columns.replace(/,\s*$/, ""), values: values.replace(/,\s*$/, "")};
647
+ return dateVal;
576
648
 
577
- }
649
+ case 'json':
650
+ case 'jsonb':
651
+ if (typeof value === 'object') {
652
+ return JSON.stringify(value);
653
+ }
654
+ return value;
578
655
 
579
- // will add double single quotes to allow sting to be saved.
580
- _santizeSingleQuotes(string){
581
- if (typeof string === 'string' || string instanceof String){
582
- return string.replace(/'/g, "''");
656
+ default:
657
+ return value;
583
658
  }
584
- else{
585
- console.log("warning - Field being passed is not a string");
586
- throw "warning - Field being passed is not a string";
587
- }
588
659
  }
589
660
 
590
- // converts any object into SQL parameter select string
591
- _convertEntityToSelectParameterString(obj, entityName){
592
- // todo: loop throgh object and append string with comma to
593
- var mainString = "";
594
- const entries = Object.keys(obj);
661
+ /**
662
+ * Convert validated value to database-specific format
663
+ * Modern ORM pattern: transparent database-specific conversions
664
+ *
665
+ * @param {*} value - Already validated value
666
+ * @param {string} fieldType - Field type from entity definition
667
+ * @returns {*} Database-ready value
668
+ */
669
+ _convertValueForDatabase(value, fieldType){
670
+ if(value === undefined || value === null){
671
+ return value;
672
+ }
595
673
 
596
- for (const [name] of entries) {
597
- mainString += `${mainString}, ${entityName}.${name}`;
674
+ // PostgreSQL accepts native booleans, but we convert to 1/0 for consistency
675
+ // The pg driver will convert to PostgreSQL TRUE/FALSE
676
+ if(fieldType === 'boolean' && typeof value === 'boolean'){
677
+ return value ? 1 : 0;
598
678
  }
599
- return mainString;;
600
- }
601
679
 
602
- _execute(query){
603
- console.log("SQL:", query);
604
- return this.db.exec(query);
680
+ return value;
605
681
  }
606
682
 
607
- _run(query){
608
- console.log("SQL:", query);
609
- return this.db.prepare(query).run();
683
+ /**
684
+ * Execute parameterized query with pg library
685
+ */
686
+ async _runWithParams(query, params = []) {
687
+ try {
688
+ console.log("SQL:", query);
689
+ console.log("Params:", params);
690
+
691
+ const client = await this.pool.connect();
692
+ try {
693
+ const result = await client.query(query, params);
694
+ return result;
695
+ } finally {
696
+ client.release();
697
+ }
698
+ } catch (error) {
699
+ console.error('PostgreSQL query error:', error);
700
+ throw error;
701
+ }
610
702
  }
611
703
 
612
- setDB(db, type){
613
- this.db = db;
614
- this.dbType = type; // this will let us know which type of sqlengine to use.
615
- }
704
+ /**
705
+ * Sanitize single quotes (legacy, prefer parameterized queries)
706
+ */
707
+ _santizeSingleQuotes(string, context = {}) {
708
+ if (typeof string === 'string' || string instanceof String) {
709
+ return string.replace(/'/g, "''");
710
+ }
711
+ console.warn(`Warning - Field ${context.entityName}.${context.fieldName} is not a string`);
712
+ throw new Error(`Field ${context.entityName}.${context.fieldName} must be a string`);
713
+ }
714
+
715
+ /**
716
+ * Set database connection
717
+ */
718
+ setDB(db, type) {
719
+ this.db = db;
720
+ this.pool = db;
721
+ this.dbType = type || 'postgres';
722
+ }
723
+
724
+ /**
725
+ * Close database connection pool
726
+ */
727
+ async close() {
728
+ if (this.pool) {
729
+ await this.pool.end();
730
+ console.log('PostgreSQL pool closed');
731
+ }
732
+ }
616
733
  }
617
734
 
618
- module.exports = postgresEngine;
735
+ module.exports = postgresEngine;