create-phoenixjs 0.1.4 → 0.1.6
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/20260108165704_TestCliMigration.ts +16 -0
- package/template/database/migrations/2026_01_08_16_57_04_CreateTestMigrationsTable.ts +21 -0
- package/template/framework/cli/artisan.ts +12 -0
- package/template/framework/cli/commands/MakeModelCommand.ts +33 -3
- 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 +1802 -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,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
|
+
}
|