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,342 @@
|
|
|
1
|
+
import { DB } from '../query/DB';
|
|
2
|
+
import type { Builder } from '../query/Builder';
|
|
3
|
+
import type { Model } from './Model';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Abstract Relation class - Base class for all relationship types.
|
|
7
|
+
*
|
|
8
|
+
* This class provides the foundation for defining relationships between models.
|
|
9
|
+
* It handles query building, constraint application, and result hydration.
|
|
10
|
+
*
|
|
11
|
+
* All relationship types (HasOne, HasMany, BelongsTo, etc.) extend this class
|
|
12
|
+
* and implement their specific logic for retrieving related models.
|
|
13
|
+
*
|
|
14
|
+
* @template TRelated - The type of the related model
|
|
15
|
+
*/
|
|
16
|
+
export abstract class Relation<TRelated extends Model> {
|
|
17
|
+
/**
|
|
18
|
+
* The parent model instance.
|
|
19
|
+
* This is the model that "owns" the relationship.
|
|
20
|
+
*/
|
|
21
|
+
protected parent: Model;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The related model instance (used as a prototype for creating new instances).
|
|
25
|
+
* This is a fresh instance used for table name inference and hydration.
|
|
26
|
+
*/
|
|
27
|
+
protected related: TRelated;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The query builder instance for the relationship.
|
|
31
|
+
* Used to build and execute queries against the related model's table.
|
|
32
|
+
*/
|
|
33
|
+
protected query: Builder;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a new Relation instance.
|
|
37
|
+
*
|
|
38
|
+
* @param parent - The parent model that owns this relationship
|
|
39
|
+
* @param related - An instance of the related model class
|
|
40
|
+
*/
|
|
41
|
+
constructor(parent: Model, related: TRelated) {
|
|
42
|
+
this.parent = parent;
|
|
43
|
+
this.related = related;
|
|
44
|
+
this.query = this.newQuery();
|
|
45
|
+
|
|
46
|
+
// Add relationship constraints to the query
|
|
47
|
+
this.addConstraints();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ==========================================
|
|
51
|
+
// Abstract Methods (Must be implemented by subclasses)
|
|
52
|
+
// ==========================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the results of the relationship.
|
|
56
|
+
* Each relationship type implements this differently:
|
|
57
|
+
* - HasOne/BelongsTo: Returns single model or null
|
|
58
|
+
* - HasMany/BelongsToMany: Returns array of models
|
|
59
|
+
*/
|
|
60
|
+
abstract getResults(): Promise<TRelated | TRelated[] | null>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Add the constraints for the relationship query.
|
|
64
|
+
* This is called during construction to set up the WHERE clauses
|
|
65
|
+
* that link the parent model to its related models.
|
|
66
|
+
*/
|
|
67
|
+
abstract addConstraints(): void;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Add constraints for eager loading.
|
|
71
|
+
* Called when loading relations for multiple parent models at once.
|
|
72
|
+
* Should add a WHERE IN constraint for all parent keys.
|
|
73
|
+
*
|
|
74
|
+
* @param models - Array of parent models to load relations for
|
|
75
|
+
*/
|
|
76
|
+
abstract addEagerConstraints(models: Model[]): void;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Match the eagerly loaded results to their parent models.
|
|
80
|
+
* This associates the loaded related models with their correct parents.
|
|
81
|
+
*
|
|
82
|
+
* @param models - Array of parent models
|
|
83
|
+
* @param results - Array of related models that were loaded
|
|
84
|
+
* @param relation - The name of the relationship being matched
|
|
85
|
+
*/
|
|
86
|
+
abstract match(models: Model[], results: TRelated[], relation: string): void;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Initialize the relation for eager loading.
|
|
90
|
+
* Creates a fresh query without the parent-specific constraints.
|
|
91
|
+
*/
|
|
92
|
+
public initializeForEagerLoading(): void {
|
|
93
|
+
this.query = this.newQuery();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
// ==========================================
|
|
98
|
+
// Query Building
|
|
99
|
+
// ==========================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a new query builder for the related model's table.
|
|
103
|
+
* This provides a fresh Builder instance targeting the related table.
|
|
104
|
+
*
|
|
105
|
+
* @returns A new Builder instance for the related model's table
|
|
106
|
+
*/
|
|
107
|
+
protected newQuery(): Builder {
|
|
108
|
+
return DB.table(this.related.getTable());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the underlying query builder instance.
|
|
113
|
+
* Useful for adding additional constraints or debugging.
|
|
114
|
+
*
|
|
115
|
+
* @returns The Builder instance for this relationship
|
|
116
|
+
*/
|
|
117
|
+
public getQuery(): Builder {
|
|
118
|
+
return this.query;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the parent model of the relationship.
|
|
123
|
+
*
|
|
124
|
+
* @returns The parent model instance
|
|
125
|
+
*/
|
|
126
|
+
public getParent(): Model {
|
|
127
|
+
return this.parent;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the related model prototype.
|
|
132
|
+
* This is the model instance used for table name inference and hydration.
|
|
133
|
+
*
|
|
134
|
+
* @returns The related model instance
|
|
135
|
+
*/
|
|
136
|
+
public getRelated(): TRelated {
|
|
137
|
+
return this.related;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ==========================================
|
|
141
|
+
// Query Execution Methods
|
|
142
|
+
// ==========================================
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Execute the relationship query and get the results.
|
|
146
|
+
* This is the primary method for retrieving related models.
|
|
147
|
+
*
|
|
148
|
+
* @returns The related model(s) based on relationship type
|
|
149
|
+
*/
|
|
150
|
+
public async get(): Promise<TRelated | TRelated[] | null> {
|
|
151
|
+
return this.getResults();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get the first result from the relationship query.
|
|
156
|
+
* Useful when you only need one result regardless of relationship type.
|
|
157
|
+
*
|
|
158
|
+
* @returns The first related model or null
|
|
159
|
+
*/
|
|
160
|
+
public async first(): Promise<TRelated | null> {
|
|
161
|
+
const row = await this.query.first<Record<string, unknown>>();
|
|
162
|
+
|
|
163
|
+
if (!row) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return this.hydrateModel(row);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get all results from the relationship query.
|
|
172
|
+
* Always returns an array, even for HasOne/BelongsTo relationships.
|
|
173
|
+
*
|
|
174
|
+
* @returns Array of related models
|
|
175
|
+
*/
|
|
176
|
+
public async getAll(): Promise<TRelated[]> {
|
|
177
|
+
const rows = await this.query.get<Record<string, unknown>>();
|
|
178
|
+
return this.hydrateModels(rows);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if any related models exist.
|
|
183
|
+
*
|
|
184
|
+
* @returns True if at least one related model exists
|
|
185
|
+
*/
|
|
186
|
+
public async exists(): Promise<boolean> {
|
|
187
|
+
return this.query.exists();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get the count of related models.
|
|
192
|
+
*
|
|
193
|
+
* @returns The number of related models
|
|
194
|
+
*/
|
|
195
|
+
public async count(): Promise<number> {
|
|
196
|
+
return this.query.count();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ==========================================
|
|
200
|
+
// Query Forwarding Methods
|
|
201
|
+
// ==========================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Add a WHERE clause to the relationship query.
|
|
205
|
+
*
|
|
206
|
+
* @param column - The column to filter on
|
|
207
|
+
* @param operatorOrValue - The operator or value (if no operator)
|
|
208
|
+
* @param value - The value to compare against
|
|
209
|
+
* @returns This relation instance for chaining
|
|
210
|
+
*/
|
|
211
|
+
public where(
|
|
212
|
+
column: string,
|
|
213
|
+
operatorOrValue?: string | number | boolean | null,
|
|
214
|
+
value?: string | number | boolean | null
|
|
215
|
+
): this {
|
|
216
|
+
this.query.where(column, operatorOrValue, value);
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Add a WHERE IN clause to the relationship query.
|
|
222
|
+
*
|
|
223
|
+
* @param column - The column to filter on
|
|
224
|
+
* @param values - Array of values to match
|
|
225
|
+
* @returns This relation instance for chaining
|
|
226
|
+
*/
|
|
227
|
+
public whereIn(column: string, values: (string | number | boolean | null)[]): this {
|
|
228
|
+
this.query.whereIn(column, values);
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Add a WHERE NULL clause to the relationship query.
|
|
234
|
+
*
|
|
235
|
+
* @param column - The column to check for NULL
|
|
236
|
+
* @returns This relation instance for chaining
|
|
237
|
+
*/
|
|
238
|
+
public whereNull(column: string): this {
|
|
239
|
+
this.query.whereNull(column);
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Add a WHERE NOT NULL clause to the relationship query.
|
|
245
|
+
*
|
|
246
|
+
* @param column - The column to check for NOT NULL
|
|
247
|
+
* @returns This relation instance for chaining
|
|
248
|
+
*/
|
|
249
|
+
public whereNotNull(column: string): this {
|
|
250
|
+
this.query.whereNotNull(column);
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Add an ORDER BY clause to the relationship query.
|
|
256
|
+
*
|
|
257
|
+
* @param column - The column to order by
|
|
258
|
+
* @param direction - The sort direction ('asc' or 'desc')
|
|
259
|
+
* @returns This relation instance for chaining
|
|
260
|
+
*/
|
|
261
|
+
public orderBy(column: string, direction: 'asc' | 'desc' = 'asc'): this {
|
|
262
|
+
this.query.orderBy(column, direction);
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Add a LIMIT clause to the relationship query.
|
|
268
|
+
*
|
|
269
|
+
* @param value - The maximum number of results
|
|
270
|
+
* @returns This relation instance for chaining
|
|
271
|
+
*/
|
|
272
|
+
public limit(value: number): this {
|
|
273
|
+
this.query.limit(value);
|
|
274
|
+
return this;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Add an OFFSET clause to the relationship query.
|
|
279
|
+
*
|
|
280
|
+
* @param value - The number of results to skip
|
|
281
|
+
* @returns This relation instance for chaining
|
|
282
|
+
*/
|
|
283
|
+
public offset(value: number): this {
|
|
284
|
+
this.query.offset(value);
|
|
285
|
+
return this;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ==========================================
|
|
289
|
+
// Model Hydration
|
|
290
|
+
// ==========================================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Create a new model instance from a database row.
|
|
294
|
+
* The model is marked as existing and its original attributes are synced.
|
|
295
|
+
*
|
|
296
|
+
* @param row - The database row to hydrate
|
|
297
|
+
* @returns A new model instance with the given attributes
|
|
298
|
+
*/
|
|
299
|
+
protected hydrateModel(row: Record<string, unknown>): TRelated {
|
|
300
|
+
const instance = this.related.newInstance({}, true);
|
|
301
|
+
instance.forceFill(row);
|
|
302
|
+
instance.syncOriginal();
|
|
303
|
+
return instance as TRelated;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Create multiple model instances from database rows.
|
|
308
|
+
*
|
|
309
|
+
* @param rows - Array of database rows to hydrate
|
|
310
|
+
* @returns Array of new model instances
|
|
311
|
+
*/
|
|
312
|
+
protected hydrateModels(rows: Record<string, unknown>[]): TRelated[] {
|
|
313
|
+
return rows.map((row) => this.hydrateModel(row));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ==========================================
|
|
317
|
+
// Key Helpers
|
|
318
|
+
// ==========================================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get the fully qualified foreign key name (table.column format).
|
|
322
|
+
* Used for JOIN operations to avoid column name ambiguity.
|
|
323
|
+
*
|
|
324
|
+
* @param table - The table name
|
|
325
|
+
* @param column - The column name
|
|
326
|
+
* @returns The qualified column name (e.g., "users.id")
|
|
327
|
+
*/
|
|
328
|
+
protected qualifyColumn(table: string, column: string): string {
|
|
329
|
+
return `${table}.${column}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get the value of the parent's local key.
|
|
334
|
+
* This is the value used to match related models.
|
|
335
|
+
*
|
|
336
|
+
* @param key - The key name on the parent model
|
|
337
|
+
* @returns The value of the key
|
|
338
|
+
*/
|
|
339
|
+
protected getParentKeyValue(key: string): unknown {
|
|
340
|
+
return this.parent.getAttribute(key);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Builder } from '../query/Builder';
|
|
2
|
+
import type { Model } from './Model';
|
|
3
|
+
|
|
4
|
+
export interface Scope {
|
|
5
|
+
/**
|
|
6
|
+
* Apply the scope to a given Eloquent query builder.
|
|
7
|
+
*/
|
|
8
|
+
apply(builder: Builder, model: Model): void;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extend the query builder with scope-specific macros.
|
|
12
|
+
*/
|
|
13
|
+
extend?(builder: Builder): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Model } from './Model';
|
|
2
|
+
import { SoftDeletingScope } from './scopes/SoftDeletingScope';
|
|
3
|
+
import { Builder } from './Builder';
|
|
4
|
+
|
|
5
|
+
type Constructor<T = {}> = new (...args: any[]) => T;
|
|
6
|
+
|
|
7
|
+
export function SoftDeletes<TBase extends Constructor<Model>>(Base: TBase) {
|
|
8
|
+
return class SoftDeletes extends Base {
|
|
9
|
+
protected forceDeleting: boolean = false;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Boot the soft deleting trait for a model.
|
|
13
|
+
*/
|
|
14
|
+
public static bootSoftDeletes() {
|
|
15
|
+
(this as any).addGlobalScope(new SoftDeletingScope());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the soft deleting trait for an instance.
|
|
20
|
+
*/
|
|
21
|
+
public initializeSoftDeletes() {
|
|
22
|
+
// Casts not fully implemented yet in Model, skipping for now
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Force a hard delete on a soft deleted model.
|
|
27
|
+
*/
|
|
28
|
+
public async forceDelete(): Promise<boolean> {
|
|
29
|
+
this.forceDeleting = true;
|
|
30
|
+
const result = await this.delete();
|
|
31
|
+
this.forceDeleting = false;
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Restore a soft-deleted model instance.
|
|
37
|
+
*/
|
|
38
|
+
public async restore(): Promise<boolean> {
|
|
39
|
+
if (!(await this.fireModelEvent('restoring' as any))) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.setAttribute(this.getDeletedAtColumn(), null);
|
|
44
|
+
this.exists = true;
|
|
45
|
+
|
|
46
|
+
// We handle the update manually to ensure we can find the deleted model
|
|
47
|
+
const query = this.newQuery().withoutGlobalScope(SoftDeletingScope);
|
|
48
|
+
const affected = await query
|
|
49
|
+
.where(this.getKeyName(), '=', this.getKey() as any)
|
|
50
|
+
.update({ [this.getDeletedAtColumn()]: null });
|
|
51
|
+
|
|
52
|
+
this.syncOriginal();
|
|
53
|
+
|
|
54
|
+
if (affected > 0) {
|
|
55
|
+
await this.fireModelEvent('restored' as any);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the name of the "deleted at" column.
|
|
64
|
+
*/
|
|
65
|
+
public getDeletedAtColumn(): string {
|
|
66
|
+
return (this.constructor as any).DELETED_AT || 'deleted_at';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the fully qualified "deleted at" column.
|
|
71
|
+
*/
|
|
72
|
+
public getQualifiedDeletedAtColumn(): string {
|
|
73
|
+
return this.getTable() + '.' + this.getDeletedAtColumn();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Perform the actual delete query on this model instance.
|
|
80
|
+
* Override the default delete behavior.
|
|
81
|
+
*/
|
|
82
|
+
public async delete(): Promise<boolean> {
|
|
83
|
+
if (!this.exists) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!(await this.fireModelEvent('deleting'))) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.forceDeleting) {
|
|
92
|
+
// Hard delete
|
|
93
|
+
// We must disable soft deleting scope to be able to delete "deleted" items if they are already soft deleted
|
|
94
|
+
// (Though forceDelete checks exists using instance, if instance is hydrated it's fine)
|
|
95
|
+
// But just in case:
|
|
96
|
+
const query = this.newQuery().withoutGlobalScope(SoftDeletingScope);
|
|
97
|
+
const affected = await query
|
|
98
|
+
.where(this.getKeyName(), '=', this.getKey() as any)
|
|
99
|
+
.delete();
|
|
100
|
+
|
|
101
|
+
this.exists = false;
|
|
102
|
+
await this.fireModelEvent('deleted');
|
|
103
|
+
return true;
|
|
104
|
+
} else {
|
|
105
|
+
// Soft delete
|
|
106
|
+
const result = await this.runSoftDelete();
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Run the soft delete.
|
|
113
|
+
*/
|
|
114
|
+
protected async runSoftDelete(): Promise<boolean> {
|
|
115
|
+
const query = this.newQuery();
|
|
116
|
+
const time = this.freshTimestamp();
|
|
117
|
+
const columns = { [this.getDeletedAtColumn()]: time };
|
|
118
|
+
|
|
119
|
+
this.setAttribute(this.getDeletedAtColumn(), time);
|
|
120
|
+
|
|
121
|
+
// Update database
|
|
122
|
+
const affected = await query
|
|
123
|
+
.where(this.getKeyName(), '=', this.getKey() as any)
|
|
124
|
+
.update(columns);
|
|
125
|
+
|
|
126
|
+
this.syncOriginal();
|
|
127
|
+
await this.fireModelEvent('deleted');
|
|
128
|
+
|
|
129
|
+
return affected > 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if the model instance has been soft-deleted.
|
|
134
|
+
*/
|
|
135
|
+
public trashed(): boolean {
|
|
136
|
+
return this.getAttribute(this.getDeletedAtColumn()) !== null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Static methods for query scopes.
|
|
141
|
+
* Since Mixins return a class, we can just add static methods here.
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
public static withTrashed(withTrashed: boolean = true) {
|
|
145
|
+
const query = new this().newQuery() as any;
|
|
146
|
+
return query.withTrashed(withTrashed);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public static onlyTrashed() {
|
|
150
|
+
const query = new this().newQuery() as any;
|
|
151
|
+
return query.onlyTrashed();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public static withoutTrashed() {
|
|
155
|
+
const query = new this().newQuery() as any;
|
|
156
|
+
return query.withoutTrashed();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS ORM Module
|
|
3
|
+
*
|
|
4
|
+
* This module provides a Laravel-style Active Record ORM implementation.
|
|
5
|
+
*
|
|
6
|
+
* Core Classes:
|
|
7
|
+
* - Model: Base class for all models with CRUD operations
|
|
8
|
+
* - Relation: Abstract base class for relationships
|
|
9
|
+
*
|
|
10
|
+
* Relationship Types:
|
|
11
|
+
* - HasOne: One-to-one (User has one Profile)
|
|
12
|
+
* - BelongsTo: Inverse (Profile belongs to User)
|
|
13
|
+
* - HasMany: One-to-many (User has many Posts)
|
|
14
|
+
* - BelongsToMany: Many-to-many with pivot table (Users <-> Roles)
|
|
15
|
+
* - HasOneThrough: One through intermediate (Country -> User -> Profile)
|
|
16
|
+
* - HasManyThrough: Many through intermediate (Country -> Users -> Posts)
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { Model, HasOne, BelongsTo, HasMany, BelongsToMany } from '@framework/database/orm';
|
|
21
|
+
*
|
|
22
|
+
* class User extends Model {
|
|
23
|
+
* profile() {
|
|
24
|
+
* return this.hasOne(Profile, 'user_id', 'id');
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* posts() {
|
|
28
|
+
* return this.hasMany(Post, 'user_id', 'id');
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* roles() {
|
|
32
|
+
* return this.belongsToMany(Role, 'role_user', 'user_id', 'role_id');
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// Core exports
|
|
39
|
+
export { Model, ModelNotFoundException } from './Model';
|
|
40
|
+
export type { ModelEventType, ModelEventCallback } from './Model';
|
|
41
|
+
|
|
42
|
+
// Relationship base class
|
|
43
|
+
export { Relation } from './Relation';
|
|
44
|
+
|
|
45
|
+
// Relationship types
|
|
46
|
+
export { HasOne } from './HasOne';
|
|
47
|
+
export { BelongsTo } from './BelongsTo';
|
|
48
|
+
export { HasMany } from './HasMany';
|
|
49
|
+
export { BelongsToMany } from './BelongsToMany';
|
|
50
|
+
export { HasOneThrough } from './HasOneThrough';
|
|
51
|
+
export { HasManyThrough } from './HasManyThrough';
|
|
52
|
+
|
|
53
|
+
// Eager loading
|
|
54
|
+
export { EagerLoadingBuilder } from './EagerLoadingBuilder';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Scope } from '../Scope';
|
|
2
|
+
import type { Builder } from '../Builder';
|
|
3
|
+
import type { Model } from '../Model';
|
|
4
|
+
|
|
5
|
+
export class SoftDeletingScope implements Scope {
|
|
6
|
+
/**
|
|
7
|
+
* Apply the scope to a given Eloquent query builder.
|
|
8
|
+
*/
|
|
9
|
+
public apply(builder: Builder, model: Model): void {
|
|
10
|
+
// We assume the model has 'getQualifiedDeletedAtColumn' from SoftDeletes trait/mixin
|
|
11
|
+
// Since we can't easily enforce Mixin types on the Model type here without circular deps or complex types,
|
|
12
|
+
// we'll cast or check.
|
|
13
|
+
const modelWithSoftDeletes = model as any;
|
|
14
|
+
|
|
15
|
+
if (typeof modelWithSoftDeletes.getQualifiedDeletedAtColumn === 'function') {
|
|
16
|
+
builder.whereNull(modelWithSoftDeletes.getQualifiedDeletedAtColumn());
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extend the query builder with the needed functions.
|
|
22
|
+
*/
|
|
23
|
+
public extend(builder: Builder): void {
|
|
24
|
+
const anyBuilder = builder as any;
|
|
25
|
+
|
|
26
|
+
// Add withTrashed macro
|
|
27
|
+
anyBuilder.withTrashed = function (this: any, withTrashed: boolean = true) {
|
|
28
|
+
if (!withTrashed) {
|
|
29
|
+
return this.withoutTrashed();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return this.withoutGlobalScope(SoftDeletingScope);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Add withoutTrashed macro
|
|
36
|
+
anyBuilder.withoutTrashed = function (this: Builder) {
|
|
37
|
+
const model = (this as any).getModel(); // We'll need to make sure Builder has reference to Model
|
|
38
|
+
const scope = new SoftDeletingScope();
|
|
39
|
+
|
|
40
|
+
scope.apply(this, model);
|
|
41
|
+
|
|
42
|
+
return this;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Add onlyTrashed macro
|
|
46
|
+
anyBuilder.onlyTrashed = function (this: Builder) {
|
|
47
|
+
const model = (this as any).getModel();
|
|
48
|
+
|
|
49
|
+
// Remove the SoftDeletingScope to allow seeing deleted items
|
|
50
|
+
this.withoutGlobalScope(SoftDeletingScope);
|
|
51
|
+
|
|
52
|
+
// Add whereNotNull condition
|
|
53
|
+
this.whereNotNull((model as any).getQualifiedDeletedAtColumn());
|
|
54
|
+
|
|
55
|
+
return this;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS ORM - LengthAwarePaginator
|
|
3
|
+
*
|
|
4
|
+
* Paginator with total count and last page calculation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Paginator } from './Paginator';
|
|
8
|
+
|
|
9
|
+
export class LengthAwarePaginator<T> extends Paginator<T> {
|
|
10
|
+
/**
|
|
11
|
+
* The total number of items.
|
|
12
|
+
*/
|
|
13
|
+
public total: number;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The last page number.
|
|
17
|
+
*/
|
|
18
|
+
public lastPage: number;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a new LengthAwarePaginator instance.
|
|
22
|
+
*/
|
|
23
|
+
constructor(
|
|
24
|
+
items: T[],
|
|
25
|
+
total: number,
|
|
26
|
+
perPage: number,
|
|
27
|
+
currentPage: number,
|
|
28
|
+
options: { path?: string; query?: Record<string, string> } = {}
|
|
29
|
+
) {
|
|
30
|
+
super(items, perPage, currentPage, options);
|
|
31
|
+
this.total = total;
|
|
32
|
+
this.lastPage = Math.max(1, Math.ceil(total / perPage));
|
|
33
|
+
this.hasMore = this.currentPage < this.lastPage;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the URL for the last page.
|
|
38
|
+
*/
|
|
39
|
+
public lastPageUrl(): string {
|
|
40
|
+
return this.url(this.lastPage);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert the instance to JSON.
|
|
45
|
+
*/
|
|
46
|
+
public override toJSON(): Record<string, unknown> {
|
|
47
|
+
const baseJson = super.toJSON();
|
|
48
|
+
return {
|
|
49
|
+
...baseJson,
|
|
50
|
+
total: this.total,
|
|
51
|
+
last_page: this.lastPage,
|
|
52
|
+
last_page_url: this.lastPageUrl(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|