masterrecord 0.2.34 → 0.3.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.
Files changed (40) hide show
  1. package/.claude/settings.local.json +25 -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 +14 -5
  12. package/SQLLiteEngine.js +309 -19
  13. package/context.js +57 -12
  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 +249 -17
  20. package/package.json +6 -6
  21. package/postgresEngine.js +434 -491
  22. package/postgresSyncConnect.js +209 -0
  23. package/readme.md +1121 -265
  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/tablePrefixTest.js +100 -0
  37. package/test/transformerTest.js +287 -0
  38. package/test/verifyFindById.js +169 -0
  39. package/test/verifyNewMethod.js +191 -0
  40. package/test/whereChainingTest.js +88 -0
package/postgresEngine.js CHANGED
@@ -1,618 +1,561 @@
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
+ if (query.arg && query.arg.query && query.arg.params) {
47
+ // Parameterized UPDATE
48
+ const params = [...query.arg.params, query.primaryKeyValue];
49
+ return await this._runWithParams(query.arg.query, params);
50
+ } else {
51
+ // Fallback for legacy support
52
+ const sqlQuery = `UPDATE ${query.tableName} SET ${query.arg} WHERE ${query.tableName}.${query.primaryKey} = $1`;
53
+ return await this._runWithParams(sqlQuery, [query.primaryKeyValue]);
54
+ }
13
55
  }
14
56
 
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);
57
+ /**
58
+ * DELETE with parameterized query
59
+ */
60
+ async delete(queryObject) {
61
+ const sqlObject = this._buildDeleteObject(queryObject);
62
+ const sqlQuery = `DELETE FROM ${sqlObject.tableName} WHERE ${sqlObject.tableName}.${sqlObject.primaryKey} = $1`;
63
+ return await this._runWithParams(sqlQuery, [sqlObject.value]);
19
64
  }
20
65
 
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
66
+ /**
67
+ * INSERT with parameterized query
68
+ * Postgres uses RETURNING to get the inserted ID
69
+ */
70
+ async insert(queryObject) {
71
+ const sqlObject = this._buildSQLInsertObjectParameterized(queryObject, queryObject.__entity);
72
+ if (sqlObject === -1) {
73
+ throw new Error('INSERT failed: No columns to insert');
74
+ }
75
+
76
+ // Get primary key name for RETURNING clause
77
+ const primaryKey = tools.getPrimaryKeyObject(queryObject.__entity);
78
+ const query = `INSERT INTO ${sqlObject.tableName} (${sqlObject.columns}) VALUES (${sqlObject.placeholders}) RETURNING ${primaryKey}`;
79
+
80
+ const result = await this._runWithParams(query, sqlObject.params);
81
+
82
+ return {
83
+ id: result.rows[0][primaryKey]
28
84
  };
29
- return open;
30
85
  }
31
86
 
