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,570 @@
1
+ import { DB } from '../query/DB';
2
+ import { Relation } from './Relation';
3
+ import type { Model } from './Model';
4
+ import type { Binding, InsertValues, UpdateValues } from '../query/types';
5
+
6
+ /**
7
+ * BelongsToMany Relationship - Represents a many-to-many relationship with a pivot table.
8
+ *
9
+ * Uses an intermediate (pivot) table to connect two models.
10
+ *
11
+ * Example Schema:
12
+ * - users table: id, name, email
13
+ * - roles table: id, name
14
+ * - role_user table (pivot): user_id, role_id, assigned_at (optional extra column)
15
+ *
16
+ * Usage:
17
+ * ```typescript
18
+ * class User extends Model {
19
+ * roles() {
20
+ * return this.belongsToMany(Role, 'role_user', 'user_id', 'role_id');
21
+ * }
22
+ * }
23
+ *
24
+ * class Role extends Model {
25
+ * users() {
26
+ * return this.belongsToMany(User, 'role_user', 'role_id', 'user_id');
27
+ * }
28
+ * }
29
+ *
30
+ * // Get user's roles
31
+ * const user = await User.find(1);
32
+ * const roles = await user.roles().get();
33
+ *
34
+ * // Attach a role
35
+ * await user.roles().attach(1);
36
+ * await user.roles().attach([1, 2, 3]);
37
+ *
38
+ * // Detach a role
39
+ * await user.roles().detach(1);
40
+ * await user.roles().detach(); // Detach all
41
+ *
42
+ * // Sync roles (attach new, detach missing)
43
+ * await user.roles().sync([1, 2, 3]);
44
+ * ```
45
+ *
46
+ * @template TRelated - The type of the related model
47
+ */
48
+ export class BelongsToMany<TRelated extends Model> extends Relation<TRelated> {
49
+ /**
50
+ * The pivot table name.
51
+ * Example: 'role_user'
52
+ */
53
+ protected table: string;
54
+
55
+ /**
56
+ * The foreign key of the parent model on the pivot table.
57
+ * Example: 'user_id' on role_user table
58
+ */
59
+ protected foreignPivotKey: string;
60
+
61
+ /**
62
+ * The foreign key of the related model on the pivot table.
63
+ * Example: 'role_id' on role_user table
64
+ */
65
+ protected relatedPivotKey: string;
66
+
67
+ /**
68
+ * The primary key on the parent model.
69
+ * Example: 'id' on users table
70
+ */
71
+ protected parentKey: string;
72
+
73
+ /**
74
+ * The primary key on the related model.
75
+ * Example: 'id' on roles table
76
+ */
77
+ protected relatedKey: string;
78
+
79
+ /**
80
+ * Extra pivot columns to include in results.
81
+ */
82
+ protected pivotColumns: string[] = [];
83
+
84
+ /**
85
+ * Create a new BelongsToMany relationship instance.
86
+ *
87
+ * @param parent - The parent model instance
88
+ * @param related - An instance of the related model class
89
+ * @param table - The pivot table name (e.g., 'role_user')
90
+ * @param foreignPivotKey - The foreign key for the parent on the pivot table (e.g., 'user_id')
91
+ * @param relatedPivotKey - The foreign key for the related model on the pivot table (e.g., 'role_id')
92
+ * @param parentKey - The primary key on the parent model (e.g., 'id')
93
+ * @param relatedKey - The primary key on the related model (e.g., 'id')
94
+ */
95
+ constructor(
96
+ parent: Model,
97
+ related: TRelated,
98
+ table: string,
99
+ foreignPivotKey: string,
100
+ relatedPivotKey: string,
101
+ parentKey: string,
102
+ relatedKey: string
103
+ ) {
104
+ super(parent, related);
105
+
106
+ this.table = table;
107
+ this.foreignPivotKey = foreignPivotKey;
108
+ this.relatedPivotKey = relatedPivotKey;
109
+ this.parentKey = parentKey;
110
+ this.relatedKey = relatedKey;
111
+
112
+ // Re-add constraints now that keys are set
113
+ this.reinitializeConstraints();
114
+ }
115
+
116
+ /**
117
+ * Re-initialize constraints after keys are set.
118
+ */
119
+ private reinitializeConstraints(): void {
120
+ this.query = this.newQuery();
121
+ const parentKeyValue = this.parent.getAttribute(this.parentKey);
122
+ const relatedTable = this.related.getTable();
123
+
124
+ // Add the join to the pivot table
125
+ this.query.join(
126
+ this.table,
127
+ `${relatedTable}.${this.relatedKey}`,
128
+ '=',
129
+ `${this.table}.${this.relatedPivotKey}`
130
+ );
131
+
132
+ // Constrain by parent's key
133
+ if (parentKeyValue !== undefined && parentKeyValue !== null) {
134
+ this.query.where(`${this.table}.${this.foreignPivotKey}`, '=', parentKeyValue as Binding);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Add the constraints for the BelongsToMany relationship.
140
+ * Called by parent constructor - keys may not be set yet.
141
+ */
142
+ public addConstraints(): void {
143
+ // Constraints are added in reinitializeConstraints after keys are set
144
+ }
145
+
146
+ /**
147
+ * Get the results of the BelongsToMany relationship.
148
+ *
149
+ * Returns an array of related models. If no related models exist,
150
+ * returns an empty array (never null).
151
+ *
152
+ * @returns Array of related models
153
+ */
154
+ public async getResults(): Promise<TRelated[]> {
155
+ const parentKeyValue = this.parent.getAttribute(this.parentKey);
156
+
157
+ // If parent doesn't have a key value, return empty array
158
+ if (parentKeyValue === undefined || parentKeyValue === null) {
159
+ return [];
160
+ }
161
+
162
+ return this.getAll();
163
+ }
164
+
165
+ /**
166
+ * Include extra pivot columns in the results.
167
+ *
168
+ * Usage:
169
+ * ```typescript
170
+ * const roles = await user.roles().withPivot('assigned_at', 'created_by').get();
171
+ * // Each role will have role.pivot.assigned_at available
172
+ * ```
173
+ *
174
+ * @param columns - Column names to include from the pivot table
175
+ * @returns This relation for chaining
176
+ */
177
+ public withPivot(...columns: string[]): this {
178
+ this.pivotColumns.push(...columns);
179
+
180
+ // Add pivot columns to select
181
+ for (const column of columns) {
182
+ this.query.addSelect(`${this.table}.${column}`);
183
+ }
184
+
185
+ return this;
186
+ }
187
+
188
+ /**
189
+ * Add a WHERE clause on a pivot table column.
190
+ *
191
+ * Usage:
192
+ * ```typescript
193
+ * const activeRoles = await user.roles()
194
+ * .wherePivot('active', true)
195
+ * .get();
196
+ * ```
197
+ *
198
+ * @param column - The pivot column to filter on
199
+ * @param operatorOrValue - The operator or value
200
+ * @param value - The value if operator provided
201
+ * @returns This relation for chaining
202
+ */
203
+ public wherePivot(
204
+ column: string,
205
+ operatorOrValue: string | number | boolean | null,
206
+ value?: string | number | boolean | null
207
+ ): this {
208
+ const qualifiedColumn = `${this.table}.${column}`;
209
+ this.query.where(qualifiedColumn, operatorOrValue, value);
210
+ return this;
211
+ }
212
+
213
+ /**
214
+ * Add a WHERE IN clause on a pivot table column.
215
+ *
216
+ * @param column - The pivot column to filter on
217
+ * @param values - Array of values to match
218
+ * @returns This relation for chaining
219
+ */
220
+ public wherePivotIn(column: string, values: (string | number | boolean | null)[]): this {
221
+ const qualifiedColumn = `${this.table}.${column}`;
222
+ this.query.whereIn(qualifiedColumn, values);
223
+ return this;
224
+ }
225
+
226
+ /**
227
+ * Attach one or more related models by their IDs.
228
+ *
229
+ * This creates records in the pivot table linking the parent to the related models.
230
+ *
231
+ * Usage:
232
+ * ```typescript
233
+ * // Attach a single role
234
+ * await user.roles().attach(1);
235
+ *
236
+ * // Attach multiple roles
237
+ * await user.roles().attach([1, 2, 3]);
238
+ *
239
+ * // Attach with extra pivot data
240
+ * await user.roles().attach(1, { assigned_at: new Date() });
241
+ * await user.roles().attach([1, 2], { assigned_by: 'admin' });
242
+ * ```
243
+ *
244
+ * @param ids - A single ID or array of IDs to attach
245
+ * @param attributes - Optional extra attributes for the pivot record
246
+ */
247
+ public async attach(
248
+ ids: number | string | (number | string)[],
249
+ attributes: Record<string, Binding> = {}
250
+ ): Promise<void> {
251
+ const parentKeyValue = this.parent.getAttribute(this.parentKey);
252
+ if (parentKeyValue === undefined || parentKeyValue === null) {
253
+ throw new Error('Cannot attach: parent model has no primary key value');
254
+ }
255
+
256
+ const idsArray = Array.isArray(ids) ? ids : [ids];
257
+
258
+ if (idsArray.length === 0) {
259
+ return;
260
+ }
261
+
262
+ // Create pivot records
263
+ const records: InsertValues[] = idsArray.map((id) => ({
264
+ [this.foreignPivotKey]: parentKeyValue as Binding,
265
+ [this.relatedPivotKey]: id as Binding,
266
+ ...attributes
267
+ }));
268
+
269
+ await DB.table(this.table).insert(records);
270
+ }
271
+
272
+ /**
273
+ * Detach one or more related models by their IDs.
274
+ *
275
+ * This removes records from the pivot table.
276
+ *
277
+ * Usage:
278
+ * ```typescript
279
+ * // Detach a single role
280
+ * await user.roles().detach(1);
281
+ *
282
+ * // Detach multiple roles
283
+ * await user.roles().detach([1, 2, 3]);
284
+ *
285
+ * // Detach all roles
286
+ * await user.roles().detach();
287
+ * ```
288
+ *
289
+ * @param ids - Optional ID or array of IDs to detach. If not provided, detaches all.
290
+ * @returns The number of detached records
291
+ */
292
+ public async detach(ids?: number | string | (number | string)[]): Promise<number> {
293
+ const parentKeyValue = this.parent.getAttribute(this.parentKey);
294
+ if (parentKeyValue === undefined || parentKeyValue === null) {
295
+ throw new Error('Cannot detach: parent model has no primary key value');
296
+ }
297
+
298
+ let query = DB.table(this.table)
299
+ .where(this.foreignPivotKey, '=', parentKeyValue as Binding);
300
+
301
+ if (ids !== undefined) {
302
+ const idsArray = Array.isArray(ids) ? ids : [ids];
303
+ if (idsArray.length > 0) {
304
+ query = query.whereIn(this.relatedPivotKey, idsArray);
305
+ }
306
+ }
307
+
308
+ return query.delete();
309
+ }
310
+
311
+ /**
312
+ * Sync the relationship with the given IDs.
313
+ *
314
+ * This will attach any IDs that are not currently attached,
315
+ * and detach any IDs that are not in the given list.
316
+ *
317
+ * Usage:
318
+ * ```typescript
319
+ * // After this, user will have exactly roles 1, 2, 3
320
+ * await user.roles().sync([1, 2, 3]);
321
+ * ```
322
+ *
323
+ * @param ids - Array of IDs that should be attached
324
+ * @returns Object with attached and detached IDs
325
+ */
326
+ public async sync(ids: (number | string)[]): Promise<{
327
+ attached: (number | string)[];
328
+ detached: (number | string)[];
329
+ }> {
330
+ const parentKeyValue = this.parent.getAttribute(this.parentKey);
331
+ if (parentKeyValue === undefined || parentKeyValue === null) {
332
+ throw new Error('Cannot sync: parent model has no primary key value');
333
+ }
334
+
335
+ // Get currently attached IDs
336
+ const currentPivotRecords = await DB.table(this.table)
337
+ .where(this.foreignPivotKey, '=', parentKeyValue as Binding)
338
+ .get<Record<string, unknown>>();
339
+
340
+ const currentIds = currentPivotRecords.map(
341
+ (record) => record[this.relatedPivotKey] as number | string
342
+ );
343
+
344
+ // Determine what to attach and detach
345
+ const toAttach = ids.filter((id) => !currentIds.includes(id));
346
+ const toDetach = currentIds.filter((id) => !ids.includes(id));
347
+
348
+ // Perform operations
349
+ if (toAttach.length > 0) {
350
+ await this.attach(toAttach);
351
+ }
352
+
353
+ if (toDetach.length > 0) {
354
+ await this.detach(toDetach);
355
+ }
356
+
357
+ return {
358
+ attached: toAttach,
359
+ detached: toDetach
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Toggle the attachment of the given IDs.
365
+ *
366
+ * If an ID is currently attached, it will be detached.
367
+ * If an ID is not attached, it will be attached.
368
+ *
369
+ * Usage:
370
+ * ```typescript
371
+ * await user.roles().toggle([1, 2, 3]);
372
+ * ```
373
+ *
374
+ * @param ids - Array of IDs to toggle
375
+ * @returns Object with attached and detached IDs
376
+ */
377
+ public async toggle(ids: (number | string)[]): Promise<{
378
+ attached: (number | string)[];
379
+ detached: (number | string)[];
380
+ }> {
381
+ const parentKeyValue = this.parent.getAttribute(this.parentKey);
382
+ if (parentKeyValue === undefined || parentKeyValue === null) {
383
+ throw new Error('Cannot toggle: parent model has no primary key value');
384
+ }
385
+
386
+ // Get currently attached IDs
387
+ const currentPivotRecords = await DB.table(this.table)
388
+ .where(this.foreignPivotKey, '=', parentKeyValue as Binding)
389
+ .get<Record<string, unknown>>();
390
+
391
+ const currentIds = currentPivotRecords.map(
392
+ (record) => record[this.relatedPivotKey] as number | string
393
+ );
394
+
395
+ // Determine what to attach and detach
396
+ const toAttach = ids.filter((id) => !currentIds.includes(id));
397
+ const toDetach = ids.filter((id) => currentIds.includes(id));
398
+
399
+ // Perform operations
400
+ if (toAttach.length > 0) {
401
+ await this.attach(toAttach);
402
+ }
403
+
404
+ if (toDetach.length > 0) {
405
+ await this.detach(toDetach);
406
+ }
407
+
408
+ return {
409
+ attached: toAttach,
410
+ detached: toDetach
411
+ };
412
+ }
413
+
414
+ /**
415
+ * Update an existing pivot record.
416
+ *
417
+ * Usage:
418
+ * ```typescript
419
+ * await user.roles().updateExistingPivot(1, { active: false });
420
+ * ```
421
+ *
422
+ * @param id - The related model's ID
423
+ * @param attributes - Attributes to update on the pivot record
424
+ * @returns Number of affected rows
425
+ */
426
+ public async updateExistingPivot(
427
+ id: number | string,
428
+ attributes: UpdateValues
429
+ ): Promise<number> {
430
+ const parentKeyValue = this.parent.getAttribute(this.parentKey);
431
+ if (parentKeyValue === undefined || parentKeyValue === null) {
432
+ throw new Error('Cannot update pivot: parent model has no primary key value');
433
+ }
434
+
435
+ return DB.table(this.table)
436
+ .where(this.foreignPivotKey, '=', parentKeyValue as Binding)
437
+ .where(this.relatedPivotKey, '=', id as Binding)
438
+ .update(attributes);
439
+ }
440
+
441
+ /**
442
+ * Get the pivot table name.
443
+ *
444
+ * @returns The pivot table name
445
+ */
446
+ public getPivotTable(): string {
447
+ return this.table;
448
+ }
449
+
450
+ /**
451
+ * Get the foreign pivot key name.
452
+ *
453
+ * @returns The foreign pivot key column name
454
+ */
455
+ public getForeignPivotKeyName(): string {
456
+ return this.foreignPivotKey;
457
+ }
458
+
459
+ /**
460
+ * Get the related pivot key name.
461
+ *
462
+ * @returns The related pivot key column name
463
+ */
464
+ public getRelatedPivotKeyName(): string {
465
+ return this.relatedPivotKey;
466
+ }
467
+
468
+ /**
469
+ * Get the fully qualified foreign pivot key name.
470
+ *
471
+ * @returns The qualified column name
472
+ */
473
+ public getQualifiedForeignPivotKeyName(): string {
474
+ return `${this.table}.${this.foreignPivotKey}`;
475
+ }
476
+
477
+ /**
478
+ * Get the fully qualified related pivot key name.
479
+ *
480
+ * @returns The qualified column name
481
+ */
482
+ public getQualifiedRelatedPivotKeyName(): string {
483
+ return `${this.table}.${this.relatedPivotKey}`;
484
+ }
485
+
486
+ // ==========================================
487
+ // Eager Loading Methods
488
+ // ==========================================
489
+
490
+ /**
491
+ * Initialize the relation for eager loading.
492
+ * Creates a fresh query with the pivot join but without parent-specific constraints.
493
+ */
494
+ public override initializeForEagerLoading(): void {
495
+ this.query = this.newQuery();
496
+ const relatedTable = this.related.getTable();
497
+
498
+ // Explicitly select all columns from related table to preserve them when adding rawSelect
499
+ this.query.select(`${relatedTable}.*`);
500
+
501
+ // Add the join to the pivot table (required for BelongsToMany)
502
+ this.query.join(
503
+ this.table,
504
+ `${relatedTable}.${this.relatedKey}`,
505
+ '=',
506
+ `${this.table}.${this.relatedPivotKey}`
507
+ );
508
+ }
509
+
510
+ /**
511
+ * Add constraints for eager loading.
512
+ * Uses WHERE IN on the pivot table to fetch all related models for multiple parents at once.
513
+ *
514
+ * @param models - Array of parent models to load relations for
515
+ */
516
+ public addEagerConstraints(models: Model[]): void {
517
+ // Collect all parent key values
518
+ const keys: unknown[] = [];
519
+ for (const model of models) {
520
+ const key = model.getAttribute(this.parentKey);
521
+ if (key !== undefined && key !== null) {
522
+ keys.push(key);
523
+ }
524
+ }
525
+
526
+ // Remove duplicates
527
+ const uniqueKeys = [...new Set(keys)];
528
+
529
+ // Add WHERE IN constraint on pivot table's foreign key
530
+ if (uniqueKeys.length > 0) {
531
+ this.query.whereIn(
532
+ `${this.table}.${this.foreignPivotKey}`,
533
+ uniqueKeys as (string | number | boolean)[]
534
+ );
535
+ }
536
+
537
+ // Also select the foreign pivot key to enable matching (use selectRaw for proper alias)
538
+ this.query.selectRaw(`${this.table}.${this.foreignPivotKey} as pivot_foreign_key`);
539
+ }
540
+
541
+ /**
542
+ * Match the eagerly loaded results to their parent models.
543
+ * For BelongsToMany, each parent gets an array of related models.
544
+ *
545
+ * @param models - Array of parent models
546
+ * @param results - Array of related models that were loaded
547
+ * @param relation - The name of the relationship being matched
548
+ */
549
+ public match(models: Model[], results: TRelated[], relation: string): void {
550
+ // Build a dictionary for O(n) grouping: parentKey value -> array of related models
551
+ // Use String conversion for consistent key comparison across types
552
+ const dictionary = new Map<string, TRelated[]>();
553
+ for (const result of results) {
554
+ // Get the foreign pivot key from the result (added as pivot_foreign_key)
555
+ const pivotForeignKeyValue = String(result.getAttribute('pivot_foreign_key'));
556
+ if (!dictionary.has(pivotForeignKeyValue)) {
557
+ dictionary.set(pivotForeignKeyValue, []);
558
+ }
559
+ dictionary.get(pivotForeignKeyValue)!.push(result);
560
+ }
561
+
562
+ // Match each parent model with its array of related models
563
+ for (const model of models) {
564
+ const parentKeyValue = String(model.getAttribute(this.parentKey));
565
+ const matches = dictionary.get(parentKeyValue) || [];
566
+ model.setRelation(relation, matches);
567
+ }
568
+ }
569
+ }
570
+
@@ -0,0 +1,160 @@
1
+ import { Builder as QueryBuilder } from '../query/Builder';
2
+ import { Model } from './Model';
3
+ import { Scope } from './Scope';
4
+
5
+ /**
6
+ * Eloquent Builder
7
+ *
8
+ * Extends the basic Query Builder to add ORM-specific features
9
+ * like Scopes, Model hydration, and Soft Deletes support.
10
+ */
11
+ export class Builder<T extends Model = Model> extends QueryBuilder {
12
+ /**
13
+ * The model being queried.
14
+ */
15
+ protected model!: T;
16
+
17
+ /**
18
+ * The applied global scopes.
19
+ */
20
+ protected scopes: Map<string, Scope> = new Map();
21
+
22
+ /**
23
+ * The scopes that should be removed.
24
+ */
25
+ protected removedScopes: string[] = [];
26
+
27
+ /**
28
+ * Create a new Eloquent Builder instance.
29
+ */
30
+ constructor(query: QueryBuilder) {
31
+ super(query['connection'], query['grammar'], query['query'].table);
32
+ // Copy query state
33
+ this.query = query['query'];
34
+ }
35
+
36
+ /**
37
+ * Set the model instance for the builder.
38
+ */
39
+ public setModel(model: T): this {
40
+ this.model = model;
41
+ return this;
42
+ }
43
+
44
+ /**
45
+ * Get the model instance.
46
+ */
47
+ public getModel(): T {
48
+ return this.model;
49
+ }
50
+
51
+ /**
52
+ * Register a new global scope.
53
+ */
54
+ public withGlobalScope(identifier: string, scope: Scope): this {
55
+ this.scopes.set(identifier, scope);
56
+ if (scope.extend) {
57
+ scope.extend(this);
58
+ }
59
+ return this;
60
+ }
61
+
62
+ /**
63
+ * Register a custom macro.
64
+ */
65
+ public macro(name: string, callback: Function): void {
66
+ (this as any)[name] = callback;
67
+ }
68
+
69
+ /**
70
+ * Remove a registered global scope.
71
+ */
72
+ public withoutGlobalScope(scope: Scope | string | Function): this {
73
+ if (typeof scope === 'string') {
74
+ this.removedScopes.push(scope);
75
+ this.scopes.delete(scope);
76
+ } else if (typeof scope === 'function') {
77
+ // Find by constructor
78
+ for (const [key, s] of this.scopes.entries()) {
79
+ if (s.constructor === scope) {
80
+ this.removedScopes.push(key);
81
+ this.scopes.delete(key);
82
+ break;
83
+ }
84
+ }
85
+ } else {
86
+ // Find by instance
87
+ for (const [key, s] of this.scopes.entries()) {
88
+ if (s.constructor.name === scope.constructor.name) {
89
+ this.removedScopes.push(key);
90
+ this.scopes.delete(key);
91
+ break;
92
+ }
93
+ }
94
+ }
95
+
96
+ return this;
97
+ }
98
+
99
+ /**
100
+ * Remove all or passed global scopes.
101
+ */
102
+ public withoutGlobalScopes(scopes: string[] | null = null): this {
103
+ if (!scopes) {
104
+ this.scopes.clear();
105
+ } else {
106
+ for (const scope of scopes) {
107
+ this.withoutGlobalScope(scope);
108
+ }
109
+ }
110
+ return this;
111
+ }
112
+
113
+ /**
114
+ * Apply the scopes to the query builder instance.
115
+ */
116
+ public applyScopes(): this {
117
+ if (this.scopes.size === 0) {
118
+ return this;
119
+ }
120
+
121
+ for (const [identifier, scope] of this.scopes.entries()) {
122
+ if (!this.removedScopes.includes(identifier)) {
123
+ scope.apply(this, this.model);
124
+ }
125
+ }
126
+
127
+ return this;
128
+ }
129
+
130
+ /**
131
+ * execute the query as a "select" statement.
132
+ * Overridden to hydrate models.
133
+ */
134
+ public async get<R = any>(): Promise<R[]> {
135
+ const builder = this.applyScopes();
136
+
137
+ const results = await super.get();
138
+
139
+ if (results.length > 0) {
140
+ return this.model['hydrateModels'](results) as unknown as R[];
141
+ }
142
+
143
+ return results as R[];
144
+ }
145
+
146
+ /**
147
+ * Execute the query and return the first result.
148
+ * Overridden to hydrate model.
149
+ */
150
+ public async first<R = any>(): Promise<R | null> {
151
+ const builder = this.applyScopes();
152
+ const result = await super.first();
153
+
154
+ if (result) {
155
+ return this.model['hydrateModel'](result) as unknown as R;
156
+ }
157
+
158
+ return null;
159
+ }
160
+ }