create-phoenixjs 0.1.4 → 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.
- package/package.json +1 -1
- package/template/config/database.ts +13 -1
- package/template/database/migrations/2024_01_01_000000_create_test_users_cli_table.ts +16 -0
- package/template/database/migrations/20260108164611_TestCliMigration.ts +16 -0
- package/template/database/migrations/2026_01_08_16_46_11_CreateTestMigrationsTable.ts +21 -0
- package/template/framework/cli/artisan.ts +12 -0
- package/template/framework/database/DatabaseManager.ts +133 -0
- package/template/framework/database/connection/Connection.ts +71 -0
- package/template/framework/database/connection/ConnectionFactory.ts +30 -0
- package/template/framework/database/connection/PostgresConnection.ts +159 -0
- package/template/framework/database/console/MakeMigrationCommand.ts +58 -0
- package/template/framework/database/console/MigrateCommand.ts +32 -0
- package/template/framework/database/console/MigrateResetCommand.ts +31 -0
- package/template/framework/database/console/MigrateRollbackCommand.ts +31 -0
- package/template/framework/database/console/MigrateStatusCommand.ts +38 -0
- package/template/framework/database/migrations/DatabaseMigrationRepository.ts +122 -0
- package/template/framework/database/migrations/Migration.ts +5 -0
- package/template/framework/database/migrations/MigrationRepository.ts +46 -0
- package/template/framework/database/migrations/Migrator.ts +249 -0
- package/template/framework/database/migrations/index.ts +4 -0
- package/template/framework/database/orm/BelongsTo.ts +246 -0
- package/template/framework/database/orm/BelongsToMany.ts +570 -0
- package/template/framework/database/orm/Builder.ts +160 -0
- package/template/framework/database/orm/EagerLoadingBuilder.ts +324 -0
- package/template/framework/database/orm/HasMany.ts +303 -0
- package/template/framework/database/orm/HasManyThrough.ts +282 -0
- package/template/framework/database/orm/HasOne.ts +201 -0
- package/template/framework/database/orm/HasOneThrough.ts +281 -0
- package/template/framework/database/orm/Model.ts +1766 -0
- package/template/framework/database/orm/Relation.ts +342 -0
- package/template/framework/database/orm/Scope.ts +14 -0
- package/template/framework/database/orm/SoftDeletes.ts +160 -0
- package/template/framework/database/orm/index.ts +54 -0
- package/template/framework/database/orm/scopes/SoftDeletingScope.ts +58 -0
- package/template/framework/database/pagination/LengthAwarePaginator.ts +55 -0
- package/template/framework/database/pagination/Paginator.ts +110 -0
- package/template/framework/database/pagination/index.ts +2 -0
- package/template/framework/database/query/Builder.ts +918 -0
- package/template/framework/database/query/DB.ts +139 -0
- package/template/framework/database/query/grammars/Grammar.ts +430 -0
- package/template/framework/database/query/grammars/PostgresGrammar.ts +224 -0
- package/template/framework/database/query/grammars/index.ts +6 -0
- package/template/framework/database/query/index.ts +8 -0
- package/template/framework/database/query/types.ts +196 -0
- package/template/framework/database/schema/Blueprint.ts +478 -0
- package/template/framework/database/schema/Schema.ts +149 -0
- package/template/framework/database/schema/SchemaBuilder.ts +152 -0
- package/template/framework/database/schema/grammars/PostgresSchemaGrammar.ts +293 -0
- package/template/framework/database/schema/grammars/index.ts +5 -0
- package/template/framework/database/schema/index.ts +9 -0
- package/template/package.json +4 -1
|
@@ -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
|
+
}
|