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.
Files changed (52) hide show
  1. package/package.json +1 -1
  2. package/template/config/database.ts +13 -1
  3. package/template/database/migrations/2024_01_01_000000_create_test_users_cli_table.ts +16 -0
  4. package/template/database/migrations/20260108165704_TestCliMigration.ts +16 -0
  5. package/template/database/migrations/2026_01_08_16_57_04_CreateTestMigrationsTable.ts +21 -0
  6. package/template/framework/cli/artisan.ts +12 -0
  7. package/template/framework/cli/commands/MakeModelCommand.ts +33 -3
  8. package/template/framework/database/DatabaseManager.ts +133 -0
  9. package/template/framework/database/connection/Connection.ts +71 -0
  10. package/template/framework/database/connection/ConnectionFactory.ts +30 -0
  11. package/template/framework/database/connection/PostgresConnection.ts +159 -0
  12. package/template/framework/database/console/MakeMigrationCommand.ts +58 -0
  13. package/template/framework/database/console/MigrateCommand.ts +32 -0
  14. package/template/framework/database/console/MigrateResetCommand.ts +31 -0
  15. package/template/framework/database/console/MigrateRollbackCommand.ts +31 -0
  16. package/template/framework/database/console/MigrateStatusCommand.ts +38 -0
  17. package/template/framework/database/migrations/DatabaseMigrationRepository.ts +122 -0
  18. package/template/framework/database/migrations/Migration.ts +5 -0
  19. package/template/framework/database/migrations/MigrationRepository.ts +46 -0
  20. package/template/framework/database/migrations/Migrator.ts +249 -0
  21. package/template/framework/database/migrations/index.ts +4 -0
  22. package/template/framework/database/orm/BelongsTo.ts +246 -0
  23. package/template/framework/database/orm/BelongsToMany.ts +570 -0
  24. package/template/framework/database/orm/Builder.ts +160 -0
  25. package/template/framework/database/orm/EagerLoadingBuilder.ts +324 -0
  26. package/template/framework/database/orm/HasMany.ts +303 -0
  27. package/template/framework/database/orm/HasManyThrough.ts +282 -0
  28. package/template/framework/database/orm/HasOne.ts +201 -0
  29. package/template/framework/database/orm/HasOneThrough.ts +281 -0
  30. package/template/framework/database/orm/Model.ts +1802 -0
  31. package/template/framework/database/orm/Relation.ts +342 -0
  32. package/template/framework/database/orm/Scope.ts +14 -0
  33. package/template/framework/database/orm/SoftDeletes.ts +160 -0
  34. package/template/framework/database/orm/index.ts +54 -0
  35. package/template/framework/database/orm/scopes/SoftDeletingScope.ts +58 -0
  36. package/template/framework/database/pagination/LengthAwarePaginator.ts +55 -0
  37. package/template/framework/database/pagination/Paginator.ts +110 -0
  38. package/template/framework/database/pagination/index.ts +2 -0
  39. package/template/framework/database/query/Builder.ts +918 -0
  40. package/template/framework/database/query/DB.ts +139 -0
  41. package/template/framework/database/query/grammars/Grammar.ts +430 -0
  42. package/template/framework/database/query/grammars/PostgresGrammar.ts +224 -0
  43. package/template/framework/database/query/grammars/index.ts +6 -0
  44. package/template/framework/database/query/index.ts +8 -0
  45. package/template/framework/database/query/types.ts +196 -0
  46. package/template/framework/database/schema/Blueprint.ts +478 -0
  47. package/template/framework/database/schema/Schema.ts +149 -0
  48. package/template/framework/database/schema/SchemaBuilder.ts +152 -0
  49. package/template/framework/database/schema/grammars/PostgresSchemaGrammar.ts +293 -0
  50. package/template/framework/database/schema/grammars/index.ts +5 -0
  51. package/template/framework/database/schema/index.ts +9 -0
  52. 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
+ }