@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.
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +180 -0
- package/dist/enum-generator.d.ts +28 -0
- package/dist/enum-generator.js +253 -0
- package/dist/generator.d.ts +19 -0
- package/dist/generator.js +330 -0
- package/dist/i18n-generator.d.ts +10 -0
- package/dist/i18n-generator.js +143 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/interface-generator.d.ts +31 -0
- package/dist/interface-generator.js +341 -0
- package/dist/php/base-model-generator.d.ts +7 -0
- package/dist/php/base-model-generator.js +70 -0
- package/dist/php/factory-generator.d.ts +7 -0
- package/dist/php/factory-generator.js +95 -0
- package/dist/php/faker-mapper.d.ts +12 -0
- package/dist/php/faker-mapper.js +206 -0
- package/dist/php/index.d.ts +18 -0
- package/dist/php/index.js +40 -0
- package/dist/php/locales-generator.d.ts +7 -0
- package/dist/php/locales-generator.js +135 -0
- package/dist/php/model-generator.d.ts +7 -0
- package/dist/php/model-generator.js +396 -0
- package/dist/php/naming-helper.d.ts +22 -0
- package/dist/php/naming-helper.js +61 -0
- package/dist/php/relation-builder.d.ts +12 -0
- package/dist/php/relation-builder.js +147 -0
- package/dist/php/request-generator.d.ts +7 -0
- package/dist/php/request-generator.js +221 -0
- package/dist/php/resource-generator.d.ts +7 -0
- package/dist/php/resource-generator.js +178 -0
- package/dist/php/schema-reader.d.ts +28 -0
- package/dist/php/schema-reader.js +79 -0
- package/dist/php/service-provider-generator.d.ts +7 -0
- package/dist/php/service-provider-generator.js +64 -0
- package/dist/php/trait-generator.d.ts +6 -0
- package/dist/php/trait-generator.js +104 -0
- package/dist/php/type-mapper.d.ts +25 -0
- package/dist/php/type-mapper.js +217 -0
- package/dist/php/types.d.ts +61 -0
- package/dist/php/types.js +68 -0
- package/dist/types.d.ts +196 -0
- package/dist/types.js +6 -0
- package/dist/zod-generator.d.ts +32 -0
- package/dist/zod-generator.js +428 -0
- 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[];
|