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,324 @@
1
+ import type { Builder } from '../query/Builder';
2
+ import type { Model } from './Model';
3
+ import type { Relation } from './Relation';
4
+
5
+ /**
6
+ * EagerLoadingBuilder - A wrapper around the query builder that supports eager loading.
7
+ *
8
+ * This class wraps the standard query builder and adds eager loading capabilities.
9
+ * It intercepts query results and loads the specified relationships.
10
+ *
11
+ * Usage:
12
+ * ```typescript
13
+ * // Load users with their posts eagerly loaded
14
+ * const users = await User.with('posts').get();
15
+ *
16
+ * // Load users with multiple relationships
17
+ * const users = await User.with('posts', 'profile').get();
18
+ *
19
+ * // Access eagerly loaded relations
20
+ * for (const user of users) {
21
+ * const posts = user.getRelation('posts'); // Already loaded, no additional query
22
+ * }
23
+ * ```
24
+ *
25
+ * @template T - The model type being queried
26
+ */
27
+ export class EagerLoadingBuilder<T extends Model> {
28
+ /**
29
+ * The underlying query builder.
30
+ */
31
+ protected builder: Builder;
32
+
33
+ /**
34
+ * The model class constructor.
35
+ */
36
+ protected modelClass: new (attrs?: Record<string, unknown>) => T;
37
+
38
+ /**
39
+ * The relationships to eager load.
40
+ */
41
+ protected eagerLoad: string[] = [];
42
+
43
+ /**
44
+ * A prototype model instance for hydration and relation methods.
45
+ */
46
+ protected modelInstance: T;
47
+
48
+ /**
49
+ * Create a new EagerLoadingBuilder instance.
50
+ *
51
+ * @param modelClass - The model class constructor
52
+ * @param builder - The underlying query builder
53
+ * @param relations - Array of relationship names to eager load
54
+ */
55
+ constructor(
56
+ modelClass: new (attrs?: Record<string, unknown>) => T,
57
+ builder: Builder,
58
+ relations: string[]
59
+ ) {
60
+ this.modelClass = modelClass;
61
+ this.builder = builder;
62
+ this.eagerLoad = relations;
63
+ this.modelInstance = new modelClass();
64
+ }
65
+
66
+ // ==========================================
67
+ // Query Execution with Eager Loading
68
+ // ==========================================
69
+
70
+ /**
71
+ * Execute the query and get all results with eager loaded relationships.
72
+ *
73
+ * @returns Array of models with relationships loaded
74
+ */
75
+ public async get(): Promise<T[]> {
76
+ // Execute the query
77
+ const results = await this.builder.get<any>();
78
+
79
+ let models: T[];
80
+
81
+ // Check if results are already models (if builder is Eloquent Builder)
82
+ if (results.length > 0 && results[0] instanceof this.modelClass) {
83
+ models = results as T[];
84
+ } else {
85
+ // Hydrate the models
86
+ models = results.map((row) => this.hydrateModel(row));
87
+ }
88
+
89
+ // If no results, return empty array
90
+ if (models.length === 0) {
91
+ return models;
92
+ }
93
+
94
+ // Eager load the relationships
95
+ await this.eagerLoadRelations(models, this.eagerLoad);
96
+
97
+ return models;
98
+ }
99
+
100
+ /**
101
+ * Execute the query and get the first result with eager loaded relationships.
102
+ *
103
+ * @returns The first model or null
104
+ */
105
+ public async first(): Promise<T | null> {
106
+ // Execute the query
107
+ const result = await this.builder.first<any>();
108
+
109
+ if (!result) {
110
+ return null;
111
+ }
112
+
113
+ let model: T;
114
+
115
+ // Check if result is already a model
116
+ if (result instanceof this.modelClass) {
117
+ model = result as T;
118
+ } else {
119
+ // Hydrate the model
120
+ model = this.hydrateModel(result);
121
+ }
122
+
123
+ // Eager load the relationships for single model
124
+ await this.eagerLoadRelations([model], this.eagerLoad);
125
+
126
+ return model;
127
+ }
128
+
129
+ /**
130
+ * Find a model by its primary key with eager loaded relationships.
131
+ *
132
+ * @param id - The primary key value
133
+ * @returns The model or null
134
+ */
135
+ public async find(id: number | string): Promise<T | null> {
136
+ const keyName = this.modelInstance.getKeyName();
137
+ this.builder.where(keyName, '=', id as string | number | boolean | null);
138
+ return this.first();
139
+ }
140
+
141
+ // ==========================================
142
+ // Eager Loading Logic
143
+ // ==========================================
144
+
145
+ /**
146
+ * Eager load relationships for a collection of models.
147
+ *
148
+ * @param models - Array of models to load relations for
149
+ * @param relations - Array of relationship names
150
+ */
151
+ protected async eagerLoadRelations(models: T[], relations: string[]): Promise<void> {
152
+ for (const relationName of relations) {
153
+ // Parse nested relations (e.g., 'posts.comments' -> 'posts', 'comments')
154
+ const segments = relationName.split('.');
155
+ const baseRelation = segments[0];
156
+ const nestedRelation = segments.slice(1).join('.');
157
+
158
+ // Load the base relation
159
+ await this.loadRelation(models, baseRelation, nestedRelation);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Load a single relationship for the models.
165
+ *
166
+ * @param models - Array of parent models
167
+ * @param relationName - The name of the relationship method
168
+ * @param nestedRelation - Any nested relations to load after this one
169
+ */
170
+ protected async loadRelation(
171
+ models: T[],
172
+ relationName: string,
173
+ nestedRelation: string
174
+ ): Promise<void> {
175
+ if (models.length === 0) {
176
+ return;
177
+ }
178
+
179
+ // Get the relationship definition from the first model
180
+ const firstModel = models[0];
181
+
182
+ // Check if the relationship method exists
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
+ const relationMethodDescriptor = (firstModel as any)[relationName];
185
+ if (typeof relationMethodDescriptor !== 'function') {
186
+ throw new Error(
187
+ `Relation method [${relationName}] not found on model [${firstModel.constructor.name}]`
188
+ );
189
+ }
190
+
191
+ // Get the relation instance from the first model
192
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
193
+ const relation: Relation<Model> = (firstModel as any)[relationName]();
194
+
195
+ // Initialize for eager loading (removes parent-specific constraints)
196
+ relation.initializeForEagerLoading();
197
+
198
+ // Add constraints for all parent models
199
+ relation.addEagerConstraints(models);
200
+
201
+ // Execute the query to get all related models
202
+ const results = await relation.getAll();
203
+
204
+ // Match the results to their parent models
205
+ relation.match(models, results, relationName);
206
+
207
+ // If there are nested relations, recursively load them
208
+ if (nestedRelation && results.length > 0) {
209
+ // Create an EagerLoadingBuilder for the related models
210
+ const relatedClass = relation.getRelated().constructor as new (
211
+ attrs?: Record<string, unknown>
212
+ ) => Model;
213
+ const nestedBuilder = new EagerLoadingBuilder(
214
+ relatedClass,
215
+ relation.getQuery(),
216
+ [nestedRelation]
217
+ );
218
+ // Load the nested relations on the results
219
+ await nestedBuilder.eagerLoadRelations(results, [nestedRelation]);
220
+ }
221
+ }
222
+
223
+ // ==========================================
224
+ // Model Hydration
225
+ // ==========================================
226
+
227
+ /**
228
+ * Create a model instance from a database row.
229
+ *
230
+ * @param row - The database row
231
+ * @returns A new model instance
232
+ */
233
+ protected hydrateModel(row: Record<string, unknown>): T {
234
+ const model = new this.modelClass();
235
+ model.forceFill(row);
236
+ model.exists = true;
237
+ model.syncOriginal();
238
+ return model;
239
+ }
240
+
241
+ // ==========================================
242
+ // Query Builder Forwarding Methods
243
+ // ==========================================
244
+
245
+ /**
246
+ * Add a WHERE clause to the query.
247
+ */
248
+ public where(
249
+ column: string,
250
+ operatorOrValue?: string | number | boolean | null,
251
+ value?: string | number | boolean | null
252
+ ): this {
253
+ this.builder.where(column, operatorOrValue, value);
254
+ return this;
255
+ }
256
+
257
+ /**
258
+ * Add an OR WHERE clause to the query.
259
+ */
260
+ public orWhere(
261
+ column: string,
262
+ operatorOrValue?: string | number | boolean | null,
263
+ value?: string | number | boolean | null
264
+ ): this {
265
+ this.builder.orWhere(column, operatorOrValue, value);
266
+ return this;
267
+ }
268
+
269
+ /**
270
+ * Add a WHERE IN clause to the query.
271
+ */
272
+ public whereIn(column: string, values: (string | number | boolean | null)[]): this {
273
+ this.builder.whereIn(column, values);
274
+ return this;
275
+ }
276
+
277
+ /**
278
+ * Add a WHERE NULL clause to the query.
279
+ */
280
+ public whereNull(column: string): this {
281
+ this.builder.whereNull(column);
282
+ return this;
283
+ }
284
+
285
+ /**
286
+ * Add a WHERE NOT NULL clause to the query.
287
+ */
288
+ public whereNotNull(column: string): this {
289
+ this.builder.whereNotNull(column);
290
+ return this;
291
+ }
292
+
293
+ /**
294
+ * Add an ORDER BY clause to the query.
295
+ */
296
+ public orderBy(column: string, direction: 'asc' | 'desc' = 'asc'): this {
297
+ this.builder.orderBy(column, direction);
298
+ return this;
299
+ }
300
+
301
+ /**
302
+ * Add a LIMIT clause to the query.
303
+ */
304
+ public limit(value: number): this {
305
+ this.builder.limit(value);
306
+ return this;
307
+ }
308
+
309
+ /**
310
+ * Add an OFFSET clause to the query.
311
+ */
312
+ public offset(value: number): this {
313
+ this.builder.offset(value);
314
+ return this;
315
+ }
316
+
317
+ /**
318
+ * Add additional relationships to eager load.
319
+ */
320
+ public with(...relations: string[]): this {
321
+ this.eagerLoad.push(...relations);
322
+ return this;
323
+ }
324
+ }
@@ -0,0 +1,303 @@
1
+ import { Relation } from './Relation';
2
+ import type { Model } from './Model';
3
+ import type { Binding } from '../query/types';
4
+
5
+ /**
6
+ * HasMany Relationship - Represents a one-to-many relationship.
7
+ *
8
+ * The foreign key is on the RELATED model's table, referencing the parent.
9
+ *
10
+ * Example Schema:
11
+ * - users table: id, name, email
12
+ * - posts table: id, user_id, title, content
13
+ *
14
+ * The `user_id` column on `posts` references `users.id`.
15
+ * User "has many" Posts.
16
+ *
17
+ * Usage:
18
+ * ```typescript
19
+ * class User extends Model {
20
+ * posts() {
21
+ * return this.hasMany(Post, 'user_id', 'id');
22
+ * }
23
+ * }
24
+ *
25
+ * // Get all user's posts
26
+ * const user = await User.find(1);
27
+ * const posts = await user.posts().get(); // Returns Post[]
28
+ *
29
+ * // Filter posts
30
+ * const recentPosts = await user.posts()
31
+ * .where('published', true)
32
+ * .orderBy('created_at', 'desc')
33
+ * .limit(5)
34
+ * .get();
35
+ * ```
36
+ *
37
+ * @template TRelated - The type of the related model
38
+ */
39
+ export class HasMany<TRelated extends Model> extends Relation<TRelated> {
40
+ /**
41
+ * The foreign key on the related model's table.
42
+ * Example: 'user_id' on posts table
43
+ */
44
+ protected foreignKey: string;
45
+
46
+ /**
47
+ * The local key on the parent model's table.
48
+ * Example: 'id' on users table
49
+ */
50
+ protected localKey: string;
51
+
52
+ /**
53
+ * Create a new HasMany relationship instance.
54
+ *
55
+ * @param parent - The parent model instance
56
+ * @param related - An instance of the related model class
57
+ * @param foreignKey - The foreign key column on the related model (e.g., 'user_id')
58
+ * @param localKey - The local key column on the parent model (e.g., 'id')
59
+ */
60
+ constructor(
61
+ parent: Model,
62
+ related: TRelated,
63
+ foreignKey: string,
64
+ localKey: string
65
+ ) {
66
+ super(parent, related);
67
+
68
+ this.foreignKey = foreignKey;
69
+ this.localKey = localKey;
70
+
71
+ // Re-add constraints now that keys are set
72
+ this.reinitializeConstraints();
73
+ }
74
+
75
+ /**
76
+ * Re-initialize constraints after keys are set.
77
+ */
78
+ private reinitializeConstraints(): void {
79
+ this.query = this.newQuery();
80
+ const localKeyValue = this.getParentKeyValue(this.localKey);
81
+
82
+ if (localKeyValue !== undefined && localKeyValue !== null) {
83
+ this.query.where(this.foreignKey, '=', localKeyValue as Binding);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Add the constraints for the HasMany relationship.
89
+ * Called by parent constructor - keys may not be set yet.
90
+ */
91
+ public addConstraints(): void {
92
+ // Constraints are added in reinitializeConstraints after keys are set
93
+ }
94
+
95
+ /**
96
+ * Get the results of the HasMany relationship.
97
+ *
98
+ * Returns an array of related models. If no related models exist,
99
+ * returns an empty array (never null).
100
+ *
101
+ * @returns Array of related models
102
+ */
103
+ public async getResults(): Promise<TRelated[]> {
104
+ const localKeyValue = this.getParentKeyValue(this.localKey);
105
+
106
+ // If parent doesn't have a key value, return empty array
107
+ if (localKeyValue === undefined || localKeyValue === null) {
108
+ return [];
109
+ }
110
+
111
+ return this.getAll();
112
+ }
113
+
114
+ /**
115
+ * Create a new related model and associate it with the parent.
116
+ *
117
+ * This creates a new related model with the given attributes,
118
+ * automatically sets the foreign key, and saves it to the database.
119
+ *
120
+ * Usage:
121
+ * ```typescript
122
+ * const user = await User.find(1);
123
+ * const post = await user.posts().create({
124
+ * title: 'My New Post',
125
+ * content: 'Hello World!'
126
+ * });
127
+ * ```
128
+ *
129
+ * @param attributes - The attributes for the new model
130
+ * @returns The newly created and saved model
131
+ */
132
+ public async create(attributes: Record<string, unknown>): Promise<TRelated> {
133
+ // Add the foreign key to the attributes
134
+ const localKeyValue = this.getParentKeyValue(this.localKey);
135
+ const createAttributes = {
136
+ ...attributes,
137
+ [this.foreignKey]: localKeyValue
138
+ };
139
+
140
+ // Create and save the model
141
+ // Use true for exists (second arg) to bypass mass assignment protection via newInstance fix
142
+ // OR ensure newInstance handles this.
143
+ // Actually, simpler to just instantiate and then forceFill to ensure attributes are set.
144
+
145
+ const instance = this.related.newInstance({}, false);
146
+ instance.forceFill(createAttributes);
147
+ await instance.save();
148
+
149
+ return instance;
150
+ }
151
+
152
+ /**
153
+ * Save an existing model and associate it with the parent.
154
+ *
155
+ * This sets the foreign key on the model and saves it to the database.
156
+ *
157
+ * Usage:
158
+ * ```typescript
159
+ * const user = await User.find(1);
160
+ * const post = new Post({ title: 'My New Post' });
161
+ * await user.posts().save(post);
162
+ * ```
163
+ *
164
+ * @param model - The model to save
165
+ * @returns The saved model
166
+ */
167
+ public async save(model: TRelated): Promise<TRelated> {
168
+ // Set the foreign key using forceFill to bypass potential guarding
169
+ const localKeyValue = this.getParentKeyValue(this.localKey);
170
+ model.forceFill({
171
+ [this.foreignKey]: localKeyValue
172
+ });
173
+
174
+ // Save the model
175
+ await model.save();
176
+
177
+ return model;
178
+ }
179
+
180
+ /**
181
+ * Save multiple models and associate them with the parent.
182
+ *
183
+ * @param models - Array of models to save
184
+ * @returns Array of saved models
185
+ */
186
+ public async saveMany(models: TRelated[]): Promise<TRelated[]> {
187
+ const savedModels: TRelated[] = [];
188
+
189
+ for (const model of models) {
190
+ savedModels.push(await this.save(model));
191
+ }
192
+
193
+ return savedModels;
194
+ }
195
+
196
+ /**
197
+ * Create multiple related models and associate them with the parent.
198
+ *
199
+ * @param records - Array of attribute objects for new models
200
+ * @returns Array of newly created models
201
+ */
202
+ public async createMany(records: Record<string, unknown>[]): Promise<TRelated[]> {
203
+ const createdModels: TRelated[] = [];
204
+
205
+ for (const attributes of records) {
206
+ createdModels.push(await this.create(attributes));
207
+ }
208
+
209
+ return createdModels;
210
+ }
211
+
212
+ /**
213
+ * Get the foreign key column name.
214
+ *
215
+ * @returns The foreign key column name
216
+ */
217
+ public getForeignKeyName(): string {
218
+ return this.foreignKey;
219
+ }
220
+
221
+ /**
222
+ * Get the fully qualified foreign key name (table.column).
223
+ *
224
+ * @returns The qualified foreign key name
225
+ */
226
+ public getQualifiedForeignKeyName(): string {
227
+ return this.qualifyColumn(this.related.getTable(), this.foreignKey);
228
+ }
229
+
230
+ /**
231
+ * Get the local key column name.
232
+ *
233
+ * @returns The local key column name
234
+ */
235
+ public getLocalKeyName(): string {
236
+ return this.localKey;
237
+ }
238
+
239
+ /**
240
+ * Get the value of the parent's local key.
241
+ *
242
+ * @returns The local key value
243
+ */
244
+ public getParentKey(): unknown {
245
+ return this.getParentKeyValue(this.localKey);
246
+ }
247
+
248
+ // ==========================================
249
+ // Eager Loading Methods
250
+ // ==========================================
251
+
252
+ /**
253
+ * Add constraints for eager loading.
254
+ * Uses WHERE IN to fetch all related models for multiple parents at once.
255
+ *
256
+ * @param models - Array of parent models to load relations for
257
+ */
258
+ public addEagerConstraints(models: Model[]): void {
259
+ // Collect all parent key values
260
+ const keys: unknown[] = [];
261
+ for (const model of models) {
262
+ const key = model.getAttribute(this.localKey);
263
+ if (key !== undefined && key !== null) {
264
+ keys.push(key);
265
+ }
266
+ }
267
+
268
+ // Remove duplicates
269
+ const uniqueKeys = [...new Set(keys)];
270
+
271
+ // Add WHERE IN constraint
272
+ if (uniqueKeys.length > 0) {
273
+ this.query.whereIn(this.foreignKey, uniqueKeys as (string | number | boolean)[]);
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Match the eagerly loaded results to their parent models.
279
+ * For HasMany, each parent gets an array of related models.
280
+ *
281
+ * @param models - Array of parent models
282
+ * @param results - Array of related models that were loaded
283
+ * @param relation - The name of the relationship being matched
284
+ */
285
+ public match(models: Model[], results: TRelated[], relation: string): void {
286
+ // Build a dictionary for O(n) grouping: foreignKey value -> array of related models
287
+ const dictionary = new Map<unknown, TRelated[]>();
288
+ for (const result of results) {
289
+ const foreignKeyValue = result.getAttribute(this.foreignKey);
290
+ if (!dictionary.has(foreignKeyValue)) {
291
+ dictionary.set(foreignKeyValue, []);
292
+ }
293
+ dictionary.get(foreignKeyValue)!.push(result);
294
+ }
295
+
296
+ // Match each parent model with its array of related models
297
+ for (const model of models) {
298
+ const localKeyValue = model.getAttribute(this.localKey);
299
+ const matches = dictionary.get(localKeyValue) || [];
300
+ model.setRelation(relation, matches);
301
+ }
302
+ }
303
+ }