@omnifyjp/ts 5.7.0 → 5.8.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.
@@ -2,7 +2,7 @@
2
2
  * Generates base + editable controller classes for schemas with `options.api`.
3
3
  */
4
4
  import { toPascalCase } from './naming-helper.js';
5
- import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass } from './types.js';
5
+ import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup } from './types.js';
6
6
  import { buildControllerMethodAttributes, openApiControllerImports } from './openapi-generator.js';
7
7
  const DEFAULT_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
8
8
  /** Generate controller classes for all schemas with api config. */
@@ -157,13 +157,19 @@ abstract class ${modelName}ControllerBase extends Controller
157
157
  ${methods.join('\n')}
158
158
  }
159
159
  `;
160
- // Issue #96: legacy honors flatBase (drops ControllerBase suffix + OmnifyBase/ subfolder).
161
- const baseDescriptor = config.structure === 'modular'
162
- ? {
160
+ // Issue #96 + #98 v5.8+: legacy honors flatBase + group-by-folder.
161
+ let baseDescriptor;
162
+ if (config.structure === 'modular') {
163
+ baseDescriptor = {
163
164
  className: `${modelName}ControllerBase`,
164
165
  path: resolveModularBasePath(config, name, 'Controllers', '', config.controllers.basePath).replace(/\/$/, ''),
165
- }
166
- : resolveBaseClass(config.controllers, `${modelName}Controller`, 'Base');
166
+ };
167
+ }
168
+ else {
169
+ const raw = resolveBaseClass(config.controllers, `${modelName}Controller`, 'Base');
170
+ const nested = nestByGroup({ path: raw.path, namespace: '' }, schema.group);
171
+ baseDescriptor = { className: raw.className, path: nested.path };
172
+ }
167
173
  const finalContent = content.replace(new RegExp(`abstract class ${modelName}ControllerBase\\b`, 'g'), `abstract class ${baseDescriptor.className}`);
168
174
  return baseFile(`${baseDescriptor.path}/${baseDescriptor.className}.php`, finalContent);
169
175
  }
@@ -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, } from './types.js';
7
+ import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, } 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). */
@@ -29,7 +29,7 @@ function generateForSchema(name, schema, reader, config) {
29
29
  const hasNestedSet = detectNestedSet(options, properties);
30
30
  return [
31
31
  generateBaseModel(name, schema, reader, config),
32
- generateUserModel(name, config, hasNestedSet),
32
+ generateUserModel(name, schema, config, hasNestedSet),
33
33
  ];
34
34
  }
35
35
  function generateBaseModel(name, schema, reader, config) {
@@ -40,16 +40,24 @@ function generateBaseModel(name, schema, reader, config) {
40
40
  // isolates base from editable). `baseDescriptor` is the resolved
41
41
  // {className, fqn, path} for the base class — `baseClass` (further
42
42
  // below) is the PARENT class string (BaseModel / Authenticatable).
43
- const baseDescriptor = config.structure === 'modular'
44
- ? {
43
+ // Issue #98 v5.8+: apply group-by-folder nesting in legacy structure
44
+ // so 100+ schemas produce a navigable tree mirroring schemas/<group>/.
45
+ let baseDescriptor;
46
+ let baseNamespace;
47
+ if (config.structure === 'modular') {
48
+ baseDescriptor = {
45
49
  className: `${modelName}BaseModel`,
46
50
  fqn: `${resolveModularBaseNamespace(config, name, 'Models', config.models.baseNamespace)}\\${modelName}BaseModel`,
47
51
  path: resolveModularBasePath(config, name, 'Models', '', config.models.basePath).replace(/\/$/, ''),
48
- }
49
- : resolveBaseClass(config.models, modelName, 'BaseModel');
50
- const baseNamespace = config.structure === 'modular'
51
- ? resolveModularBaseNamespace(config, name, 'Models', config.models.baseNamespace)
52
- : baseDescriptor.fqn.substring(0, baseDescriptor.fqn.lastIndexOf('\\'));
52
+ };
53
+ baseNamespace = resolveModularBaseNamespace(config, name, 'Models', config.models.baseNamespace);
54
+ }
55
+ else {
56
+ const raw = resolveBaseClass(config.models, modelName, 'BaseModel');
57
+ const nested = nestByGroup({ path: raw.path, namespace: raw.fqn.substring(0, raw.fqn.lastIndexOf('\\')) }, schema.group);
58
+ baseDescriptor = { className: raw.className, path: nested.path, fqn: `${nested.namespace}\\${raw.className}` };
59
+ baseNamespace = nested.namespace;
60
+ }
53
61
  const localesNamespace = resolveModularBaseNamespace(config, name, 'Locales', config.models.baseNamespace + '\\Locales');
54
62
  // Issue #33: HasLocalizedDisplayName is a global Omnify trait, not a per-shared module file.
55
63
  const traitsNamespace = resolveGlobalTraitNamespace(config, config.models.baseNamespace + '\\Traits');
@@ -207,18 +215,27 @@ ${auditLogProperties}${relations}${accessors}${fileAccessors}${auditSection}${ne
207
215
  : `${baseDescriptor.path}/${baseDescriptor.className}.php`;
208
216
  return baseFile(filePath, finalContent);
209
217
  }
210
- function generateUserModel(name, config, hasNestedSet = false) {
218
+ function generateUserModel(name, schema, config, hasNestedSet = false) {
211
219
  const modelName = toPascalCase(name);
212
220
  // Issue #96: editable stub may live at a different path/namespace
213
221
  // than the base (e.g. `app/Models/<Name>.php` while base is at
214
222
  // `app/Omnify/Models/<Name>.php`). resolveEditableClass +
215
223
  // resolveBaseClass keep the extends-clause + use-import in sync.
216
- const editable = config.structure === 'modular'
217
- ? { fileName: `${modelName}.php`, namespace: config.models.namespace, path: config.models.path, fqn: `${config.models.namespace}\\${modelName}`, className: modelName }
218
- : resolveEditableClass(config.models, modelName);
219
- const base = config.structure === 'modular'
220
- ? { className: `${modelName}BaseModel`, fqn: `${resolveModularBaseNamespace(config, name, 'Models', config.models.baseNamespace)}\\${modelName}BaseModel` }
221
- : resolveBaseClass(config.models, modelName, 'BaseModel');
224
+ // Issue #98 v5.8+: same group-by-folder nesting on the editable side.
225
+ let editable;
226
+ let base;
227
+ if (config.structure === 'modular') {
228
+ editable = { fileName: `${modelName}.php`, namespace: config.models.namespace, path: config.models.path, fqn: `${config.models.namespace}\\${modelName}`, className: modelName };
229
+ base = { className: `${modelName}BaseModel`, fqn: `${resolveModularBaseNamespace(config, name, 'Models', config.models.baseNamespace)}\\${modelName}BaseModel` };
230
+ }
231
+ else {
232
+ const editRaw = resolveEditableClass(config.models, modelName);
233
+ const editNested = nestByGroup({ path: editRaw.path, namespace: editRaw.namespace }, schema.group);
234
+ editable = { className: editRaw.className, fileName: editRaw.fileName, namespace: editNested.namespace, path: editNested.path, fqn: `${editNested.namespace}\\${editRaw.className}` };
235
+ const baseRaw = resolveBaseClass(config.models, modelName, 'BaseModel');
236
+ const baseNs = nestByGroup({ path: baseRaw.path, namespace: baseRaw.fqn.substring(0, baseRaw.fqn.lastIndexOf('\\')) }, schema.group).namespace;
237
+ base = { className: baseRaw.className, fqn: `${baseNs}\\${baseRaw.className}` };
238
+ }
222
239
  const factoryNamespace = config.factories.namespace;
223
240
  const nestedSetNamespace = config.nestedset.namespace;
224
241
  // Issue #98: when flatBase + userEditablePath both set, base.className
@@ -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 } from './types.js';
8
+ import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup } from './types.js';
9
9
  // ============================================================================
10
10
  // Action mapping: Omnify → Laravel
11
11
  // ============================================================================
@@ -40,7 +40,7 @@ export function generatePolicies(reader, config) {
40
40
  function generateForSchema(name, schema, reader, config) {
41
41
  return [
42
42
  generateBasePolicyClass(name, schema, reader, config),
43
- generateUserPolicyClass(name, config),
43
+ generateUserPolicyClass(name, schema, config),
44
44
  ];
45
45
  }
46
46
  function generateBasePolicyClass(name, schema, reader, config) {
@@ -89,28 +89,42 @@ class ${modelName}PolicyBase
89
89
  ${methods.join('\n')}${cidrHelper}
90
90
  }
91
91
  `;