32
- get(query, entity, context){
33
- var queryString = {};
87
+ /**
88
+ * SELECT single record
89
+ */
90
+ async get(query, entity, context) {
34
91
  try {
35
- if(query.raw){
36
- queryString.query = query.raw;
37
- }
38
- else{
39
- if(typeof query === 'string'){
40
- queryString.query = query;
41
- }
42
- else{
43
- queryString = this.buildQuery(query, entity, context);
44
- }
92
+ let queryString;
93
+ if (query.raw) {
94
+ queryString = { query: query.raw, params: [] };
95
+ } else if (typeof query === 'string') {
96
+ queryString = { query: query, params: [] };
97
+ } else {
98
+ queryString = this.buildQuery(query, entity, context);
45
99
  }
46
- if(queryString.query){
100
+
101
+ if (queryString.query) {
47
102
  console.log("SQL:", queryString.query);
48
- var queryReturn = this.db.prepare(queryString.query).get();
49
- return queryReturn;
103
+ console.log("Params:", queryString.params || []);
104
+ const result = await this._runWithParams(queryString.query, queryString.params || []);
105
+ return result.rows[0] || null;
50
106
  }
51
107
  return null;
52
108
  } catch (err) {
53
- console.error(err);
109
+ console.error('PostgreSQL get error:', err);
54
110
  return null;
55
111
  }
56
112
  }
57
113
 
58
- getCount(queryObject, entity, context){
59
- var query = queryObject.script;
60
- var queryString = {};
114
+ /**
115
+ * SELECT COUNT
116
+ */
117
+ async getCount(queryObject, entity, context) {
118
+ const query = queryObject.script;
61
119
  try {
62
- if(query.raw){
63
- queryString.query = query.raw;
64
- }
65
- else{
66
- if(query.count === undefined){
120
+ let queryString;
121
+ if (query.raw) {
122
+ queryString = { query: query.raw, params: [] };
123
+ } else {
124
+ if (query.count === undefined) {
67
125
  query.count = "none";
68
126
  }
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)}`
127
+ const entityAlias = this.getEntity(entity.__name, query.entityMap);
128
+ queryString = {
129
+ query: `SELECT ${this.buildCount(query, entity)} ${this.buildFrom(query, entity)} ${this.buildWhere(query, entity)}`,
130
+ params: query.parameters ? query.parameters.getParams() : []
131
+ };
71
132
  }
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;
133
+
134
+ if (queryString.query) {
135
+ console.log("SQL:", queryString.query);
136
+ console.log("Params:", queryString.params);
137
+ const result = await this._runWithParams(queryString.query, queryString.params);
138
+ return result.rows[0] || null;
77
139
  }
78
140
  return null;
79
141
  } catch (err) {
80
- console.error(err);
142
+ console.error('PostgreSQL getCount error:', err);
81
143
  return null;
82
144
  }
83
145
  }
84
146
 
85
- all(query, entity, context){
86
- var selectQuery = {};
147
+ /**
148
+ * SELECT multiple records
149
+ */
150
+ async all(query, entity, context) {
87
151
  try {
88
- if(query.raw){
89
- selectQuery.query = query.raw;
90
- }
91
- else{
92
-
152
+ let selectQuery;
153
+ if (query.raw) {
154
+ selectQuery = { query: query.raw, params: [] };
155
+ } else {
93
156
  selectQuery = this.buildQuery(query, entity, context);
94
157
  }
95
- if(selectQuery.query){
158
+
159
+ if (selectQuery.query) {
96
160
  console.log("SQL:", selectQuery.query);
97
- var queryReturn = this.db.prepare(selectQuery.query).all();
98
- return queryReturn;
161
+ console.log("Params:", selectQuery.params || []);
162
+ const result = await this._runWithParams(selectQuery.query, selectQuery.params || []);
163
+ return result.rows || [];
99
164
  }
100
- return null;
165
+ return [];
101
166
  } catch (err) {
102
- console.error(err);
103
- return null;
167
+ console.error('PostgreSQL all error:', err);
168
+ return [];
104
169
  }
105
170
  }
106
171
 
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
-
172
+ /**
173
+ * Execute raw SQL with parameters
174
+ */
175
+ async exec(query, params = []) {
176
+ return await this._runWithParams(query, params);
117
177
  }
118
178
 
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
- }
179
+ /**
180
+ * Build complete SELECT query with parameters
181
+ */
182
+ buildQuery(query, entity, context) {
183
+ const entityStr = this.getEntity(entity.__name, query.entityMap);
184
+ const params = query.parameters ? query.parameters.getParams() : [];
133
185
 
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
- }
186
+ 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
187
 
188
+ return {
189
+ query: sql,
190
+ params: params
191
+ };
155
192
  }
156
193
 
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}`;
194
+ buildSelectString(query, entity) {
195
+ if (query.select) {
196
+ return query.select;
176
197
  }
177
- return strQuery;
198
+ return `${tools.convertEntityToSelectParameterString(entity)}`;
178
199
  }
179
200
 
180
- buildTake(query){
181
- if(query.take){
182
- return `LIMIT ${query.take}`
183
- }
184
- else{
185
- return "";
201
+ buildCount(query, entity) {
202
+ const entityStr = this.getEntity(entity.__name, query.entityMap);
203
+ if (query.count === "none") {
204
+ return `COUNT(${entityStr}.*)`;
186
205
  }
206
+ return `COUNT(${entityStr}.${query.count})`;
187
207
  }
188
208
 
189
- buildSkip(query){
190
- if(query.skip){
191
- return `OFFSET ${query.skip}`
192
- }
193
- else{
194
- return "";
195
- }
209
+ buildFrom(query, entity) {
210
+ return `FROM ${entity.__name}`;
196
211
  }
197
212
 
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];
213
+ /**
214
+ * Build AND clause with placeholder detection
215
+ */
216
+ buildAnd(query, mainQuery) {
217
+ const andEntity = query.and;
218
+ let strQuery = "";
219
+ const $that = this;
220
+
221
+ if (andEntity) {
222
+ const entity = this.getEntity(query.parentName, query.entityMap);
223
+ const andList = [];
224
+
225
+ for (let entityPart in andEntity) {
226
+ const itemEntity = andEntity[entityPart];
227
+ for (let table in itemEntity[query.parentName]) {
228
+ const item = itemEntity[query.parentName][table];
213
229
  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
- }
230
+ let field = tools.capitalizeFirstLetter(item.expressions[exp].field);
231
+ let entityRef = entity;
232
+
233
+ if (mainQuery[field] && mainQuery[field].isNavigational) {
234
+ entityRef = $that.getEntity(field, query.entityMap);
235
+ field = item.fields[1];
220
236
  }
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
- }
237
+
238
+ let func = item.expressions[exp].func;
239
+ const arg = item.expressions[exp].arg;
240
+
241
+ // Handle NULL
242
+ if (arg === "null") {
243
+ if (func === "=") func = "IS";
244
+ if (func === "!=") func = "IS NOT";
228
245
  }
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}'`;
246
+
247
+ if (strQuery === "") {
248
+ if (arg === "null") {
249
+ strQuery = `${entityRef}.${field} ${func} ${arg}`;
250
+ } else {
251
+ // Check if arg is a parameterized placeholder
252
+ const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
253
+ if (isPlaceholder || func === "IN") {
254
+ strQuery = `${entityRef}.${field} ${func} ${arg}`;
255
+ } else {
256
+ strQuery = `${entityRef}.${field} ${func} '${arg}'`;
257
+ }
234
258
  }
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}'`;
259
+ } else {
260
+ if (arg === "null") {
261
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
262
+ } else {
263
+ // Check if arg is a parameterized placeholder
264
+ const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
265
+ if (isPlaceholder || func === "IN") {
266
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
267
+ } else {
268
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} '${arg}'`;
269
+ }
241
270
  }
242
-
243
271
  }
244
272
  }
245
273
  andList.push(strQuery);
246
274
  }
247
275
  }
276
+
277
+ if (andList.length > 0) {
278
+ return `AND ${andList.join(" AND ")}`;
279
+ }
248
280
  }
249
-
250
- if(andList.length > 0){
251
- str = `and ${andList.join(" and ")}`;
252
- }
253
- return str
281
+
282
+ return "";
254
283
  }
255
284
 
256
- buildWhere(query, mainQuery){
257
- var whereEntity = query.where;
285
+ /**
286
+ * Build WHERE clause with placeholder detection
287
+ */
288
+ buildWhere(query, mainQuery) {
289
+ const whereEntity = query.where;
290
+ let strQuery = "";
291
+ const $that = this;
258
292
 
259
- var strQuery = "";
260
- var $that = this;
261
- if(whereEntity){
262
- var entity = this.getEntity(query.parentName, query.entityMap);
293
+ if (whereEntity) {
294
+ const entity = this.getEntity(query.parentName, query.entityMap);
295
+ const item = whereEntity[query.parentName].query;
263
296
 
264
- var item = whereEntity[query.parentName].query;
265
297
  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
- }
272
- }
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
- }
280
- }
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}'`;
290
- }
291
- }
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}'`;
298
- }
299
-
298
+ let field = tools.capitalizeFirstLetter(item.expressions[exp].field);
299
+ let entityRef = entity;
300
+
301
+ if (mainQuery[field] && mainQuery[field].isNavigational) {
302
+ entityRef = $that.getEntity(field, query.entityMap);
303
+ field = item.fields[1];
300
304
  }
301
- }
302
305
 
303
-
304
-
305
- }
306
- return strQuery;
307
- }
306
+ let func = item.expressions[exp].func;
307
+ const arg = item.expressions[exp].arg;
308
308
 
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])];
309
+ // Handle NULL
310
+ if (arg === "null") {
311
+ if (func === "=") func = "IS";
312
+ if (func === "!=") func = "IS NOT";
319
313
  }
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);
314
+
315
+ if (strQuery === "") {
316
+ if (arg === "null") {
317
+ strQuery = `WHERE ${entityRef}.${field} ${func} ${arg}`;
318
+ } else if (func === "IN") {
319
+ strQuery = `WHERE ${entityRef}.${field} ${func} ${arg}`;
320
+ } else {
321
+ // Check if arg is a parameterized placeholder ($1, $2, etc.)
322
+ const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
323
+ if (isPlaceholder) {
324
+ strQuery = `WHERE ${entityRef}.${field} ${func} ${arg}`;
325
+ } else {
326
+ strQuery = `WHERE ${entityRef}.${field} ${func} '${arg}'`;
327
+ }
331
328
  }
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);
329
+ } else {
330
+ if (arg === "null") {
331
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
332
+ } else if (func === "IN") {
333
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
334
+ } else {
335
+ // Check if arg is a parameterized placeholder
336
+ const isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
337
+ if (isPlaceholder) {
338
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} ${arg}`;
339
+ } else {
340
+ strQuery = `${strQuery} AND ${entityRef}.${field} ${func} '${arg}'`;
341
+ }
339
342
  }
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
343
  }
346
344
  }
347
345
  }
348
- return includeQuery;
349
- }
350
-
351
- buildFrom(query, entity){
352
- var entityName = this.getEntity(entity.__name, query.entityMap);
353
- if(entityName ){
354
- return `FROM ${entity.__name } AS ${entityName}`;
355
- }
356
- else{ return "" }
357
- }
358
346
 
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
-
369
- }
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} `;
347
+ return strQuery;
378
348
  }
379
349
 
380
- getForeignKey(name, entity){
381
- if(entity && name){
382
- return entity[name].foreignKey;
350
+ buildLimit(query) {
351
+ if (query.take) {
352
+ return `LIMIT ${query.take}`;
383
353
  }
354
+ return "";
384
355
  }
385
356
 
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;
391
- }
392
- }
393
- };
394
- }
395
-
396
- getForeignTable(name, entity){
397
- if(entity && name){
398
- return entity[name].foreignTable;
357
+ buildSkip(query) {
358
+ if (query.skip) {
359
+ return `OFFSET ${query.skip}`;
399
360
  }
361
+ return "";
400
362
  }
401
363
 
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
- }
409
- }
410
- }
411
- else{
412
- return "";
364
+ buildOrderBy(query) {
365
+ if (query.orderBy) {
366
+ const entityStr = this.getEntity(query.parentName, query.entityMap);
367
+ return `ORDER BY ${entityStr}.${query.orderBy} ASC`;
368
+ } else if (query.orderByDescending) {
369
+ const entityStr = this.getEntity(query.parentName, query.entityMap);
370
+ return `ORDER BY ${entityStr}.${query.orderByDescending} DESC`;
413
371
  }
372
+ return "";
414
373
  }
415
374
 
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
375
+ getEntity(name, list) {
376
+ for (let i = 0; i < list.length; i++) {
377
+ if (list[i].name === name) {
378
+ return list[i].entity;
421
379
  }
422
380
  }
423
- return "";
424
- }
381
+ return name;
382
+ }
383
+
384
+ /**
385
+ * Build parameterized INSERT object for PostgreSQL
386
+ * Uses $1, $2, $3... instead of ?
387
+ */
388
+ _buildSQLInsertObjectParameterized(fields, modelEntity) {
389
+ const $that = this;
390
+ const columnNames = [];
391
+ const params = [];
392
+ let paramIndex = 1;
393
+
394
+ for (const column in modelEntity) {
395
+ if (column.indexOf("__") === -1) {
396
+ let fieldColumn = fields[column];
397
+
398
+ if ((fieldColumn !== undefined && fieldColumn !== null) && typeof(fieldColumn) !== "object") {
399
+ // Apply toDatabase transformer
400
+ try {
401
+ fieldColumn = FieldTransformer.toDatabase(fieldColumn, modelEntity[column], modelEntity.__name, column);
402
+ } catch (transformError) {
403
+ throw new Error(`INSERT failed: ${transformError.message}`);
404
+ }
425
405
 
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
- }
406
+ // Validate and coerce type
407
+ try {
408
+ fieldColumn = $that._validateAndCoerceFieldType(fieldColumn, modelEntity[column], modelEntity.__name, column);
409
+ } catch (typeError) {
410
+ throw new Error(`INSERT failed: ${typeError.message}`);
439
411
  }
440
- else{
441
- if(entity[ent].type === 'belongsTo'){
442
- entitiesList.push(`${entity[ent].foreignKey}`);
443
- }
412
+
413
+ // Skip auto-increment primary keys
414
+ if (modelEntity[column].auto !== true) {
415
+ columnNames.push(column);
416
+ params.push(fieldColumn);
444
417
  }
445
418
  }
446
419
  }
447
- return entitiesList
448
- }
420
+ }
449
421
 
