@nitronjs/framework 0.1.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 (87) hide show
  1. package/README.md +429 -0
  2. package/cli/create.js +260 -0
  3. package/cli/njs.js +164 -0
  4. package/lib/Auth/Manager.js +111 -0
  5. package/lib/Build/Manager.js +1232 -0
  6. package/lib/Console/Commands/BuildCommand.js +25 -0
  7. package/lib/Console/Commands/DevCommand.js +385 -0
  8. package/lib/Console/Commands/MakeCommand.js +110 -0
  9. package/lib/Console/Commands/MigrateCommand.js +98 -0
  10. package/lib/Console/Commands/MigrateFreshCommand.js +97 -0
  11. package/lib/Console/Commands/SeedCommand.js +92 -0
  12. package/lib/Console/Commands/StorageLinkCommand.js +31 -0
  13. package/lib/Console/Stubs/controller.js +19 -0
  14. package/lib/Console/Stubs/middleware.js +9 -0
  15. package/lib/Console/Stubs/migration.js +23 -0
  16. package/lib/Console/Stubs/model.js +7 -0
  17. package/lib/Console/Stubs/page-hydration.tsx +54 -0
  18. package/lib/Console/Stubs/seeder.js +9 -0
  19. package/lib/Console/Stubs/vendor.tsx +11 -0
  20. package/lib/Core/Config.js +86 -0
  21. package/lib/Core/Environment.js +21 -0
  22. package/lib/Core/Paths.js +188 -0
  23. package/lib/Database/Connection.js +61 -0
  24. package/lib/Database/DB.js +84 -0
  25. package/lib/Database/Drivers/MySQLDriver.js +234 -0
  26. package/lib/Database/Manager.js +162 -0
  27. package/lib/Database/Model.js +161 -0
  28. package/lib/Database/QueryBuilder.js +714 -0
  29. package/lib/Database/QueryValidation.js +62 -0
  30. package/lib/Database/Schema/Blueprint.js +126 -0
  31. package/lib/Database/Schema/Manager.js +116 -0
  32. package/lib/Date/DateTime.js +108 -0
  33. package/lib/Date/Locale.js +68 -0
  34. package/lib/Encryption/Manager.js +47 -0
  35. package/lib/Filesystem/Manager.js +49 -0
  36. package/lib/Hashing/Manager.js +25 -0
  37. package/lib/Http/Server.js +317 -0
  38. package/lib/Logging/Manager.js +153 -0
  39. package/lib/Mail/Manager.js +120 -0
  40. package/lib/Route/Loader.js +81 -0
  41. package/lib/Route/Manager.js +265 -0
  42. package/lib/Runtime/Entry.js +11 -0
  43. package/lib/Session/File.js +299 -0
  44. package/lib/Session/Manager.js +259 -0
  45. package/lib/Session/Memory.js +67 -0
  46. package/lib/Session/Session.js +196 -0
  47. package/lib/Support/Str.js +100 -0
  48. package/lib/Translation/Manager.js +49 -0
  49. package/lib/Validation/MimeTypes.js +39 -0
  50. package/lib/Validation/Validator.js +691 -0
  51. package/lib/View/Manager.js +544 -0
  52. package/lib/View/Templates/default/Home.tsx +262 -0
  53. package/lib/View/Templates/default/MainLayout.tsx +44 -0
  54. package/lib/View/Templates/errors/404.tsx +13 -0
  55. package/lib/View/Templates/errors/500.tsx +13 -0
  56. package/lib/View/Templates/errors/ErrorLayout.tsx +112 -0
  57. package/lib/View/Templates/messages/Maintenance.tsx +17 -0
  58. package/lib/View/Templates/messages/MessageLayout.tsx +136 -0
  59. package/lib/index.js +57 -0
  60. package/package.json +47 -0
  61. package/skeleton/.env.example +26 -0
  62. package/skeleton/app/Controllers/HomeController.js +9 -0
  63. package/skeleton/app/Kernel.js +11 -0
  64. package/skeleton/app/Middlewares/Authentication.js +9 -0
  65. package/skeleton/app/Middlewares/Guest.js +9 -0
  66. package/skeleton/app/Middlewares/VerifyCsrf.js +24 -0
  67. package/skeleton/app/Models/User.js +7 -0
  68. package/skeleton/config/app.js +4 -0
  69. package/skeleton/config/auth.js +16 -0
  70. package/skeleton/config/database.js +27 -0
  71. package/skeleton/config/hash.js +3 -0
  72. package/skeleton/config/server.js +28 -0
  73. package/skeleton/config/session.js +21 -0
  74. package/skeleton/database/migrations/2025_01_01_00_00_users.js +20 -0
  75. package/skeleton/database/seeders/UserSeeder.js +15 -0
  76. package/skeleton/globals.d.ts +1 -0
  77. package/skeleton/package.json +24 -0
  78. package/skeleton/public/.gitkeep +0 -0
  79. package/skeleton/resources/css/.gitkeep +0 -0
  80. package/skeleton/resources/langs/.gitkeep +0 -0
  81. package/skeleton/resources/views/Site/Home.tsx +66 -0
  82. package/skeleton/routes/web.js +4 -0
  83. package/skeleton/storage/app/private/.gitkeep +0 -0
  84. package/skeleton/storage/app/public/.gitkeep +0 -0
  85. package/skeleton/storage/framework/sessions/.gitkeep +0 -0
  86. package/skeleton/storage/logs/.gitkeep +0 -0
  87. package/skeleton/tsconfig.json +33 -0
