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,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
|
+
}
|