450
- chechUnsupportedWords(word){
451
- for (var item in this.unsupportedWords) {
452
- var text = this.unsupportedWords[item];
453
- if(text === word){
454
- return true
455
- }
422
+ if (columnNames.length === 0) {
423
+ return -1;
456
424
  }
457
- return false;
458
- }
459
425
 
460
- startTransaction(){
461
- this.db.prepare('BEGIN').run();
462
- }
426
+ // Generate PostgreSQL placeholders: $1, $2, $3...
427
+ const placeholders = params.map((_, index) => `$${index + 1}`).join(', ');
463
428
 
464
- endTransaction(){
465
- this.db.prepare('COMMIT').run();
429
+ return {
430
+ tableName: modelEntity.__name,
431
+ columns: columnNames.join(', '),
432
+ placeholders: placeholders,
433
+ params: params
434
+ };
466
435
  }
467
436
 
468
- errorTransaction(){
469
- this.db.prepare('ROLLBACK').run();
437
+ _buildDeleteObject(queryObject) {
438
+ const primaryKey = tools.getPrimaryKeyObject(queryObject.__entity);
439
+ return {
440
+ tableName: queryObject.__entity.__name,
441
+ primaryKey: primaryKey,
442
+ value: queryObject[primaryKey]
443
+ };
470
444
  }
471
445
 
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]]}',`;
446
+ /**
447
+ * Validate and coerce field type
448
+ */
449
+ _validateAndCoerceFieldType(value, fieldDef, entityName, fieldName) {
450
+ if (value === null || value === undefined) {
451
+ if (fieldDef.nullable === false || fieldDef.notNullable === true) {
452
+ throw new Error(`Field '${entityName}.${fieldName}' cannot be null`);
511
453
  }
454
+ return null;
512
455
  }
513
- return argument.replace(/,\s*$/, "");
514
- }
515
456
 
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
- }
457
+ const fieldType = fieldDef.type;
523
458
 
