create-phoenixjs 0.1.3 → 0.1.5

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 (59) hide show
  1. package/package.json +1 -1
  2. package/template/app/controllers/HealthController.ts +22 -0
  3. package/template/app/gateways/EchoGateway.ts +58 -0
  4. package/template/bootstrap/app.ts +2 -7
  5. package/template/config/app.ts +12 -0
  6. package/template/config/database.ts +13 -1
  7. package/template/database/migrations/2024_01_01_000000_create_test_users_cli_table.ts +16 -0
  8. package/template/database/migrations/20260108164611_TestCliMigration.ts +16 -0
  9. package/template/database/migrations/2026_01_08_16_46_11_CreateTestMigrationsTable.ts +21 -0
  10. package/template/framework/cli/artisan.ts +12 -0
  11. package/template/framework/core/Application.ts +15 -0
  12. package/template/framework/database/DatabaseManager.ts +133 -0
  13. package/template/framework/database/connection/Connection.ts +71 -0
  14. package/template/framework/database/connection/ConnectionFactory.ts +30 -0
  15. package/template/framework/database/connection/PostgresConnection.ts +159 -0
  16. package/template/framework/database/console/MakeMigrationCommand.ts +58 -0
  17. package/template/framework/database/console/MigrateCommand.ts +32 -0
  18. package/template/framework/database/console/MigrateResetCommand.ts +31 -0
  19. package/template/framework/database/console/MigrateRollbackCommand.ts +31 -0
  20. package/template/framework/database/console/MigrateStatusCommand.ts +38 -0
  21. package/template/framework/database/migrations/DatabaseMigrationRepository.ts +122 -0
  22. package/template/framework/database/migrations/Migration.ts +5 -0
  23. package/template/framework/database/migrations/MigrationRepository.ts +46 -0
  24. package/template/framework/database/migrations/Migrator.ts +249 -0
  25. package/template/framework/database/migrations/index.ts +4 -0
  26. package/template/framework/database/orm/BelongsTo.ts +246 -0
  27. package/template/framework/database/orm/BelongsToMany.ts +570 -0
  28. package/template/framework/database/orm/Builder.ts +160 -0
  29. package/template/framework/database/orm/EagerLoadingBuilder.ts +324 -0
  30. package/template/framework/database/orm/HasMany.ts +303 -0
  31. package/template/framework/database/orm/HasManyThrough.ts +282 -0
  32. package/template/framework/database/orm/HasOne.ts +201 -0
  33. package/template/framework/database/orm/HasOneThrough.ts +281 -0
  34. package/template/framework/database/orm/Model.ts +1766 -0
  35. package/template/framework/database/orm/Relation.ts +342 -0
  36. package/template/framework/database/orm/Scope.ts +14 -0
  37. package/template/framework/database/orm/SoftDeletes.ts +160 -0
  38. package/template/framework/database/orm/index.ts +54 -0
  39. package/template/framework/database/orm/scopes/SoftDeletingScope.ts +58 -0
  40. package/template/framework/database/pagination/LengthAwarePaginator.ts +55 -0
  41. package/template/framework/database/pagination/Paginator.ts +110 -0
  42. package/template/framework/database/pagination/index.ts +2 -0
  43. package/template/framework/database/query/Builder.ts +918 -0
  44. package/template/framework/database/query/DB.ts +139 -0
  45. package/template/framework/database/query/grammars/Grammar.ts +430 -0
  46. package/template/framework/database/query/grammars/PostgresGrammar.ts +224 -0
  47. package/template/framework/database/query/grammars/index.ts +6 -0
  48. package/template/framework/database/query/index.ts +8 -0
  49. package/template/framework/database/query/types.ts +196 -0
  50. package/template/framework/database/schema/Blueprint.ts +478 -0
  51. package/template/framework/database/schema/Schema.ts +149 -0
  52. package/template/framework/database/schema/SchemaBuilder.ts +152 -0
  53. package/template/framework/database/schema/grammars/PostgresSchemaGrammar.ts +293 -0
  54. package/template/framework/database/schema/grammars/index.ts +5 -0
  55. package/template/framework/database/schema/index.ts +9 -0
  56. package/template/framework/log/Logger.ts +195 -0
  57. package/template/package.json +4 -1
  58. package/template/routes/api.ts +13 -35
  59. package/template/app/controllers/ExampleController.ts +0 -61
