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,282 @@
1
+ import { Relation } from './Relation';
2
+ import type { Model } from './Model';
3
+ import type { Binding } from '../query/types';
4
+
5
+ /**
6
+ * HasManyThrough Relationship - Retrieves multiple related models through an intermediate model.
7
+ *
8
+ * This is useful when you need to access data that is related through another table.
9
+ *
10
+ * Example Schema:
11
+ * - countries table: id, name
12
+ * - users table: id, country_id, name
13
+ * - posts table: id, user_id, title, content
14
+ *
15
+ * A Country has many Posts through Users.
16
+ *
17
+ * Usage:
18
+ * ```typescript
19
+ * class Country extends Model {
20
+ * posts() {
21
+ * return this.hasManyThrough(
22
+ * Post, // Final related model
23
+ * User, // Intermediate model
24
+ * 'country_id', // Foreign key on users table (links to countries)
25
+ * 'user_id', // Foreign key on posts table (links to users)
26
+ * 'id', // Local key on countries table
27
+ * 'id' // Local key on users table
28
+ * );
29
+ * }
30
+ * }
31
+ *
32
+ * const country = await Country.find(1);
33
+ * const posts = await country.posts().get(); // All posts from users in this country
34
+ * ```
35
+ *
36
+ * @template TRelated - The type of the final related model
37
+ */
38
+ export class HasManyThrough<TRelated extends Model> extends Relation<TRelated> {
39
+ /**
40
+ * The intermediate ("through") model instance.
41
+ */
42
+ protected throughModel: Model;
43
+
44
+ /**
45
+ * The foreign key on the intermediate table (referencing parent).
46
+ * Example: 'country_id' on users table
47
+ */
48
+ protected firstKey: string;
49
+
50
+ /**
51
+ * The foreign key on the final related table (referencing intermediate).
52
+ * Example: 'user_id' on posts table
53
+ */
54
+ protected secondKey: string;
55
+
56
+ /**
57
+ * The local key on the parent model.
58
+ * Example: 'id' on countries table
59
+ */
60
+ protected localKey: string;
61
+
62
+ /**
63
+ * The local key on the intermediate model.
64
+ * Example: 'id' on users table
65
+ */
66
+ protected secondLocalKey: string;
67
+
68
+ /**
69
+ * Create a new HasManyThrough relationship instance.
70
+ *
71
+ * @param parent - The parent model instance (e.g., Country)
72
+ * @param related - An instance of the final related model class (e.g., Post)
73
+ * @param through - An instance of the intermediate model class (e.g., User)
74
+ * @param firstKey - Foreign key on intermediate table referencing parent (e.g., 'country_id')
75
+ * @param secondKey - Foreign key on final table referencing intermediate (e.g., 'user_id')
76
+ * @param localKey - Primary key on parent model (e.g., 'id')
77
+ * @param secondLocalKey - Primary key on intermediate model (e.g., 'id')
78
+ */
79
+ constructor(
80
+ parent: Model,
81
+ related: TRelated,
82
+ through: Model,
83
+ firstKey: string,
84
+ secondKey: string,
85
+ localKey: string,
86
+ secondLocalKey: string
87
+ ) {
88
+ super(parent, related);
89
+
90
+ this.throughModel = through;
91
+ this.firstKey = firstKey;
92
+ this.secondKey = secondKey;
93
+ this.localKey = localKey;
94
+ this.secondLocalKey = secondLocalKey;
95
+
96
+ // Re-add constraints now that keys are set
97
+ this.reinitializeConstraints();
98
+ }
99
+
100
+ /**
101
+ * Re-initialize constraints after keys are set.
102
+ */
103
+ private reinitializeConstraints(): void {
104
+ this.query = this.newQuery();
105
+ const parentKeyValue = this.parent.getAttribute(this.localKey);
106
+ const throughTable = this.throughModel.getTable();
107
+ const relatedTable = this.related.getTable();
108
+
109
+ // Join the intermediate table to the final table
110
+ // posts JOIN users ON posts.user_id = users.id
111
+ this.query.join(
112
+ throughTable,
113
+ `${relatedTable}.${this.secondKey}`,
114
+ '=',
115
+ `${throughTable}.${this.secondLocalKey}`
116
+ );
117
+
118
+ // Constrain by parent's key through the intermediate table
119
+ // WHERE users.country_id = {country.id}
120
+ if (parentKeyValue !== undefined && parentKeyValue !== null) {
121
+ this.query.where(`${throughTable}.${this.firstKey}`, '=', parentKeyValue as Binding);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Add the constraints for the HasManyThrough relationship.
127
+ * Called by parent constructor - keys may not be set yet.
128
+ */
129
+ public addConstraints(): void {
130
+ // Constraints are added in reinitializeConstraints after keys are set
131
+ }
132
+
133
+ /**
134
+ * Get the results of the HasManyThrough relationship.
135
+ *
136
+ * Returns an array of related models. If no related models exist,
137
+ * returns an empty array (never null).
138
+ *
139
+ * @returns Array of related models
140
+ */
141
+ public async getResults(): Promise<TRelated[]> {
142
+ const parentKeyValue = this.parent.getAttribute(this.localKey);
143
+
144
+ // If parent doesn't have a key value, return empty array
145
+ if (parentKeyValue === undefined || parentKeyValue === null) {
146
+ return [];
147
+ }
148
+
149
+ return this.getAll();
150
+ }
151
+
152
+ /**
153
+ * Get the intermediate model.
154
+ *
155
+ * @returns The through model instance
156
+ */
157
+ public getThroughModel(): Model {
158
+ return this.throughModel;
159
+ }
160
+
161
+ /**
162
+ * Get the first key (foreign key on intermediate table).
163
+ *
164
+ * @returns The first key column name
165
+ */
166
+ public getFirstKeyName(): string {
167
+ return this.firstKey;
168
+ }
169
+
170
+ /**
171
+ * Get the second key (foreign key on final table).
172
+ *
173
+ * @returns The second key column name
174
+ */
175
+ public getSecondKeyName(): string {
176
+ return this.secondKey;
177
+ }
178
+
179
+ /**
180
+ * Get the qualified first key name.
181
+ *
182
+ * @returns The qualified column name
183
+ */
184
+ public getQualifiedFirstKeyName(): string {
185
+ return `${this.throughModel.getTable()}.${this.firstKey}`;
186
+ }
187
+
188
+ /**
189
+ * Get the qualified second key name.
190
+ *
191
+ * @returns The qualified column name
192
+ */
193
+ public getQualifiedSecondKeyName(): string {
194
+ return `${this.related.getTable()}.${this.secondKey}`;
195
+ }
196
+
197
+ // ==========================================
198
+ // Eager Loading Methods
199
+ // ==========================================
200
+
201
+ /**
202
+ * Initialize the relation for eager loading.
203
+ * Creates a fresh query with the through join but without parent-specific constraints.
204
+ */
205
+ public override initializeForEagerLoading(): void {
206
+ this.query = this.newQuery();
207
+ const throughTable = this.throughModel.getTable();
208
+ const relatedTable = this.related.getTable();
209
+
210
+ // Explicitly select all columns from related table to preserve them when adding rawSelect
211
+ this.query.select(`${relatedTable}.*`);
212
+
213
+ // Join the intermediate table to the final table (required for HasManyThrough)
214
+ this.query.join(
215
+ throughTable,
216
+ `${relatedTable}.${this.secondKey}`,
217
+ '=',
218
+ `${throughTable}.${this.secondLocalKey}`
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Add constraints for eager loading.
224
+ * Uses WHERE IN on the through table to fetch all related models for multiple parents at once.
225
+ *
226
+ * @param models - Array of parent models to load relations for
227
+ */
228
+ public addEagerConstraints(models: Model[]): void {
229
+ // Collect all parent key values
230
+ const keys: unknown[] = [];
231
+ for (const model of models) {
232
+ const key = model.getAttribute(this.localKey);
233
+ if (key !== undefined && key !== null) {
234
+ keys.push(key);
235
+ }
236
+ }
237
+
238
+ // Remove duplicates
239
+ const uniqueKeys = [...new Set(keys)];
240
+
241
+ // Add WHERE IN constraint on the through table's first key
242
+ const throughTable = this.throughModel.getTable();
243
+ if (uniqueKeys.length > 0) {
244
+ this.query.whereIn(
245
+ `${throughTable}.${this.firstKey}`,
246
+ uniqueKeys as (string | number | boolean)[]
247
+ );
248
+ }
249
+
250
+ // Select the firstKey from through table to enable matching (use selectRaw for proper alias)
251
+ this.query.selectRaw(`${throughTable}.${this.firstKey} as laravel_through_key`);
252
+ }
253
+
254
+ /**
255
+ * Match the eagerly loaded results to their parent models.
256
+ * For HasManyThrough, each parent gets an array of related models.
257
+ *
258
+ * @param models - Array of parent models
259
+ * @param results - Array of related models that were loaded
260
+ * @param relation - The name of the relationship being matched
261
+ */
262
+ public match(models: Model[], results: TRelated[], relation: string): void {
263
+ // Build a dictionary for O(n) grouping: parent key value -> array of related models
264
+ // Use String conversion for consistent key comparison across types
265
+ const dictionary = new Map<string, TRelated[]>();
266
+ for (const result of results) {
267
+ // Get the through key from the result (added as laravel_through_key)
268
+ const throughKeyValue = String(result.getAttribute('laravel_through_key'));
269
+ if (!dictionary.has(throughKeyValue)) {
270
+ dictionary.set(throughKeyValue, []);
271
+ }
272
+ dictionary.get(throughKeyValue)!.push(result);
273
+ }
274
+
275
+ // Match each parent model with its array of related models
276
+ for (const model of models) {
277
+ const localKeyValue = String(model.getAttribute(this.localKey));
278
+ const matches = dictionary.get(localKeyValue) || [];
279
+ model.setRelation(relation, matches);
280
+ }
281
+ }
282
+ }
@@ -0,0 +1,201 @@
1
+ import { Relation } from './Relation';
2
+ import type { Model } from './Model';
3
+ import type { Binding } from '../query/types';
4
+
5
+ /**
6
+ * HasOne Relationship - Represents a one-to-one 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
+ * - profiles table: id, user_id, bio
13
+ *
14
+ * The `user_id` column on `profiles` references `users.id`.
15
+ *
16
+ * Usage:
17
+ * ```typescript
18
+ * class User extends Model {
19
+ * profile() {
20
+ * return this.hasOne(Profile, 'user_id', 'id');
21
+ * }
22
+ * }
23
+ *
24
+ * // Get the user's profile
25
+ * const user = await User.find(1);
26
+ * const profile = await user.profile().get();
27
+ * ```
28
+ *
29
+ * @template TRelated - The type of the related model
30
+ */
31
+ export class HasOne<TRelated extends Model> extends Relation<TRelated> {
32
+ /**
33
+ * The foreign key on the related model's table.
34
+ * Example: 'user_id' on profiles table
35
+ */
36
+ protected foreignKey: string;
37
+
38
+ /**
39
+ * The local key on the parent model's table.
40
+ * Example: 'id' on users table
41
+ */
42
+ protected localKey: string;
43
+
44
+ /**
45
+ * Create a new HasOne relationship instance.
46
+ *
47
+ * @param parent - The parent model instance
48
+ * @param related - An instance of the related model class
49
+ * @param foreignKey - The foreign key column on the related model (e.g., 'user_id')
50
+ * @param localKey - The local key column on the parent model (e.g., 'id')
51
+ */
52
+ constructor(
53
+ parent: Model,
54
+ related: TRelated,
55
+ foreignKey: string,
56
+ localKey: string
57
+ ) {
58
+ // Initialize keys before calling super
59
+ super(parent, related);
60
+
61
+ this.foreignKey = foreignKey;
62
+ this.localKey = localKey;
63
+
64
+ // Re-add constraints now that keys are set
65
+ this.reinitializeConstraints();
66
+ }
67
+
68
+ /**
69
+ * Re-initialize constraints after keys are set.
70
+ * Called after constructor completes key assignment.
71
+ */
72
+ private reinitializeConstraints(): void {
73
+ // Reset query and add constraints with proper keys
74
+ this.query = this.newQuery();
75
+ const localKeyValue = this.getParentKeyValue(this.localKey);
76
+
77
+ if (localKeyValue !== undefined && localKeyValue !== null) {
78
+ this.query.where(this.foreignKey, '=', localKeyValue as Binding);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Add the constraints for the HasOne relationship.
84
+ * Called by parent constructor - keys may not be set yet.
85
+ */
86
+ public addConstraints(): void {
87
+ // Constraints are added in reinitializeConstraints after keys are set
88
+ }
89
+
90
+ /**
91
+ * Get the results of the HasOne relationship.
92
+ *
93
+ * Returns a single related model or null if none exists.
94
+ * Even though the underlying query might return multiple rows,
95
+ * this method only returns the first result.
96
+ *
97
+ * @returns The related model or null
98
+ */
99
+ public async getResults(): Promise<TRelated | null> {
100
+ const localKeyValue = this.getParentKeyValue(this.localKey);
101
+
102
+ // If parent doesn't have a key value, return null
103
+ if (localKeyValue === undefined || localKeyValue === null) {
104
+ return null;
105
+ }
106
+
107
+ return this.first();
108
+ }
109
+
110
+ /**
111
+ * Get the foreign key column name.
112
+ *
113
+ * @returns The foreign key column name
114
+ */
115
+ public getForeignKeyName(): string {
116
+ return this.foreignKey;
117
+ }
118
+
119
+ /**
120
+ * Get the fully qualified foreign key name (table.column).
121
+ *
122
+ * @returns The qualified foreign key name
123
+ */
124
+ public getQualifiedForeignKeyName(): string {
125
+ return this.qualifyColumn(this.related.getTable(), this.foreignKey);
126
+ }
127
+
128
+ /**
129
+ * Get the local key column name.
130
+ *
131
+ * @returns The local key column name
132
+ */
133
+ public getLocalKeyName(): string {
134
+ return this.localKey;
135
+ }
136
+
137
+ /**
138
+ * Get the value of the parent's local key.
139
+ *
140
+ * @returns The local key value
141
+ */
142
+ public getParentKey(): unknown {
143
+ return this.getParentKeyValue(this.localKey);
144
+ }
145
+
146
+ // ==========================================
147
+ // Eager Loading Methods
148
+ // ==========================================
149
+
150
+ /**
151
+ * Add constraints for eager loading.
152
+ * Uses WHERE IN to fetch all related models for multiple parents at once.
153
+ *
154
+ * @param models - Array of parent models to load relations for
155
+ */
156
+ public addEagerConstraints(models: Model[]): void {
157
+ // Collect all parent key values
158
+ const keys: unknown[] = [];
159
+ for (const model of models) {
160
+ const key = model.getAttribute(this.localKey);
161
+ if (key !== undefined && key !== null) {
162
+ keys.push(key);
163
+ }
164
+ }
165
+
166
+ // Remove duplicates
167
+ const uniqueKeys = [...new Set(keys)];
168
+
169
+ // Add WHERE IN constraint
170
+ if (uniqueKeys.length > 0) {
171
+ this.query.whereIn(this.foreignKey, uniqueKeys as (string | number | boolean)[]);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Match the eagerly loaded results to their parent models.
177
+ * For HasOne, each parent gets at most one related model.
178
+ *
179
+ * @param models - Array of parent models
180
+ * @param results - Array of related models that were loaded
181
+ * @param relation - The name of the relationship being matched
182
+ */
183
+ public match(models: Model[], results: TRelated[], relation: string): void {
184
+ // Build a dictionary for O(1) lookup: foreignKey value -> related model
185
+ const dictionary = new Map<unknown, TRelated>();
186
+ for (const result of results) {
187
+ const foreignKeyValue = result.getAttribute(this.foreignKey);
188
+ // For HasOne, only keep the first match (if duplicates exist)
189
+ if (!dictionary.has(foreignKeyValue)) {
190
+ dictionary.set(foreignKeyValue, result);
191
+ }
192
+ }
193
+
194
+ // Match each parent model with its related model
195
+ for (const model of models) {
196
+ const localKeyValue = model.getAttribute(this.localKey);
197
+ const match = dictionary.get(localKeyValue) || null;
198
+ model.setRelation(relation, match);
199
+ }
200
+ }
201
+ }