459
+ switch (fieldType) {
460
+ case 'string':
461
+ case 'text':
462
+ return String(value);
524
463
 
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
- }
464
+ case 'integer':
465
+ case 'int':
466
+ const intVal = parseInt(value, 10);
467
+ if (isNaN(intVal)) {
468
+ throw new Error(`Field '${entityName}.${fieldName}' must be an integer, got: ${value}`);
469
+ }
470
+ return intVal;
471
+
472
+ case 'float':
473
+ case 'double':
474
+ case 'decimal':
475
+ const floatVal = parseFloat(value);
476
+ if (isNaN(floatVal)) {
477
+ throw new Error(`Field '${entityName}.${fieldName}' must be a number, got: ${value}`);
478
+ }
479
+ return floatVal;
550
480
 
551
- columns = columns === null ? `'${column}',` : `${columns} '${column}',`;
552
- values = values === null ? `${fieldColumn},` : `${values} ${fieldColumn},`;
481
+ case 'boolean':
482
+ case 'bool':
483
+ return Boolean(value);
553
484
 
485
+ case 'date':
486
+ case 'datetime':
487
+ case 'timestamp':
488
+ if (value instanceof Date) {
489
+ return value;
554
490
  }
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
-
491
+ const dateVal = new Date(value);
492
+ if (isNaN(dateVal.getTime())) {
493
+ throw new Error(`Field '${entityName}.${fieldName}' must be a valid date, got: ${value}`);
572
494
  }