92
- // Issue #96: legacy honors flatBase (drops PolicyBase suffix + Base/ subfolder).
93
- const baseDescriptor = config.structure === 'modular'
94
- ? {
92
+ // Issue #96 + #98 v5.8+: legacy honors flatBase + group-by-folder.
93
+ let baseDescriptor;
94
+ if (config.structure === 'modular') {
95
+ baseDescriptor = {
95
96
  className: `${modelName}PolicyBase`,
96
97
  path: resolveModularBasePath(config, name, 'Policies', '', config.policies.basePath).replace(/\/$/, ''),
97
- }
98
- : resolveBaseClass(config.policies, `${modelName}Policy`, 'Base');
98
+ };
99
+ }
100
+ else {
101
+ const raw = resolveBaseClass(config.policies, `${modelName}Policy`, 'Base');
102
+ const nested = nestByGroup({ path: raw.path, namespace: '' }, schema.group);
103
+ baseDescriptor = { className: raw.className, path: nested.path };
104
+ }
99
105
  const finalContent = content.replace(new RegExp(`class ${modelName}PolicyBase\\b`, 'g'), `class ${baseDescriptor.className}`);
100
106
  return baseFile(`${baseDescriptor.path}/${baseDescriptor.className}.php`, finalContent);
101
107
  }
