@omnifyjp/omnify 5.8.3 → 5.8.5
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/omnify-config-schema.json +5 -0
- package/package.json +6 -6
- package/ts-dist/php/controller-generator.js +22 -5
- package/ts-dist/php/factory-generator.js +9 -2
- package/ts-dist/php/model-generator.js +70 -12
- package/ts-dist/php/policy-generator.js +9 -3
- package/ts-dist/php/request-generator.js +2 -2
- package/ts-dist/php/resource-generator.js +2 -2
- package/ts-dist/php/schema-config-generator.js +9 -2
- package/ts-dist/php/service-generator.js +11 -3
- package/ts-dist/php/service-provider-generator.js +12 -4
- package/ts-dist/php/types.d.ts +44 -0
- package/ts-dist/php/types.js +18 -1
- package/types/config.d.ts +10 -0
|
@@ -474,6 +474,11 @@
|
|
|
474
474
|
"type": "boolean",
|
|
475
475
|
"default": false,
|
|
476
476
|
"description": "Drop the `Base/` (or `OmnifyBase/`) subfolder + `*Base*` class suffix. Base lives at `path` with class name `<Schema>` (models) or `<Schema><Layer>` (service / resource / etc.). Editable stub extends the bare base FQN. Recommended when `path` already isolates generated code (e.g. `app/Omnify/Models/`). Issue #96, v5.4+."
|
|
477
|
+
},
|
|
478
|
+
"userEditableGroupByFolder": {
|
|
479
|
+
"type": "boolean",
|
|
480
|
+
"default": false,
|
|
481
|
+
"description": "Mirror the schema's group folder onto the USER-EDITABLE layer's path + namespace (issue #98 v5.8.5). Default false — Laravel-canonical flat editable (e.g. `app/Models/User.php`, `App\\Models\\User`); group is encoded only in the base layer + the `use ... as Base` import. Set true to restore v5.8.x grouped editable shape (e.g. `app/Models/Auth/User.php`, `App\\Models\\Auth\\User`). Per layer."
|
|
477
482
|
}
|
|
478
483
|
}
|
|
479
484
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnifyjp/omnify",
|
|
3
|
-
"version": "5.8.
|
|
3
|
+
"version": "5.8.5",
|
|
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": "5.8.
|
|
40
|
-
"@omnifyjp/omnify-darwin-x64": "5.8.
|
|
41
|
-
"@omnifyjp/omnify-linux-x64": "5.8.
|
|
42
|
-
"@omnifyjp/omnify-linux-arm64": "5.8.
|
|
43
|
-
"@omnifyjp/omnify-win32-x64": "5.8.
|
|
39
|
+
"@omnifyjp/omnify-darwin-arm64": "5.8.5",
|
|
40
|
+
"@omnifyjp/omnify-darwin-x64": "5.8.5",
|
|
41
|
+
"@omnifyjp/omnify-linux-x64": "5.8.5",
|
|
42
|
+
"@omnifyjp/omnify-linux-arm64": "5.8.5",
|
|
43
|
+
"@omnifyjp/omnify-win32-x64": "5.8.5"
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -26,11 +26,28 @@ function generateBaseController(name, schema, reader, config) {
|
|
|
26
26
|
const lookup = api.lookup ?? true;
|
|
27
27
|
const bulkDelete = api.bulkDelete ?? false;
|
|
28
28
|
const restore = api.restore ?? (schema.options?.softDelete ?? false);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// Issue #98 v5.8.4 / v5.8.5: cross-layer imports must mirror where
|
|
30
|
+
// the imported file actually lives. BASE layers (Models, Services
|
|
31
|
+
// base, Controllers base) are ALWAYS group-nested in legacy structure.
|
|
32
|
+
// EDITABLE layers (Requests, Resources) are nested only when the
|
|
33
|
+
// target layer's `userEditableGroupByFolder` is `true` — Laravel-
|
|
34
|
+
// canonical defaults to flat. Modular structure already isolates
|
|
35
|
+
// per-schema so all paths skip group nesting.
|
|
36
|
+
const groupNestBase = (ns) => config.structure === 'modular'
|
|
37
|
+
? ns
|
|
38
|
+
: nestByGroup({ path: '', namespace: ns }, schema.group).namespace;
|
|
39
|
+
const groupNestEditable = (ns, layer) => config.structure === 'modular'
|
|
40
|
+
? ns
|
|
41
|
+
: (layer.userEditableGroupByFolder ? nestByGroup({ path: '', namespace: ns }, schema.group).namespace : ns);
|
|
42
|
+
const baseNs = config.structure === 'modular'
|
|
43
|
+
? resolveModularBaseNamespace(config, name, 'Controllers', config.controllers.baseNamespace)
|
|
44
|
+
: groupNestBase(config.controllers.baseNamespace);
|
|
45
|
+
const modelNs = groupNestBase(config.models.namespace);
|
|
46
|
+
const serviceBaseNs = config.structure === 'modular'
|
|
47
|
+
? resolveModularBaseNamespace(config, name, 'Services', config.services.baseNamespace)
|
|
48
|
+
: groupNestBase(config.services.baseNamespace);
|
|
49
|
+
const requestNs = groupNestEditable(config.requests.namespace, config.requests);
|
|
50
|
+
const resourceNs = groupNestEditable(config.resources.namespace, config.resources);
|
|
34
51
|
// Build imports
|
|
35
52
|
const imports = [
|
|
36
53
|
`use App\\Http\\Controllers\\Controller;`,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { toPascalCase } from './naming-helper.js';
|
|
5
5
|
import { toFaker, compoundFaker, associationFaker } from './faker-mapper.js';
|
|
6
|
-
import { userFile } from './types.js';
|
|
6
|
+
import { userFile, nestByGroup } from './types.js';
|
|
7
7
|
/** Generate Factory classes for all project-owned visible object schemas. */
|
|
8
8
|
export function generateFactories(reader, config) {
|
|
9
9
|
const files = [];
|
|
@@ -17,7 +17,14 @@ export function generateFactories(reader, config) {
|
|
|
17
17
|
}
|
|
18
18
|
function generateFactory(name, schema, reader, schemas, config) {
|
|
19
19
|
const modelName = toPascalCase(name);
|
|
20
|
-
|
|
20
|
+
// Issue #98 v5.8.4: legacy structure groups Models by folder, so the
|
|
21
|
+
// factory's `use ${modelNamespace}\\${modelName}` import must include
|
|
22
|
+
// the schema's group too — otherwise the factory references a flat
|
|
23
|
+
// path that no longer holds the model file. Modular structure
|
|
24
|
+
// already isolates per-schema so it skips the nesting.
|
|
25
|
+
const modelNamespace = config.structure === 'modular'
|
|
26
|
+
? config.models.namespace
|
|
27
|
+
: nestByGroup({ path: '', namespace: config.models.namespace }, schema.group).namespace;
|
|
21
28
|
const factoryNamespace = config.factories.namespace;
|
|
22
29
|
const properties = (schema.properties ?? {});
|
|
23
30
|
const expandedProperties = reader.getExpandedProperties(name);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
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
|
-
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, } from './types.js';
|
|
7
|
+
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup, } from './types.js';
|
|
8
8
|
import { enumClassName } from './enum-generator.js';
|
|
9
9
|
/** Generate base model and user model for all project-owned object schemas,
|
|
10
10
|
* plus user models for package schemas (extending the package model). */
|
|
@@ -42,6 +42,13 @@ function generateBaseModel(name, schema, reader, config) {
|
|
|
42
42
|
// below) is the PARENT class string (BaseModel / Authenticatable).
|
|
43
43
|
// Issue #98 v5.8+: apply group-by-folder nesting in legacy structure
|
|
44
44
|
// so 100+ schemas produce a navigable tree mirroring schemas/<group>/.
|
|
45
|
+
// v5.8.5: pivot schemas (`kind: pivot`) override group → all pivot
|
|
46
|
+
// models live under a dedicated `Pivot/` subfolder regardless of the
|
|
47
|
+
// YAML group, mirroring the dedicated `Translation/` subfolder. Devs
|
|
48
|
+
// rarely interact with pivot models directly (Eloquent uses them
|
|
49
|
+
// implicitly), so consolidating them keeps the main model namespace
|
|
50
|
+
// clean.
|
|
51
|
+
const isPivot = schema.kind === 'pivot';
|
|
45
52
|
let baseDescriptor;
|
|
46
53
|
let baseNamespace;
|
|
47
54
|
if (config.structure === 'modular') {
|
|
@@ -54,7 +61,9 @@ function generateBaseModel(name, schema, reader, config) {
|
|
|
54
61
|
}
|
|
55
62
|
else {
|
|
56
63
|
const raw = resolveBaseClass(config.models, modelName, 'BaseModel');
|
|
57
|
-
const nested =
|
|
64
|
+
const nested = isPivot
|
|
65
|
+
? { path: `${raw.path}/Pivot`, namespace: `${raw.fqn.substring(0, raw.fqn.lastIndexOf('\\'))}\\Pivot` }
|
|
66
|
+
: nestByGroup({ path: raw.path, namespace: raw.fqn.substring(0, raw.fqn.lastIndexOf('\\')) }, schema.group);
|
|
58
67
|
baseDescriptor = { className: raw.className, path: nested.path, fqn: `${nested.namespace}\\${raw.className}` };
|
|
59
68
|
baseNamespace = nested.namespace;
|
|
60
69
|
}
|
|
@@ -102,7 +111,7 @@ function generateBaseModel(name, schema, reader, config) {
|
|
|
102
111
|
const hidden = buildHidden(properties, expandedProperties, propertyOrder);
|
|
103
112
|
const appends = buildAppends(expandedProperties);
|
|
104
113
|
const casts = buildCasts(properties, expandedProperties, propertyOrder, reader, config);
|
|
105
|
-
const relations = buildRelations(name, properties, propertyOrder, modelNamespace, reader);
|
|
114
|
+
const relations = buildRelations(name, properties, propertyOrder, modelNamespace, reader, config);
|
|
106
115
|
const accessors = buildAccessors(expandedProperties);
|
|
107
116
|
const fileAccessors = hasFiles ? buildFileAccessors(properties, propertyOrder, modelNamespace) : '';
|
|
108
117
|
const auditSection = buildAuditSection(options, reader, modelNamespace);
|
|
@@ -221,7 +230,11 @@ function generateUserModel(name, schema, config, hasNestedSet = false) {
|
|
|
221
230
|
// than the base (e.g. `app/Models/<Name>.php` while base is at
|
|
222
231
|
// `app/Omnify/Models/<Name>.php`). resolveEditableClass +
|
|
223
232
|
// resolveBaseClass keep the extends-clause + use-import in sync.
|
|
224
|
-
// Issue #98 v5.8
|
|
233
|
+
// Issue #98 v5.8.5: pivot schemas always nest under `Pivot/` on
|
|
234
|
+
// both base AND editable sides regardless of `userEditableGroupByFolder`.
|
|
235
|
+
// Pivots are infrastructure — keeping them out of the main namespace
|
|
236
|
+
// mirrors how Laravel devs treat `Illuminate\Database\Eloquent\Relations\Pivot`.
|
|
237
|
+
const isPivot = schema.kind === 'pivot';
|
|
225
238
|
let editable;
|
|
226
239
|
let base;
|
|
227
240
|
if (config.structure === 'modular') {
|
|
@@ -230,10 +243,14 @@ function generateUserModel(name, schema, config, hasNestedSet = false) {
|
|
|
230
243
|
}
|
|
231
244
|
else {
|
|
232
245
|
const editRaw = resolveEditableClass(config.models, modelName);
|
|
233
|
-
const editNested =
|
|
246
|
+
const editNested = isPivot
|
|
247
|
+
? { path: `${editRaw.path}/Pivot`, namespace: `${editRaw.namespace}\\Pivot` }
|
|
248
|
+
: nestEditableByGroup(config.models, { path: editRaw.path, namespace: editRaw.namespace }, schema.group);
|
|
234
249
|
editable = { className: editRaw.className, fileName: editRaw.fileName, namespace: editNested.namespace, path: editNested.path, fqn: `${editNested.namespace}\\${editRaw.className}` };
|
|
235
250
|
const baseRaw = resolveBaseClass(config.models, modelName, 'BaseModel');
|
|
236
|
-
const baseNs =
|
|
251
|
+
const baseNs = isPivot
|
|
252
|
+
? `${baseRaw.fqn.substring(0, baseRaw.fqn.lastIndexOf('\\'))}\\Pivot`
|
|
253
|
+
: nestByGroup({ path: baseRaw.path, namespace: baseRaw.fqn.substring(0, baseRaw.fqn.lastIndexOf('\\')) }, schema.group).namespace;
|
|
237
254
|
base = { className: baseRaw.className, fqn: `${baseNs}\\${baseRaw.className}` };
|
|
238
255
|
}
|
|
239
256
|
const factoryNamespace = config.factories.namespace;
|
|
@@ -561,7 +578,42 @@ function buildCasts(properties, expandedProperties, propertyOrder, reader, confi
|
|
|
561
578
|
return '';
|
|
562
579
|
return casts.map(c => ` ${c}`).join('\n') + '\n';
|
|
563
580
|
}
|
|
564
|
-
|
|
581
|
+
/**
|
|
582
|
+
* Issue #98 v5.8.6: resolve the FQCN namespace of a related Model
|
|
583
|
+
* for inline `belongsTo / hasMany / belongsToMany / morphTo` references.
|
|
584
|
+
* The namespace must match the TARGET's actual file location, not the
|
|
585
|
+
* source schema's. Three knobs:
|
|
586
|
+
* 1. Package schemas — return the package's namespace verbatim
|
|
587
|
+
* (already handled by reader.resolveModelNamespace).
|
|
588
|
+
* 2. Pivot schemas — always nest under `Pivot/` regardless of any
|
|
589
|
+
* group / userEditableGroupByFolder setting.
|
|
590
|
+
* 3. Group nesting — only when `userEditableGroupByFolder: true`
|
|
591
|
+
* (matches where the editable lives so the relation resolves to
|
|
592
|
+
* the user's customizable class). Default off → flat FQCN
|
|
593
|
+
* matches the Laravel-canonical flat editable layout.
|
|
594
|
+
*
|
|
595
|
+
* Pre-fix every relation emitted `\${modelNamespace}\${target}::class`
|
|
596
|
+
* (flat FQCN) regardless of where the target's file actually lived.
|
|
597
|
+
* For grouped projects this dispatched to a non-existent class at
|
|
598
|
+
* runtime — `ErrorException: include(... no such file)`.
|
|
599
|
+
*/
|
|
600
|
+
function resolveRelatedModelNamespace(target, modelNamespace, config, reader) {
|
|
601
|
+
const baseNs = reader.resolveModelNamespace(target, modelNamespace);
|
|
602
|
+
// Package schemas — leave as-is.
|
|
603
|
+
if (baseNs !== modelNamespace)
|
|
604
|
+
return baseNs;
|
|
605
|
+
const targetSchema = reader.getSchema(target);
|
|
606
|
+
if (!targetSchema)
|
|
607
|
+
return baseNs;
|
|
608
|
+
if (targetSchema.kind === 'pivot') {
|
|
609
|
+
return `${baseNs}\\Pivot`;
|
|
610
|
+
}
|
|
611
|
+
if (config.models.userEditableGroupByFolder && targetSchema.group) {
|
|
612
|
+
return `${baseNs}\\${toPascalCase(targetSchema.group)}`;
|
|
613
|
+
}
|
|
614
|
+
return baseNs;
|
|
615
|
+
}
|
|
616
|
+
function buildRelations(schemaName, properties, propertyOrder, modelNamespace, reader, config) {
|
|
565
617
|
const methods = [];
|
|
566
618
|
const sourceTableName = reader.getTableName(schemaName);
|
|
567
619
|
// Track method names already declared on this model so reverse-relation
|
|
@@ -574,9 +626,11 @@ function buildRelations(schemaName, properties, propertyOrder, modelNamespace, r
|
|
|
574
626
|
const prop = properties[propName];
|
|
575
627
|
if (!prop || prop['type'] !== 'Association')
|
|
576
628
|
continue;
|
|
577
|
-
// Resolve namespace per-target (package schemas use their own namespace)
|
|
629
|
+
// Resolve namespace per-target (package schemas use their own namespace).
|
|
630
|
+
// Issue #98 v5.8.6: also nest by target's group / Pivot/ so the
|
|
631
|
+
// FQCN matches the target file's actual location.
|
|
578
632
|
const target = prop['target'] ?? '';
|
|
579
|
-
const ns = target ?
|
|
633
|
+
const ns = target ? resolveRelatedModelNamespace(target, modelNamespace, config, reader) : modelNamespace;
|
|
580
634
|
const targetTableName = target ? reader.getTableName(target) : '';
|
|
581
635
|
// For ManyToMany, look up the explicit `kind: pivot` schema (the
|
|
582
636
|
// sorted PascalCase concat of source + target — auto-created on
|
|
@@ -606,7 +660,7 @@ function buildRelations(schemaName, properties, propertyOrder, modelNamespace, r
|
|
|
606
660
|
// Auto-generate reverse HasMany/HasOne for any child schema that declares a
|
|
607
661
|
// ManyToOne/OneToOne pointing at this schema, unless the user has already
|
|
608
662
|
// declared a relation method with the same name. Issue #39.
|
|
609
|
-
const inverseRelations = buildInverseRelations(schemaName, modelNamespace, reader, declaredMethods, explicitInverses);
|
|
663
|
+
const inverseRelations = buildInverseRelations(schemaName, modelNamespace, reader, declaredMethods, explicitInverses, config);
|
|
610
664
|
if (inverseRelations) {
|
|
611
665
|
methods.push(inverseRelations);
|
|
612
666
|
}
|
|
@@ -649,7 +703,7 @@ const LARAVEL_RESERVED_RELATION_NAMES = new Set([
|
|
|
649
703
|
* a unique method name, or add `mappedBy: <customName>` to the child schema
|
|
650
704
|
* so the derived method name changes.
|
|
651
705
|
*/
|
|
652
|
-
function buildInverseRelations(parentName, modelNamespace, reader, declaredMethods, explicitInverses) {
|
|
706
|
+
function buildInverseRelations(parentName, modelNamespace, reader, declaredMethods, explicitInverses, config) {
|
|
653
707
|
const methods = [];
|
|
654
708
|
const allSchemas = reader.getSchemas();
|
|
655
709
|
// Stable iteration order — sort child names so output is deterministic
|
|
@@ -697,7 +751,11 @@ function buildInverseRelations(parentName, modelNamespace, reader, declaredMetho
|
|
|
697
751
|
// Here we're emitting the *parent* accessor, so the child's ManyToOne
|
|
698
752
|
// (or owning OneToOne) is what we react to.
|
|
699
753
|
const fkColumn = `${toSnakeCase(childPropName)}_id`;
|
|
700
|
-
|
|
754
|
+
// Issue #98 v5.8.6: nest the inverse-side FQCN by the CHILD's
|
|
755
|
+
// group / Pivot/ so the relation resolves to the child's actual
|
|
756
|
+
// file location (which lives in `<childGroup>/<Child>.php` or
|
|
757
|
+
// `Pivot/<Child>.php`, not flat).
|
|
758
|
+
const childNs = resolveRelatedModelNamespace(childName, modelNamespace, config, reader);
|
|
701
759
|
const fqcn = `\\${childNs}\\${childName}`;
|
|
702
760
|
const returnType = isOneToOne ? 'HasOne' : 'HasMany';
|
|
703
761
|
const eloquentMethod = isOneToOne ? 'hasOne' : 'hasMany';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Only generates for schemas that have `policies` defined.
|
|
6
6
|
*/
|
|
7
7
|
import { toPascalCase, toSnakeCase, toCamelCase } from './naming-helper.js';
|
|
8
|
-
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup } from './types.js';
|
|
8
|
+
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup } from './types.js';
|
|
9
9
|
// ============================================================================
|
|
10
10
|
// Action mapping: Omnify → Laravel
|
|
11
11
|
// ============================================================================
|
|
@@ -46,7 +46,13 @@ function generateForSchema(name, schema, reader, config) {
|
|
|
46
46
|
function generateBasePolicyClass(name, schema, reader, config) {
|
|
47
47
|
const modelName = toPascalCase(name);
|
|
48
48
|
const baseNamespace = resolveModularBaseNamespace(config, name, 'Policies', config.policies.baseNamespace);
|
|
49
|
-
|
|
49
|
+
// Issue #98 v5.8.4: legacy structure groups Models by folder. The
|
|
50
|
+
// policy's `use ${modelNamespace}\\${modelName}` import must include
|
|
51
|
+
// the schema's group too, otherwise the policy fails to autoload at
|
|
52
|
+
// runtime when Laravel resolves it from the gate registration.
|
|
53
|
+
const modelNamespace = config.structure === 'modular'
|
|
54
|
+
? config.models.namespace
|
|
55
|
+
: nestByGroup({ path: '', namespace: config.models.namespace }, schema.group).namespace;
|
|
50
56
|
const policies = schema.policies;
|
|
51
57
|
const hasSoftDelete = schema.options?.softDelete ?? false;
|
|
52
58
|
const needsCidrHelper = policiesUseCidr(policies);
|
|
@@ -122,7 +128,7 @@ function generateUserPolicyClass(name, schema, config) {
|
|
|
122
128
|
const baseNs = nestByGroup({ path: baseRaw.path, namespace: baseRaw.fqn.substring(0, baseRaw.fqn.lastIndexOf('\\')) }, schema.group).namespace;
|
|
123
129
|
baseDescriptor = { className: baseRaw.className, fqn: `${baseNs}\\${baseRaw.className}` };
|
|
124
130
|
const editRaw = resolveEditableClass(config.policies, modelName, 'Policy');
|
|
125
|
-
const editNested =
|
|
131
|
+
const editNested = nestEditableByGroup(config.policies, { path: editRaw.path, namespace: editRaw.namespace }, schema.group);
|
|
126
132
|
editable = { className: editRaw.className, fileName: editRaw.fileName, namespace: editNested.namespace, path: editNested.path };
|
|
127
133
|
}
|
|
128
134
|
// Issue #98: alias on class-name collision (flatBase + userEditablePath).
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { toPascalCase, toSnakeCase, toCamelCase } from './naming-helper.js';
|
|
5
5
|
import { toStoreRules, toUpdateRules, formatRules, hasRuleObject } from './type-mapper.js';
|
|
6
|
-
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup } from './types.js';
|
|
6
|
+
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup } from './types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Resolve the Laravel validation rule type ('integer', 'uuid', or 'string') for
|
|
9
9
|
* a foreign key column based on the target schema's primary key type.
|
|
@@ -294,7 +294,7 @@ function generateUserRequest(name, schema, config, action) {
|
|
|
294
294
|
const baseNs = nestByGroup({ path: baseRaw.path, namespace: baseRaw.fqn.substring(0, baseRaw.fqn.lastIndexOf('\\')) }, schema.group).namespace;
|
|
295
295
|
baseDescriptor = { className: baseRaw.className, fqn: `${baseNs}\\${baseRaw.className}` };
|
|
296
296
|
const editRaw = resolveEditableClass(config.requests, modelName, `${action}Request`);
|
|
297
|
-
const editNested =
|
|
297
|
+
const editNested = nestEditableByGroup(config.requests, { path: editRaw.path, namespace: editRaw.namespace }, schema.group);
|
|
298
298
|
editable = { className: editRaw.className, fileName: editRaw.fileName, namespace: editNested.namespace, path: editNested.path };
|
|
299
299
|
}
|
|
300
300
|
// Issue #98: alias on class-name collision (flatBase + userEditablePath).
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { toPascalCase, toSnakeCase, toCamelCase } from './naming-helper.js';
|
|
5
5
|
import { isHiddenByDefault, toResourceExpression } from './type-mapper.js';
|
|
6
|
-
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup } from './types.js';
|
|
6
|
+
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup } from './types.js';
|
|
7
7
|
/** Generate Resource classes for all project-owned visible object schemas. */
|
|
8
8
|
export function generateResources(reader, config) {
|
|
9
9
|
const files = [];
|
|
@@ -158,7 +158,7 @@ function generateUserResource(name, schema, config) {
|
|
|
158
158
|
const baseNs = nestByGroup({ path: baseRaw.path, namespace: baseRaw.fqn.substring(0, baseRaw.fqn.lastIndexOf('\\')) }, schema.group).namespace;
|
|
159
159
|
baseDescriptor = { className: baseRaw.className, fqn: `${baseNs}\\${baseRaw.className}` };
|
|
160
160
|
const editRaw = resolveEditableClass(config.resources, modelName, 'Resource');
|
|
161
|
-
const editNested =
|
|
161
|
+
const editNested = nestEditableByGroup(config.resources, { path: editRaw.path, namespace: editRaw.namespace }, schema.group);
|
|
162
162
|
editable = { className: editRaw.className, fileName: editRaw.fileName, namespace: editNested.namespace, path: editNested.path };
|
|
163
163
|
}
|
|
164
164
|
// Issue #98: alias on class-name collision (flatBase + userEditablePath).
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Only includes properties that have searchable/filterable/sortable flags.
|
|
6
6
|
*/
|
|
7
7
|
import { toPascalCase, toSnakeCase } from './naming-helper.js';
|
|
8
|
-
import { baseFile } from './types.js';
|
|
8
|
+
import { baseFile, nestByGroup } from './types.js';
|
|
9
9
|
/** Generate the omnify-schemas config file. */
|
|
10
10
|
export function generateSchemaConfig(reader, config) {
|
|
11
11
|
const apiSchemas = reader.getSchemasWithApi();
|
|
@@ -64,9 +64,16 @@ function generateSchemaEntry(name, schema, reader, config) {
|
|
|
64
64
|
const propsBlock = propLines.length > 0
|
|
65
65
|
? `\n 'properties' => [\n${propLines.join('\n')}\n ],`
|
|
66
66
|
: `\n 'properties' => [],`;
|
|
67
|
+
// Issue #98 v5.8.4: Query Engine resolves `model` via reflection, so
|
|
68
|
+
// the FQN must reflect the actual file location (which now includes
|
|
69
|
+
// the schema's group folder for legacy structure). Modular structure
|
|
70
|
+
// already isolates per-schema so it skips the nesting.
|
|
71
|
+
const modelNs = config.structure === 'modular'
|
|
72
|
+
? config.models.namespace
|
|
73
|
+
: nestByGroup({ path: '', namespace: config.models.namespace }, schema.group).namespace;
|
|
67
74
|
return ` '${modelName}' => [
|
|
68
75
|
'table' => '${tableName}',
|
|
69
|
-
'model' => \\${
|
|
76
|
+
'model' => \\${modelNs}\\${modelName}::class,
|
|
70
77
|
'softDelete' => ${softDelete ? 'true' : 'false'},
|
|
71
78
|
'perPage' => ${perPage},${propsBlock}
|
|
72
79
|
],
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* eagerCount}` keys are deprecated and emit warnings at generate time.
|
|
14
14
|
*/
|
|
15
15
|
import { toPascalCase, toSnakeCase } from './naming-helper.js';
|
|
16
|
-
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup } from './types.js';
|
|
16
|
+
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup } from './types.js';
|
|
17
17
|
// ============================================================================
|
|
18
18
|
// Public entry point
|
|
19
19
|
// ============================================================================
|
|
@@ -510,7 +510,15 @@ function generateBaseService(name, schema, reader, config) {
|
|
|
510
510
|
const baseNs = config.structure === 'modular'
|
|
511
511
|
? resolveModularBaseNamespace(config, name, 'Services', config.services.baseNamespace)
|
|
512
512
|
: nestByGroup({ path: '', namespace: config.services.baseNamespace }, schema.group).namespace;
|
|
513
|
-
|
|
513
|
+
// Issue #98 v5.8.4: cross-layer Model imports must include the
|
|
514
|
+
// schema's group folder so they resolve to where Models actually
|
|
515
|
+
// live now. Pre-fix the service base emitted
|
|
516
|
+
// `use App\Omnify\Models\Banner;` while the model file was at
|
|
517
|
+
// `app/Omnify/Models/Cms/Banner.php` — autoload failed at runtime
|
|
518
|
+
// for every consumer (TypeError + missing-file include).
|
|
519
|
+
const modelNs = config.structure === 'modular'
|
|
520
|
+
? config.models.namespace
|
|
521
|
+
: nestByGroup({ path: '', namespace: config.models.namespace }, schema.group).namespace;
|
|
514
522
|
// Build imports
|
|
515
523
|
const imports = [
|
|
516
524
|
`use ${modelNs}\\${modelName};`,
|
|
@@ -1784,7 +1792,7 @@ function generateUserService(name, schema, config) {
|
|
|
1784
1792
|
const baseNs = nestByGroup({ path: baseRaw.path, namespace: baseRaw.fqn.substring(0, baseRaw.fqn.lastIndexOf('\\')) }, schema.group).namespace;
|
|
1785
1793
|
baseDescriptor = { className: baseRaw.className, fqn: `${baseNs}\\${baseRaw.className}` };
|
|
1786
1794
|
const editRaw = resolveEditableClass(config.services, modelName, 'Service');
|
|
1787
|
-
const editNested =
|
|
1795
|
+
const editNested = nestEditableByGroup(config.services, { path: editRaw.path, namespace: editRaw.namespace }, schema.group);
|
|
1788
1796
|
editable = { className: editRaw.className, fileName: editRaw.fileName, namespace: editNested.namespace, path: editNested.path };
|
|
1789
1797
|
}
|
|
1790
1798
|
// Group nesting already applied via nestByGroup above; no additional
|
|
@@ -2,15 +2,23 @@
|
|
|
2
2
|
* Port of AppServiceProviderGenerator.php — generates OmnifyServiceProvider.
|
|
3
3
|
*/
|
|
4
4
|
import { toPascalCase } from './naming-helper.js';
|
|
5
|
-
import { baseFile } from './types.js';
|
|
5
|
+
import { baseFile, nestByGroup } from './types.js';
|
|
6
6
|
/** Generate the OmnifyServiceProvider for the application. */
|
|
7
7
|
export function generateServiceProvider(reader, config) {
|
|
8
8
|
const modelNamespace = config.models.namespace;
|
|
9
|
-
// Build morph map entries (project-owned schemas only)
|
|
9
|
+
// Build morph map entries (project-owned schemas only). Issue #98 v5.8.4:
|
|
10
|
+
// each schema's actual model FQN includes its group folder, so the
|
|
11
|
+
// morph map must echo that — pre-fix every entry pointed to the flat
|
|
12
|
+
// `App\Omnify\Models\<Name>::class` and Laravel's morphMap resolved to
|
|
13
|
+
// a non-existent class for every grouped schema, breaking polymorphic
|
|
14
|
+
// relationships project-wide.
|
|
10
15
|
const morphEntries = [];
|
|
11
|
-
for (const name of Object.
|
|
16
|
+
for (const [name, schema] of Object.entries(reader.getProjectVisibleObjectSchemas())) {
|
|
12
17
|
const modelName = toPascalCase(name);
|
|
13
|
-
|
|
18
|
+
const ns = config.structure === 'modular'
|
|
19
|
+
? modelNamespace
|
|
20
|
+
: nestByGroup({ path: '', namespace: modelNamespace }, schema.group).namespace;
|
|
21
|
+
morphEntries.push(` '${modelName}' => \\${ns}\\${modelName}::class,`);
|
|
14
22
|
}
|
|
15
23
|
// Add File to morph map when any schema uses File type
|
|
16
24
|
if (reader.hasFileProperties()) {
|
package/ts-dist/php/types.d.ts
CHANGED
|
@@ -113,6 +113,26 @@ export interface LaravelPathOverride {
|
|
|
113
113
|
* isolates generated code (e.g. `app/Omnify/Models/`).
|
|
114
114
|
*/
|
|
115
115
|
flatBase?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Issue #98 v5.8.5: per-layer toggle for whether the USER-EDITABLE
|
|
118
|
+
* stub is grouped by the schema's parent folder. The base layer is
|
|
119
|
+
* always grouped (canonical layout — domain-organized regenerated
|
|
120
|
+
* code), but Laravel ecosystem code (`app/Models/`, `app/Services/`,
|
|
121
|
+
* `app/Http/Requests/`, `app/Http/Resources/`) is canonically FLAT —
|
|
122
|
+
* `App\Models\User`, not `App\Models\Auth\User`. `config/auth.php`,
|
|
123
|
+
* Sanctum traits, factory discovery and dozens of ecosystem packages
|
|
124
|
+
* assume the flat shape.
|
|
125
|
+
*
|
|
126
|
+
* Default: `false` (Laravel-canonical flat editable). The grouped
|
|
127
|
+
* base + flat editable layout is what new `composer create-project
|
|
128
|
+
* laravel/laravel` apps produce, and the use-statement in the editable
|
|
129
|
+
* stub still aliases the GROUPED base FQN — group is encoded in the
|
|
130
|
+
* `use ... as ...Base` import, not the editable's namespace.
|
|
131
|
+
*
|
|
132
|
+
* Projects that adopted v5.8.x and want to keep their grouped editable
|
|
133
|
+
* layout flip this to `true` per layer.
|
|
134
|
+
*/
|
|
135
|
+
userEditableGroupByFolder?: boolean;
|
|
116
136
|
}
|
|
117
137
|
/** Nested set package configuration. */
|
|
118
138
|
export interface NestedSetOverride {
|
|
@@ -225,6 +245,15 @@ export interface BaseEditableLayer {
|
|
|
225
245
|
* extends `\{namespace}\{Schema}` (no `BaseModel` indirection). Issue #96.
|
|
226
246
|
*/
|
|
227
247
|
flatBase: boolean;
|
|
248
|
+
/**
|
|
249
|
+
* Issue #98 v5.8.5: when `true`, the user-editable layer is also
|
|
250
|
+
* grouped by the schema's parent folder (mirrors the base layer).
|
|
251
|
+
* Default `false` — Laravel-canonical flat layout (`app/Models/User.php`,
|
|
252
|
+
* not `app/Models/Auth/User.php`). Group is encoded only in the base
|
|
253
|
+
* layer + the `use ... as Base` import inside the editable stub.
|
|
254
|
+
* The base layer is unaffected — it is always grouped.
|
|
255
|
+
*/
|
|
256
|
+
userEditableGroupByFolder: boolean;
|
|
228
257
|
}
|
|
229
258
|
/** PHP codegen configuration (resolved with defaults). */
|
|
230
259
|
export interface PhpConfig {
|
|
@@ -347,6 +376,21 @@ export declare function resolveEditableClass(layer: BaseEditableLayer, schemaNam
|
|
|
347
376
|
path: string;
|
|
348
377
|
fqn: string;
|
|
349
378
|
};
|
|
379
|
+
/**
|
|
380
|
+
* Issue #98 v5.8.5: gate group nesting for the user-editable layer on
|
|
381
|
+
* `layer.userEditableGroupByFolder`. Default flat (Laravel-canonical) —
|
|
382
|
+
* `app/Models/User.php`, `App\Models\User`. Opt-in to mirror-base via
|
|
383
|
+
* `userEditableGroupByFolder: true` per layer. Auxiliary subfolders
|
|
384
|
+
* (Translation/, Pivot/, Enum/) are imposed elsewhere and are NOT
|
|
385
|
+
* affected by this flag — they always nest.
|
|
386
|
+
*/
|
|
387
|
+
export declare function nestEditableByGroup(layer: BaseEditableLayer, loc: {
|
|
388
|
+
path: string;
|
|
389
|
+
namespace: string;
|
|
390
|
+
}, group: string | undefined): {
|
|
391
|
+
path: string;
|
|
392
|
+
namespace: string;
|
|
393
|
+
};
|
|
350
394
|
/**
|
|
351
395
|
* Derive full PHP config from optional overrides.
|
|
352
396
|
* All paths and namespaces fall back to sensible defaults.
|
package/ts-dist/php/types.js
CHANGED
|
@@ -197,7 +197,11 @@ nsFromPath) {
|
|
|
197
197
|
: path;
|
|
198
198
|
const userEditableNamespace = override?.userEditableNamespace
|
|
199
199
|
?? (override?.userEditablePath ? nsFromPath(userEditablePath) : namespace);
|
|
200
|
-
|
|
200
|
+
// Issue #98 v5.8.5: the user-editable layer is FLAT (Laravel-canonical)
|
|
201
|
+
// by default. Project explicitly opts back into the v5.8.x grouped
|
|
202
|
+
// editable shape via `userEditableGroupByFolder: true` per layer.
|
|
203
|
+
const userEditableGroupByFolder = override?.userEditableGroupByFolder ?? false;
|
|
204
|
+
return { namespace, baseNamespace, path, basePath, userEditablePath, userEditableNamespace, flatBase, userEditableGroupByFolder };
|
|
201
205
|
}
|
|
202
206
|
/**
|
|
203
207
|
* Class-name + file-name resolver for the BASE class of a layer. Returns
|
|
@@ -234,6 +238,19 @@ export function resolveEditableClass(layer, schemaName, classSuffix = '') {
|
|
|
234
238
|
fqn: `${layer.userEditableNamespace}\\${className}`,
|
|
235
239
|
};
|
|
236
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Issue #98 v5.8.5: gate group nesting for the user-editable layer on
|
|
243
|
+
* `layer.userEditableGroupByFolder`. Default flat (Laravel-canonical) —
|
|
244
|
+
* `app/Models/User.php`, `App\Models\User`. Opt-in to mirror-base via
|
|
245
|
+
* `userEditableGroupByFolder: true` per layer. Auxiliary subfolders
|
|
246
|
+
* (Translation/, Pivot/, Enum/) are imposed elsewhere and are NOT
|
|
247
|
+
* affected by this flag — they always nest.
|
|
248
|
+
*/
|
|
249
|
+
export function nestEditableByGroup(layer, loc, group) {
|
|
250
|
+
if (!layer.userEditableGroupByFolder)
|
|
251
|
+
return loc;
|
|
252
|
+
return nestByGroup(loc, group);
|
|
253
|
+
}
|
|
237
254
|
// Default paths.
|
|
238
255
|
//
|
|
239
256
|
// Hybrid projects (manual team code + Omnify codegen) need Omnify-generated
|
package/types/config.d.ts
CHANGED
|
@@ -68,6 +68,16 @@ export interface CodegenLaravelPathConfig {
|
|
|
68
68
|
* generated code (e.g. `app/Omnify/Models/`). Issue #96, v5.4+.
|
|
69
69
|
*/
|
|
70
70
|
flatBase?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Issue #98 v5.8.5: when `true`, the user-editable layer's path +
|
|
73
|
+
* namespace mirrors the schema's group folder (e.g.
|
|
74
|
+
* `app/Models/Auth/User.php`, namespace `App\Models\Auth`). Default
|
|
75
|
+
* `false` — Laravel-canonical FLAT layout (`app/Models/User.php`,
|
|
76
|
+
* namespace `App\Models`); group is encoded only in the base layer
|
|
77
|
+
* and the `use ... as Base` import inside the editable stub. Per
|
|
78
|
+
* layer (model / service / request / resource / policy / controller).
|
|
79
|
+
*/
|
|
80
|
+
userEditableGroupByFolder?: boolean;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
/** Nested set package configuration. */
|