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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-phoenixjs",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Create a new PhoenixJS project - A TypeScript framework inspired by Laravel, powered by Bun",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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, any>): Promise<void> {
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
- const template = `export class ${className} {
29
- // Model definition
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 _fillable: string[] = [];
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[] // Default to guarding everything
86
+ * @var string[]
93
87
  */
94
- protected _guarded: string[] = ['*'];
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
- public get guarded(): string[] {
97
- return this._guarded;
113
+ // Default to the instance property
114
+ return this.fillable;
98
115
  }
99
116
 
100
- public set guarded(value: string[]) {
101
- this._guarded = value;
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.fill(attributes);
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
- const fillable = this.isFillable(key);
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 (this.fillable.includes(key)) {
344
+ if (fillableFields.includes(key)) {
309
345
  return true;
310
346
  }
311
347
 
312
- // If the model is totally guarded, return false
313
- if (this.guarded.length === 1 && this.guarded[0] === '*') {
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 model is not guarded and the key is not in the guarded array, it can be filled
318
- if (!this.guarded.includes(key) && !this.guarded.includes('*')) {
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.fill(result);
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
- }