102
- function generateUserPolicyClass(name, config) {
108
+ function generateUserPolicyClass(name, schema, config) {
103
109
  const modelName = toPascalCase(name);
104
- // Issue #96: editable stub via resolveEditableClass + extends adapts to flatBase.
105
- const baseDescriptor = config.structure === 'modular'
106
- ? {
110
+ // Issue #96 + #98 v5.8+: split paths + flatBase + group-by-folder.
111
+ let baseDescriptor;
112
+ let editable;
113
+ if (config.structure === 'modular') {
114
+ baseDescriptor = {
107
115
  className: `${modelName}PolicyBase`,
108
116
  fqn: `${resolveModularBaseNamespace(config, name, 'Policies', config.policies.baseNamespace)}\\${modelName}PolicyBase`,
109
- }
110
- : resolveBaseClass(config.policies, `${modelName}Policy`, 'Base');
111
- const editable = config.structure === 'modular'
112
- ? { className: `${modelName}Policy`, fileName: `${modelName}Policy.php`, namespace: config.policies.namespace, path: config.policies.path }
113
- : resolveEditableClass(config.policies, modelName, 'Policy');
117
+ };
118
+ editable = { className: `${modelName}Policy`, fileName: `${modelName}Policy.php`, namespace: config.policies.namespace, path: config.policies.path };
119
+ }
120
+ else {
121
+ const baseRaw = resolveBaseClass(config.policies, `${modelName}Policy`, 'Base');
122
+ const baseNs = nestByGroup({ path: baseRaw.path, namespace: baseRaw.fqn.substring(0, baseRaw.fqn.lastIndexOf('\\')) }, schema.group).namespace;
123
+ baseDescriptor = { className: baseRaw.className, fqn: `${baseNs}\\${baseRaw.className}` };
124
+ const editRaw = resolveEditableClass(config.policies, modelName, 'Policy');
125
+ const editNested = nestByGroup({ path: editRaw.path, namespace: editRaw.namespace }, schema.group);
126
+ editable = { className: editRaw.className, fileName: editRaw.fileName, namespace: editNested.namespace, path: editNested.path };
127
+ }
114
128
  // Issue #98: alias on class-name collision (flatBase + userEditablePath).
115
129
  const baseAlias = baseDescriptor.className === editable.className
116
130
  ? `${baseDescriptor.className}Base`
@@ -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 } from './types.js';
6
+ import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup } 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.
@@ -36,8 +36,8 @@ function generateForSchema(name, schema, reader, config) {
36
36
  return [
37
37
  generateBaseRequest(name, schema, reader, config, 'Store'),
38
38
  generateBaseRequest(name, schema, reader, config, 'Update'),
39
- generateUserRequest(name, config, 'Store'),
40
- generateUserRequest(name, config, 'Update'),
39
+ generateUserRequest(name, schema, config, 'Store'),
40
+ generateUserRequest(name, schema, config, 'Update'),
41
41
  ];
42
42
  }