@@ -0,0 +1,714 @@
1
+ import { validateDirection, validateIdentifier, validateWhereOperator } from "./QueryValidation.js";
2
+
3
+ class QueryBuilder {
4
+ #table = null;
5
+ #connection = null;
6
+ #connectionGetter = null;
7
+ #selectColumns = ['*'];
8
+ #wheres = [];
9
+ #bindings = [];
10
+ #joins = [];
11
+ #orders = [];
12
+ #groups = [];
13
+ #havings = [];
14
+ #limitValue = null;
15
+ #offsetValue = null;
16
+ #distinctFlag = false;
17
+ #modelClass = null;
18
+
19
+ constructor(table, connectionOrGetter, modelClass = null) {
20
+ this.#table = this.#validateIdentifier(table);
21
+ // connectionOrGetter can be:
22
+ // - null for SQL compilation (toSql())
23
+ // - a connection object directly
24
+ // - an async function that returns a connection (lazy loading)
25
+ if (typeof connectionOrGetter === 'function') {
26
+ this.#connectionGetter = connectionOrGetter;
27
+ this.#connection = null;
28
+ } else {
29
+ this.#connection = connectionOrGetter;
30
+ this.#connectionGetter = null;
31
+ }
32
+ this.#modelClass = modelClass;
33
+ }
34
+
35
+ async #getConnection() {
36
+ if (this.#connection) {
37
+ return this.#connection;
38
+ }
39
+ if (this.#connectionGetter) {
40
+ this.#connection = await this.#connectionGetter();
41
+ return this.#connection;
42
+ }
43
+ throw new Error('QueryBuilder: No database connection. Cannot execute query. Use DB.table() after DB.setup().');
44
+ }
45
+
46
+ #validateIdentifier(identifier) {
47
+ return validateIdentifier(identifier);
48
+ }
49
+
50
+ #validateWhereOperator(operator) {
51
+ return validateWhereOperator(operator);
52
+ }
53
+
54
+ #validateDirection(direction) {
55
+ return validateDirection(direction);
56
+ }
57
+
58
+ #validateJoinOperator(operator) {
59
+ const validOperators = ['=', '!=', '<>', '<', '>', '<=', '>='];
60
+ if (!validOperators.includes(operator)) {
61
+ throw new Error(`Invalid JOIN operator: "${operator}". Must be one of: ${validOperators.join(', ')}.`);
62
+ }
63
+
64
+ return operator;
65
+ }
66
+
67
+ #validateInteger(value) {
68
+ const stringValue = String(value).trim();
69
+ if (!/^\d+$/.test(stringValue)) {
70
+ throw new Error(`Invalid integer: "${value}". Must be a pure positive number.`);
71
+ }
72
+
73
+ const num = parseInt(stringValue, 10);
74
+ if (isNaN(num) || num < 0) {
75
+ throw new Error(`Invalid integer: "${value}". Must be a non-negative number.`);
76
+ }
77
+
78
+ if (num > 100000) {
79
+ throw new Error(`Invalid integer: "${value}". Maximum allowed value is 100000.`);
80
+ }
81
+
82
+ return num;
83
+ }
84
+
85
+ #isRawExpression(value) {
86
+ return value instanceof RawExpression;
87
+ }
88
+
89
+ #validateWhereValue(value) {
90
+ if (value === null || value === undefined) {
91
+ return value;
92
+ }
93
+
94
+ const type = typeof value;
95
+
96
+ if (type !== 'string' && type !== 'number' && type !== 'boolean') {
97
+ throw new Error(
98
+ `Invalid WHERE value type: ${type}. ` +
99
+ `Only string, number, boolean, or null are allowed.`
100
+ );
101
+ }
102
+
103
+ return value;
104
+ }
105
+
106
+ #sanitizeError(error) {
107
+ const isProduction = process.env.APP_DEV === 'false';
108
+
109
+ if (isProduction) {
110
+ console.error('[QueryBuilder] Database error:', {
111
+ message: error.message,
112
+ code: error.code,
113
+ errno: error.errno,
114
+ sql: error.sql?.substring(0, 100),
115
+ sqlState: error.sqlState
116
+ });
117
+
118
+ const sanitized = new Error('Database query failed. Please contact support if the problem persists.');
119
+ sanitized.code = 'DATABASE_ERROR';
120
+
121
+ return sanitized;
122
+ }
123
+
124
+ return error;
125
+ }
126
+
127
+ #reset() {
128
+ this.#selectColumns = ['*'];
129
+ this.#wheres = [];
130
+ this.#bindings = [];
131
+ this.#joins = [];
132
+ this.#orders = [];
133
+ this.#groups = [];
134
+ this.#havings = [];
135
+ this.#limitValue = null;
136
+ this.#offsetValue = null;
137
+ this.#distinctFlag = false;
138
+ }
139
+
140
+ select(...columns) {
141
+ if (columns.length === 0) {
142
+ return this;
143
+ }
144
+
145
+ this.#selectColumns = columns.map(col => {
146
+ if (this.#isRawExpression(col)) {
147
+ return col;
148
+ }
149
+
150
+ return this.#validateIdentifier(col);
151
+ });
152
+
153
+ return this;
154
+ }
155
+
156
+ distinct() {
157
+ this.#distinctFlag = true;
158
+
159
+ return this;
160
+ }
161
+
162
+ where(column, operator, value) {
163
+ if (arguments.length === 2) {
164
+ value = operator;
165
+ operator = '=';
166
+ }
167
+
168
+ operator = this.#validateWhereOperator(operator);
169
+ value = this.#validateWhereValue(value);
170
+
171
+ this.#wheres.push({
172
+ type: 'basic',
173
+ column: this.#validateIdentifier(column),
174
+ operator,
175
+ value,
176
+ boolean: 'AND'
177
+ });
178
+
179
+ this.#bindings.push(value);
180
+
181
+ return this;
182
+ }
183
+
184
+ orWhere(column, operator, value) {
185
+ if (arguments.length === 2) {
186
+ value = operator;
187
+ operator = '=';
188
+ }
189
+
190
+ operator = this.#validateWhereOperator(operator);
191
+ value = this.#validateWhereValue(value);
192
+
193
+ this.#wheres.push({
194
+ type: 'basic',
195
+ column: this.#validateIdentifier(column),
196
+ operator,
197
+ value,
198
+ boolean: 'OR'
199
+ });
200
+
201
+ this.#bindings.push(value);
202
+
203
+ return this;
204
+ }
205
+
206
+ whereIn(column, values) {
207
+ if (!Array.isArray(values) || values.length === 0) {
208
+ return this;
209
+ }
210
+
211
+ if (values.length > 1000) {
212
+ throw new Error(
213
+ `whereIn() array too large (${values.length} values). ` +
214
+ 'Maximum allowed is 1000. Consider splitting into multiple queries or using a temporary table.'
215
+ );
216
+ }
217
+
218
+ for (const value of values) {
219
+ const type = typeof value;
220
+ if (type !== 'string' && type !== 'number' && type !== 'boolean' && value !== null) {
221
+ throw new Error(
222
+ `whereIn() values must be primitives (string, number, boolean, null). ` +
223
+ `Found: ${type}`
224
+ );
225
+ }
226
+ }
227
+
228
+ this.#wheres.push({
229
+ type: 'in',
230
+ column: this.#validateIdentifier(column),
231
+ values,
232
+ boolean: 'AND'
233
+ });
234
+
235
+ this.#bindings.push(...values);
236
+
237
+ return this;
238
+ }
239
+
240
+ whereNotIn(column, values) {
241
+ if (!Array.isArray(values) || values.length === 0) {
242
+ return this;
243
+ }
244
+
245
+ if (values.length > 1000) {
246
+ throw new Error(
247
+ `whereNotIn() array too large (${values.length} values). ` +
248
+ 'Maximum allowed is 1000. Consider splitting into multiple queries.'
249
+ );
250
+ }
251
+
252
+ this.#wheres.push({
253
+ type: 'notIn',
254
+ column: this.#validateIdentifier(column),
255
+ values,
256
+ boolean: 'AND'
257
+ });
258
+
259
+ this.#bindings.push(...values);
260
+
261
+ return this;
262
+ }
263
+
264
+ whereNull(column) {
265
+ this.#wheres.push({
266
+ type: 'null',
267
+ column: this.#validateIdentifier(column),
268
+ boolean: 'AND'
269
+ });
270
+
271
+ return this;
272
+ }
273
+
274
+ whereNotNull(column) {
275
+ this.#wheres.push({
276
+ type: 'notNull',
277
+ column: this.#validateIdentifier(column),
278
+ boolean: 'AND'
279
+ });
280
+
281
+ return this;
282
+ }
283
+
284
+ whereBetween(column, values) {
285
+ if (!Array.isArray(values) || values.length !== 2) {
286
+ throw new Error('whereBetween requires array with exactly 2 values');
287
+ }
288
+
289
+ this.#wheres.push({
290
+ type: 'between',
291
+ column: this.#validateIdentifier(column),
292
+ values,
293
+ boolean: 'AND'
294
+ });
295
+
296
+ this.#bindings.push(...values);
297
+
298
+ return this;
299
+ }
300
+
301
+ whereRaw(sql, bindings = []) {
302
+ if (typeof sql !== 'string' || sql.length === 0) {
303
+ throw new Error('whereRaw requires non-empty SQL string');
304
+ }
305
+
306
+ this.#wheres.push({
307
+ type: 'raw',
308
+ sql,
309
+ boolean: 'AND'
310
+ });
311
+
312
+ this.#bindings.push(...bindings);
313
+
314
+ return this;
315
+ }
316
+
317
+ join(table, first, operator, second, type = 'inner') {
318
+ if (arguments.length === 3) {
319
+ second = operator;
320
+ operator = '=';
321
+ }
322
+
323
+ const validTypes = ['inner', 'left', 'right'];
324
+ const joinType = validTypes.includes(type) ? type : 'inner';
325
+
326
+ this.#joins.push({
327
+ type: joinType,
328
+ table: this.#validateIdentifier(table),
329
+ first: this.#validateIdentifier(first),
330
+ operator: this.#validateJoinOperator(operator),
331
+ second: this.#validateIdentifier(second)
332
+ });
333
+
334
+ return this;
335
+ }
336
+
337
+ leftJoin(table, first, operator, second) {
338
+ return this.join(table, first, operator, second, 'left');
339
+ }
340
+
341
+ rightJoin(table, first, operator, second) {
342
+ return this.join(table, first, operator, second, 'right');
343
+ }
344
+
345
+ orderBy(column, direction = 'ASC') {
346
+ this.#orders.push({
347
+ column: this.#validateIdentifier(column),
348
+ direction: this.#validateDirection(direction)
349
+ });
350
+
351
+ return this;
352
+ }
353
+
354
+ groupBy(...columns) {
355
+ const validated = columns.map(col => this.#validateIdentifier(col));
356
+ this.#groups.push(...validated);
357
+
358
+ return this;
359
+ }
360
+
361
+ having(column, operator, value) {
362
+ if (arguments.length === 2) {
363
+ value = operator;
364
+ operator = '=';
365
+ }
366
+
367
+ operator = this.#validateWhereOperator(operator);
368
+
369
+ this.#havings.push({
370
+ column: this.#validateIdentifier(column),
371
+ operator,
372
+ value
373
+ });
374
+ this.#bindings.push(value);
375
+
376
+
377
+ return this;
378
+ }
379
+
380
+ limit(value) {
381
+ this.#limitValue = this.#validateInteger(value);
382
+
383
+ return this;
384
+ }
385
+
386
+ offset(value) {
387
+ this.#offsetValue = this.#validateInteger(value);
388
+
389
+ return this;
390
+ }
391
+
392
+ forPage(page, perPage = 15) {
393
+ return this.offset((page - 1) * perPage).limit(perPage);
394
+ }
395
+
396
+ async get() {
397
+ const connection = await this.#getConnection();
398
+
399
+ try {
400
+ const sql = this.#toSql();
401
+ const [rows] = await connection.query(sql, this.#bindings);
402
+
403
+ if (this.#modelClass) {
404
+ return rows.map(row => this.#hydrateModel(row));
405
+ }
406
+
407
+ return rows;
408
+ }
409
+
410
+ catch (error) {
411
+ throw this.#sanitizeError(error);
412
+ }
413
+
414
+ finally {
415
+ this.#reset();
416
+ }
417
+ }
418
+
419
+ async first() {
420
+ const connection = await this.#getConnection();
421
+
422
+ const originalLimit = this.#limitValue;
423
+ this.#limitValue = 1;
424
+
425
+ try {
426
+ const sql = this.#toSql();
427
+ const [rows] = await connection.query(sql, this.#bindings);
428
+
429
+ if (!rows[0]) return null;
430
+
431
+ if (this.#modelClass) {
432
+ return this.#hydrateModel(rows[0]);
433
+ }
434
+
435
+ return rows[0];
436
+ }
437
+
438
+ catch (error) {
439
+ throw this.#sanitizeError(error);
440
+ }
441
+
442
+ finally {
443
+ this.#limitValue = originalLimit;
444
+ this.#reset();
445
+ }
446
+ }
447
+
448
+ #hydrateModel(row) {
449
+ const instance = new this.#modelClass();
450
+
451
+ for (const [key, value] of Object.entries(row)) {
452
+ if (value instanceof Date) {
453
+ instance._attributes[key] = value.toISOString().slice(0, 19).replace('T', ' ');
454
+ } else {
455
+ instance._attributes[key] = value;
456
+ }
457
+ }
458
+
459
+ instance._exists = true;
460
+ instance._original = { ...instance._attributes };
461
+
462
+ return instance;
463
+ }
464
+
465
+ async find(id) {
466
+ return this.where('id', id).first();
467
+ }
468
+
469
+ async count(column = '*') {
470
+ const result = await this.#aggregate('COUNT', column);
471
+
472
+ return parseInt(result) || 0;
473
+ }
474
+
475
+ async #aggregate(func, column) {
476
+ const alias = func.toLowerCase();
477
+ const originalSelect = this.#selectColumns;
478
+
479
+ try {
480
+ const validatedColumn = column === '*' ? '*' : this.#validateIdentifier(column);
481
+ this.#selectColumns = [new RawExpression(`${func}(${validatedColumn}) as ${alias}`)];
482
+
483
+ const row = await this.first(); // first() handles reset internally
484
+
485
+ return row ? row[alias] : null;
486
+ }
487
+
488
+ finally {
489
+ this.#selectColumns = originalSelect;
490
+ }
491
+ }
492
+
493
+ async insert(data) {
494
+ const connection = await this.#getConnection();
495
+ try {
496
+ const isArray = Array.isArray(data);
497
+ const rows = isArray ? data : [data];
498
+
499
+ if (rows.length === 0) {
500
+ throw new Error('Cannot insert empty data');
501
+ }
502
+
503
+ const columns = Object.keys(rows[0]);
504
+ columns.forEach(col => this.#validateIdentifier(col));
505
+
506
+ const placeholders = rows.map(() => `(${columns.map(() => '?').join(', ')})`).join(', ');
507
+ const values = rows.flatMap(row => columns.map(col => row[col]));
508
+
509
+ const sql = `INSERT INTO ${this.#table} (${columns.join(', ')}) VALUES ${placeholders}`;
510
+ const [result] = await connection.query(sql, values);
511
+
512
+
513
+ return result.insertId;
514
+ }
515
+
516
+ catch (error) {
517
+ throw this.#sanitizeError(error);
518
+ }
519
+
520
+ finally {
521
+ this.#reset();
522
+ }
523
+ }
524
+
525
+ async update(data) {
526
+ const connection = await this.#getConnection();
527
+ try {
528
+ const columns = Object.keys(data);
529
+ const values = [];
530
+ const sets = [];
531
+
532
+ for (const col of columns) {
533
+ const validatedCol = this.#validateIdentifier(col);
534
+ const val = data[col];
535
+ if (this.#isRawExpression(val)) {
536
+ sets.push(`${validatedCol} = ${val.value}`);
537
+ }
538
+
539
+ else {
540
+ sets.push(`${validatedCol} = ?`);
541
+ values.push(val);
542
+ }
543
+ }
544
+
545
+ const sql = `UPDATE ${this.#table} SET ${sets.join(', ')}${this.#compileWheres()}`;
546
+ const [result] = await connection.query(sql, [...values, ...this.#bindings]);
547
+
548
+ return result.affectedRows;
549
+ }
550
+
551
+ catch (error) {
552
+ throw this.#sanitizeError(error);
553
+ }
554
+
555
+ finally {
556
+ this.#reset();
557
+ }
558
+ }
559
+
560
+ async delete() {
561
+ const connection = await this.#getConnection();
562
+ try {
563
+ const sql = `DELETE FROM ${this.#table}${this.#compileWheres()}`;
564
+ const [result] = await connection.query(sql, this.#bindings);
565
+
566
+ return result.affectedRows;
567
+ }
568
+
569
+ catch (error) {
570
+ throw this.#sanitizeError(error);
571
+ }
572
+
573
+ finally {
574
+ this.#reset();
575
+ }
576
+ }
577
+
578
+ #toSql() {
579
+ const distinct = this.#distinctFlag ? 'DISTINCT ' : '';
580
+ const columns = this.#selectColumns.map(col => {
581
+ return this.#isRawExpression(col) ? col.value : col;
582
+ }).join(', ');
583
+
584
+ const joins = this.#compileJoins();
585
+ const wheres = this.#compileWheres();
586
+ const groups = this.#compileGroupBy();
587
+ const havings = this.#compileHaving();
588
+ const orders = this.#compileOrders();
589
+ const limit = this.#compileLimit();
590
+
591
+ return `SELECT ${distinct}${columns} FROM ${this.#table}${joins}${wheres}${groups}${havings}${orders}${limit}`;
592
+ }
593
+
594
+ #compileJoins() {
595
+ if (this.#joins.length === 0) {
596
+ return '';
597
+ }
598
+
599
+ return this.#joins.map(join => {
600
+ const type = join.type.toUpperCase();
601
+
602
+ return ` ${type} JOIN ${join.table} ON ${join.first} ${join.operator} ${join.second}`;
603
+ }).join('');
604
+ }
605
+
606
+ #compileWheres() {
607
+ if (this.#wheres.length === 0) {
608
+ return '';
609
+ }
610
+
611
+ const compiled = this.#wheres.map((where, index) => {
612
+ const boolean = index === 0 ? '' : ` ${where.boolean} `;
613
+
614
+ switch (where.type) {
615
+ case 'basic':
616
+ return `${boolean}${where.column} ${where.operator} ?`;
617
+ case 'in':
618
+ const inPlaceholders = where.values.map(() => '?').join(', ');
619
+
620
+ return `${boolean}${where.column} IN (${inPlaceholders})`;
621
+ case 'notIn':
622
+ const notInPlaceholders = where.values.map(() => '?').join(', ');
623
+
624
+ return `${boolean}${where.column} NOT IN (${notInPlaceholders})`;
625
+ case 'null':
626
+ return `${boolean}${where.column} IS NULL`;
627
+ case 'notNull':
628
+ return `${boolean}${where.column} IS NOT NULL`;
629
+ case 'between':
630
+ return `${boolean}${where.column} BETWEEN ? AND ?`;
631
+ case 'raw':
632
+ return `${boolean}${where.sql}`;
633
+ default:
634
+ return '';
635
+ }
636
+ }).join('');
637
+
638
+ return ` WHERE ${compiled}`;
639
+ }
640
+
641
+ #compileGroupBy() {
642
+ if (this.#groups.length === 0) {
643
+ return '';
644
+ }
645
+
646
+ return ` GROUP BY ${this.#groups.join(', ')}`;
647
+ }
648
+
649
+ #compileHaving() {
650
+ if (this.#havings.length === 0) {
651
+ return '';
652
+ }
653
+
654
+ const compiled = this.#havings.map((having, index) => {
655
+ const boolean = index === 0 ? '' : ' AND ';
656
+
657
+ return `${boolean}${having.column} ${having.operator} ?`;
658
+ }).join('');
659
+
660
+ return ` HAVING ${compiled}`;
661
+ }
662
+
663
+ #compileOrders() {
664
+ if (this.#orders.length === 0) {
665
+ return '';
666
+ }
667
+
668
+ const compiled = this.#orders.map(order => `${order.column} ${order.direction}`).join(', ');
669
+
670
+ return ` ORDER BY ${compiled}`;
671
+ }
672
+
673
+ #compileLimit() {
674
+ let sql = '';
675
+
676
+ if (this.#limitValue !== null) {
677
+ sql += ` LIMIT ${this.#limitValue}`;
678
+ }
679
+
680
+ if (this.#offsetValue !== null) {
681
+ sql += ` OFFSET ${this.#offsetValue}`;
682
+ }
683
+
684
+ return sql;
685
+ }
686
+
687
+ toSql() {
688
+ return this.#toSql();
689
+ }
690
+
691
+ getBindings() {
692
+ return [...this.#bindings];
693
+ }
694
+ }
695
+
696
+ export class RawExpression {
697
+ constructor(value) {
698
+ if (typeof value !== 'string') {
699
+ throw new Error('RawExpression value must be a string');
700
+ }
701
+
702
+ this.value = value;
703
+ }
704
+
705
+ toString() {
706
+ return this.value;
707
+ }
708
+ }
709
+
710
+ export function query(table, connection = null, modelClass = null) {
711
+ return new QueryBuilder(table, connection, modelClass);
712
+ }
713
+
714
+ export default QueryBuilder;