@omnifyjp/ts 2.1.11 → 3.0.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/php/controller-generator.d.ts +7 -0
- package/dist/php/controller-generator.js +178 -0
- package/dist/php/index.js +11 -0
- package/dist/php/route-generator.d.ts +10 -0
- package/dist/php/route-generator.js +86 -0
- package/dist/php/schema-config-generator.d.ts +10 -0
- package/dist/php/schema-config-generator.js +74 -0
- package/dist/php/schema-reader.d.ts +4 -0
- package/dist/php/schema-reader.js +14 -0
- package/dist/php/service-generator.d.ts +7 -0
- package/dist/php/service-generator.js +319 -0
- package/dist/php/types.d.ts +18 -0
- package/dist/php/types.js +23 -0
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates base + editable controller classes for schemas with `options.api`.
|
|
3
|
+
*/
|
|
4
|
+
import { SchemaReader } from './schema-reader.js';
|
|
5
|
+
import type { GeneratedFile, PhpConfig } from './types.js';
|
|
6
|
+
/** Generate controller classes for all schemas with api config. */
|
|
7
|
+
export declare function generateControllers(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates base + editable controller classes for schemas with `options.api`.
|
|
3
|
+
*/
|
|
4
|
+
import { toPascalCase } from './naming-helper.js';
|
|
5
|
+
import { baseFile, userFile } from './types.js';
|
|
6
|
+
const DEFAULT_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
|
|
7
|
+
/** Generate controller classes for all schemas with api config. */
|
|
8
|
+
export function generateControllers(reader, config) {
|
|
9
|
+
const files = [];
|
|
10
|
+
for (const [name, schema] of Object.entries(reader.getSchemasWithApi())) {
|
|
11
|
+
files.push(...generateForSchema(name, schema, reader, config));
|
|
12
|
+
}
|
|
13
|
+
return files;
|
|
14
|
+
}
|
|
15
|
+
function generateForSchema(name, schema, reader, config) {
|
|
16
|
+
return [
|
|
17
|
+
generateBaseController(name, schema, reader, config),
|
|
18
|
+
generateUserController(name, schema, config),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
function generateBaseController(name, schema, reader, config) {
|
|
22
|
+
const modelName = toPascalCase(name);
|
|
23
|
+
const api = schema.options?.api ?? {};
|
|
24
|
+
const actions = [...(api.actions ?? DEFAULT_ACTIONS)];
|
|
25
|
+
const lookup = api.lookup ?? true;
|
|
26
|
+
const bulkDelete = api.bulkDelete ?? false;
|
|
27
|
+
const restore = api.restore ?? (schema.options?.softDelete ?? false);
|
|
28
|
+
const baseNs = config.controllers.baseNamespace;
|
|
29
|
+
const modelNs = config.models.namespace;
|
|
30
|
+
const serviceBaseNs = config.services.baseNamespace;
|
|
31
|
+
const requestNs = config.requests.namespace;
|
|
32
|
+
const resourceNs = config.resources.namespace;
|
|
33
|
+
// Build imports
|
|
34
|
+
const imports = [
|
|
35
|
+
`use App\\Http\\Controllers\\Controller;`,
|
|
36
|
+
`use ${serviceBaseNs}\\${modelName}ServiceBase;`,
|
|
37
|
+
];
|
|
38
|
+
if (actions.includes('store')) {
|
|
39
|
+
imports.push(`use ${requestNs}\\${modelName}StoreRequest;`);
|
|
40
|
+
}
|
|
41
|
+
if (actions.includes('update')) {
|
|
42
|
+
imports.push(`use ${requestNs}\\${modelName}UpdateRequest;`);
|
|
43
|
+
}
|
|
44
|
+
if (actions.includes('show') || actions.includes('update') || actions.includes('destroy') || restore) {
|
|
45
|
+
imports.push(`use ${resourceNs}\\${modelName}Resource;`);
|
|
46
|
+
}
|
|
47
|
+
imports.push(`use ${modelNs}\\${modelName};`);
|
|
48
|
+
imports.push(`use Illuminate\\Http\\JsonResponse;`);
|
|
49
|
+
imports.push(`use Illuminate\\Http\\Request;`);
|
|
50
|
+
if (actions.includes('index') || lookup) {
|
|
51
|
+
imports.push(`use Illuminate\\Http\\Resources\\Json\\AnonymousResourceCollection;`);
|
|
52
|
+
}
|
|
53
|
+
// Build methods
|
|
54
|
+
const methods = [];
|
|
55
|
+
if (actions.includes('index')) {
|
|
56
|
+
methods.push(`
|
|
57
|
+
public function index(Request $request): AnonymousResourceCollection
|
|
58
|
+
{
|
|
59
|
+
$result = $this->service->list($request->all());
|
|
60
|
+
|
|
61
|
+
return ${modelName}Resource::collection($result);
|
|
62
|
+
}`);
|
|
63
|
+
}
|
|
64
|
+
if (actions.includes('store')) {
|
|
65
|
+
methods.push(`
|
|
66
|
+
public function store(${modelName}StoreRequest $request): JsonResponse
|
|
67
|
+
{
|
|
68
|
+
$model = $this->service->create($request->validated());
|
|
69
|
+
|
|
70
|
+
return (new ${modelName}Resource($model))
|
|
71
|
+
->response()
|
|
72
|
+
->setStatusCode(201);
|
|
73
|
+
}`);
|
|
74
|
+
}
|
|
75
|
+
if (actions.includes('show')) {
|
|
76
|
+
methods.push(`
|
|
77
|
+
public function show(${modelName} $model): ${modelName}Resource
|
|
78
|
+
{
|
|
79
|
+
return new ${modelName}Resource($model);
|
|
80
|
+
}`);
|
|
81
|
+
}
|
|
82
|
+
if (actions.includes('update')) {
|
|
83
|
+
methods.push(`
|
|
84
|
+
public function update(${modelName}UpdateRequest $request, ${modelName} $model): ${modelName}Resource
|
|
85
|
+
{
|
|
86
|
+
$model = $this->service->update($model, $request->validated());
|
|
87
|
+
|
|
88
|
+
return new ${modelName}Resource($model);
|
|
89
|
+
}`);
|
|
90
|
+
}
|
|
91
|
+
if (actions.includes('destroy')) {
|
|
92
|
+
methods.push(`
|
|
93
|
+
public function destroy(${modelName} $model): JsonResponse
|
|
94
|
+
{
|
|
95
|
+
$this->service->delete($model);
|
|
96
|
+
|
|
97
|
+
return response()->json(null, 204);
|
|
98
|
+
}`);
|
|
99
|
+
}
|
|
100
|
+
if (lookup) {
|
|
101
|
+
methods.push(`
|
|
102
|
+
public function lookup(Request $request): AnonymousResourceCollection
|
|
103
|
+
{
|
|
104
|
+
$result = $this->service->lookup($request->all());
|
|
105
|
+
|
|
106
|
+
return ${modelName}Resource::collection($result);
|
|
107
|
+
}`);
|
|
108
|
+
}
|
|
109
|
+
if (bulkDelete) {
|
|
110
|
+
methods.push(`
|
|
111
|
+
public function bulkDelete(Request $request): JsonResponse
|
|
112
|
+
{
|
|
113
|
+
$this->service->bulkDelete($request->input('ids', []));
|
|
114
|
+
|
|
115
|
+
return response()->json(null, 204);
|
|
116
|
+
}`);
|
|
117
|
+
}
|
|
118
|
+
if (restore) {
|
|
119
|
+
methods.push(`
|
|
120
|
+
public function restore(string $id): ${modelName}Resource
|
|
121
|
+
{
|
|
122
|
+
$model = $this->service->restore($id);
|
|
123
|
+
|
|
124
|
+
return new ${modelName}Resource($model);
|
|
125
|
+
}`);
|
|
126
|
+
}
|
|
127
|
+
const content = `<?php
|
|
128
|
+
|
|
129
|
+
namespace ${baseNs};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
133
|
+
* Any changes will be overwritten on next generation.
|
|
134
|
+
*
|
|
135
|
+
* @generated by omnify
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
${imports.join('\n')}
|
|
139
|
+
|
|
140
|
+
abstract class ${modelName}ControllerBase extends Controller
|
|
141
|
+
{
|
|
142
|
+
public function __construct(
|
|
143
|
+
protected readonly ${modelName}ServiceBase $service,
|
|
144
|
+
) {}
|
|
145
|
+
${methods.join('\n')}
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
return baseFile(`${config.controllers.basePath}/${modelName}ControllerBase.php`, content);
|
|
149
|
+
}
|
|
150
|
+
function generateUserController(name, schema, config) {
|
|
151
|
+
const modelName = toPascalCase(name);
|
|
152
|
+
const group = schema.group ? toPascalCase(schema.group) : null;
|
|
153
|
+
const baseNs = config.controllers.baseNamespace;
|
|
154
|
+
const userNs = group
|
|
155
|
+
? `${config.controllers.namespace}\\Api\\V1\\${group}`
|
|
156
|
+
: `${config.controllers.namespace}\\Api\\V1`;
|
|
157
|
+
const userPath = group
|
|
158
|
+
? `${config.controllers.path}/Api/V1/${group}`
|
|
159
|
+
: `${config.controllers.path}/Api/V1`;
|
|
160
|
+
const content = `<?php
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* ${modelName} Controller
|
|
164
|
+
*
|
|
165
|
+
* SAFE TO EDIT - This file is never overwritten by Omnify.
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
namespace ${userNs};
|
|
169
|
+
|
|
170
|
+
use ${baseNs}\\${modelName}ControllerBase;
|
|
171
|
+
|
|
172
|
+
class ${modelName}Controller extends ${modelName}ControllerBase
|
|
173
|
+
{
|
|
174
|
+
//
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
177
|
+
return userFile(`${userPath}/${modelName}Controller.php`, content);
|
|
178
|
+
}
|
package/dist/php/index.js
CHANGED
|
@@ -25,6 +25,10 @@ import { generatePolicies } from './policy-generator.js';
|
|
|
25
25
|
import { generateFileModels } from './file-model-generator.js';
|
|
26
26
|
import { generateFileTrait } from './file-trait-generator.js';
|
|
27
27
|
import { generateFileCleanup } from './file-cleanup-generator.js';
|
|
28
|
+
import { generateSchemaConfig } from './schema-config-generator.js';
|
|
29
|
+
import { generateControllers } from './controller-generator.js';
|
|
30
|
+
import { generateServices } from './service-generator.js';
|
|
31
|
+
import { generateRoutes } from './route-generator.js';
|
|
28
32
|
export { derivePhpConfig } from './types.js';
|
|
29
33
|
/** Generate all PHP files from schemas.json data. */
|
|
30
34
|
export function generatePhp(data, overrides) {
|
|
@@ -42,6 +46,13 @@ export function generatePhp(data, overrides) {
|
|
|
42
46
|
files.push(...generateFileFactory(reader, config));
|
|
43
47
|
files.push(...generateFileCleanup(reader, config));
|
|
44
48
|
}
|
|
49
|
+
// CRUD API files (only for schemas with options.api)
|
|
50
|
+
if (reader.hasApiSchemas()) {
|
|
51
|
+
files.push(...generateSchemaConfig(reader, config));
|
|
52
|
+
files.push(...generateControllers(reader, config));
|
|
53
|
+
files.push(...generateServices(reader, config));
|
|
54
|
+
files.push(...generateRoutes(reader, config));
|
|
55
|
+
}
|
|
45
56
|
// Per-schema files
|
|
46
57
|
files.push(...generateLocales(reader, config));
|
|
47
58
|
files.push(...generateModels(reader, config));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates route files for schemas with `options.api`.
|
|
3
|
+
*
|
|
4
|
+
* Each schema gets its own route file at routes/api/omnify/{snake_name}.php.
|
|
5
|
+
* Always overwritten.
|
|
6
|
+
*/
|
|
7
|
+
import { SchemaReader } from './schema-reader.js';
|
|
8
|
+
import type { GeneratedFile, PhpConfig } from './types.js';
|
|
9
|
+
/** Generate route files for all schemas with api config. */
|
|
10
|
+
export declare function generateRoutes(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates route files for schemas with `options.api`.
|
|
3
|
+
*
|
|
4
|
+
* Each schema gets its own route file at routes/api/omnify/{snake_name}.php.
|
|
5
|
+
* Always overwritten.
|
|
6
|
+
*/
|
|
7
|
+
import { toPascalCase, toCamelCase, toSnakeCase, pluralize } from './naming-helper.js';
|
|
8
|
+
import { baseFile } from './types.js';
|
|
9
|
+
const DEFAULT_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
|
|
10
|
+
/** Generate route files for all schemas with api config. */
|
|
11
|
+
export function generateRoutes(reader, config) {
|
|
12
|
+
const files = [];
|
|
13
|
+
for (const [name, schema] of Object.entries(reader.getSchemasWithApi())) {
|
|
14
|
+
files.push(generateRouteFile(name, schema, config));
|
|
15
|
+
}
|
|
16
|
+
return files;
|
|
17
|
+
}
|
|
18
|
+
function generateRouteFile(name, schema, config) {
|
|
19
|
+
const modelName = toPascalCase(name);
|
|
20
|
+
const api = schema.options?.api ?? {};
|
|
21
|
+
const actions = [...(api.actions ?? DEFAULT_ACTIONS)];
|
|
22
|
+
const lookup = api.lookup ?? true;
|
|
23
|
+
const bulkDelete = api.bulkDelete ?? false;
|
|
24
|
+
const restore = api.restore ?? (schema.options?.softDelete ?? false);
|
|
25
|
+
const middleware = api.middleware ?? [];
|
|
26
|
+
// Route prefix: use api.prefix or default to snake_case plural
|
|
27
|
+
const prefix = api.prefix ?? pluralize(toSnakeCase(name));
|
|
28
|
+
// Route param name: camelCase singular
|
|
29
|
+
const routeParam = toCamelCase(name);
|
|
30
|
+
// Controller namespace
|
|
31
|
+
const group = schema.group ? toPascalCase(schema.group) : null;
|
|
32
|
+
const controllerNs = group
|
|
33
|
+
? `${config.controllers.namespace}\\Api\\V1\\${group}`
|
|
34
|
+
: `${config.controllers.namespace}\\Api\\V1`;
|
|
35
|
+
// Build use statements
|
|
36
|
+
const useStatements = [
|
|
37
|
+
`use ${controllerNs}\\${modelName}Controller;`,
|
|
38
|
+
`use Illuminate\\Support\\Facades\\Route;`,
|
|
39
|
+
];
|
|
40
|
+
// Build route lines
|
|
41
|
+
const routeLines = [];
|
|
42
|
+
// Lookup must come before resource-like routes (before {param})
|
|
43
|
+
if (lookup) {
|
|
44
|
+
routeLines.push(` Route::get('lookup', [${modelName}Controller::class, 'lookup'])->name('lookup');`);
|
|
45
|
+
}
|
|
46
|
+
// Bulk delete (POST to avoid conflicts with single delete)
|
|
47
|
+
if (bulkDelete) {
|
|
48
|
+
routeLines.push(` Route::post('bulk-delete', [${modelName}Controller::class, 'bulkDelete'])->name('bulkDelete');`);
|
|
49
|
+
}
|
|
50
|
+
// Standard CRUD routes
|
|
51
|
+
if (actions.includes('index')) {
|
|
52
|
+
routeLines.push(` Route::get('/', [${modelName}Controller::class, 'index'])->name('index');`);
|
|
53
|
+
}
|
|
54
|
+
if (actions.includes('store')) {
|
|
55
|
+
routeLines.push(` Route::post('/', [${modelName}Controller::class, 'store'])->name('store');`);
|
|
56
|
+
}
|
|
57
|
+
if (actions.includes('show')) {
|
|
58
|
+
routeLines.push(` Route::get('{${routeParam}}', [${modelName}Controller::class, 'show'])->name('show');`);
|
|
59
|
+
}
|
|
60
|
+
if (actions.includes('update')) {
|
|
61
|
+
routeLines.push(` Route::put('{${routeParam}}', [${modelName}Controller::class, 'update'])->name('update');`);
|
|
62
|
+
}
|
|
63
|
+
if (actions.includes('destroy')) {
|
|
64
|
+
routeLines.push(` Route::delete('{${routeParam}}', [${modelName}Controller::class, 'destroy'])->name('destroy');`);
|
|
65
|
+
}
|
|
66
|
+
// Restore (soft delete)
|
|
67
|
+
if (restore) {
|
|
68
|
+
routeLines.push(` Route::post('{${routeParam}}/restore', [${modelName}Controller::class, 'restore'])->name('restore');`);
|
|
69
|
+
}
|
|
70
|
+
// Build middleware block
|
|
71
|
+
const middlewareChain = middleware.length > 0
|
|
72
|
+
? `->middleware([${middleware.map(m => `'${m}'`).join(', ')}])`
|
|
73
|
+
: '';
|
|
74
|
+
const content = `<?php
|
|
75
|
+
|
|
76
|
+
// Auto-generated by Omnify. DO NOT EDIT.
|
|
77
|
+
|
|
78
|
+
${useStatements.join('\n')}
|
|
79
|
+
|
|
80
|
+
Route::prefix('${prefix}')->name('api.v1.${prefix}.')${middlewareChain}->group(function () {
|
|
81
|
+
${routeLines.join('\n')}
|
|
82
|
+
});
|
|
83
|
+
`;
|
|
84
|
+
const snakeName = toSnakeCase(name);
|
|
85
|
+
return baseFile(`${config.routes.path}/${snakeName}.php`, content);
|
|
86
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates config/omnify-schemas.php — schema metadata for the Query Engine.
|
|
3
|
+
*
|
|
4
|
+
* Only includes schemas with `options.api` set.
|
|
5
|
+
* Only includes properties that have searchable/filterable/sortable flags.
|
|
6
|
+
*/
|
|
7
|
+
import { SchemaReader } from './schema-reader.js';
|
|
8
|
+
import type { GeneratedFile, PhpConfig } from './types.js';
|
|
9
|
+
/** Generate the omnify-schemas config file. */
|
|
10
|
+
export declare function generateSchemaConfig(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates config/omnify-schemas.php — schema metadata for the Query Engine.
|
|
3
|
+
*
|
|
4
|
+
* Only includes schemas with `options.api` set.
|
|
5
|
+
* Only includes properties that have searchable/filterable/sortable flags.
|
|
6
|
+
*/
|
|
7
|
+
import { toPascalCase, toSnakeCase } from './naming-helper.js';
|
|
8
|
+
import { baseFile } from './types.js';
|
|
9
|
+
/** Generate the omnify-schemas config file. */
|
|
10
|
+
export function generateSchemaConfig(reader, config) {
|
|
11
|
+
const apiSchemas = reader.getSchemasWithApi();
|
|
12
|
+
if (Object.keys(apiSchemas).length === 0)
|
|
13
|
+
return [];
|
|
14
|
+
const entries = [];
|
|
15
|
+
for (const [name, schema] of Object.entries(apiSchemas)) {
|
|
16
|
+
entries.push(generateSchemaEntry(name, schema, reader, config));
|
|
17
|
+
}
|
|
18
|
+
const content = `<?php
|
|
19
|
+
|
|
20
|
+
// Auto-generated by Omnify. DO NOT EDIT.
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
${entries.join('\n')}];
|
|
24
|
+
`;
|
|
25
|
+
return [baseFile('config/omnify-schemas.php', content)];
|
|
26
|
+
}
|
|
27
|
+
function generateSchemaEntry(name, schema, reader, config) {
|
|
28
|
+
const modelName = toPascalCase(name);
|
|
29
|
+
const tableName = reader.getTableName(name);
|
|
30
|
+
const softDelete = schema.options?.softDelete ?? false;
|
|
31
|
+
const perPage = schema.options?.api?.perPage ?? 15;
|
|
32
|
+
const properties = schema.properties ?? {};
|
|
33
|
+
const propertyOrder = reader.getPropertyOrder(name);
|
|
34
|
+
const propLines = [];
|
|
35
|
+
for (const propName of propertyOrder) {
|
|
36
|
+
const prop = properties[propName];
|
|
37
|
+
if (!prop)
|
|
38
|
+
continue;
|
|
39
|
+
const searchable = prop.searchable ?? false;
|
|
40
|
+
const filterable = prop.filterable ?? false;
|
|
41
|
+
const sortable = prop.sortable ?? false;
|
|
42
|
+
// Only include properties with at least one query flag
|
|
43
|
+
if (!searchable && !filterable && !sortable)
|
|
44
|
+
continue;
|
|
45
|
+
const colName = toSnakeCase(propName);
|
|
46
|
+
const attrs = [];
|
|
47
|
+
attrs.push(`'type' => '${prop.type}'`);
|
|
48
|
+
if (searchable)
|
|
49
|
+
attrs.push(`'searchable' => true`);
|
|
50
|
+
if (filterable)
|
|
51
|
+
attrs.push(`'filterable' => true`);
|
|
52
|
+
if (sortable)
|
|
53
|
+
attrs.push(`'sortable' => true`);
|
|
54
|
+
// Include enum values for Enum types
|
|
55
|
+
if ((prop.type === 'Enum' || prop.type === 'EnumRef') && prop.enum) {
|
|
56
|
+
const enumArr = typeof prop.enum === 'string' ? [prop.enum] : [...prop.enum];
|
|
57
|
+
if (enumArr.length > 0) {
|
|
58
|
+
const enumValues = enumArr.map(v => `'${v}'`).join(', ');
|
|
59
|
+
attrs.push(`'enum' => [${enumValues}]`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
propLines.push(` '${colName}' => [${attrs.join(', ')}],`);
|
|
63
|
+
}
|
|
64
|
+
const propsBlock = propLines.length > 0
|
|
65
|
+
? `\n 'properties' => [\n${propLines.join('\n')}\n ],`
|
|
66
|
+
: `\n 'properties' => [],`;
|
|
67
|
+
return ` '${modelName}' => [
|
|
68
|
+
'table' => '${tableName}',
|
|
69
|
+
'model' => \\${config.models.namespace}\\${modelName}::class,
|
|
70
|
+
'softDelete' => ${softDelete ? 'true' : 'false'},
|
|
71
|
+
'perPage' => ${perPage},${propsBlock}
|
|
72
|
+
],
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
@@ -47,6 +47,10 @@ export declare class SchemaReader {
|
|
|
47
47
|
getExpandedProperties(schemaName: string): Record<string, ExpandedProperty>;
|
|
48
48
|
getPropertyOrder(schemaName: string): string[];
|
|
49
49
|
getTableName(schemaName: string): string;
|
|
50
|
+
/** Get schemas that have api options configured. */
|
|
51
|
+
getSchemasWithApi(): Record<string, SchemaDefinition>;
|
|
52
|
+
/** Check if any schema has api options. */
|
|
53
|
+
hasApiSchemas(): boolean;
|
|
50
54
|
/** Check if any schema has File-type properties. */
|
|
51
55
|
hasFileProperties(): boolean;
|
|
52
56
|
/** Get the file config from schemas.json. */
|
|
@@ -158,6 +158,20 @@ export class SchemaReader {
|
|
|
158
158
|
const schema = this.getSchema(schemaName);
|
|
159
159
|
return schema?.tableName ?? '';
|
|
160
160
|
}
|
|
161
|
+
/** Get schemas that have api options configured. */
|
|
162
|
+
getSchemasWithApi() {
|
|
163
|
+
const result = {};
|
|
164
|
+
for (const [name, schema] of Object.entries(this.getProjectVisibleObjectSchemas())) {
|
|
165
|
+
if (schema.options?.api) {
|
|
166
|
+
result[name] = schema;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
/** Check if any schema has api options. */
|
|
172
|
+
hasApiSchemas() {
|
|
173
|
+
return Object.keys(this.getSchemasWithApi()).length > 0;
|
|
174
|
+
}
|
|
161
175
|
/** Check if any schema has File-type properties. */
|
|
162
176
|
hasFileProperties() {
|
|
163
177
|
for (const schema of Object.values(this.getObjectSchemas())) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates base + editable service classes for schemas with `options.api`.
|
|
3
|
+
*/
|
|
4
|
+
import { SchemaReader } from './schema-reader.js';
|
|
5
|
+
import type { GeneratedFile, PhpConfig } from './types.js';
|
|
6
|
+
/** Generate service classes for all schemas with api config. */
|
|
7
|
+
export declare function generateServices(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates base + editable service classes for schemas with `options.api`.
|
|
3
|
+
*/
|
|
4
|
+
import { toPascalCase, toSnakeCase } from './naming-helper.js';
|
|
5
|
+
import { baseFile, userFile } from './types.js';
|
|
6
|
+
/** Generate service classes for all schemas with api config. */
|
|
7
|
+
export function generateServices(reader, config) {
|
|
8
|
+
const files = [];
|
|
9
|
+
for (const [name, schema] of Object.entries(reader.getSchemasWithApi())) {
|
|
10
|
+
files.push(...generateForSchema(name, schema, reader, config));
|
|
11
|
+
}
|
|
12
|
+
return files;
|
|
13
|
+
}
|
|
14
|
+
function generateForSchema(name, schema, reader, config) {
|
|
15
|
+
return [
|
|
16
|
+
generateBaseService(name, schema, reader, config),
|
|
17
|
+
generateUserService(name, schema, config),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Base service generation
|
|
22
|
+
// ============================================================================
|
|
23
|
+
function generateBaseService(name, schema, reader, config) {
|
|
24
|
+
const modelName = toPascalCase(name);
|
|
25
|
+
const api = schema.options?.api ?? {};
|
|
26
|
+
const bulkDelete = api.bulkDelete ?? false;
|
|
27
|
+
const restore = api.restore ?? (schema.options?.softDelete ?? false);
|
|
28
|
+
const perPage = api.perPage ?? 15;
|
|
29
|
+
const lookup = api.lookup ?? true;
|
|
30
|
+
const baseNs = config.services.baseNamespace;
|
|
31
|
+
const modelNs = config.models.namespace;
|
|
32
|
+
const properties = schema.properties ?? {};
|
|
33
|
+
const propertyOrder = reader.getPropertyOrder(name);
|
|
34
|
+
// Collect searchable, filterable, sortable fields
|
|
35
|
+
const searchableFields = [];
|
|
36
|
+
const filterableFields = [];
|
|
37
|
+
const sortableFields = [];
|
|
38
|
+
for (const propName of propertyOrder) {
|
|
39
|
+
const prop = properties[propName];
|
|
40
|
+
if (!prop)
|
|
41
|
+
continue;
|
|
42
|
+
const colName = toSnakeCase(propName);
|
|
43
|
+
if (prop.searchable) {
|
|
44
|
+
searchableFields.push({ propName, colName });
|
|
45
|
+
}
|
|
46
|
+
if (prop.filterable) {
|
|
47
|
+
filterableFields.push({ propName, colName, type: prop.type, prop });
|
|
48
|
+
}
|
|
49
|
+
if (prop.sortable) {
|
|
50
|
+
sortableFields.push(colName);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Build search block
|
|
54
|
+
const searchBlock = buildSearchBlock(searchableFields);
|
|
55
|
+
// Build filter block
|
|
56
|
+
const filterBlock = buildFilterBlock(filterableFields);
|
|
57
|
+
// Build sort block
|
|
58
|
+
const sortBlock = buildSortBlock(sortableFields);
|
|
59
|
+
// Build additional imports
|
|
60
|
+
const imports = [
|
|
61
|
+
`use ${modelNs}\\${modelName};`,
|
|
62
|
+
`use Illuminate\\Contracts\\Pagination\\LengthAwarePaginator;`,
|
|
63
|
+
];
|
|
64
|
+
if (restore) {
|
|
65
|
+
imports.push(`use Illuminate\\Database\\Eloquent\\Collection;`);
|
|
66
|
+
}
|
|
67
|
+
// Build methods
|
|
68
|
+
const methods = [];
|
|
69
|
+
// list method
|
|
70
|
+
methods.push(`
|
|
71
|
+
/**
|
|
72
|
+
* List ${modelName} records with search, filter, and sort.
|
|
73
|
+
*
|
|
74
|
+
* @param array<string, mixed> $filters
|
|
75
|
+
*/
|
|
76
|
+
public function list(array $filters): LengthAwarePaginator
|
|
77
|
+
{
|
|
78
|
+
$query = ${modelName}::query();
|
|
79
|
+
|
|
80
|
+
// Organization scope
|
|
81
|
+
if (isset($filters['organization_id'])) {
|
|
82
|
+
$query->where('organization_id', $filters['organization_id']);
|
|
83
|
+
}
|
|
84
|
+
${searchBlock}${filterBlock}${sortBlock}
|
|
85
|
+
return $query->paginate($filters['per_page'] ?? ${perPage});
|
|
86
|
+
}`);
|
|
87
|
+
// lookup method
|
|
88
|
+
if (lookup) {
|
|
89
|
+
methods.push(`
|
|
90
|
+
/**
|
|
91
|
+
* Lookup ${modelName} records for select/combobox (lightweight).
|
|
92
|
+
*
|
|
93
|
+
* @param array<string, mixed> $filters
|
|
94
|
+
*/
|
|
95
|
+
public function lookup(array $filters): LengthAwarePaginator
|
|
96
|
+
{
|
|
97
|
+
$query = ${modelName}::query();
|
|
98
|
+
|
|
99
|
+
if (isset($filters['organization_id'])) {
|
|
100
|
+
$query->where('organization_id', $filters['organization_id']);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if ($search = $filters['search'] ?? null) {
|
|
104
|
+
$query->where(function ($q) use ($search) {${searchableFields.length > 0 ? `\n${searchableFields.map((f, i) => ` $q->${i === 0 ? 'where' : 'orWhere'}('${f.colName}', 'like', "%{$search}%");`).join('\n')}` : `\n // No searchable fields defined`}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return $query->paginate($filters['per_page'] ?? 50);
|
|
109
|
+
}`);
|
|
110
|
+
}
|
|
111
|
+
// create method
|
|
112
|
+
methods.push(`
|
|
113
|
+
/**
|
|
114
|
+
* Create a new ${modelName}.
|
|
115
|
+
*
|
|
116
|
+
* @param array<string, mixed> $data
|
|
117
|
+
*/
|
|
118
|
+
public function create(array $data): ${modelName}
|
|
119
|
+
{
|
|
120
|
+
return ${modelName}::create($data);
|
|
121
|
+
}`);
|
|
122
|
+
// update method
|
|
123
|
+
methods.push(`
|
|
124
|
+
/**
|
|
125
|
+
* Update an existing ${modelName}.
|
|
126
|
+
*
|
|
127
|
+
* @param array<string, mixed> $data
|
|
128
|
+
*/
|
|
129
|
+
public function update(${modelName} $model, array $data): ${modelName}
|
|
130
|
+
{
|
|
131
|
+
$model->update($data);
|
|
132
|
+
|
|
133
|
+
return $model->fresh();
|
|
134
|
+
}`);
|
|
135
|
+
// delete method
|
|
136
|
+
methods.push(`
|
|
137
|
+
/**
|
|
138
|
+
* Delete a ${modelName}.
|
|
139
|
+
*/
|
|
140
|
+
public function delete(${modelName} $model): void
|
|
141
|
+
{
|
|
142
|
+
$model->delete();
|
|
143
|
+
}`);
|
|
144
|
+
// bulkDelete method
|
|
145
|
+
if (bulkDelete) {
|
|
146
|
+
methods.push(`
|
|
147
|
+
/**
|
|
148
|
+
* Delete multiple ${modelName} records by IDs.
|
|
149
|
+
*
|
|
150
|
+
* @param array<int|string> $ids
|
|
151
|
+
*/
|
|
152
|
+
public function bulkDelete(array $ids): void
|
|
153
|
+
{
|
|
154
|
+
${modelName}::whereIn('id', $ids)->delete();
|
|
155
|
+
}`);
|
|
156
|
+
}
|
|
157
|
+
// restore method
|
|
158
|
+
if (restore) {
|
|
159
|
+
methods.push(`
|
|
160
|
+
/**
|
|
161
|
+
* Restore a soft-deleted ${modelName}.
|
|
162
|
+
*/
|
|
163
|
+
public function restore(string $id): ${modelName}
|
|
164
|
+
{
|
|
165
|
+
$model = ${modelName}::withTrashed()->findOrFail($id);
|
|
166
|
+
$model->restore();
|
|
167
|
+
|
|
168
|
+
return $model->fresh();
|
|
169
|
+
}`);
|
|
170
|
+
}
|
|
171
|
+
const content = `<?php
|
|
172
|
+
|
|
173
|
+
namespace ${baseNs};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
177
|
+
* Any changes will be overwritten on next generation.
|
|
178
|
+
*
|
|
179
|
+
* @generated by omnify
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
${imports.join('\n')}
|
|
183
|
+
|
|
184
|
+
class ${modelName}ServiceBase
|
|
185
|
+
{
|
|
186
|
+
${methods.join('\n')}
|
|
187
|
+
}
|
|
188
|
+
`;
|
|
189
|
+
return baseFile(`${config.services.basePath}/${modelName}ServiceBase.php`, content);
|
|
190
|
+
}
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Search / Filter / Sort block builders
|
|
193
|
+
// ============================================================================
|
|
194
|
+
function buildSearchBlock(fields) {
|
|
195
|
+
if (fields.length === 0)
|
|
196
|
+
return '';
|
|
197
|
+
const clauses = fields.map((f, i) => {
|
|
198
|
+
const method = i === 0 ? 'where' : 'orWhere';
|
|
199
|
+
return ` $q->${method}('${f.colName}', 'like', "%{$search}%");`;
|
|
200
|
+
});
|
|
201
|
+
return `
|
|
202
|
+
// Search
|
|
203
|
+
if ($search = $filters['search'] ?? null) {
|
|
204
|
+
$query->where(function ($q) use ($search) {
|
|
205
|
+
${clauses.join('\n')}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
function buildFilterBlock(fields) {
|
|
211
|
+
if (fields.length === 0)
|
|
212
|
+
return '';
|
|
213
|
+
const lines = [];
|
|
214
|
+
lines.push('');
|
|
215
|
+
lines.push(' // Filters');
|
|
216
|
+
for (const f of fields) {
|
|
217
|
+
const { colName, type } = f;
|
|
218
|
+
switch (type) {
|
|
219
|
+
case 'Decimal':
|
|
220
|
+
case 'Int':
|
|
221
|
+
case 'Float':
|
|
222
|
+
case 'Integer':
|
|
223
|
+
// Range filters: field_min, field_max
|
|
224
|
+
lines.push(` if (isset($filters['${colName}_min'])) {`);
|
|
225
|
+
lines.push(` $query->where('${colName}', '>=', $filters['${colName}_min']);`);
|
|
226
|
+
lines.push(` }`);
|
|
227
|
+
lines.push(` if (isset($filters['${colName}_max'])) {`);
|
|
228
|
+
lines.push(` $query->where('${colName}', '<=', $filters['${colName}_max']);`);
|
|
229
|
+
lines.push(` }`);
|
|
230
|
+
break;
|
|
231
|
+
case 'Date':
|
|
232
|
+
case 'Timestamp':
|
|
233
|
+
case 'DateTime':
|
|
234
|
+
// Date range filters: field_from, field_to
|
|
235
|
+
lines.push(` if (isset($filters['${colName}_from'])) {`);
|
|
236
|
+
lines.push(` $query->where('${colName}', '>=', $filters['${colName}_from']);`);
|
|
237
|
+
lines.push(` }`);
|
|
238
|
+
lines.push(` if (isset($filters['${colName}_to'])) {`);
|
|
239
|
+
lines.push(` $query->where('${colName}', '<=', $filters['${colName}_to']);`);
|
|
240
|
+
lines.push(` }`);
|
|
241
|
+
break;
|
|
242
|
+
case 'Boolean':
|
|
243
|
+
lines.push(` if (isset($filters['${colName}'])) {`);
|
|
244
|
+
lines.push(` $query->where('${colName}', filter_var($filters['${colName}'], FILTER_VALIDATE_BOOLEAN));`);
|
|
245
|
+
lines.push(` }`);
|
|
246
|
+
break;
|
|
247
|
+
case 'Enum':
|
|
248
|
+
case 'EnumRef':
|
|
249
|
+
case 'String':
|
|
250
|
+
default:
|
|
251
|
+
// Exact match
|
|
252
|
+
lines.push(` if (isset($filters['${colName}'])) {`);
|
|
253
|
+
lines.push(` $query->where('${colName}', $filters['${colName}']);`);
|
|
254
|
+
lines.push(` }`);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return lines.join('\n') + '\n';
|
|
259
|
+
}
|
|
260
|
+
function buildSortBlock(sortableFields) {
|
|
261
|
+
if (sortableFields.length === 0) {
|
|
262
|
+
return `
|
|
263
|
+
// Sort
|
|
264
|
+
$query->orderBy('created_at', 'desc');
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
const allowedList = sortableFields.map(f => `'${f}'`).join(', ');
|
|
268
|
+
return `
|
|
269
|
+
// Sort
|
|
270
|
+
$sortParam = $filters['sort'] ?? '-created_at';
|
|
271
|
+
$allowedSorts = [${allowedList}];
|
|
272
|
+
|
|
273
|
+
foreach (explode(',', $sortParam) as $sortField) {
|
|
274
|
+
$sortField = trim($sortField);
|
|
275
|
+
$direction = 'asc';
|
|
276
|
+
|
|
277
|
+
if (str_starts_with($sortField, '-')) {
|
|
278
|
+
$direction = 'desc';
|
|
279
|
+
$sortField = substr($sortField, 1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (in_array($sortField, $allowedSorts, true) || $sortField === 'created_at' || $sortField === 'updated_at') {
|
|
283
|
+
$query->orderBy($sortField, $direction);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
`;
|
|
287
|
+
}
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// User service
|
|
290
|
+
// ============================================================================
|
|
291
|
+
function generateUserService(name, schema, config) {
|
|
292
|
+
const modelName = toPascalCase(name);
|
|
293
|
+
const group = schema.group ? toPascalCase(schema.group) : null;
|
|
294
|
+
const baseNs = config.services.baseNamespace;
|
|
295
|
+
const userNs = group
|
|
296
|
+
? `${config.services.namespace}\\${group}`
|
|
297
|
+
: config.services.namespace;
|
|
298
|
+
const userPath = group
|
|
299
|
+
? `${config.services.path}/${group}`
|
|
300
|
+
: config.services.path;
|
|
301
|
+
const content = `<?php
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* ${modelName} Service
|
|
305
|
+
*
|
|
306
|
+
* SAFE TO EDIT - This file is never overwritten by Omnify.
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
namespace ${userNs};
|
|
310
|
+
|
|
311
|
+
use ${baseNs}\\${modelName}ServiceBase;
|
|
312
|
+
|
|
313
|
+
class ${modelName}Service extends ${modelName}ServiceBase
|
|
314
|
+
{
|
|
315
|
+
//
|
|
316
|
+
}
|
|
317
|
+
`;
|
|
318
|
+
return userFile(`${userPath}/${modelName}Service.php`, content);
|
|
319
|
+
}
|
package/dist/php/types.d.ts
CHANGED
|
@@ -29,6 +29,9 @@ export interface LaravelCodegenOverrides {
|
|
|
29
29
|
factory?: LaravelPathOverride;
|
|
30
30
|
provider?: LaravelPathOverride;
|
|
31
31
|
policy?: LaravelPathOverride;
|
|
32
|
+
controller?: LaravelPathOverride;
|
|
33
|
+
service?: LaravelPathOverride;
|
|
34
|
+
route?: LaravelPathOverride;
|
|
32
35
|
nestedset?: NestedSetOverride;
|
|
33
36
|
}
|
|
34
37
|
/** PHP codegen configuration (resolved with defaults). */
|
|
@@ -65,6 +68,21 @@ export interface PhpConfig {
|
|
|
65
68
|
path: string;
|
|
66
69
|
basePath: string;
|
|
67
70
|
};
|
|
71
|
+
controllers: {
|
|
72
|
+
namespace: string;
|
|
73
|
+
baseNamespace: string;
|
|
74
|
+
path: string;
|
|
75
|
+
basePath: string;
|
|
76
|
+
};
|
|
77
|
+
services: {
|
|
78
|
+
namespace: string;
|
|
79
|
+
baseNamespace: string;
|
|
80
|
+
path: string;
|
|
81
|
+
basePath: string;
|
|
82
|
+
};
|
|
83
|
+
routes: {
|
|
84
|
+
path: string;
|
|
85
|
+
};
|
|
68
86
|
nestedset: {
|
|
69
87
|
namespace: string;
|
|
70
88
|
};
|
package/dist/php/types.js
CHANGED
|
@@ -23,6 +23,9 @@ const DEFAULT_RESOURCE_PATH = 'app/Http/Resources';
|
|
|
23
23
|
const DEFAULT_FACTORY_PATH = 'database/factories';
|
|
24
24
|
const DEFAULT_PROVIDER_PATH = 'app/Providers';
|
|
25
25
|
const DEFAULT_POLICY_PATH = 'app/Policies/Omnify';
|
|
26
|
+
const DEFAULT_CONTROLLER_PATH = 'app/Http/Controllers';
|
|
27
|
+
const DEFAULT_SERVICE_PATH = 'app/Services';
|
|
28
|
+
const DEFAULT_ROUTE_PATH = 'routes/api/omnify';
|
|
26
29
|
/**
|
|
27
30
|
* Derive full PHP config from optional overrides.
|
|
28
31
|
* All paths and namespaces fall back to sensible defaults.
|
|
@@ -40,6 +43,11 @@ export function derivePhpConfig(overrides) {
|
|
|
40
43
|
const providerNs = overrides?.provider?.namespace ?? pathToNamespace(providerPath);
|
|
41
44
|
const policyPath = overrides?.policy?.path ?? DEFAULT_POLICY_PATH;
|
|
42
45
|
const policyNs = overrides?.policy?.namespace ?? pathToNamespace(policyPath);
|
|
46
|
+
const controllerPath = overrides?.controller?.path ?? DEFAULT_CONTROLLER_PATH;
|
|
47
|
+
const controllerNs = overrides?.controller?.namespace ?? pathToNamespace(controllerPath);
|
|
48
|
+
const servicePath = overrides?.service?.path ?? DEFAULT_SERVICE_PATH;
|
|
49
|
+
const serviceNs = overrides?.service?.namespace ?? pathToNamespace(servicePath);
|
|
50
|
+
const routePath = overrides?.route?.path ?? DEFAULT_ROUTE_PATH;
|
|
43
51
|
const nestedsetNs = overrides?.nestedset?.namespace ?? 'Aimeos\\Nestedset';
|
|
44
52
|
return {
|
|
45
53
|
models: {
|
|
@@ -74,6 +82,21 @@ export function derivePhpConfig(overrides) {
|
|
|
74
82
|
path: policyPath,
|
|
75
83
|
basePath: `${policyPath}/Base`,
|
|
76
84
|
},
|
|
85
|
+
controllers: {
|
|
86
|
+
namespace: controllerNs,
|
|
87
|
+
baseNamespace: `${controllerNs}\\OmnifyBase`,
|
|
88
|
+
path: controllerPath,
|
|
89
|
+
basePath: `${controllerPath}/OmnifyBase`,
|
|
90
|
+
},
|
|
91
|
+
services: {
|
|
92
|
+
namespace: serviceNs,
|
|
93
|
+
baseNamespace: `${serviceNs}\\OmnifyBase`,
|
|
94
|
+
path: servicePath,
|
|
95
|
+
basePath: `${servicePath}/OmnifyBase`,
|
|
96
|
+
},
|
|
97
|
+
routes: {
|
|
98
|
+
path: routePath,
|
|
99
|
+
},
|
|
77
100
|
nestedset: {
|
|
78
101
|
namespace: nestedsetNs,
|
|
79
102
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -91,6 +91,16 @@ export interface ExpandedColumn {
|
|
|
91
91
|
readonly nullable?: boolean;
|
|
92
92
|
readonly enum?: readonly string[];
|
|
93
93
|
}
|
|
94
|
+
/** API generation options for a schema. */
|
|
95
|
+
export interface ApiOptions {
|
|
96
|
+
readonly prefix?: string;
|
|
97
|
+
readonly actions?: readonly string[];
|
|
98
|
+
readonly lookup?: boolean;
|
|
99
|
+
readonly bulkDelete?: boolean;
|
|
100
|
+
readonly restore?: boolean;
|
|
101
|
+
readonly middleware?: readonly string[];
|
|
102
|
+
readonly perPage?: number;
|
|
103
|
+
}
|
|
94
104
|
/** Schema options. */
|
|
95
105
|
export interface SchemaOptions {
|
|
96
106
|
readonly id?: boolean | string;
|
|
@@ -101,6 +111,7 @@ export interface SchemaOptions {
|
|
|
101
111
|
readonly tableName?: string;
|
|
102
112
|
readonly indexes?: readonly unknown[];
|
|
103
113
|
readonly unique?: readonly unknown[];
|
|
114
|
+
readonly api?: ApiOptions;
|
|
104
115
|
}
|
|
105
116
|
/** Schema definition from schemas.json. */
|
|
106
117
|
export interface SchemaDefinition {
|
|
@@ -212,6 +223,9 @@ export interface PropertyDefinition {
|
|
|
212
223
|
readonly joinTable?: string;
|
|
213
224
|
readonly mappedBy?: string;
|
|
214
225
|
readonly useCurrent?: boolean;
|
|
226
|
+
readonly searchable?: boolean;
|
|
227
|
+
readonly filterable?: boolean;
|
|
228
|
+
readonly sortable?: boolean;
|
|
215
229
|
readonly fields?: Record<string, FieldOverride>;
|
|
216
230
|
}
|
|
217
231
|
/** Field override for compound type properties. */
|