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.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/template/config/database.ts +13 -1
  3. package/template/database/migrations/2024_01_01_000000_create_test_users_cli_table.ts +16 -0
  4. package/template/database/migrations/20260108164611_TestCliMigration.ts +16 -0
  5. package/template/database/migrations/2026_01_08_16_46_11_CreateTestMigrationsTable.ts +21 -0
  6. package/template/framework/cli/artisan.ts +12 -0
  7. package/template/framework/database/DatabaseManager.ts +133 -0
  8. package/template/framework/database/connection/Connection.ts +71 -0
  9. package/template/framework/database/connection/ConnectionFactory.ts +30 -0
  10. package/template/framework/database/connection/PostgresConnection.ts +159 -0
  11. package/template/framework/database/console/MakeMigrationCommand.ts +58 -0
  12. package/template/framework/database/console/MigrateCommand.ts +32 -0
  13. package/template/framework/database/console/MigrateResetCommand.ts +31 -0
  14. package/template/framework/database/console/MigrateRollbackCommand.ts +31 -0
  15. package/template/framework/database/console/MigrateStatusCommand.ts +38 -0
  16. package/template/framework/database/migrations/DatabaseMigrationRepository.ts +122 -0
  17. package/template/framework/database/migrations/Migration.ts +5 -0
  18. package/template/framework/database/migrations/MigrationRepository.ts +46 -0
  19. package/template/framework/database/migrations/Migrator.ts +249 -0
  20. package/template/framework/database/migrations/index.ts +4 -0
  21. package/template/framework/database/orm/BelongsTo.ts +246 -0
  22. package/template/framework/database/orm/BelongsToMany.ts +570 -0
  23. package/template/framework/database/orm/Builder.ts +160 -0
  24. package/template/framework/database/orm/EagerLoadingBuilder.ts +324 -0
  25. package/template/framework/database/orm/HasMany.ts +303 -0
  26. package/template/framework/database/orm/HasManyThrough.ts +282 -0
  27. package/template/framework/database/orm/HasOne.ts +201 -0
  28. package/template/framework/database/orm/HasOneThrough.ts +281 -0
  29. package/template/framework/database/orm/Model.ts +1766 -0
  30. package/template/framework/database/orm/Relation.ts +342 -0
  31. package/template/framework/database/orm/Scope.ts +14 -0
  32. package/template/framework/database/orm/SoftDeletes.ts +160 -0
  33. package/template/framework/database/orm/index.ts +54 -0
  34. package/template/framework/database/orm/scopes/SoftDeletingScope.ts +58 -0
  35. package/template/framework/database/pagination/LengthAwarePaginator.ts +55 -0
  36. package/template/framework/database/pagination/Paginator.ts +110 -0
  37. package/template/framework/database/pagination/index.ts +2 -0
  38. package/template/framework/database/query/Builder.ts +918 -0
  39. package/template/framework/database/query/DB.ts +139 -0
  40. package/template/framework/database/query/grammars/Grammar.ts +430 -0
  41. package/template/framework/database/query/grammars/PostgresGrammar.ts +224 -0
  42. package/template/framework/database/query/grammars/index.ts +6 -0
  43. package/template/framework/database/query/index.ts +8 -0
  44. package/template/framework/database/query/types.ts +196 -0
  45. package/template/framework/database/schema/Blueprint.ts +478 -0
  46. package/template/framework/database/schema/Schema.ts +149 -0
  47. package/template/framework/database/schema/SchemaBuilder.ts +152 -0
  48. package/template/framework/database/schema/grammars/PostgresSchemaGrammar.ts +293 -0
  49. package/template/framework/database/schema/grammars/index.ts +5 -0
  50. package/template/framework/database/schema/index.ts +9 -0
  51. package/template/package.json +4 -1
