@omnifyjp/ts 0.3.0

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 (47) hide show
  1. package/dist/cli.d.ts +13 -0
  2. package/dist/cli.js +180 -0
  3. package/dist/enum-generator.d.ts +28 -0
  4. package/dist/enum-generator.js +253 -0
  5. package/dist/generator.d.ts +19 -0
  6. package/dist/generator.js +330 -0
  7. package/dist/i18n-generator.d.ts +10 -0
  8. package/dist/i18n-generator.js +143 -0
  9. package/dist/index.d.ts +10 -0
  10. package/dist/index.js +9 -0
  11. package/dist/interface-generator.d.ts +31 -0
  12. package/dist/interface-generator.js +341 -0
  13. package/dist/php/base-model-generator.d.ts +7 -0
  14. package/dist/php/base-model-generator.js +70 -0
  15. package/dist/php/factory-generator.d.ts +7 -0
  16. package/dist/php/factory-generator.js +95 -0
  17. package/dist/php/faker-mapper.d.ts +12 -0
  18. package/dist/php/faker-mapper.js +206 -0
  19. package/dist/php/index.d.ts +18 -0
  20. package/dist/php/index.js +40 -0
  21. package/dist/php/locales-generator.d.ts +7 -0
  22. package/dist/php/locales-generator.js +135 -0
  23. package/dist/php/model-generator.d.ts +7 -0
  24. package/dist/php/model-generator.js +396 -0
  25. package/dist/php/naming-helper.d.ts +22 -0
  26. package/dist/php/naming-helper.js +61 -0
  27. package/dist/php/relation-builder.d.ts +12 -0
  28. package/dist/php/relation-builder.js +147 -0
  29. package/dist/php/request-generator.d.ts +7 -0
  30. package/dist/php/request-generator.js +221 -0
  31. package/dist/php/resource-generator.d.ts +7 -0
  32. package/dist/php/resource-generator.js +178 -0
  33. package/dist/php/schema-reader.d.ts +28 -0
  34. package/dist/php/schema-reader.js +79 -0
  35. package/dist/php/service-provider-generator.d.ts +7 -0
  36. package/dist/php/service-provider-generator.js +64 -0
  37. package/dist/php/trait-generator.d.ts +6 -0
  38. package/dist/php/trait-generator.js +104 -0
  39. package/dist/php/type-mapper.d.ts +25 -0
  40. package/dist/php/type-mapper.js +217 -0
  41. package/dist/php/types.d.ts +61 -0
  42. package/dist/php/types.js +68 -0
  43. package/dist/types.d.ts +196 -0
  44. package/dist/types.js +6 -0
  45. package/dist/zod-generator.d.ts +32 -0
  46. package/dist/zod-generator.js +428 -0
  47. package/package.json +31 -0
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Port of ModelGenerator.php — generates Eloquent model base + user classes.
3
+ */
4
+ import { toPascalCase, toSnakeCase } from './naming-helper.js';
5
+ import { toCast, toPhpDocType, isHiddenByDefault } from './type-mapper.js';
6
+ import { buildRelation } from './relation-builder.js';
7
+ import { baseFile, userFile } from './types.js';
8
+ /** Generate base model and user model for all object schemas. */
9
+ export function generateModels(reader, config) {
10
+ const files = [];
11
+ for (const [name, schema] of Object.entries(reader.getObjectSchemas())) {
12
+ files.push(...generateForSchema(name, schema, reader, config));
13
+ }
14
+ return files;
15
+ }
16
+ function generateForSchema(name, schema, reader, config) {
17
+ return [
18
+ generateBaseModel(name, schema, reader, config),
19
+ generateUserModel(name, config),
20
+ ];
21
+ }
22
+ function generateBaseModel(name, schema, reader, config) {
23
+ const modelName = toPascalCase(name);
24
+ const baseNamespace = config.models.baseNamespace;
25
+ const modelNamespace = config.models.namespace;
26
+ const options = schema.options ?? {};
27
+ const properties = (schema.properties ?? {});
28
+ const expandedProperties = reader.getExpandedProperties(name);
29
+ const propertyOrder = reader.getPropertyOrder(name);
30
+ const tableName = reader.getTableName(name);
31
+ const isAuthenticatable = options['authenticatable'] ?? false;
32
+ const hasSoftDelete = options.softDelete ?? false;
33
+ const hasTimestamps = options.timestamps ?? false;
34
+ const imports = buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable, properties);
35
+ const docProperties = buildDocProperties(properties, expandedProperties, propertyOrder);
36
+ const baseClass = isAuthenticatable ? 'Authenticatable' : 'BaseModel';
37
+ const traits = buildTraits(hasSoftDelete, isAuthenticatable);
38
+ const fillable = buildFillable(properties, expandedProperties, propertyOrder);
39
+ const hidden = buildHidden(properties, expandedProperties, propertyOrder);
40
+ const appends = buildAppends(expandedProperties);
41
+ const casts = buildCasts(properties, expandedProperties, propertyOrder);
42
+ const relations = buildRelations(properties, propertyOrder, modelNamespace);
43
+ const accessors = buildAccessors(expandedProperties);
44
+ const primaryKey = options['primaryKey'] ?? 'id';
45
+ const idType = options.idType ?? 'BigInt';
46
+ let keyTypeSection = '';
47
+ if (idType === 'Uuid' || idType === 'String') {
48
+ keyTypeSection = `
49
+ /**
50
+ * The type of the primary key.
51
+ */
52
+ protected $keyType = 'string';
53
+
54
+ /**
55
+ * Indicates if the IDs are auto-incrementing.
56
+ */
57
+ public $incrementing = false;`;
58
+ }
59
+ const content = `<?php
60
+
61
+ namespace ${baseNamespace};
62
+
63
+ /**
64
+ * DO NOT EDIT - This file is auto-generated by Omnify.
65
+ * Any changes will be overwritten on next generation.
66
+ *
67
+ * @generated by omnify
68
+ */
69
+
70
+ ${imports}
71
+ /**
72
+ * ${modelName}BaseModel
73
+ *
74
+ ${docProperties} */
75
+ class ${modelName}BaseModel extends ${baseClass}
76
+ {
77
+ ${traits} /**
78
+ * The table associated with the model.
79
+ */
80
+ protected $table = '${tableName}';
81
+
82
+ /**
83
+ * The primary key for the model.
84
+ */
85
+ protected $primaryKey = '${primaryKey}';
86
+ ${keyTypeSection}
87
+
88
+ /**
89
+ * Indicates if the model should be timestamped.
90
+ */
91
+ public $timestamps = ${hasTimestamps ? 'true' : 'false'};
92
+
93
+ /**
94
+ * Localized display names for this model.
95
+ *
96
+ * @var array<string, string>
97
+ */
98
+ protected static array $localizedDisplayNames = ${modelName}Locales::DISPLAY_NAMES;
99
+
100
+ /**
101
+ * Localized display names for properties.
102
+ *
103
+ * @var array<string, array<string, string>>
104
+ */
105
+ protected static array $localizedPropertyDisplayNames = ${modelName}Locales::PROPERTY_DISPLAY_NAMES;
106
+
107
+ /**
108
+ * The attributes that are mass assignable.
109
+ */
110
+ protected $fillable = [
111
+ ${fillable} ];
112
+
113
+ /**
114
+ * The attributes that should be hidden for serialization.
115
+ */
116
+ protected $hidden = [
117
+ ${hidden} ];
118
+
119
+ /**
120
+ * The accessors to append to the model's array form.
121
+ */
122
+ protected $appends = [
123
+ ${appends} ];
124
+
125
+ /**
126
+ * Get the attributes that should be cast.
127
+ */
128
+ protected function casts(): array
129
+ {
130
+ return [
131
+ ${casts} ];
132
+ }
133
+ ${relations}${accessors}
134
+ }
135
+ `;
136
+ return baseFile(`${config.models.basePath}/${modelName}BaseModel.php`, content);
137
+ }
138
+ function generateUserModel(name, config) {
139
+ const modelName = toPascalCase(name);
140
+ const modelNamespace = config.models.namespace;
141
+ const baseNamespace = config.models.baseNamespace;
142
+ const content = `<?php
143
+
144
+ namespace ${modelNamespace};
145
+
146
+ use ${baseNamespace}\\${modelName}BaseModel;
147
+ use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
148
+
149
+ /**
150
+ * ${modelName} Model
151
+ *
152
+ * This file is generated once and can be customized.
153
+ * Add your custom methods and logic here.
154
+ */
155
+ class ${modelName} extends ${modelName}BaseModel
156
+ {
157
+ use HasFactory;
158
+
159
+ /**
160
+ * Create a new model instance.
161
+ */
162
+ public function __construct(array $attributes = [])
163
+ {
164
+ parent::__construct($attributes);
165
+ }
166
+
167
+ // Add your custom methods here
168
+ }
169
+ `;
170
+ return userFile(`${config.models.path}/${modelName}.php`, content);
171
+ }
172
+ function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable, properties) {
173
+ const lines = [];
174
+ if (isAuthenticatable) {
175
+ lines.push('use Illuminate\\Foundation\\Auth\\User as Authenticatable;');
176
+ lines.push('use Illuminate\\Notifications\\Notifiable;');
177
+ }
178
+ // Always include all relation types for simplicity (matching PHP)
179
+ const allRelationTypes = ['BelongsTo', 'HasMany', 'HasOne', 'BelongsToMany', 'MorphTo', 'MorphOne', 'MorphMany', 'MorphToMany'];
180
+ for (const type of allRelationTypes) {
181
+ lines.push(`use Illuminate\\Database\\Eloquent\\Relations\\${type};`);
182
+ }
183
+ lines.push('use Illuminate\\Database\\Eloquent\\Collection as EloquentCollection;');
184
+ lines.push(`use ${baseNamespace}\\Traits\\HasLocalizedDisplayName;`);
185
+ lines.push(`use ${baseNamespace}\\Locales\\${modelName}Locales;`);
186
+ if (hasSoftDelete) {
187
+ lines.push('use Illuminate\\Database\\Eloquent\\SoftDeletes;');
188
+ }
189
+ return lines.join('\n') + '\n';
190
+ }
191
+ function buildDocProperties(properties, expandedProperties, propertyOrder) {
192
+ const lines = [];
193
+ for (const propName of propertyOrder) {
194
+ const prop = properties[propName];
195
+ if (!prop)
196
+ continue;
197
+ const type = prop['type'] ?? 'String';
198
+ if (type === 'Association') {
199
+ const relation = prop['relation'] ?? '';
200
+ if (relation === 'ManyToOne') {
201
+ const snakeName = toSnakeCase(propName) + '_id';
202
+ const nullable = prop['nullable'] ?? false;
203
+ const phpType = nullable ? 'int|null' : 'int';
204
+ lines.push(` * @property ${phpType} $${snakeName}`);
205
+ }
206
+ continue;
207
+ }
208
+ if (expandedProperties[propName]) {
209
+ for (const col of expandedProperties[propName].columns ?? []) {
210
+ const colName = col.name ?? '';
211
+ const colType = col.type ?? 'String';
212
+ const colNullable = col.nullable ?? false;
213
+ const phpDocType = toPhpDocType(colType, colNullable);
214
+ lines.push(` * @property ${phpDocType} $${colName}`);
215
+ }
216
+ continue;
217
+ }
218
+ const nullable = prop['nullable'] ?? false;
219
+ const snakeName = toSnakeCase(propName);
220
+ const phpDocType = toPhpDocType(type, nullable);
221
+ lines.push(` * @property ${phpDocType} $${snakeName}`);
222
+ }
223
+ return lines.length === 0 ? '' : lines.join('\n') + '\n';
224
+ }
225
+ function buildTraits(hasSoftDelete, isAuthenticatable) {
226
+ const lines = [];
227
+ lines.push(' use HasLocalizedDisplayName;');
228
+ if (isAuthenticatable)
229
+ lines.push(' use Notifiable;');
230
+ if (hasSoftDelete)
231
+ lines.push(' use SoftDeletes;');
232
+ return lines.join('\n') + '\n';
233
+ }
234
+ function buildFillable(properties, expandedProperties, propertyOrder) {
235
+ const fields = [];
236
+ for (const propName of propertyOrder) {
237
+ const prop = properties[propName];
238
+ if (!prop)
239
+ continue;
240
+ const type = prop['type'] ?? 'String';
241
+ const fillable = prop['fillable'] ?? true;
242
+ if (!fillable)
243
+ continue;
244
+ if (type === 'Association') {
245
+ const relation = prop['relation'] ?? '';
246
+ if (relation === 'ManyToOne') {
247
+ fields.push(toSnakeCase(propName) + '_id');
248
+ }
249
+ continue;
250
+ }
251
+ if (expandedProperties[propName]) {
252
+ for (const col of expandedProperties[propName].columns ?? []) {
253
+ if (col.name)
254
+ fields.push(col.name);
255
+ }
256
+ continue;
257
+ }
258
+ fields.push(toSnakeCase(propName));
259
+ }
260
+ if (fields.length === 0)
261
+ return '';
262
+ return fields.map(f => ` '${f}',`).join('\n') + '\n';
263
+ }
264
+ function buildHidden(properties, expandedProperties, propertyOrder) {
265
+ const fields = [];
266
+ for (const propName of propertyOrder) {
267
+ const prop = properties[propName];
268
+ if (!prop)
269
+ continue;
270
+ const type = prop['type'] ?? 'String';
271
+ const hidden = prop['hidden'] ?? false;
272
+ if (isHiddenByDefault(type) || hidden) {
273
+ if (expandedProperties[propName]) {
274
+ for (const col of expandedProperties[propName].columns ?? []) {
275
+ if (col.name)
276
+ fields.push(col.name);
277
+ }
278
+ }
279
+ else {
280
+ fields.push(toSnakeCase(propName));
281
+ }
282
+ continue;
283
+ }
284
+ // Check individual field hidden overrides for compound types
285
+ if (expandedProperties[propName]) {
286
+ const fieldOverrides = prop['fields'] ?? {};
287
+ for (const col of expandedProperties[propName].columns ?? []) {
288
+ const suffix = col.suffix ?? '';
289
+ if (fieldOverrides[suffix]?.['hidden']) {
290
+ fields.push(col.name ?? '');
291
+ }
292
+ }
293
+ }
294
+ }
295
+ if (fields.length === 0)
296
+ return '';
297
+ return fields.map(f => ` '${f}',`).join('\n') + '\n';
298
+ }
299
+ function buildAppends(expandedProperties) {
300
+ const appends = [];
301
+ for (const [propName, expansion] of Object.entries(expandedProperties)) {
302
+ const sourceType = expansion.sourceType ?? '';
303
+ const snakeName = toSnakeCase(propName);
304
+ switch (sourceType) {
305
+ case 'JapaneseName':
306
+ appends.push(`${snakeName}_full_name`);
307
+ appends.push(`${snakeName}_full_name_kana`);
308
+ break;
309
+ case 'JapaneseAddress':
310
+ appends.push(`${snakeName}_full_address`);
311
+ break;
312
+ }
313
+ }
314
+ if (appends.length === 0)
315
+ return '';
316
+ return appends.map(a => ` '${a}',`).join('\n') + '\n';
317
+ }
318
+ function buildCasts(properties, expandedProperties, propertyOrder) {
319
+ const casts = [];
320
+ for (const propName of propertyOrder) {
321
+ const prop = properties[propName];
322
+ if (!prop)
323
+ continue;
324
+ const type = prop['type'] ?? 'String';
325
+ if (type === 'Association')
326
+ continue;
327
+ if (expandedProperties[propName]) {
328
+ for (const col of expandedProperties[propName].columns ?? []) {
329
+ const colType = col.type ?? 'String';
330
+ const cast = toCast(colType, col);
331
+ if (cast) {
332
+ casts.push(`'${col.name}' => '${cast}',`);
333
+ }
334
+ }
335
+ continue;
336
+ }
337
+ const cast = toCast(type, prop);
338
+ if (cast) {
339
+ const snakeName = toSnakeCase(propName);
340
+ casts.push(`'${snakeName}' => '${cast}',`);
341
+ }
342
+ }
343
+ if (casts.length === 0)
344
+ return '';
345
+ return casts.map(c => ` ${c}`).join('\n') + '\n';
346
+ }
347
+ function buildRelations(properties, propertyOrder, modelNamespace) {
348
+ const methods = [];
349
+ for (const propName of propertyOrder) {
350
+ const prop = properties[propName];
351
+ if (!prop || prop['type'] !== 'Association')
352
+ continue;
353
+ const result = buildRelation(propName, prop, modelNamespace);
354
+ if (result) {
355
+ methods.push('\n' + result.method);
356
+ }
357
+ }
358
+ return methods.join('\n');
359
+ }
360
+ function buildAccessors(expandedProperties) {
361
+ const methods = [];
362
+ for (const [propName, expansion] of Object.entries(expandedProperties)) {
363
+ const sourceType = expansion.sourceType ?? '';
364
+ const snakeName = toSnakeCase(propName);
365
+ switch (sourceType) {
366
+ case 'JapaneseName':
367
+ methods.push(fullNameAccessor(snakeName, 'FullName', [
368
+ `${snakeName}_lastname`, `${snakeName}_firstname`,
369
+ ], ' '));
370
+ methods.push(fullNameAccessor(snakeName, 'FullNameKana', [
371
+ `${snakeName}_kana_lastname`, `${snakeName}_kana_firstname`,
372
+ ], ' '));
373
+ break;
374
+ case 'JapaneseAddress':
375
+ methods.push(fullNameAccessor(snakeName, 'FullAddress', [
376
+ `${snakeName}_address1`, `${snakeName}_address2`, `${snakeName}_address3`,
377
+ ], ''));
378
+ break;
379
+ }
380
+ }
381
+ return methods.join('\n');
382
+ }
383
+ function fullNameAccessor(prefix, suffix, fields, separator) {
384
+ const methodName = 'get' + toPascalCase(prefix) + suffix + 'Attribute';
385
+ const fieldAccess = fields.map(f => `$this->${f}`).join(', ');
386
+ const separatorArg = separator === '' ? "''" : `'${separator}'`;
387
+ return `
388
+ /**
389
+ * Get the ${prefix} ${suffix} attribute.
390
+ */
391
+ public function ${methodName}(): ?string
392
+ {
393
+ $parts = array_filter([${fieldAccess}], fn($v) => $v !== null && $v !== '');
394
+ return count($parts) > 0 ? implode(${separatorArg}, $parts) : null;
395
+ }`;
396
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Port of NamingHelper.php — case conversion utilities.
3
+ */
4
+ /** Convert camelCase/PascalCase to snake_case. */
5
+ export declare function toSnakeCase(value: string): string;
6
+ /** Convert any string to PascalCase. */
7
+ export declare function toPascalCase(value: string): string;
8
+ /** Convert any string to camelCase. */
9
+ export declare function toCamelCase(value: string): string;
10
+ /**
11
+ * Pluralize a word (simple English rules).
12
+ * Handles common cases: -y → -ies, -s/-sh/-ch/-x/-z → -es, default → -s
13
+ */
14
+ export declare function pluralize(value: string): string;
15
+ /** Convert a schema name to a table name (snake_case + pluralized). */
16
+ export declare function toTableName(schemaName: string): string;
17
+ /**
18
+ * Resolve a displayName from a localized map or string.
19
+ */
20
+ export declare function resolveDisplayName(displayName: string | Record<string, string> | null | undefined, locale: string, fallback: string): string;
21
+ /** Build expanded column name from property name and suffix. */
22
+ export declare function expandedColumnName(propertyName: string, suffix: string): string;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Port of NamingHelper.php — case conversion utilities.
3
+ */
4
+ /** Convert camelCase/PascalCase to snake_case. */
5
+ export function toSnakeCase(value) {
6
+ return value
7
+ .replace(/([A-Z])/g, '_$1')
8
+ .replace(/^_/, '')
9
+ .toLowerCase();
10
+ }
11
+ /** Convert any string to PascalCase. */
12
+ export function toPascalCase(value) {
13
+ return value
14
+ .replace(/[-_](\w)/g, (_, c) => c.toUpperCase())
15
+ .replace(/^(\w)/, (_, c) => c.toUpperCase());
16
+ }
17
+ /** Convert any string to camelCase. */
18
+ export function toCamelCase(value) {
19
+ const pascal = toPascalCase(value);
20
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
21
+ }
22
+ /**
23
+ * Pluralize a word (simple English rules).
24
+ * Handles common cases: -y → -ies, -s/-sh/-ch/-x/-z → -es, default → -s
25
+ */
26
+ export function pluralize(value) {
27
+ if (value.endsWith('y') && !['a', 'e', 'i', 'o', 'u'].includes(value.charAt(value.length - 2))) {
28
+ return value.slice(0, -1) + 'ies';
29
+ }
30
+ if (value.endsWith('s') || value.endsWith('sh') || value.endsWith('ch') || value.endsWith('x') || value.endsWith('z')) {
31
+ return value + 'es';
32
+ }
33
+ if (value.endsWith('fe')) {
34
+ return value.slice(0, -2) + 'ves';
35
+ }
36
+ if (value.endsWith('f')) {
37
+ return value.slice(0, -1) + 'ves';
38
+ }
39
+ return value + 's';
40
+ }
41
+ /** Convert a schema name to a table name (snake_case + pluralized). */
42
+ export function toTableName(schemaName) {
43
+ return pluralize(toSnakeCase(schemaName));
44
+ }
45
+ /**
46
+ * Resolve a displayName from a localized map or string.
47
+ */
48
+ export function resolveDisplayName(displayName, locale, fallback) {
49
+ if (displayName == null)
50
+ return fallback;
51
+ if (typeof displayName === 'string')
52
+ return displayName;
53
+ if (typeof displayName === 'object') {
54
+ return displayName[locale] ?? displayName['en'] ?? Object.values(displayName)[0] ?? fallback;
55
+ }
56
+ return fallback;
57
+ }
58
+ /** Build expanded column name from property name and suffix. */
59
+ export function expandedColumnName(propertyName, suffix) {
60
+ return `${toSnakeCase(propertyName)}_${toSnakeCase(suffix)}`;
61
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Port of RelationBuilder.php — generates Eloquent relation methods.
3
+ */
4
+ interface RelationResult {
5
+ method: string;
6
+ imports: string[];
7
+ }
8
+ /** Build Eloquent relation method PHP code. */
9
+ export declare function buildRelation(propName: string, property: Record<string, unknown>, modelNamespace: string): RelationResult | null;
10
+ /** Get the return type hint for a relation. */
11
+ export declare function getReturnType(relation: string): string;
12
+ export {};
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Port of RelationBuilder.php — generates Eloquent relation methods.
3
+ */
4
+ import { toSnakeCase, toCamelCase } from './naming-helper.js';
5
+ /** Build Eloquent relation method PHP code. */
6
+ export function buildRelation(propName, property, modelNamespace) {
7
+ const relation = property['relation'] ?? '';
8
+ const target = property['target'] ?? '';
9
+ const imports = [];
10
+ let method = null;
11
+ switch (relation) {
12
+ case 'ManyToOne':
13
+ method = belongsTo(propName, target, modelNamespace);
14
+ break;
15
+ case 'OneToOne':
16
+ method = oneToOne(propName, property, target, modelNamespace);
17
+ break;
18
+ case 'OneToMany':
19
+ method = hasMany(propName, property, target, modelNamespace);
20
+ break;
21
+ case 'ManyToMany':
22
+ method = belongsToMany(propName, property, target, modelNamespace);
23
+ break;
24
+ case 'MorphTo':
25
+ method = morphTo(propName);
26
+ break;
27
+ case 'MorphOne':
28
+ method = morphOne(propName, property, target, modelNamespace);
29
+ break;
30
+ case 'MorphMany':
31
+ method = morphMany(propName, property, target, modelNamespace);
32
+ break;
33
+ }
34
+ if (method === null)
35
+ return null;
36
+ return { method, imports };
37
+ }
38
+ /** Get the return type hint for a relation. */
39
+ export function getReturnType(relation) {
40
+ switch (relation) {
41
+ case 'ManyToOne': return 'BelongsTo';
42
+ case 'OneToOne': return 'HasOne';
43
+ case 'OneToMany': return 'HasMany';
44
+ case 'ManyToMany': return 'BelongsToMany';
45
+ case 'MorphTo': return 'MorphTo';
46
+ case 'MorphOne': return 'MorphOne';
47
+ case 'MorphMany': return 'MorphMany';
48
+ default: return 'BelongsTo';
49
+ }
50
+ }
51
+ function belongsTo(propName, target, ns) {
52
+ const snakeName = toSnakeCase(propName);
53
+ const foreignKey = `${snakeName}_id`;
54
+ const methodName = toCamelCase(propName);
55
+ const fqcn = `\\${ns}\\${target}`;
56
+ return ` /**
57
+ * Get the ${propName} that owns this model.
58
+ */
59
+ public function ${methodName}(): BelongsTo
60
+ {
61
+ return $this->belongsTo(${fqcn}::class, '${foreignKey}');
62
+ }`;
63
+ }
64
+ function oneToOne(propName, property, target, ns) {
65
+ const mappedBy = property['mappedBy'];
66
+ const methodName = toCamelCase(propName);
67
+ const fqcn = `\\${ns}\\${target}`;
68
+ if (mappedBy) {
69
+ const foreignKey = `${toSnakeCase(mappedBy)}_id`;
70
+ return ` /**
71
+ * Get the ${propName} associated with this model.
72
+ */
73
+ public function ${methodName}(): HasOne
74
+ {
75
+ return $this->hasOne(${fqcn}::class, '${foreignKey}');
76
+ }`;
77
+ }
78
+ return belongsTo(propName, target, ns);
79
+ }
80
+ function hasMany(propName, property, target, ns) {
81
+ const inverse = property['mappedBy'] ?? property['inversedBy'] ?? null;
82
+ const foreignKey = inverse ? `${toSnakeCase(inverse)}_id` : `${toSnakeCase(propName)}_id`;
83
+ const methodName = toCamelCase(propName);
84
+ const fqcn = `\\${ns}\\${target}`;
85
+ return ` /**
86
+ * Get the ${propName} for this model.
87
+ */
88
+ public function ${methodName}(): HasMany
89
+ {
90
+ return $this->hasMany(${fqcn}::class, '${foreignKey}');
91
+ }`;
92
+ }
93
+ function belongsToMany(propName, property, target, ns) {
94
+ const joinTable = property['joinTable'] ?? '';
95
+ const methodName = toCamelCase(propName);
96
+ const pivotFields = property['pivotFields'] ?? {};
97
+ const fqcn = `\\${ns}\\${target}`;
98
+ let chain = `$this->belongsToMany(${fqcn}::class, '${joinTable}')`;
99
+ const fieldKeys = Object.keys(pivotFields);
100
+ if (fieldKeys.length > 0) {
101
+ const fields = fieldKeys.map(k => `'${toSnakeCase(k)}'`).join(', ');
102
+ chain += `\n ->withPivot(${fields})`;
103
+ }
104
+ chain += `\n ->withTimestamps()`;
105
+ return ` /**
106
+ * The ${propName} that belong to this model.
107
+ */
108
+ public function ${methodName}(): BelongsToMany
109
+ {
110
+ return ${chain};
111
+ }`;
112
+ }
113
+ function morphTo(propName) {
114
+ const methodName = toCamelCase(propName);
115
+ const morphName = toSnakeCase(propName);
116
+ return ` /**
117
+ * Get the parent ${propName} model.
118
+ */
119
+ public function ${methodName}(): MorphTo
120
+ {
121
+ return $this->morphTo('${morphName}');
122
+ }`;
123
+ }
124
+ function morphOne(propName, property, target, ns) {
125
+ const morphName = property['morphName'] ?? toSnakeCase(propName);
126
+ const methodName = toCamelCase(propName);
127
+ const fqcn = `\\${ns}\\${target}`;
128
+ return ` /**
129
+ * Get the ${propName} for this model.
130
+ */
131
+ public function ${methodName}(): MorphOne
132
+ {
133
+ return $this->morphOne(${fqcn}::class, '${morphName}');
134
+ }`;
135
+ }
136
+ function morphMany(propName, property, target, ns) {
137
+ const morphName = property['morphName'] ?? toSnakeCase(propName);
138
+ const methodName = toCamelCase(propName);
139
+ const fqcn = `\\${ns}\\${target}`;
140
+ return ` /**
141
+ * Get the ${propName} for this model.
142
+ */
143
+ public function ${methodName}(): MorphMany
144
+ {
145
+ return $this->morphMany(${fqcn}::class, '${morphName}');
146
+ }`;
147
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Port of RequestGenerator.php — generates Store/Update request classes.
3
+ */
4
+ import { SchemaReader } from './schema-reader.js';
5
+ import type { GeneratedFile, PhpConfig } from './types.js';
6
+ /** Generate Store/Update request classes for all visible object schemas. */
7
+ export declare function generateRequests(reader: SchemaReader, config: PhpConfig): GeneratedFile[];