43
43
  function generateBaseRequest(name, schema, reader, config, action) {
@@ -257,30 +257,42 @@ ${attributeKeysList}
257
257
  }
258
258
  }
259
259
  `;
260
- // Issue #96: legacy honors flatBase. Suffix is `${action}RequestBase`
261
- // when not flat (e.g. BannerStoreRequestBase), bare `${action}Request`
262
- // when flat (BannerStoreRequest is the base; user-editable extends it).
263
- const baseDescriptor = config.structure === 'modular'
264
- ? {
260
+ // Issue #96 + #98 v5.8+: legacy honors flatBase + group-by-folder.
261
+ let baseDescriptor;
262
+ if (config.structure === 'modular') {
263
+ baseDescriptor = {
265
264
  className: `${modelName}${action}RequestBase`,
266
265
  path: resolveModularBasePath(config, name, 'Requests', '', config.requests.basePath).replace(/\/$/, ''),
267
- }
268
- : resolveBaseClass(config.requests, `${modelName}${action}Request`, 'Base');
266
+ };
267
+ }
268
+ else {
269
+ const raw = resolveBaseClass(config.requests, `${modelName}${action}Request`, 'Base');
270
+ const nested = nestByGroup({ path: raw.path, namespace: '' }, schema.group);
271
+ baseDescriptor = { className: raw.className, path: nested.path };
272
+ }
269
273
  const finalContent = content.replace(new RegExp(`class ${modelName}${action}RequestBase\\b`, 'g'), `class ${baseDescriptor.className}`).replace(new RegExp(`abstract class ${modelName}${action}RequestBase\\b`, 'g'), `abstract class ${baseDescriptor.className}`);
270
274
  return baseFile(`${baseDescriptor.path}/${baseDescriptor.className}.php`, finalContent);
271
275
  }
272
- function generateUserRequest(name, config, action) {
276
+ function generateUserRequest(name, schema, config, action) {
273
277
  const modelName = toPascalCase(name);
274
- // Issue #96: editable stub may live at userEditablePath; extends-clause adapts to flatBase.
275
- const baseDescriptor = config.structure === 'modular'
276
- ? {
278
+ // Issue #96 + #98 v5.8+: split paths + flatBase + group-by-folder.
279
+ let baseDescriptor;
280
+ let editable;
281
+ if (config.structure === 'modular') {
282
+ baseDescriptor = {
277
283
  className: `${modelName}${action}RequestBase`,
278
284
  fqn: `${resolveModularBaseNamespace(config, name, 'Requests', config.requests.baseNamespace)}\\${modelName}${action}RequestBase`,
279
- }
280
- : resolveBaseClass(config.requests, `${modelName}${action}Request`, 'Base');
281
- const editable = config.structure === 'modular'
282
- ? { className: `${modelName}${action}Request`, fileName: `${modelName}${action}Request.php`, namespace: config.requests.namespace, path: config.requests.path }
283
- : resolveEditableClass(config.requests, modelName, `${action}Request`);
285
+ };
286
+ editable = { className: `${modelName}${action}Request`, fileName: `${modelName}${action}Request.php`, namespace: config.requests.namespace, path: config.requests.path };
287
+ }
288
+ else {
289
+ const baseRaw = resolveBaseClass(config.requests, `${modelName}${action}Request`, 'Base');
290
+ const baseNs = nestByGroup({ path: baseRaw.path, namespace: baseRaw.fqn.substring(0, baseRaw.fqn.lastIndexOf('\\')) }, schema.group).namespace;
291
+ baseDescriptor = { className: baseRaw.className, fqn: `${baseNs}\\${baseRaw.className}` };
292
+ const editRaw = resolveEditableClass(config.requests, modelName, `${action}Request`);
293
+ const editNested = nestByGroup({ path: editRaw.path, namespace: editRaw.namespace }, schema.group);
294
+ editable = { className: editRaw.className, fileName: editRaw.fileName, namespace: editNested.namespace, path: editNested.path };
295
+ }
284
296
  // Issue #98: alias on class-name collision (flatBase + userEditablePath).
285
297
  const baseAlias = baseDescriptor.className === editable.className
286
298
  ? `${baseDescriptor.className}Base`
@@ -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 } from './types.js';
6
+ import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup } 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 = [];
@@ -11,7 +11,7 @@ export function generateResources(reader, config) {
11
11
  if (schema.kind === 'extend')
12
12
  continue;
13
13
  files.push(generateBaseResource(name, schema, reader, config));
14
- files.push(generateUserResource(name, config));
14
+ files.push(generateUserResource(name, schema, config));
15
15
  }
16
16
  return files;
17
17
  }
@@ -119,30 +119,42 @@ ${fieldsContent}
119
119
  }
120
120
  }
121
121
  `;
