create-phoenixjs 0.1.5 → 0.1.7
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/framework/cli/commands/MakeModelCommand.ts +33 -3
- package/template/framework/database/orm/Model.ts +71 -34
- package/template/database/migrations/20260108164611_TestCliMigration.ts +0 -16
- package/template/database/migrations/2026_01_08_16_46_11_CreateTestMigrationsTable.ts +0 -21
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ export class MakeModelCommand extends Command {
|
|
|
6
6
|
signature = 'make:model {name}';
|
|
7
7
|
description = 'Create a new model class';
|
|
8
8
|
|
|
9
|
-
async handle(args: Record<string,
|
|
9
|
+
async handle(args: Record<string, string>): Promise<void> {
|
|
10
10
|
const name = args.name;
|
|
11
11
|
if (!name) {
|
|
12
12
|
console.error('Error: Model name is required.');
|
|
@@ -25,12 +25,42 @@ export class MakeModelCommand extends Command {
|
|
|
25
25
|
|
|
26
26
|
fs.mkdirSync(directory, { recursive: true });
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Generate snake_case table name from class name (e.g., UserProfile -> user_profiles)
|
|
29
|
+
const tableName = this.generateTableName(className);
|
|
30
|
+
|
|
31
|
+
const template = `import { Model } from '@framework/database/orm/Model';
|
|
32
|
+
|
|
33
|
+
export class ${className} extends Model {
|
|
34
|
+
protected table = '${tableName}';
|
|
35
|
+
protected fillable: string[] = [];
|
|
30
36
|
}
|
|
31
37
|
`;
|
|
32
38
|
|
|
33
39
|
fs.writeFileSync(fullPath, template);
|
|
34
40
|
console.log(`Model created successfully: app/models/${relativePath}`);
|
|
35
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate a table name from the class name.
|
|
45
|
+
* Converts PascalCase to snake_case and pluralizes.
|
|
46
|
+
* e.g., User -> users, UserProfile -> user_profiles
|
|
47
|
+
*/
|
|
48
|
+
private generateTableName(className: string): string {
|
|
49
|
+
// Convert PascalCase to snake_case
|
|
50
|
+
let result = '';
|
|
51
|
+
for (let i = 0; i < className.length; i++) {
|
|
52
|
+
const char = className[i];
|
|
53
|
+
if (char >= 'A' && char <= 'Z') {
|
|
54
|
+
if (i > 0) {
|
|
55
|
+
result += '_';
|
|
56
|
+
}
|
|
57
|
+
result += char.toLowerCase();
|
|
58
|
+
} else {
|
|
59
|
+
result += char;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Simple pluralization (add 's')
|
|
64
|
+
return result + 's';
|
|
65
|
+
}
|
|
36
66
|
}
|
|
@@ -73,32 +73,72 @@ export abstract class Model {
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* The attributes that are mass assignable.
|
|
76
|
+
* Child classes should override this with their fillable fields.
|
|
76
77
|
*
|
|
77
78
|
* @var string[]
|
|
78
79
|
*/
|
|
79
|
-
protected
|
|
80
|
-
|
|
81
|
-
public get fillable(): string[] {
|
|
82
|
-
return this._fillable;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
public set fillable(value: string[]) {
|
|
86
|
-
this._fillable = value;
|
|
87
|
-
}
|
|
80
|
+
protected fillable: string[] = [];
|
|
88
81
|
|
|
89
82
|
/**
|
|
90
83
|
* The attributes that aren't mass assignable.
|
|
84
|
+
* Default to guarding everything for security.
|
|
91
85
|
*
|
|
92
|
-
* @var string[]
|
|
86
|
+
* @var string[]
|
|
93
87
|
*/
|
|
94
|
-
protected
|
|
88
|
+
protected guarded: string[] = ['*'];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the fillable fields for this model.
|
|
92
|
+
* This method properly handles inheritance by checking instance and prototype.
|
|
93
|
+
* Due to JS class initialization order, we need to check the prototype chain
|
|
94
|
+
* when called from the parent constructor before child properties are initialized.
|
|
95
|
+
*/
|
|
96
|
+
public getFillableFields(): string[] {
|
|
97
|
+
// First check if 'this' has its own fillable property (already initialized)
|
|
98
|
+
if (Object.prototype.hasOwnProperty.call(this, 'fillable') &&
|
|
99
|
+
Array.isArray(this.fillable) && this.fillable.length > 0) {
|
|
100
|
+
return this.fillable;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Walk up the prototype chain looking for fillable definition
|
|
104
|
+
let proto = Object.getPrototypeOf(this);
|
|
105
|
+
while (proto && proto.constructor.name !== 'Model' && proto.constructor.name !== 'Object') {
|
|
106
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, 'fillable');
|
|
107
|
+
if (descriptor && descriptor.value && Array.isArray(descriptor.value)) {
|
|
108
|
+
return descriptor.value;
|
|
109
|
+
}
|
|
110
|
+
proto = Object.getPrototypeOf(proto);
|
|
111
|
+
}
|
|
95
112
|
|
|
96
|
-
|
|
97
|
-
return this.
|
|
113
|
+
// Default to the instance property
|
|
114
|
+
return this.fillable;
|
|
98
115
|
}
|
|
99
116
|
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Get the guarded fields for this model.
|
|
119
|
+
* This method properly handles inheritance by checking instance and prototype.
|
|
120
|
+
* Due to JS class initialization order, we need to check the prototype chain
|
|
121
|
+
* when called from the parent constructor before child properties are initialized.
|
|
122
|
+
*/
|
|
123
|
+
public getGuardedFields(): string[] {
|
|
124
|
+
// First check if 'this' has its own guarded property (already initialized)
|
|
125
|
+
// We need to detect if the child class set a different value
|
|
126
|
+
if (Object.prototype.hasOwnProperty.call(this, 'guarded')) {
|
|
127
|
+
return this.guarded;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Walk up the prototype chain looking for guarded definition
|
|
131
|
+
let proto = Object.getPrototypeOf(this);
|
|
132
|
+
while (proto && proto.constructor.name !== 'Model' && proto.constructor.name !== 'Object') {
|
|
133
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, 'guarded');
|
|
134
|
+
if (descriptor && descriptor.value && Array.isArray(descriptor.value)) {
|
|
135
|
+
return descriptor.value;
|
|
136
|
+
}
|
|
137
|
+
proto = Object.getPrototypeOf(proto);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Default to the instance property (will be ['*'] from Model base)
|
|
141
|
+
return this.guarded;
|
|
102
142
|
}
|
|
103
143
|
|
|
104
144
|
/**
|
|
@@ -219,9 +259,13 @@ export abstract class Model {
|
|
|
219
259
|
|
|
220
260
|
/**
|
|
221
261
|
* Create a new model instance.
|
|
262
|
+
*
|
|
263
|
+
* Note: We use forceFill in the constructor because constructor arguments
|
|
264
|
+
* are trusted (not external user input). Mass assignment protection is
|
|
265
|
+
* meant for external input like HTTP request bodies, not constructor init.
|
|
222
266
|
*/
|
|
223
267
|
constructor(attributes: Record<string, unknown> = {}) {
|
|
224
|
-
this.
|
|
268
|
+
this.forceFill(attributes);
|
|
225
269
|
|
|
226
270
|
// Return a proxy to handle attribute access directly on the model instance
|
|
227
271
|
return new Proxy(this, {
|
|
@@ -248,7 +292,6 @@ export abstract class Model {
|
|
|
248
292
|
// Otherwise set attribute
|
|
249
293
|
if (typeof prop === 'string') {
|
|
250
294
|
if (prop === 'table' || prop === 'primaryKey' || prop === 'forceDeleting') {
|
|
251
|
-
console.log(`Preventing ${prop} from being set as attribute. defining on target.`);
|
|
252
295
|
Object.defineProperty(target, prop, { value, writable: true, configurable: true, enumerable: true });
|
|
253
296
|
return true;
|
|
254
297
|
}
|
|
@@ -271,19 +314,9 @@ export abstract class Model {
|
|
|
271
314
|
* @throws Error if mass assignment protection is violated (optional, traditionally just ignored)
|
|
272
315
|
*/
|
|
273
316
|
public fill(attributes: Record<string, unknown>): this {
|
|
274
|
-
const totallyGuarded = this.guarded.length === 1 && this.guarded[0] === '*';
|
|
275
|
-
console.log(`Filling model ${this.constructor.name}:`, attributes);
|
|
276
|
-
console.log(`Guarded:`, this.guarded);
|
|
277
|
-
console.log(`Totally Guarded:`, totallyGuarded);
|
|
278
|
-
|
|
279
317
|
for (const key in attributes) {
|
|
280
|
-
|
|
281
|
-
console.log(`Key '${key}' fillable:`, fillable);
|
|
282
|
-
if (fillable) {
|
|
318
|
+
if (this.isFillable(key)) {
|
|
283
319
|
this.setAttribute(key, attributes[key]);
|
|
284
|
-
} else if (totallyGuarded) {
|
|
285
|
-
// Optional: We could throw an error here or log a warning
|
|
286
|
-
// console.warn(`Mass assignment to guarded attribute '${key}' on model '${this.constructor.name}'`);
|
|
287
320
|
}
|
|
288
321
|
}
|
|
289
322
|
return this;
|
|
@@ -304,18 +337,21 @@ export abstract class Model {
|
|
|
304
337
|
* Determine if the given attribute may be mass assigned.
|
|
305
338
|
*/
|
|
306
339
|
public isFillable(key: string): boolean {
|
|
340
|
+
const fillableFields = this.getFillableFields();
|
|
341
|
+
const guardedFields = this.getGuardedFields();
|
|
342
|
+
|
|
307
343
|
// If the key is in the fillable array, it can be filled
|
|
308
|
-
if (
|
|
344
|
+
if (fillableFields.includes(key)) {
|
|
309
345
|
return true;
|
|
310
346
|
}
|
|
311
347
|
|
|
312
|
-
// If the model is totally guarded, return false
|
|
313
|
-
if (
|
|
348
|
+
// If the model is totally guarded (guarded = ['*']), return false
|
|
349
|
+
if (guardedFields.length === 1 && guardedFields[0] === '*') {
|
|
314
350
|
return false;
|
|
315
351
|
}
|
|
316
352
|
|
|
317
|
-
// If the
|
|
318
|
-
if (!
|
|
353
|
+
// If the key is not in the guarded array, it can be filled
|
|
354
|
+
if (!guardedFields.includes(key) && !guardedFields.includes('*')) {
|
|
319
355
|
return true;
|
|
320
356
|
}
|
|
321
357
|
|
|
@@ -989,7 +1025,8 @@ export abstract class Model {
|
|
|
989
1025
|
|
|
990
1026
|
if (result) {
|
|
991
1027
|
// Update model with returned values (including generated ID)
|
|
992
|
-
this
|
|
1028
|
+
// Use forceFill since this is trusted data from the database
|
|
1029
|
+
this.forceFill(result);
|
|
993
1030
|
this.exists = true;
|
|
994
1031
|
this.syncOriginal();
|
|
995
1032
|
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { Schema, Blueprint } from '@framework/database/schema';
|
|
3
|
-
import { Migration } from '@framework/database/migrations';
|
|
4
|
-
|
|
5
|
-
export class TestCliMigration implements Migration {
|
|
6
|
-
async up() {
|
|
7
|
-
await Schema.create('test_cli_table', (table) => {
|
|
8
|
-
table.id();
|
|
9
|
-
table.string('name');
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async down() {
|
|
14
|
-
await Schema.dropIfExists('test_cli_table');
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Schema, Blueprint } from '@framework/database/schema';
|
|
2
|
-
import { Migration } from '@framework/database/migrations';
|
|
3
|
-
|
|
4
|
-
export class CreateTestMigrationsTable implements Migration {
|
|
5
|
-
/**
|
|
6
|
-
* Run the migrations.
|
|
7
|
-
*/
|
|
8
|
-
async up(): Promise<void> {
|
|
9
|
-
// await Schema.create('table_name', (table: Blueprint) => {
|
|
10
|
-
// table.id();
|
|
11
|
-
// table.timestamps();
|
|
12
|
-
// });
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Reverse the migrations.
|
|
17
|
-
*/
|
|
18
|
-
async down(): Promise<void> {
|
|
19
|
-
// await Schema.dropIfExists('table_name');
|
|
20
|
-
}
|
|
21
|
-
}
|