573
- }
574
- }
575
- return {tableName: modelEntity.__name, columns: columns.replace(/,\s*$/, ""), values: values.replace(/,\s*$/, "")};
495
+ return dateVal;
576
496
 
577
- }
497
+ case 'json':
498
+ case 'jsonb':
499
+ if (typeof value === 'object') {
500
+ return JSON.stringify(value);
501
+ }
502
+ return value;
578
503
 
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, "''");
504
+ default:
505
+ return value;
583
506
  }
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
507
  }
589
508
 
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);
595
-
596
- for (const [name] of entries) {
597
- mainString += `${mainString}, ${entityName}.${name}`;
509
+ /**
510
+ * Execute parameterized query with pg library
511
+ */
512
+ async _runWithParams(query, params = []) {
513
+ try {
514
+ console.log("SQL:", query);
515
+ console.log("Params:", params);
516
+
517
+ const client = await this.pool.connect();
518
+ try {
519
+ const result = await client.query(query, params);
520
+ return result;
521
+ } finally {
522
+ client.release();
523
+ }
524
+ } catch (error) {
525
+ console.error('PostgreSQL query error:', error);
526
+ throw error;
598
527
  }
599
- return mainString;;
600
- }
601
-
602
- _execute(query){
603
- console.log("SQL:", query);
604
- return this.db.exec(query);
605
528
  }
606
529
 
607
- _run(query){
608
- console.log("SQL:", query);
609
- return this.db.prepare(query).run();
530
+ /**
531
+ * Sanitize single quotes (legacy, prefer parameterized queries)
532
+ */
533
+ _santizeSingleQuotes(string, context = {}) {
534
+ if (typeof string === 'string' || string instanceof String) {
535
+ return string.replace(/'/g, "''");
536
+ }
537
+ console.warn(`Warning - Field ${context.entityName}.${context.fieldName} is not a string`);
538
+ throw new Error(`Field ${context.entityName}.${context.fieldName} must be a string`);
539
+ }
540
+
541
+ /**
542
+ * Set database connection
543
+ */
544
+ setDB(db, type) {
545
+ this.db = db;
546
+ this.pool = db;
547
+ this.dbType = type || 'postgres';
548
+ }
549
+
550
+ /**
551
+ * Close database connection pool
552
+ */
553
+ async close() {
554
+ if (this.pool) {
555
+ await this.pool.end();
556
+ console.log('PostgreSQL pool closed');
557
+ }
610
558
  }
611
-
612
- setDB(db, type){
613
- this.db = db;
614
- this.dbType = type; // this will let us know which type of sqlengine to use.
615
- }
616
559
  }
617
560
 
618
- module.exports = postgresEngine;
561
+ module.exports = postgresEngine;