@omnifyjp/omnify 3.7.2 → 3.9.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnifyjp/omnify",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "Schema-driven code generation for Laravel, TypeScript, and SQL",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"zod": "^3.24.0"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@omnifyjp/omnify-darwin-arm64": "3.
|
|
40
|
-
"@omnifyjp/omnify-darwin-x64": "3.
|
|
41
|
-
"@omnifyjp/omnify-linux-x64": "3.
|
|
42
|
-
"@omnifyjp/omnify-linux-arm64": "3.
|
|
43
|
-
"@omnifyjp/omnify-win32-x64": "3.
|
|
39
|
+
"@omnifyjp/omnify-darwin-arm64": "3.9.0",
|
|
40
|
+
"@omnifyjp/omnify-darwin-x64": "3.9.0",
|
|
41
|
+
"@omnifyjp/omnify-linux-x64": "3.9.0",
|
|
42
|
+
"@omnifyjp/omnify-linux-arm64": "3.9.0",
|
|
43
|
+
"@omnifyjp/omnify-win32-x64": "3.9.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Port of ModelGenerator.php — generates Eloquent model base + user classes.
|
|
3
3
|
*/
|
|
4
|
-
import { toPascalCase, toSnakeCase, toCamelCase } from './naming-helper.js';
|
|
4
|
+
import { toPascalCase, toSnakeCase, toCamelCase, pluralize } from './naming-helper.js';
|
|
5
5
|
import { toCast, toPhpDocType, isHiddenByDefault } from './type-mapper.js';
|
|
6
6
|
import { buildRelation } from './relation-builder.js';
|
|
7
7
|
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, } from './types.js';
|
|
@@ -490,6 +490,12 @@ function buildCasts(properties, expandedProperties, propertyOrder, reader, confi
|
|
|
490
490
|
function buildRelations(schemaName, properties, propertyOrder, modelNamespace, reader) {
|
|
491
491
|
const methods = [];
|
|
492
492
|
const sourceTableName = reader.getTableName(schemaName);
|
|
493
|
+
// Track method names already declared on this model so reverse-relation
|
|
494
|
+
// inference (#39) can skip any name the user has explicitly defined.
|
|
495
|
+
const declaredMethods = new Set();
|
|
496
|
+
// Track explicit OneToMany/HasOne declarations by `<targetSchema>.<mappedBy>`
|
|
497
|
+
// so the auto-inverse pass can skip child relations the user already covered.
|
|
498
|
+
const explicitInverses = new Set();
|
|
493
499
|
for (const propName of propertyOrder) {
|
|
494
500
|
const prop = properties[propName];
|
|
495
501
|
if (!prop || prop['type'] !== 'Association')
|
|
@@ -501,6 +507,78 @@ function buildRelations(schemaName, properties, propertyOrder, modelNamespace, r
|
|
|
501
507
|
const result = buildRelation(propName, prop, ns, { sourceTableName, targetTableName });
|
|
502
508
|
if (result) {
|
|
503
509
|
methods.push('\n' + result.method);
|
|
510
|
+
declaredMethods.add(toCamelCase(propName));
|
|
511
|
+
const relation = prop['relation'] ?? '';
|
|
512
|
+
const mappedBy = prop['mappedBy'] ?? '';
|
|
513
|
+
if ((relation === 'OneToMany' || relation === 'OneToOne') && target && mappedBy) {
|
|
514
|
+
explicitInverses.add(`${target}.${mappedBy}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Auto-generate reverse HasMany/HasOne for any child schema that declares a
|
|
519
|
+
// ManyToOne/OneToOne pointing at this schema, unless the user has already
|
|
520
|
+
// declared a relation method with the same name. Issue #39.
|
|
521
|
+
const inverseRelations = buildInverseRelations(schemaName, modelNamespace, reader, declaredMethods, explicitInverses);
|
|
522
|
+
if (inverseRelations) {
|
|
523
|
+
methods.push(inverseRelations);
|
|
524
|
+
}
|
|
525
|
+
return methods.join('\n');
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Discover child schemas that point at `parentName` via ManyToOne/OneToOne and
|
|
529
|
+
* generate the corresponding HasMany/HasOne accessor on the parent. Skips any
|
|
530
|
+
* inverse relation whose derived method name collides with a user-declared
|
|
531
|
+
* relation. Issue #39.
|
|
532
|
+
*/
|
|
533
|
+
function buildInverseRelations(parentName, modelNamespace, reader, declaredMethods, explicitInverses) {
|
|
534
|
+
const methods = [];
|
|
535
|
+
const allSchemas = reader.getSchemas();
|
|
536
|
+
// Stable iteration order — sort child names so output is deterministic
|
|
537
|
+
// regardless of how schemas were loaded into the map.
|
|
538
|
+
const childNames = Object.keys(allSchemas).sort();
|
|
539
|
+
for (const childName of childNames) {
|
|
540
|
+
if (childName === parentName)
|
|
541
|
+
continue;
|
|
542
|
+
const childSchema = allSchemas[childName];
|
|
543
|
+
if (!childSchema)
|
|
544
|
+
continue;
|
|
545
|
+
const childProps = (childSchema.properties ?? {});
|
|
546
|
+
for (const [childPropName, childProp] of Object.entries(childProps)) {
|
|
547
|
+
if (childProp['type'] !== 'Association')
|
|
548
|
+
continue;
|
|
549
|
+
const relation = childProp['relation'] ?? '';
|
|
550
|
+
if (relation !== 'ManyToOne' && relation !== 'OneToOne')
|
|
551
|
+
continue;
|
|
552
|
+
if (childProp['target'] !== parentName)
|
|
553
|
+
continue;
|
|
554
|
+
// Skip when an explicit OneToMany/HasOne on the parent already covers
|
|
555
|
+
// this child relation (matched by `<childSchema>.<childPropName>`).
|
|
556
|
+
if (explicitInverses.has(`${childName}.${childPropName}`))
|
|
557
|
+
continue;
|
|
558
|
+
const isOneToOne = relation === 'OneToOne';
|
|
559
|
+
const methodName = isOneToOne
|
|
560
|
+
? toCamelCase(childName)
|
|
561
|
+
: toCamelCase(pluralize(childName));
|
|
562
|
+
if (declaredMethods.has(methodName))
|
|
563
|
+
continue;
|
|
564
|
+
declaredMethods.add(methodName);
|
|
565
|
+
// OneToOne with mappedBy is the reverse side already — skip; it would
|
|
566
|
+
// generate a child-side accessor, not a parent-side one.
|
|
567
|
+
// Here we're emitting the *parent* accessor, so the child's ManyToOne
|
|
568
|
+
// (or owning OneToOne) is what we react to.
|
|
569
|
+
const fkColumn = `${toSnakeCase(childPropName)}_id`;
|
|
570
|
+
const childNs = reader.resolveModelNamespace(childName, modelNamespace);
|
|
571
|
+
const fqcn = `\\${childNs}\\${childName}`;
|
|
572
|
+
const returnType = isOneToOne ? 'HasOne' : 'HasMany';
|
|
573
|
+
const eloquentMethod = isOneToOne ? 'hasOne' : 'hasMany';
|
|
574
|
+
methods.push(`
|
|
575
|
+
/**
|
|
576
|
+
* Auto-generated inverse of ${childName}.${childPropName} (${relation}).
|
|
577
|
+
*/
|
|
578
|
+
public function ${methodName}(): ${returnType}
|
|
579
|
+
{
|
|
580
|
+
return $this->${eloquentMethod}(${fqcn}::class, '${fkColumn}');
|
|
581
|
+
}`);
|
|
504
582
|
}
|
|
505
583
|
}
|
|
506
584
|
return methods.join('\n');
|
|
@@ -599,12 +677,12 @@ function buildNestedSetParentIdMethod(parentColumn) {
|
|
|
599
677
|
function buildAuditSection(options, reader, modelNamespace) {
|
|
600
678
|
const opts = options;
|
|
601
679
|
const audit = opts?.['audit'];
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
const
|
|
607
|
-
if (!hasCreatedBy && !hasUpdatedBy && !hasDeletedBy)
|
|
680
|
+
const defaultOrder = opts?.['defaultOrder'];
|
|
681
|
+
const hasCreatedBy = audit?.['createdBy'] ?? false;
|
|
682
|
+
const hasUpdatedBy = audit?.['updatedBy'] ?? false;
|
|
683
|
+
const hasDeletedBy = audit?.['deletedBy'] ?? false;
|
|
684
|
+
const hasDefaultOrder = Array.isArray(defaultOrder) && defaultOrder.length > 0;
|
|
685
|
+
if (!hasCreatedBy && !hasUpdatedBy && !hasDeletedBy && !hasDefaultOrder)
|
|
608
686
|
return '';
|
|
609
687
|
// Get audit model name from schemas.json auditConfig
|
|
610
688
|
const auditModel = reader.data?.auditConfig?.model ?? 'User';
|
|
@@ -612,6 +690,17 @@ function buildAuditSection(options, reader, modelNamespace) {
|
|
|
612
690
|
const parts = [];
|
|
613
691
|
// Model events (booted)
|
|
614
692
|
const events = [];
|
|
693
|
+
// defaultOrder global scope (issue #40 phase 2). Bypass with
|
|
694
|
+
// ->withoutGlobalScope('defaultOrder') in queries that need a different sort.
|
|
695
|
+
if (hasDefaultOrder) {
|
|
696
|
+
const chain = defaultOrder
|
|
697
|
+
.map(o => ` ->orderBy('${o.column}', '${(o.direction ?? 'asc').toLowerCase()}')`)
|
|
698
|
+
.join('\n');
|
|
699
|
+
events.push(` static::addGlobalScope('defaultOrder', function ($query) {
|
|
700
|
+
$query
|
|
701
|
+
${chain};
|
|
702
|
+
});`);
|
|
703
|
+
}
|
|
615
704
|
if (hasCreatedBy) {
|
|
616
705
|
events.push(` static::creating(function ($model) {
|
|
617
706
|
if (auth()->check() && !$model->created_by_id) {
|
|
@@ -15,4 +15,9 @@ export declare function buildRelation(propName: string, property: Record<string,
|
|
|
15
15
|
export declare function generatePivotTableName(sourceTable: string, targetTable: string): string;
|
|
16
16
|
/** Get the return type hint for a relation. */
|
|
17
17
|
export declare function getReturnType(relation: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Build a chained `->orderBy('col', 'dir')` PHP fragment from an OrderByList.
|
|
20
|
+
* Returns an empty string if no ordering is configured. Issue #40.
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatOrderByChain(orderBy: unknown): string;
|
|
18
23
|
export {};
|
|
@@ -95,14 +95,33 @@ function hasMany(propName, property, target, ns) {
|
|
|
95
95
|
const foreignKey = inverse ? `${toSnakeCase(inverse)}_id` : `${toSnakeCase(propName)}_id`;
|
|
96
96
|
const methodName = toCamelCase(propName);
|
|
97
97
|
const fqcn = `\\${ns}\\${target}`;
|
|
98
|
+
const orderChain = formatOrderByChain(property['orderBy']);
|
|
98
99
|
return ` /**
|
|
99
100
|
* Get the ${propName} for this model.
|
|
100
101
|
*/
|
|
101
102
|
public function ${methodName}(): HasMany
|
|
102
103
|
{
|
|
103
|
-
return $this->hasMany(${fqcn}::class, '${foreignKey}');
|
|
104
|
+
return $this->hasMany(${fqcn}::class, '${foreignKey}')${orderChain};
|
|
104
105
|
}`;
|
|
105
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Build a chained `->orderBy('col', 'dir')` PHP fragment from an OrderByList.
|
|
109
|
+
* Returns an empty string if no ordering is configured. Issue #40.
|
|
110
|
+
*/
|
|
111
|
+
export function formatOrderByChain(orderBy) {
|
|
112
|
+
if (!Array.isArray(orderBy) || orderBy.length === 0)
|
|
113
|
+
return '';
|
|
114
|
+
return orderBy
|
|
115
|
+
.map(item => {
|
|
116
|
+
const col = item.column ?? '';
|
|
117
|
+
if (!col)
|
|
118
|
+
return '';
|
|
119
|
+
const dir = (item.direction ?? 'asc').toLowerCase();
|
|
120
|
+
return `\n ->orderBy('${col}', '${dir}')`;
|
|
121
|
+
})
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join('');
|
|
124
|
+
}
|
|
106
125
|
function belongsToMany(propName, property, target, ns, context) {
|
|
107
126
|
let joinTable = property['joinTable'] ?? '';
|
|
108
127
|
if (!joinTable && context?.sourceTableName && context?.targetTableName) {
|
|
@@ -118,6 +137,7 @@ function belongsToMany(propName, property, target, ns, context) {
|
|
|
118
137
|
chain += `\n ->withPivot(${fields})`;
|
|
119
138
|
}
|
|
120
139
|
chain += `\n ->withTimestamps()`;
|
|
140
|
+
chain += formatOrderByChain(property['orderBy']);
|
|
121
141
|
return ` /**
|
|
122
142
|
* The ${propName} that belong to this model.
|
|
123
143
|
*/
|
|
@@ -153,12 +173,13 @@ function morphMany(propName, property, target, ns) {
|
|
|
153
173
|
const morphName = property['morphName'] ?? toSnakeCase(propName);
|
|
154
174
|
const methodName = toCamelCase(propName);
|
|
155
175
|
const fqcn = `\\${ns}\\${target}`;
|
|
176
|
+
const orderChain = formatOrderByChain(property['orderBy']);
|
|
156
177
|
return ` /**
|
|
157
178
|
* Get the ${propName} for this model.
|
|
158
179
|
*/
|
|
159
180
|
public function ${methodName}(): MorphMany
|
|
160
181
|
{
|
|
161
|
-
return $this->morphMany(${fqcn}::class, '${morphName}');
|
|
182
|
+
return $this->morphMany(${fqcn}::class, '${morphName}')${orderChain};
|
|
162
183
|
}`;
|
|
163
184
|
}
|
|
164
185
|
function morphToMany(propName, property, target, ns, context) {
|
package/ts-dist/types.d.ts
CHANGED
|
@@ -123,6 +123,8 @@ export interface SchemaOptions {
|
|
|
123
123
|
readonly updatedBy?: boolean;
|
|
124
124
|
readonly deletedBy?: boolean;
|
|
125
125
|
};
|
|
126
|
+
/** Schema-level default ordering — generates a global Eloquent scope. Issue #40. */
|
|
127
|
+
readonly defaultOrder?: readonly OrderByItem[];
|
|
126
128
|
}
|
|
127
129
|
/** Schema definition from schemas.json. */
|
|
128
130
|
export interface SchemaDefinition {
|
|
@@ -233,12 +235,18 @@ export interface PropertyDefinition {
|
|
|
233
235
|
readonly morphName?: string;
|
|
234
236
|
readonly joinTable?: string;
|
|
235
237
|
readonly mappedBy?: string;
|
|
238
|
+
readonly orderBy?: readonly OrderByItem[];
|
|
236
239
|
readonly useCurrent?: boolean;
|
|
237
240
|
readonly searchable?: boolean;
|
|
238
241
|
readonly filterable?: boolean;
|
|
239
242
|
readonly sortable?: boolean;
|
|
240
243
|
readonly fields?: Record<string, FieldOverride>;
|
|
241
244
|
}
|
|
245
|
+
/** Single column ordering. Direction defaults to 'asc'. Issue #40. */
|
|
246
|
+
export interface OrderByItem {
|
|
247
|
+
readonly column: string;
|
|
248
|
+
readonly direction?: string;
|
|
249
|
+
}
|
|
242
250
|
/** Field override for compound type properties. */
|
|
243
251
|
export interface FieldOverride {
|
|
244
252
|
readonly nullable?: boolean;
|