@@ -0,0 +1,139 @@
1
+ /**
2
+ * PhoenixJS ORM - DB Facade
3
+ *
4
+ * Static facade for database operations, providing a Laravel-like
5
+ * DB::table('users') interface for query building.
6
+ */
7
+
8
+ import type { Connection } from '../connection/Connection';
9
+ import type { Grammar } from './grammars/Grammar';
10
+ import type { Binding } from './types';
11
+ import { Builder } from './Builder';
12
+ import { RawExpression } from './types';
13
+ import { PostgresGrammar } from './grammars/PostgresGrammar';
14
+
15
+ /**
16
+ * DB Facade
17
+ *
18
+ * Provides a static interface for database operations.
19
+ * Must be initialized with a connection before use.
20
+ */
21
+ export class DB {
22
+ /** The database connection */
23
+ private static connection: Connection | null = null;
24
+
25
+ /** The grammar instance */
26
+ private static grammar: Grammar | null = null;
27
+
28
+ /**
29
+ * Initialize the DB facade with a connection
30
+ */
31
+ static init(connection: Connection, grammar?: Grammar): void {
32
+ DB.connection = connection;
33
+ DB.grammar = grammar || new PostgresGrammar();
34
+ }
35
+
36
+ /**
37
+ * Set the database connection
38
+ */
39
+ static setConnection(connection: Connection): void {
40
+ DB.connection = connection;
41
+ }
42
+
43
+ /**
44
+ * Set the grammar
45
+ */
46
+ static setGrammar(grammar: Grammar): void {
47
+ DB.grammar = grammar;
48
+ }
49
+
50
+ /**
51
+ * Get the current connection
52
+ */
53
+ static getConnection(): Connection {
54
+ if (!DB.connection) {
55
+ throw new Error('DB facade not initialized. Call DB.init(connection) first.');
56
+ }
57
+ return DB.connection;
58
+ }
59
+
60
+ /**
61
+ * Get the current grammar
62
+ */
63
+ static getGrammar(): Grammar {
64
+ if (!DB.grammar) {
65
+ DB.grammar = new PostgresGrammar();
66
+ }
67
+ return DB.grammar;
68
+ }
69
+
70
+ /**
71
+ * Begin a fluent query against a database table
72
+ */
73
+ static table(name: string, alias?: string): Builder {
74
+ const connection = DB.getConnection();
75
+ const grammar = DB.getGrammar();
76
+ const builder = new Builder(connection, grammar, name);
77
+
78
+ if (alias) {
79
+ builder.table(name, alias);
80
+ }
81
+
82
+ return builder;
83
+ }
84
+
85
+ /**
86
+ * Execute a raw SQL query
87
+ */
88
+ static async query<T = Record<string, unknown>>(sql: string, bindings: unknown[] = []): Promise<T[]> {
89
+ return DB.getConnection().query<T>(sql, bindings);
90
+ }
91
+
92
+ /**
93
+ * Execute a raw SQL statement
94
+ */
95
+ static async execute(sql: string, bindings: unknown[] = []): Promise<number> {
96
+ return DB.getConnection().execute(sql, bindings);
97
+ }
98
+
99
+ /**
100
+ * Get a single row from a raw query
101
+ */
102
+ static async get<T = Record<string, unknown>>(sql: string, bindings: unknown[] = []): Promise<T | null> {
103
+ return DB.getConnection().get<T>(sql, bindings);
104
+ }
105
+
106
+ /**
107
+ * Execute operations within a transaction
108
+ */
109
+ static async transaction<T>(callback: (tx: unknown) => Promise<T>): Promise<T> {
110
+ return DB.getConnection().transaction(callback);
111
+ }
112
+
113
+ /**
114
+ * Create a new Builder instance with a custom connection
115
+ */
116
+ static withConnection(connection: Connection): Builder {
117
+ const grammar = DB.getGrammar();
118
+ return new Builder(connection, grammar);
119
+ }
120
+
121
+ /**
122
+ * Create a raw expression for use in queries
123
+ *
124
+ * @example
125
+ * DB.table('users').selectRaw('count(*) as user_count')
126
+ * DB.table('orders').selectRaw('SUM(total) as total_sales')
127
+ */
128
+ static raw(expression: string, bindings: Binding[] = []): RawExpression {
129
+ return new RawExpression(expression, bindings);
130
+ }
131
+
132
+ /**
133
+ * Reset the DB facade (useful for testing)
134
+ */
135
+ static reset(): void {
136
+ DB.connection = null;
137
+ DB.grammar = null;
138
+ }
139
+ }
@@ -0,0 +1,430 @@
1
+ /**
2
+ * PhoenixJS ORM - Abstract Grammar Class
3
+ *
4
+ * Base class for SQL grammar compilation. Defines the contract for all
5
+ * database-specific grammar implementations. The Grammar is responsible
6
+ * for converting internal query representations to database-specific SQL syntax.
7
+ */
8
+
9
+ import type {
10
+ QueryComponents,
11
+ CompiledQuery,
12
+ InsertValues,
13
+ UpdateValues,
14
+ Binding,
15
+ WhereClause,
16
+ JoinClause,
17
+ OrderClause,
18
+ } from '../types';
19
+
20
+ /**
21
+ * Abstract Grammar base class
22
+ *
23
+ * Subclasses must implement database-specific SQL generation.
24
+ */
25
+ export abstract class Grammar {
26
+ /**
27
+ * The grammar-specific components for SELECT statements
28
+ */
29
+ protected selectComponents: (keyof QueryComponents)[] = [
30
+ 'aggregate',
31
+ 'columns',
32
+ 'table',
33
+ 'joins',
34
+ 'wheres',
35
+ 'groups',
36
+ 'havings',
37
+ 'orders',
38
+ 'limit',
39
+ 'offset',
40
+ 'lock',
41
+ ];
42
+
43
+ /**
44
+ * Compile a SELECT query into SQL
45
+ */
46
+ compileSelect(query: QueryComponents): CompiledQuery {
47
+ const bindings: Binding[] = [];
48
+ const sql = this.concatenate(this.compileComponents(query, bindings));
49
+ return { sql: sql.trim(), bindings };
50
+ }
51
+
52
+ /**
53
+ * Compile the components of a query
54
+ */
55
+ protected compileComponents(query: QueryComponents, bindings: Binding[]): string[] {
56
+ const segments: string[] = [];
57
+
58
+ for (const component of this.selectComponents) {
59
+ const value = query[component];
60
+ if (value !== undefined && value !== null) {
61
+ const method = `compile${this.capitalize(component)}` as keyof this;
62
+ if (typeof this[method] === 'function') {
63
+ const sql = (this[method] as (query: QueryComponents, bindings: Binding[]) => string)(
64
+ query,
65
+ bindings
66
+ );
67
+ if (sql) {
68
+ segments.push(sql);
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ return segments;
75
+ }
76
+
77
+ /**
78
+ * Compile the "select" portion of the query
79
+ */
80
+ protected compileColumns(query: QueryComponents, bindings: Binding[]): string {
81
+ // If an aggregate is set, skip columns (aggregate already includes SELECT)
82
+ if (query.aggregate) {
83
+ return '';
84
+ }
85
+
86
+ const select = query.distinct ? 'SELECT DISTINCT' : 'SELECT';
87
+ const columns: string[] = [];
88
+
89
+ // Add regular columns
90
+ if (query.columns.length > 0) {
91
+ columns.push(this.columnize(query.columns));
92
+ }
93
+
94
+ // Add raw select expressions
95
+ if (query.rawSelects.length > 0) {
96
+ for (const raw of query.rawSelects) {
97
+ columns.push(raw.expression);
98
+ bindings.push(...raw.bindings);
99
+ }
100
+ }
101
+
102
+ if (columns.length === 0) {
103
+ return `${select} *`;
104
+ }
105
+
106
+ return `${select} ${columns.join(', ')}`;
107
+ }
108
+
109
+ /**
110
+ * Compile the "from" portion of the query
111
+ */
112
+ protected compileTable(query: QueryComponents, _bindings: Binding[]): string {
113
+ const table = this.wrapTable(query.table);
114
+ if (query.alias) {
115
+ return `FROM ${table} AS ${this.wrap(query.alias)}`;
116
+ }
117
+ return `FROM ${table}`;
118
+ }
119
+
120
+ /**
121
+ * Compile the aggregate function portion
122
+ */
123
+ protected compileAggregate(query: QueryComponents, _bindings: Binding[]): string {
124
+ if (!query.aggregate) return '';
125
+
126
+ const column =
127
+ query.aggregate.column === '*' ? '*' : this.wrap(query.aggregate.column);
128
+ return `SELECT ${query.aggregate.function.toUpperCase()}(${column})`;
129
+ }
130
+
131
+ /**
132
+ * Compile the "join" portions of the query
133
+ */
134
+ protected compileJoins(query: QueryComponents, _bindings: Binding[]): string {
135
+ if (query.joins.length === 0) return '';
136
+
137
+ return query.joins.map((join) => this.compileJoin(join)).join(' ');
138
+ }
139
+
140
+ /**
141
+ * Compile a single join clause
142
+ */
143
+ protected compileJoin(join: JoinClause): string {
144
+ const table = this.wrapTable(join.table);
145
+ const joinType = this.getJoinType(join.type);
146
+
147
+ // Cross join doesn't have ON clause
148
+ if (join.type === 'cross') {
149
+ return `${joinType} JOIN ${table}`;
150
+ }
151
+
152
+ const first = this.wrap(join.first);
153
+ const second = this.wrap(join.second);
154
+
155
+ return `${joinType} JOIN ${table} ON ${first} ${join.operator} ${second}`;
156
+ }
157
+
158
+ /**
159
+ * Get the join type keyword
160
+ */
161
+ protected getJoinType(type: JoinClause['type']): string {
162
+ switch (type) {
163
+ case 'inner':
164
+ return 'INNER';
165
+ case 'left':
166
+ return 'LEFT';
167
+ case 'right':
168
+ return 'RIGHT';
169
+ case 'cross':
170
+ return 'CROSS';
171
+ default:
172
+ return 'INNER';
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Compile the "where" portions of the query
178
+ */
179
+ compileWheres(query: QueryComponents, bindings: Binding[]): string {
180
+ if (query.wheres.length === 0) return '';
181
+
182
+ const sql = query.wheres
183
+ .map((where, index) => {
184
+ const prefix = index === 0 ? '' : `${where.boolean.toUpperCase()} `;
185
+ return prefix + this.compileWhere(where, bindings);
186
+ })
187
+ .join(' ');
188
+
189
+ return `WHERE ${sql}`;
190
+ }
191
+
192
+ /**
193
+ * Compile a single where clause
194
+ */
195
+ protected compileWhere(where: WhereClause, bindings: Binding[]): string {
196
+ switch (where.type) {
197
+ case 'basic':
198
+ return this.compileBasicWhere(where, bindings);
199
+ case 'in':
200
+ return this.compileInWhere(where, bindings);
201
+ case 'null':
202
+ return this.compileNullWhere(where);
203
+ case 'between':
204
+ return this.compileBetweenWhere(where, bindings);
205
+ case 'raw':
206
+ return this.compileRawWhere(where, bindings);
207
+ case 'nested':
208
+ return this.compileNestedWhere(where, bindings);
209
+ default:
210
+ throw new Error(`Unknown where type: ${where.type}`);
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Compile a basic where clause
216
+ */
217
+ protected abstract compileBasicWhere(where: WhereClause, bindings: Binding[]): string;
218
+
219
+ /**
220
+ * Compile a where in clause
221
+ */
222
+ protected abstract compileInWhere(where: WhereClause, bindings: Binding[]): string;
223
+
224
+ /**
225
+ * Compile a where null clause
226
+ */
227
+ protected compileNullWhere(where: WhereClause): string {
228
+ const column = this.wrap(where.column!);
229
+ return where.not ? `${column} IS NOT NULL` : `${column} IS NULL`;
230
+ }
231
+
232
+ /**
233
+ * Compile a where between clause
234
+ */
235
+ protected abstract compileBetweenWhere(where: WhereClause, bindings: Binding[]): string;
236
+
237
+ /**
238
+ * Compile a raw where clause
239
+ */
240
+ protected compileRawWhere(where: WhereClause, bindings: Binding[]): string {
241
+ if (where.bindings) {
242
+ bindings.push(...where.bindings);
243
+ }
244
+ return where.sql!;
245
+ }
246
+
247
+ /**
248
+ * Compile a nested where clause
249
+ */
250
+ protected compileNestedWhere(where: WhereClause, bindings: Binding[]): string {
251
+ if (!where.nested || where.nested.length === 0) return '';
252
+
253
+ const nestedSql = where.nested
254
+ .map((nested, index) => {
255
+ const prefix = index === 0 ? '' : `${nested.boolean.toUpperCase()} `;
256
+ return prefix + this.compileWhere(nested, bindings);
257
+ })
258
+ .join(' ');
259
+
260
+ return `(${nestedSql})`;
261
+ }
262
+
263
+ /**
264
+ * Compile the "group by" portions of the query
265
+ */
266
+ protected compileGroups(query: QueryComponents, bindings: Binding[]): string {
267
+ const groups: string[] = [];
268
+
269
+ // Add regular group by columns
270
+ if (query.groups.length > 0) {
271
+ groups.push(this.columnize(query.groups));
272
+ }
273
+
274
+ // Add raw group by expressions
275
+ if (query.rawGroups.length > 0) {
276
+ for (const raw of query.rawGroups) {
277
+ groups.push(raw.expression);
278
+ bindings.push(...raw.bindings);
279
+ }
280
+ }
281
+
282
+ if (groups.length === 0) return '';
283
+
284
+ return `GROUP BY ${groups.join(', ')}`;
285
+ }
286
+
287
+ /**
288
+ * Compile the "having" portions of the query
289
+ */
290
+ protected compileHavings(query: QueryComponents, bindings: Binding[]): string {
291
+ if (query.havings.length === 0) return '';
292
+
293
+ const sql = query.havings
294
+ .map((having, index) => {
295
+ const prefix = index === 0 ? '' : `${having.boolean.toUpperCase()} `;
296
+ return prefix + this.compileWhere(having, bindings);
297
+ })
298
+ .join(' ');
299
+
300
+ return `HAVING ${sql}`;
301
+ }
302
+
303
+ /**
304
+ * Compile the "order by" portions of the query
305
+ */
306
+ protected compileOrders(query: QueryComponents, bindings: Binding[]): string {
307
+ if (query.orders.length === 0) return '';
308
+
309
+ // Add order bindings (for raw order expressions)
310
+ if (query.orderBindings.length > 0) {
311
+ bindings.push(...query.orderBindings);
312
+ }
313
+
314
+ return `ORDER BY ${query.orders.map((order) => this.compileOrder(order)).join(', ')}`;
315
+ }
316
+
317
+ /**
318
+ * Compile a single order clause
319
+ */
320
+ protected compileOrder(order: OrderClause): string {
321
+ // Raw order expressions are used as-is
322
+ if (order.raw) {
323
+ return order.column;
324
+ }
325
+ return `${this.wrap(order.column)} ${order.direction.toUpperCase()}`;
326
+ }
327
+
328
+ /**
329
+ * Compile the "limit" portions of the query
330
+ */
331
+ /**
332
+ * Compile the "limit" portions of the query
333
+ */
334
+ protected compileLimit(query: QueryComponents, bindings: Binding[]): string {
335
+ if (query.limit === undefined) return '';
336
+
337
+ bindings.push(query.limit);
338
+ return `LIMIT ${this.parameterize([query.limit], bindings.length)}`;
339
+ }
340
+
341
+ /**
342
+ * Compile the "offset" portions of the query
343
+ */
344
+ protected compileOffset(query: QueryComponents, bindings: Binding[]): string {
345
+ if (query.offset === undefined) return '';
346
+
347
+ bindings.push(query.offset);
348
+ return `OFFSET ${this.parameterize([query.offset], bindings.length)}`;
349
+ }
350
+
351
+ /**
352
+ * Compile the lock clause
353
+ */
354
+ protected compileLock(query: QueryComponents, _bindings: Binding[]): string {
355
+ if (!query.lock) return '';
356
+ return query.lock.toUpperCase();
357
+ }
358
+
359
+ /**
360
+ * Compile an INSERT statement
361
+ */
362
+ abstract compileInsert(query: QueryComponents, values: InsertValues | InsertValues[]): CompiledQuery;
363
+
364
+ /**
365
+ * Compile an INSERT statement with RETURNING
366
+ */
367
+ abstract compileInsertReturning(
368
+ query: QueryComponents,
369
+ values: InsertValues | InsertValues[],
370
+ returning: string[]
371
+ ): CompiledQuery;
372
+
373
+ /**
374
+ * Compile an UPDATE statement
375
+ */
376
+ abstract compileUpdate(query: QueryComponents, values: UpdateValues): CompiledQuery;
377
+
378
+ /**
379
+ * Compile a DELETE statement
380
+ */
381
+ abstract compileDelete(query: QueryComponents): CompiledQuery;
382
+
383
+ /**
384
+ * Compile a truncate table statement
385
+ */
386
+ abstract compileTruncate(table: string): string;
387
+
388
+ /**
389
+ * Wrap a table in keyword identifiers
390
+ */
391
+ wrapTable(table: string): string {
392
+ return this.wrap(table);
393
+ }
394
+
395
+ /**
396
+ * Wrap a value in keyword identifiers
397
+ */
398
+ abstract wrap(value: string): string;
399
+
400
+ /**
401
+ * Convert an array of column names into a delimited string
402
+ */
403
+ columnize(columns: string[]): string {
404
+ return columns.map((column) => this.wrap(column)).join(', ');
405
+ }
406
+
407
+ /**
408
+ * Create query parameter placeholders for values
409
+ */
410
+ abstract parameterize(values: Binding[], startIndex?: number): string;
411
+
412
+ /**
413
+ * Get the grammar's parameter prefix
414
+ */
415
+ abstract getParameterPrefix(): string;
416
+
417
+ /**
418
+ * Concatenate an array of segments into a single string
419
+ */
420
+ protected concatenate(segments: string[]): string {
421
+ return segments.filter((segment) => segment !== '').join(' ');
422
+ }
423
+
424
+ /**
425
+ * Capitalize the first letter of a string
426
+ */
427
+ protected capitalize(str: string): string {
428
+ return str.charAt(0).toUpperCase() + str.slice(1);
429
+ }
430
+ }