122
- // Issue #96: legacy honors flatBase (drops ResourceBase suffix +
123
- // OmnifyBase/ subfolder); modular keeps the per-module path.
124
- const baseDescriptor = config.structure === 'modular'
125
- ? {
122
+ // Issue #96 + #98 v5.8+: legacy honors flatBase + group-by-folder.
123
+ let baseDescriptor;
124
+ if (config.structure === 'modular') {
125
+ baseDescriptor = {
126
126
  className: `${modelName}ResourceBase`,
127
127
  path: resolveModularBasePath(config, name, 'Resources', '', config.resources.basePath).replace(/\/$/, ''),
128
- }
129
- : resolveBaseClass(config.resources, `${modelName}Resource`, 'Base');
128
+ };
129
+ }
130
+ else {
131
+ const raw = resolveBaseClass(config.resources, `${modelName}Resource`, 'Base');
132
+ const nested = nestByGroup({ path: raw.path, namespace: '' }, schema.group);
133
+ baseDescriptor = { className: raw.className, path: nested.path };
134
+ }
130
135
  const finalContent = content.replace(new RegExp(`class ${modelName}ResourceBase\\b`, 'g'), `class ${baseDescriptor.className}`);
131
136
  return baseFile(`${baseDescriptor.path}/${baseDescriptor.className}.php`, finalContent);
132
137
  }
133
- function generateUserResource(name, config) {
138
+ function generateUserResource(name, schema, config) {
134
139
  const modelName = toPascalCase(name);
135
- // Issue #96: editable stub may live at userEditablePath; extends-clause
136
- // adapts to flatBase too.
137
- const baseDescriptor = config.structure === 'modular'
138
- ? {
140
+ // Issue #96 + #98 v5.8+: split paths + flatBase + group-by-folder nesting.
141
+ let baseDescriptor;
142
+ let editable;
143
+ if (config.structure === 'modular') {
144
+ baseDescriptor = {
139
145
  className: `${modelName}ResourceBase`,
140
146
  fqn: `${resolveModularBaseNamespace(config, name, 'Resources', config.resources.baseNamespace)}\\${modelName}ResourceBase`,
141
- }
142
- : resolveBaseClass(config.resources, `${modelName}Resource`, 'Base');
143
- const editable = config.structure === 'modular'
144
- ? { className: `${modelName}Resource`, fileName: `${modelName}Resource.php`, namespace: config.resources.namespace, path: config.resources.path }
145
- : resolveEditableClass(config.resources, modelName, 'Resource');
147
+ };
148
+ editable = { className: `${modelName}Resource`, fileName: `${modelName}Resource.php`, namespace: config.resources.namespace, path: config.resources.path };
149
+ }
150
+ else {
151
+ const baseRaw = resolveBaseClass(config.resources, `${modelName}Resource`, 'Base');
152
+ const baseNs = nestByGroup({ path: baseRaw.path, namespace: baseRaw.fqn.substring(0, baseRaw.fqn.lastIndexOf('\\')) }, schema.group).namespace;
153
+ baseDescriptor = { className: baseRaw.className, fqn: `${baseNs}\\${baseRaw.className}` };
154
+ const editRaw = resolveEditableClass(config.resources, modelName, 'Resource');
155
+ const editNested = nestByGroup({ path: editRaw.path, namespace: editRaw.namespace }, schema.group);
156
+ editable = { className: editRaw.className, fileName: editRaw.fileName, namespace: editNested.namespace, path: editNested.path };
157
+ }
146
158
  // Issue #98: alias on class-name collision (flatBase + userEditablePath).
147
159
  const baseAlias = baseDescriptor.className === editable.className
148
160
  ? `${baseDescriptor.className}Base`
@@ -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 } from './types.js';
16
+ import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup } from './types.js';
17
17
  // ============================================================================