@@ -0,0 +1,918 @@
1
+ /**
2
+ * PhoenixJS ORM - Query Builder
3
+ *
4
+ * Fluent interface for building and executing SQL queries.
5
+ * Inspired by Laravel's Query Builder with full TypeScript support.
6
+ */
7
+
8
+ import type { Connection } from '../connection/Connection';
9
+ import type { Grammar } from './grammars/Grammar';
10
+ import type {
11
+ QueryComponents,
12
+ WhereClause,
13
+ OrderClause,
14
+ JoinClause,
15
+ JoinType,
16
+ Binding,
17
+ WhereOperator,
18
+ OrderDirection,
19
+ AggregateFunction,
20
+ InsertValues,
21
+ UpdateValues,
22
+ } from './types';
23
+ import { createQueryComponents } from './types';
24
+ import { Paginator } from '../pagination/Paginator';
25
+ import { LengthAwarePaginator } from '../pagination/LengthAwarePaginator';
26
+
27
+ /**
28
+ * Query Builder
29
+ *
30
+ * Provides a fluent interface for building SQL queries.
31
+ * Works with any Grammar implementation for database-specific SQL generation.
32
+ */
33
+ export class Builder {
34
+ /** Query state components */
35
+ protected query: QueryComponents;
36
+
37
+ /** The database connection */
38
+ protected connection: Connection;
39
+
40
+ /** The grammar for compiling queries */
41
+ protected grammar: Grammar;
42
+
43
+ constructor(connection: Connection, grammar: Grammar, table?: string) {
44
+ this.connection = connection;
45
+ this.grammar = grammar;
46
+ this.query = createQueryComponents(table || '');
47
+ }
48
+
49
+ // ==========================================
50
+ // Table & Column Selection
51
+ // ==========================================
52
+
53
+ /**
54
+ * Set the table the query is targeting
55
+ */
56
+ table(name: string, alias?: string): this {
57
+ this.query.table = name;
58
+ if (alias) {
59
+ this.query.alias = alias;
60
+ }
61
+ return this;
62
+ }
63
+
64
+ /**
65
+ * Alias for table()
66
+ */
67
+ from(name: string, alias?: string): this {
68
+ return this.table(name, alias);
69
+ }
70
+
71
+ /**
72
+ * Set the columns to be selected
73
+ */
74
+ select(...columns: string[]): this {
75
+ this.query.columns = columns.length > 0 ? columns : ['*'];
76
+ return this;
77
+ }
78
+
79
+ /**
80
+ * Add a column to the select list
81
+ */
82
+ addSelect(...columns: string[]): this {
83
+ this.query.columns.push(...columns);
84
+ return this;
85
+ }
86
+
87
+ /**
88
+ * Force the query to use DISTINCT
89
+ */
90
+ distinct(): this {
91
+ this.query.distinct = true;
92
+ return this;
93
+ }
94
+
95
+ /**
96
+ * Add a raw expression to the select clause
97
+ */
98
+ selectRaw(expression: string, bindings: Binding[] = []): this {
99
+ this.query.rawSelects.push({ expression, bindings });
100
+ return this;
101
+ }
102
+
103
+ // ==========================================
104
+ // JOIN Clauses
105
+ // ==========================================
106
+
107
+ /**
108
+ * Add an inner join clause
109
+ */
110
+ join(table: string, first: string, operator: string, second: string): this {
111
+ return this.addJoin('inner', table, first, operator, second);
112
+ }
113
+
114
+ /**
115
+ * Add a left join clause
116
+ */
117
+ leftJoin(table: string, first: string, operator: string, second: string): this {
118
+ return this.addJoin('left', table, first, operator, second);
119
+ }
120
+
121
+ /**
122
+ * Add a right join clause
123
+ */
124
+ rightJoin(table: string, first: string, operator: string, second: string): this {
125
+ return this.addJoin('right', table, first, operator, second);
126
+ }
127
+
128
+ /**
129
+ * Add a cross join clause (no ON clause)
130
+ */
131
+ crossJoin(table: string): this {
132
+ // Cross join doesn't have ON clause, use empty strings for first/second
133
+ const joinClause: JoinClause = {
134
+ type: 'cross',
135
+ table,
136
+ first: '',
137
+ operator: '',
138
+ second: '',
139
+ };
140
+ this.query.joins.push(joinClause);
141
+ return this;
142
+ }
143
+
144
+ /**
145
+ * Internal method to add a join clause
146
+ */
147
+ protected addJoin(
148
+ type: JoinType,
149
+ table: string,
150
+ first: string,
151
+ operator: string,
152
+ second: string
153
+ ): this {
154
+ const joinClause: JoinClause = {
155
+ type,
156
+ table,
157
+ first,
158
+ operator,
159
+ second,
160
+ };
161
+ this.query.joins.push(joinClause);
162
+ return this;
163
+ }
164
+
165
+ // ==========================================
166
+ // WHERE Clauses
167
+ // ==========================================
168
+
169
+ /**
170
+ * Add a basic WHERE clause
171
+ */
172
+ where(column: string, operatorOrValue?: WhereOperator | string | Binding, value?: Binding): this {
173
+ return this.addWhere(column, operatorOrValue, value, 'and');
174
+ }
175
+
176
+ /**
177
+ * Add an OR WHERE clause
178
+ */
179
+ orWhere(column: string, operatorOrValue?: WhereOperator | string | Binding, value?: Binding): this {
180
+ return this.addWhere(column, operatorOrValue, value, 'or');
181
+ }
182
+
183
+ /**
184
+ * Internal method to add a WHERE clause
185
+ */
186
+ protected addWhere(
187
+ column: string,
188
+ operatorOrValue?: WhereOperator | string | Binding,
189
+ value?: Binding,
190
+ boolean: 'and' | 'or' = 'and'
191
+ ): this {
192
+ // Handle (column, value) syntax - default to '=' operator
193
+ let operator: WhereOperator | string = '=';
194
+ let finalValue: Binding;
195
+
196
+ if (value === undefined) {
197
+ finalValue = operatorOrValue as Binding;
198
+ } else {
199
+ operator = operatorOrValue as WhereOperator | string;
200
+ finalValue = value;
201
+ }
202
+
203
+ const whereClause: WhereClause = {
204
+ type: 'basic',
205
+ column,
206
+ operator,
207
+ value: finalValue,
208
+ boolean,
209
+ };
210
+
211
+ this.query.wheres.push(whereClause);
212
+ return this;
213
+ }
214
+
215
+ /**
216
+ * Add a WHERE IN clause
217
+ */
218
+ whereIn(column: string, values: Binding[]): this {
219
+ this.query.wheres.push({
220
+ type: 'in',
221
+ column,
222
+ value: values,
223
+ boolean: 'and',
224
+ not: false,
225
+ });
226
+ return this;
227
+ }
228
+
229
+ /**
230
+ * Add an OR WHERE IN clause
231
+ */
232
+ orWhereIn(column: string, values: Binding[]): this {
233
+ this.query.wheres.push({
234
+ type: 'in',
235
+ column,
236
+ value: values,
237
+ boolean: 'or',
238
+ not: false,
239
+ });
240
+ return this;
241
+ }
242
+
243
+ /**
244
+ * Add a WHERE NOT IN clause
245
+ */
246
+ whereNotIn(column: string, values: Binding[]): this {
247
+ this.query.wheres.push({
248
+ type: 'in',
249
+ column,
250
+ value: values,
251
+ boolean: 'and',
252
+ not: true,
253
+ });
254
+ return this;
255
+ }
256
+
257
+ /**
258
+ * Add an OR WHERE NOT IN clause
259
+ */
260
+ orWhereNotIn(column: string, values: Binding[]): this {
261
+ this.query.wheres.push({
262
+ type: 'in',
263
+ column,
264
+ value: values,
265
+ boolean: 'or',
266
+ not: true,
267
+ });
268
+ return this;
269
+ }
270
+
271
+ /**
272
+ * Add a WHERE IS NULL clause
273
+ */
274
+ whereNull(column: string): this {
275
+ this.query.wheres.push({
276
+ type: 'null',
277
+ column,
278
+ boolean: 'and',
279
+ not: false,
280
+ });
281
+ return this;
282
+ }
283
+
284
+ /**
285
+ * Add an OR WHERE IS NULL clause
286
+ */
287
+ orWhereNull(column: string): this {
288
+ this.query.wheres.push({
289
+ type: 'null',
290
+ column,
291
+ boolean: 'or',
292
+ not: false,
293
+ });
294
+ return this;
295
+ }
296
+
297
+ /**
298
+ * Add a WHERE IS NOT NULL clause
299
+ */
300
+ whereNotNull(column: string): this {
301
+ this.query.wheres.push({
302
+ type: 'null',
303
+ column,
304
+ boolean: 'and',
305
+ not: true,
306
+ });
307
+ return this;
308
+ }
309
+
310
+ /**
311
+ * Add an OR WHERE IS NOT NULL clause
312
+ */
313
+ orWhereNotNull(column: string): this {
314
+ this.query.wheres.push({
315
+ type: 'null',
316
+ column,
317
+ boolean: 'or',
318
+ not: true,
319
+ });
320
+ return this;
321
+ }
322
+
323
+ /**
324
+ * Add a WHERE BETWEEN clause
325
+ */
326
+ whereBetween(column: string, values: [Binding, Binding]): this {
327
+ this.query.wheres.push({
328
+ type: 'between',
329
+ column,
330
+ values,
331
+ boolean: 'and',
332
+ not: false,
333
+ });
334
+ return this;
335
+ }
336
+
337
+ /**
338
+ * Add an OR WHERE BETWEEN clause
339
+ */
340
+ orWhereBetween(column: string, values: [Binding, Binding]): this {
341
+ this.query.wheres.push({
342
+ type: 'between',
343
+ column,
344
+ values,
345
+ boolean: 'or',
346
+ not: false,
347
+ });
348
+ return this;
349
+ }
350
+
351
+ /**
352
+ * Add a WHERE NOT BETWEEN clause
353
+ */
354
+ whereNotBetween(column: string, values: [Binding, Binding]): this {
355
+ this.query.wheres.push({
356
+ type: 'between',
357
+ column,
358
+ values,
359
+ boolean: 'and',
360
+ not: true,
361
+ });
362
+ return this;
363
+ }
364
+
365
+ /**
366
+ * Add an OR WHERE NOT BETWEEN clause
367
+ */
368
+ orWhereNotBetween(column: string, values: [Binding, Binding]): this {
369
+ this.query.wheres.push({
370
+ type: 'between',
371
+ column,
372
+ values,
373
+ boolean: 'or',
374
+ not: true,
375
+ });
376
+ return this;
377
+ }
378
+
379
+ /**
380
+ * Add a raw WHERE clause
381
+ */
382
+ whereRaw(sql: string, bindings: Binding[] = []): this {
383
+ this.query.wheres.push({
384
+ type: 'raw',
385
+ sql,
386
+ bindings,
387
+ boolean: 'and',
388
+ });
389
+ return this;
390
+ }
391
+
392
+ /**
393
+ * Add a raw OR WHERE clause
394
+ */
395
+ orWhereRaw(sql: string, bindings: Binding[] = []): this {
396
+ this.query.wheres.push({
397
+ type: 'raw',
398
+ sql,
399
+ bindings,
400
+ boolean: 'or',
401
+ });
402
+ return this;
403
+ }
404
+
405
+ // ==========================================
406
+ // Ordering, Grouping, Limit & Offset
407
+ // ==========================================
408
+
409
+ /**
410
+ * Add an ORDER BY clause
411
+ */
412
+ orderBy(column: string, direction: OrderDirection = 'asc'): this {
413
+ const order: OrderClause = {
414
+ column,
415
+ direction: direction.toLowerCase() as OrderDirection,
416
+ };
417
+ this.query.orders.push(order);
418
+ return this;
419
+ }
420
+
421
+ /**
422
+ * Add a descending ORDER BY clause
423
+ */
424
+ orderByDesc(column: string): this {
425
+ return this.orderBy(column, 'desc');
426
+ }
427
+
428
+ /**
429
+ * Order by latest (created_at DESC)
430
+ */
431
+ latest(column: string = 'created_at'): this {
432
+ return this.orderBy(column, 'desc');
433
+ }
434
+
435
+ /**
436
+ * Order by oldest (created_at ASC)
437
+ */
438
+ oldest(column: string = 'created_at'): this {
439
+ return this.orderBy(column, 'asc');
440
+ }
441
+
442
+ /**
443
+ * Add a raw ORDER BY expression
444
+ */
445
+ orderByRaw(expression: string, bindings: Binding[] = []): this {
446
+ const order: OrderClause = {
447
+ column: expression,
448
+ direction: 'asc', // Direction is ignored for raw expressions
449
+ raw: true,
450
+ };
451
+ this.query.orders.push(order);
452
+ this.query.orderBindings.push(...bindings);
453
+ return this;
454
+ }
455
+
456
+ // ==========================================
457
+ // GROUP BY & HAVING Clauses
458
+ // ==========================================
459
+
460
+ /**
461
+ * Add GROUP BY columns
462
+ */
463
+ groupBy(...columns: string[]): this {
464
+ this.query.groups.push(...columns);
465
+ return this;
466
+ }
467
+
468
+ /**
469
+ * Add a raw GROUP BY expression
470
+ */
471
+ groupByRaw(expression: string, bindings: Binding[] = []): this {
472
+ this.query.rawGroups.push({ expression, bindings });
473
+ return this;
474
+ }
475
+
476
+ /**
477
+ * Add a HAVING clause
478
+ */
479
+ having(column: string, operatorOrValue: string | Binding, value?: Binding): this {
480
+ return this.addHaving(column, operatorOrValue, value, 'and');
481
+ }
482
+
483
+ /**
484
+ * Add an OR HAVING clause
485
+ */
486
+ orHaving(column: string, operatorOrValue: string | Binding, value?: Binding): this {
487
+ return this.addHaving(column, operatorOrValue, value, 'or');
488
+ }
489
+
490
+ /**
491
+ * Add a raw HAVING clause
492
+ */
493
+ havingRaw(sql: string, bindings: Binding[] = []): this {
494
+ this.query.havings.push({
495
+ type: 'raw',
496
+ sql,
497
+ bindings,
498
+ boolean: 'and',
499
+ });
500
+ return this;
501
+ }
502
+
503
+ /**
504
+ * Add a raw OR HAVING clause
505
+ */
506
+ orHavingRaw(sql: string, bindings: Binding[] = []): this {
507
+ this.query.havings.push({
508
+ type: 'raw',
509
+ sql,
510
+ bindings,
511
+ boolean: 'or',
512
+ });
513
+ return this;
514
+ }
515
+
516
+ /**
517
+ * Internal method to add a HAVING clause
518
+ */
519
+ protected addHaving(
520
+ column: string,
521
+ operatorOrValue: string | Binding,
522
+ value: Binding | undefined,
523
+ boolean: 'and' | 'or'
524
+ ): this {
525
+ // Handle (column, value) syntax - default to '=' operator
526
+ let operator: string = '=';
527
+ let finalValue: Binding;
528
+
529
+ if (value === undefined) {
530
+ finalValue = operatorOrValue as Binding;
531
+ } else {
532
+ operator = operatorOrValue as string;
533
+ finalValue = value;
534
+ }
535
+
536
+ const havingClause: WhereClause = {
537
+ type: 'basic',
538
+ column,
539
+ operator,
540
+ value: finalValue,
541
+ boolean,
542
+ };
543
+
544
+ this.query.havings.push(havingClause);
545
+ return this;
546
+ }
547
+
548
+ // ==========================================
549
+ // Limit & Offset
550
+ // ==========================================
551
+
552
+ /**
553
+ * Set the LIMIT value
554
+ */
555
+ limit(value: number): this {
556
+ this.query.limit = Math.max(0, parseInt(String(value), 10));
557
+ return this;
558
+ }
559
+
560
+ /**
561
+ * Alias for limit()
562
+ */
563
+ take(value: number): this {
564
+ return this.limit(value);
565
+ }
566
+
567
+ /**
568
+ * Set the OFFSET value
569
+ */
570
+ offset(value: number): this {
571
+ this.query.offset = Math.max(0, parseInt(String(value), 10));
572
+ return this;
573
+ }
574
+
575
+ /**
576
+ * Alias for offset()
577
+ */
578
+ skip(value: number): this {
579
+ return this.offset(value);
580
+ }
581
+
582
+ /**
583
+ * Set limit and offset for pagination
584
+ */
585
+ forPage(page: number, perPage: number = 15): this {
586
+ const safePage = Math.max(1, parseInt(String(page), 10));
587
+ const safePerPage = Math.max(1, parseInt(String(perPage), 10));
588
+ return this.offset((safePage - 1) * safePerPage).limit(safePerPage);
589
+ }
590
+
591
+ /**
592
+ * Paginate the given query.
593
+ */
594
+ async paginate<T = Record<string, unknown>>(
595
+ perPage: number = 15,
596
+ columns: string[] = ['*'],
597
+ pageName: string = 'page',
598
+ page?: number
599
+ ): Promise<LengthAwarePaginator<T>> {
600
+ // Determine current page
601
+ // In a real app, this would come from the request.
602
+ // For now, we rely on the passed 'page' argument or default to 1.
603
+ const currentPage = page || 1;
604
+
605
+ // Get total count
606
+ // const total = await this.count(); // Removed incorrect call
607
+
608
+ // Get items
609
+ // We clone the builder to avoid modifying the original query for the count
610
+ const countBuilder = this.clone();
611
+ // Remove orders and limits for count
612
+ countBuilder.query.orders = [];
613
+ countBuilder.query.limit = undefined;
614
+ countBuilder.query.offset = undefined;
615
+
616
+ const totalCount = await countBuilder.count();
617
+
618
+ // Get items
619
+ this.forPage(currentPage, perPage);
620
+ this.query.columns = columns.length > 0 ? columns : ['*'];
621
+ const results = await this.get<T>();
622
+
623
+ return new LengthAwarePaginator<T>(results, totalCount, perPage, currentPage, {
624
+ path: '/', // TODO: Get from request
625
+ query: {}, // TODO: Get from request
626
+ });
627
+ }
628
+
629
+ /**
630
+ * Paginate the given query into a simple paginator.
631
+ */
632
+ async simplePaginate<T = Record<string, unknown>>(
633
+ perPage: number = 15,
634
+ columns: string[] = ['*'],
635
+ pageName: string = 'page',
636
+ page?: number
637
+ ): Promise<Paginator<T>> {
638
+ const currentPage = page || 1;
639
+
640
+ // We request one more item to check if there are more pages
641
+ this.offset((currentPage - 1) * perPage).limit(perPage + 1);
642
+ this.query.columns = columns.length > 0 ? columns : ['*'];
643
+
644
+ const results = await this.get<T>();
645
+
646
+ // Check if we have more items
647
+ const hasMore = results.length > perPage;
648
+
649
+ // Remove the extra item
650
+ if (hasMore) {
651
+ results.pop();
652
+ }
653
+
654
+ return new Paginator<T>(results, perPage, currentPage, {
655
+ path: '/', // TODO: Get from request
656
+ query: {}, // TODO: Get from request
657
+ hasMore,
658
+ });
659
+ }
660
+
661
+ // ==========================================
662
+ // Execution Methods
663
+ // ==========================================
664
+
665
+ /**
666
+ * Execute the query and return all results
667
+ */
668
+ async get<T = Record<string, unknown>>(): Promise<T[]> {
669
+ const { sql, bindings } = this.grammar.compileSelect(this.query);
670
+ return this.connection.query<T>(sql, bindings);
671
+ }
672
+
673
+ /**
674
+ * Execute the query and return the first result
675
+ */
676
+ async first<T = Record<string, unknown>>(): Promise<T | null> {
677
+ this.limit(1);
678
+ const { sql, bindings } = this.grammar.compileSelect(this.query);
679
+ return this.connection.get<T>(sql, bindings);
680
+ }
681
+
682
+ /**
683
+ * Find a record by its primary key
684
+ */
685
+ async find<T = Record<string, unknown>>(id: number | string, primaryKey: string = 'id'): Promise<T | null> {
686
+ return this.where(primaryKey, '=', id).first<T>();
687
+ }
688
+
689
+ /**
690
+ * Get the value of a single column from the first result
691
+ */
692
+ async value<T = Binding>(column: string): Promise<T | null> {
693
+ this.select(column);
694
+ const result = await this.first<Record<string, T>>();
695
+ return result ? result[column] : null;
696
+ }
697
+
698
+ /**
699
+ * Get an array of values from a single column
700
+ */
701
+ async pluck<T = Binding>(column: string): Promise<T[]> {
702
+ this.select(column);
703
+ const results = await this.get<Record<string, T>>();
704
+ return results.map((row) => row[column]);
705
+ }
706
+
707
+ /**
708
+ * Check if any records exist
709
+ */
710
+ async exists(): Promise<boolean> {
711
+ const result = await this.limit(1).get();
712
+ return result.length > 0;
713
+ }
714
+
715
+ /**
716
+ * Check if no records exist
717
+ */
718
+ async doesntExist(): Promise<boolean> {
719
+ return !(await this.exists());
720
+ }
721
+
722
+ // ==========================================
723
+ // Aggregate Functions
724
+ // ==========================================
725
+
726
+ /**
727
+ * Get the count of records
728
+ */
729
+ async count(column: string = '*'): Promise<number> {
730
+ return this.aggregate('count', column);
731
+ }
732
+
733
+ /**
734
+ * Get the sum of a column
735
+ */
736
+ async sum(column: string): Promise<number> {
737
+ return this.aggregate('sum', column);
738
+ }
739
+
740
+ /**
741
+ * Get the average of a column
742
+ */
743
+ async avg(column: string): Promise<number> {
744
+ return this.aggregate('avg', column);
745
+ }
746
+
747
+ /**
748
+ * Get the minimum value of a column
749
+ */
750
+ async min(column: string): Promise<number> {
751
+ return this.aggregate('min', column);
752
+ }
753
+
754
+ /**
755
+ * Get the maximum value of a column
756
+ */
757
+ async max(column: string): Promise<number> {
758
+ return this.aggregate('max', column);
759
+ }
760
+
761
+ /**
762
+ * Execute an aggregate function
763
+ */
764
+ protected async aggregate(fn: AggregateFunction, column: string): Promise<number> {
765
+ this.query.aggregate = { function: fn, column };
766
+ const { sql, bindings } = this.grammar.compileSelect(this.query);
767
+ const result = await this.connection.get<Record<string, number | string | null>>(sql, bindings);
768
+
769
+ // Reset aggregate for potential reuse
770
+ this.query.aggregate = undefined;
771
+
772
+ if (!result) return 0;
773
+
774
+ // PostgreSQL returns the aggregate in the first column
775
+ const value = Object.values(result)[0];
776
+ return value !== null ? Number(value) : 0;
777
+ }
778
+
779
+ // ==========================================
780
+ // Insert Operations
781
+ // ==========================================
782
+
783
+ /**
784
+ * Insert a new record
785
+ */
786
+ async insert(values: InsertValues | InsertValues[]): Promise<boolean> {
787
+ const { sql, bindings } = this.grammar.compileInsert(this.query, values);
788
+ const affected = await this.connection.execute(sql, bindings);
789
+ return affected > 0;
790
+ }
791
+
792
+ /**
793
+ * Insert a record and get the ID
794
+ */
795
+ async insertGetId(values: InsertValues, primaryKey: string = 'id'): Promise<number | null> {
796
+ const { sql, bindings } = this.grammar.compileInsertReturning(this.query, values, [primaryKey]);
797
+ const result = await this.connection.query<Record<string, number>>(sql, bindings);
798
+ return result.length > 0 ? result[0][primaryKey] : null;
799
+ }
800
+
801
+ /**
802
+ * Insert a record and return the inserted row
803
+ */
804
+ async insertReturning<T = Record<string, unknown>>(
805
+ values: InsertValues,
806
+ returning: string[] = []
807
+ ): Promise<T | null> {
808
+ const { sql, bindings } = this.grammar.compileInsertReturning(this.query, values, returning);
809
+ const result = await this.connection.query<T>(sql, bindings);
810
+ return result.length > 0 ? result[0] : null;
811
+ }
812
+
813
+ // ==========================================
814
+ // Update Operations
815
+ // ==========================================
816
+
817
+ /**
818
+ * Update records
819
+ */
820
+ async update(values: UpdateValues): Promise<number> {
821
+ const { sql, bindings } = this.grammar.compileUpdate(this.query, values);
822
+ return this.connection.execute(sql, bindings);
823
+ }
824
+
825
+ /**
826
+ * Increment a column value
827
+ */
828
+ async increment(column: string, amount: number = 1, extra: UpdateValues = {}): Promise<number> {
829
+ // For increment, we need raw SQL
830
+ const updateValues: UpdateValues = { ...extra };
831
+ // Use raw expression for increment
832
+ const incrementSql = `${this.grammar.wrap(column)} + ${amount}`;
833
+
834
+ // Build update manually with raw increment
835
+ const table = this.grammar.wrapTable(this.query.table);
836
+ const bindings: Binding[] = [];
837
+
838
+ // Extra values first
839
+ const setClauses: string[] = [];
840
+ for (const [key, value] of Object.entries(extra)) {
841
+ bindings.push(value);
842
+ setClauses.push(`${this.grammar.wrap(key)} = $${bindings.length}`);
843
+ }
844
+
845
+ // Add increment column
846
+ setClauses.push(`${this.grammar.wrap(column)} = ${incrementSql}`);
847
+
848
+ // Compile where clause
849
+ const { sql: whereSql, bindings: whereBindings } = this.compileWhereClause();
850
+ bindings.push(...whereBindings);
851
+
852
+ const sql = `UPDATE ${table} SET ${setClauses.join(', ')}${whereSql ? ` ${whereSql}` : ''}`;
853
+ return this.connection.execute(sql, bindings);
854
+ }
855
+
856
+ /**
857
+ * Decrement a column value
858
+ */
859
+ async decrement(column: string, amount: number = 1, extra: UpdateValues = {}): Promise<number> {
860
+ return this.increment(column, -amount, extra);
861
+ }
862
+
863
+ // ==========================================
864
+ // Delete Operations
865
+ // ==========================================
866
+
867
+ /**
868
+ * Delete records
869
+ */
870
+ async delete(): Promise<number> {
871
+ const { sql, bindings } = this.grammar.compileDelete(this.query);
872
+ return this.connection.execute(sql, bindings);
873
+ }
874
+
875
+ /**
876
+ * Truncate the table
877
+ */
878
+ async truncate(): Promise<void> {
879
+ const sql = this.grammar.compileTruncate(this.query.table);
880
+ await this.connection.execute(sql);
881
+ }
882
+
883
+ // ==========================================
884
+ // Utility Methods
885
+ // ==========================================
886
+
887
+ /**
888
+ * Get the raw query components
889
+ */
890
+ getQuery(): QueryComponents {
891
+ return this.query;
892
+ }
893
+
894
+ /**
895
+ * Get the compiled SQL and bindings
896
+ */
897
+ toSql(): { sql: string; bindings: Binding[] } {
898
+ return this.grammar.compileSelect(this.query);
899
+ }
900
+
901
+ /**
902
+ * Clone the builder
903
+ */
904
+ clone(): Builder {
905
+ const cloned = new Builder(this.connection, this.grammar, this.query.table);
906
+ cloned.query = JSON.parse(JSON.stringify(this.query));
907
+ return cloned;
908
+ }
909
+
910
+ /**
911
+ * Compile just the WHERE clause
912
+ */
913
+ protected compileWhereClause(): { sql: string; bindings: Binding[] } {
914
+ const bindings: Binding[] = [];
915
+ const sql = this.grammar.compileWheres(this.query, bindings);
916
+ return { sql, bindings };
917
+ }
918
+ }