18
18
  // Public entry point
19
19
  // ============================================================================
@@ -593,15 +593,24 @@ class ${modelName}ServiceBase
593
593
  ${sections.join('\n')}
594
594
  }
595
595
  `;
596
- // Issue #96: legacy structure honors flatBase (drops `ServiceBase`
597
- // suffix + OmnifyBase/ subfolder); modular keeps the per-module path.
598
- const baseDescriptor = config.structure === 'modular'
599
- ? {
596
+ // Issue #96 + #98 v5.8+: legacy structure honors flatBase + group-by-folder.
597
+ let baseDescriptor;
598
+ if (config.structure === 'modular') {
599
+ baseDescriptor = {
600
600
  className: `${modelName}ServiceBase`,
601
601
  fileName: `${modelName}ServiceBase.php`,
602
602
  path: resolveModularBasePath(config, name, 'Services', '', config.services.basePath).replace(/\/$/, ''),
603
- }
604
- : resolveBaseClass(config.services, `${modelName}Service`, 'Base');
603
+ };
604
+ }
605
+ else {
606
+ const raw = resolveBaseClass(config.services, `${modelName}Service`, 'Base');
607
+ const nested = nestByGroup({ path: raw.path, namespace: '' }, schema.group);
608
+ baseDescriptor = {
609
+ className: raw.className,
610
+ fileName: `${raw.className}.php`,
611
+ path: nested.path,
612
+ };
613
+ }
605
614
  const finalContent = content.replace(new RegExp(`class ${modelName}ServiceBase\\b`, 'g'), `class ${baseDescriptor.className}`);
606
615
  return baseFile(`${baseDescriptor.path}/${baseDescriptor.fileName}`, finalContent);
607
616
  }
@@ -1749,21 +1758,31 @@ function buildFlushTranslationsMethod(modelName) {
1749
1758
  function generateUserService(name, schema, config) {
1750
1759
  const modelName = toPascalCase(name);
1751
1760
  const group = schema.group ? toPascalCase(schema.group) : null;
1752
- // Issue #96: route editable stub via resolveEditableClass +
1761
+ // Issue #96 + #98 v5.8+: route editable stub via resolveEditableClass +
1753
1762
  // resolveBaseClass so userEditablePath / userEditableNamespace and
1754
- // flatBase (drops ServiceBase suffix) are honored. Group nesting
1755
- // (`<userPath>/<Group>/<Schema>Service.php`) preserved.
1756
- const baseDescriptor = config.structure === 'modular'
1757
- ? {
1763
+ // flatBase (drops ServiceBase suffix) are honored, then nest by group.
1764
+ let baseDescriptor;
1765
+ let editable;
1766
+ if (config.structure === 'modular') {
1767
+ baseDescriptor = {
1758
1768
  className: `${modelName}ServiceBase`,
1759
1769
  fqn: `${resolveModularBaseNamespace(config, name, 'Services', config.services.baseNamespace)}\\${modelName}ServiceBase`,
1760
- }
1761
- : resolveBaseClass(config.services, `${modelName}Service`, 'Base');
1762
- const editable = config.structure === 'modular'
1763
- ? { className: `${modelName}Service`, fileName: `${modelName}Service.php`, namespace: config.services.namespace, path: config.services.path }
1764
- : resolveEditableClass(config.services, modelName, 'Service');
1765
- const userNs = group ? `${editable.namespace}\\${group}` : editable.namespace;
1766
- const userPath = group ? `${editable.path}/${group}` : editable.path;
1770
+ };
1771
+ editable = { className: `${modelName}Service`, fileName: `${modelName}Service.php`, namespace: config.services.namespace, path: config.services.path };
1772
+ }
1773
+ else {
1774
+ const baseRaw = resolveBaseClass(config.services, `${modelName}Service`, 'Base');
1775
+ const baseNs = nestByGroup({ path: baseRaw.path, namespace: baseRaw.fqn.substring(0, baseRaw.fqn.lastIndexOf('\\')) }, schema.group).namespace;
1776
+ baseDescriptor = { className: baseRaw.className, fqn: `${baseNs}\\${baseRaw.className}` };
1777
+ const editRaw = resolveEditableClass(config.services, modelName, 'Service');
1778
+ const editNested = nestByGroup({ path: editRaw.path, namespace: editRaw.namespace }, schema.group);
1779
+ editable = { className: editRaw.className, fileName: editRaw.fileName, namespace: editNested.namespace, path: editNested.path };
1780
+ }
1781
+ // Group nesting already applied via nestByGroup above; no additional
1782
+ // <Group>/ subfolder needed here. (Pre-#98 had explicit group var; now
1783
+ // baked into the nested editable.path/namespace.)
1784
+ const userNs = editable.namespace;
1785
+ const userPath = editable.path;
1767
1786
  // Issue #98: alias on class-name collision (flatBase + userEditablePath)
1768
1787
  // so `class BannerService extends BannerService` becomes
1769
1788
  // `class BannerService extends BannerServiceBase` via aliased import.
@@ -295,6 +295,29 @@ export interface PhpConfig {
295
295
  securityScheme: string;
296
296
  };
297
297
  }
298
+ /**
299
+ * Apply group-by-folder nesting (issue #98 v5.8+). When the schema's
300
+ * `group:` field is non-empty, append `<PascalGroup>` as a subfolder
301
+ * AND a sub-namespace. Schemas without `group:` are unchanged (no
302
+ * synthetic `Default/` / `Misc/` folder per the issue thread).
303
+ *
304
+ * Used by every generator's base + editable path resolution so files
305
+ * mirror the schema directory layout. With 100+ schemas across many
306
+ * groups, the flat output is unusable; nesting matches what devs
307
+ * already expect since the schemas themselves live at
308
+ * `schemas/<group>/<Name>.yaml`.
309
+ *
310
+ * Only applies to LEGACY structure; modular structure already nests
311
+ * per-Schema (`app/Omnify/Modules/<Schema>/<Layer>/`) so adding group
312
+ * on top would be redundant.
313
+ */
314
+ export declare function nestByGroup(base: {
315
+ path: string;
316
+ namespace: string;
317
+ }, group: string | undefined): {
318
+ path: string;
319
+ namespace: string;
320
+ };
298
321
  /**
299
322
  * Class-name + file-name resolver for the BASE class of a layer. Returns
300
323
  * the bare schema name when `layer.flatBase` is true (e.g. `Banner.php`
package/dist/php/types.js CHANGED
@@ -101,6 +101,31 @@ export function resolveGlobalTraitNamespace(config, legacyNamespace) {
101
101
  }
102
102
  return legacyNamespace;
103
103
  }
104
+ /**
105
+ * Apply group-by-folder nesting (issue #98 v5.8+). When the schema's
106
+ * `group:` field is non-empty, append `<PascalGroup>` as a subfolder
107
+ * AND a sub-namespace. Schemas without `group:` are unchanged (no
108
+ * synthetic `Default/` / `Misc/` folder per the issue thread).
109
+ *
110
+ * Used by every generator's base + editable path resolution so files
111
+ * mirror the schema directory layout. With 100+ schemas across many
112
+ * groups, the flat output is unusable; nesting matches what devs
113
+ * already expect since the schemas themselves live at
114
+ * `schemas/<group>/<Name>.yaml`.
115
+ *
116
+ * Only applies to LEGACY structure; modular structure already nests
117
+ * per-Schema (`app/Omnify/Modules/<Schema>/<Layer>/`) so adding group
118
+ * on top would be redundant.
119
+ */
120
+ export function nestByGroup(base, group) {
121
+ if (!group)
122
+ return base;
123
+ const pascal = group.charAt(0).toUpperCase() + group.slice(1).replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
124
+ return {
125
+ path: `${base.path}/${pascal}`,
126
+ namespace: `${base.namespace}\\${pascal}`,
127
+ };
128
+ }
104
129
  /** Convert a file path to a PHP namespace (e.g., app/Models/Omnify → App\Models\Omnify). */
105
130
  function pathToNamespace(p) {
106
131
  return p
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "5.7.0",
3
+ "version": "5.8.0",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",