@omnifyjp/omnify 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/ts-dist/generator.js +77 -8
- package/ts-dist/interface-generator.js +1 -1
- package/ts-dist/php/factory-generator.d.ts +2 -0
- package/ts-dist/php/factory-generator.js +97 -0
- package/ts-dist/php/file-cleanup-generator.d.ts +8 -0
- package/ts-dist/php/file-cleanup-generator.js +79 -0
- package/ts-dist/php/file-model-generator.d.ts +11 -0
- package/ts-dist/php/file-model-generator.js +359 -0
- package/ts-dist/php/file-trait-generator.d.ts +6 -0
- package/ts-dist/php/file-trait-generator.js +96 -0
- package/ts-dist/php/index.js +11 -1
- package/ts-dist/php/model-generator.js +52 -6
- package/ts-dist/php/request-generator.js +23 -0
- package/ts-dist/php/schema-reader.d.ts +6 -0
- package/ts-dist/php/schema-reader.js +29 -0
- package/ts-dist/php/service-provider-generator.js +4 -0
- package/ts-dist/php/type-mapper.js +19 -0
- package/ts-dist/types.d.ts +13 -0
- package/ts-dist/zod-generator.js +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnifyjp/omnify",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Schema-driven code generation for Laravel, TypeScript, and SQL",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"zod": "^3.24.0"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@omnifyjp/omnify-darwin-arm64": "2.
|
|
40
|
-
"@omnifyjp/omnify-darwin-x64": "2.
|
|
41
|
-
"@omnifyjp/omnify-linux-x64": "2.
|
|
42
|
-
"@omnifyjp/omnify-linux-arm64": "2.
|
|
43
|
-
"@omnifyjp/omnify-win32-x64": "2.
|
|
39
|
+
"@omnifyjp/omnify-darwin-arm64": "2.1.0",
|
|
40
|
+
"@omnifyjp/omnify-darwin-x64": "2.1.0",
|
|
41
|
+
"@omnifyjp/omnify-linux-x64": "2.1.0",
|
|
42
|
+
"@omnifyjp/omnify-linux-arm64": "2.1.0",
|
|
43
|
+
"@omnifyjp/omnify-win32-x64": "2.1.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/ts-dist/generator.js
CHANGED
|
@@ -46,16 +46,26 @@ function generateBaseInterfaceFile(schemaName, schemas, options) {
|
|
|
46
46
|
const parts = [generateBaseHeader()];
|
|
47
47
|
// Zod import
|
|
48
48
|
parts.push(`import { z } from 'zod';\n`);
|
|
49
|
-
// DateTimeString / DateString imports
|
|
49
|
+
// DateTimeString / DateString / OmnifyFile imports
|
|
50
50
|
const dateImports = needsDateTimeImports(iface);
|
|
51
51
|
const commonImports = [];
|
|
52
|
+
const commonValueImports = [];
|
|
52
53
|
if (dateImports.dateTime)
|
|
53
54
|
commonImports.push('DateTimeString');
|
|
54
55
|
if (dateImports.date)
|
|
55
56
|
commonImports.push('DateString');
|
|
57
|
+
// Check if any property uses OmnifyFile
|
|
58
|
+
const usesFile = iface.properties.some(p => p.type.includes('OmnifyFile'));
|
|
59
|
+
if (usesFile) {
|
|
60
|
+
commonImports.push('OmnifyFile');
|
|
61
|
+
commonValueImports.push('OmnifyFileSchema');
|
|
62
|
+
}
|
|
56
63
|
if (commonImports.length > 0) {
|
|
57
64
|
parts.push(`import type { ${commonImports.join(', ')} } from '../common';\n`);
|
|
58
65
|
}
|
|
66
|
+
if (commonValueImports.length > 0) {
|
|
67
|
+
parts.push(`import { ${commonValueImports.join(', ')} } from '../common';\n`);
|
|
68
|
+
}
|
|
59
69
|
// Enum imports (schema enums + plugin enums are all in ../enum/)
|
|
60
70
|
if (iface.enumDependencies && iface.enumDependencies.length > 0) {
|
|
61
71
|
for (const enumName of iface.enumDependencies) {
|
|
@@ -125,8 +135,54 @@ function generateModelFile(schemaName) {
|
|
|
125
135
|
};
|
|
126
136
|
}
|
|
127
137
|
/** Generate common.ts with shared types. */
|
|
128
|
-
function generateCommonFile(options) {
|
|
138
|
+
function generateCommonFile(options, hasFiles) {
|
|
129
139
|
const localeUnion = options.locales.map(l => `'${l}'`).join(' | ');
|
|
140
|
+
const types = ['LocaleMap', 'Locale', 'DateTimeString', 'DateString'];
|
|
141
|
+
let fileSection = '';
|
|
142
|
+
if (hasFiles) {
|
|
143
|
+
types.push('OmnifyFile', 'OmnifyFileSchema');
|
|
144
|
+
fileSection = `
|
|
145
|
+
import { z } from 'zod';
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Omnify file attachment.
|
|
149
|
+
*/
|
|
150
|
+
export interface OmnifyFile {
|
|
151
|
+
readonly id: string;
|
|
152
|
+
readonly collection: string;
|
|
153
|
+
readonly disk: string;
|
|
154
|
+
readonly path: string;
|
|
155
|
+
readonly original_name: string;
|
|
156
|
+
readonly mime_type: string;
|
|
157
|
+
readonly size: number;
|
|
158
|
+
readonly status: 'temporary' | 'permanent';
|
|
159
|
+
readonly url: string | null;
|
|
160
|
+
readonly expires_at: DateTimeString | null;
|
|
161
|
+
readonly sort_order: number;
|
|
162
|
+
readonly created_at: DateTimeString;
|
|
163
|
+
readonly updated_at: DateTimeString;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Zod schema for OmnifyFile.
|
|
168
|
+
*/
|
|
169
|
+
export const OmnifyFileSchema = z.object({
|
|
170
|
+
id: z.string(),
|
|
171
|
+
collection: z.string(),
|
|
172
|
+
disk: z.string(),
|
|
173
|
+
path: z.string(),
|
|
174
|
+
original_name: z.string(),
|
|
175
|
+
mime_type: z.string(),
|
|
176
|
+
size: z.number(),
|
|
177
|
+
status: z.enum(['temporary', 'permanent']),
|
|
178
|
+
url: z.string().nullable(),
|
|
179
|
+
expires_at: z.string().nullable(),
|
|
180
|
+
sort_order: z.number(),
|
|
181
|
+
created_at: z.string(),
|
|
182
|
+
updated_at: z.string(),
|
|
183
|
+
});
|
|
184
|
+
`;
|
|
185
|
+
}
|
|
130
186
|
const content = `${generateBaseHeader()}/**
|
|
131
187
|
* Locale map for multi-language support.
|
|
132
188
|
*/
|
|
@@ -148,11 +204,11 @@ export type DateTimeString = string;
|
|
|
148
204
|
* ISO 8601 date string (YYYY-MM-DD).
|
|
149
205
|
*/
|
|
150
206
|
export type DateString = string;
|
|
151
|
-
`;
|
|
207
|
+
${fileSection}`;
|
|
152
208
|
return {
|
|
153
209
|
filePath: 'common.ts',
|
|
154
210
|
content,
|
|
155
|
-
types
|
|
211
|
+
types,
|
|
156
212
|
overwrite: true,
|
|
157
213
|
};
|
|
158
214
|
}
|
|
@@ -166,11 +222,17 @@ function generateI18nFile(options) {
|
|
|
166
222
|
};
|
|
167
223
|
}
|
|
168
224
|
/** Generate index.ts re-exports. */
|
|
169
|
-
function generateIndexFile(schemas, schemaEnums, pluginEnums, typeAliases) {
|
|
225
|
+
function generateIndexFile(schemas, schemaEnums, pluginEnums, typeAliases, hasFiles = false) {
|
|
170
226
|
const parts = [generateBaseHeader()];
|
|
171
227
|
// Common types
|
|
172
228
|
parts.push(`// Common Types\n`);
|
|
173
|
-
|
|
229
|
+
if (hasFiles) {
|
|
230
|
+
parts.push(`export type { LocaleMap, Locale, DateTimeString, DateString, OmnifyFile } from './common';\n`);
|
|
231
|
+
parts.push(`export { OmnifyFileSchema } from './common';\n`);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
parts.push(`export type { LocaleMap, Locale, DateTimeString, DateString } from './common';\n`);
|
|
235
|
+
}
|
|
174
236
|
// I18n
|
|
175
237
|
parts.push(`// i18n (Internationalization)\n`);
|
|
176
238
|
parts.push(`export {\n`);
|
|
@@ -320,11 +382,18 @@ export function generateTypeScript(input) {
|
|
|
320
382
|
continue;
|
|
321
383
|
files.push(generateModelFile(schema.name));
|
|
322
384
|
}
|
|
385
|
+
// Detect if any schema has File properties
|
|
386
|
+
const hasFiles = Object.values(schemas).some(s => {
|
|
387
|
+
if (s.kind === 'enum')
|
|
388
|
+
return false;
|
|
389
|
+
const props = s.properties ?? {};
|
|
390
|
+
return Object.values(props).some(p => p.type === 'File');
|
|
391
|
+
});
|
|
323
392
|
// Common types
|
|
324
|
-
files.push(generateCommonFile(options));
|
|
393
|
+
files.push(generateCommonFile(options, hasFiles));
|
|
325
394
|
// I18n
|
|
326
395
|
files.push(generateI18nFile(options));
|
|
327
396
|
// Index re-exports
|
|
328
|
-
files.push(generateIndexFile(schemas, schemaEnums, pluginEnums, inlineTypeAliases));
|
|
397
|
+
files.push(generateIndexFile(schemas, schemaEnums, pluginEnums, inlineTypeAliases, hasFiles));
|
|
329
398
|
return files;
|
|
330
399
|
}
|
|
@@ -53,7 +53,7 @@ function getIdType(options) {
|
|
|
53
53
|
/** Gets TypeScript type for a property. */
|
|
54
54
|
export function getPropertyType(property, allSchemas) {
|
|
55
55
|
if (property.type === 'File') {
|
|
56
|
-
return '
|
|
56
|
+
return property.multiple ? 'OmnifyFile[]' : 'OmnifyFile | null';
|
|
57
57
|
}
|
|
58
58
|
if (property.type === 'Association') {
|
|
59
59
|
const targetName = property.target ?? 'unknown';
|
|
@@ -5,3 +5,5 @@ import { SchemaReader } from './schema-reader.js';
|
|
|
5
5
|
import type { GeneratedFile, PhpConfig } from './types.js';
|
|
6
6
|
/** Generate Factory classes for all project-owned visible object schemas. */
|
|
7
7
|
export declare function generateFactories(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
|
|
8
|
+
/** Generate the FileFactory class (only when schemas use File type). */
|
|
9
|
+
export declare function generateFileFactory(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
|
|
@@ -42,6 +42,9 @@ function generateFactory(name, schema, reader, schemas, config) {
|
|
|
42
42
|
}
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
|
+
// Skip File type (files are relational, not direct columns)
|
|
46
|
+
if (type === 'File')
|
|
47
|
+
continue;
|
|
45
48
|
// Handle compound types
|
|
46
49
|
if (expandedProperties[propName]) {
|
|
47
50
|
const expansion = expandedProperties[propName];
|
|
@@ -96,3 +99,97 @@ ${attributesStr}
|
|
|
96
99
|
`;
|
|
97
100
|
return userFile(`${config.factories.path}/${modelName}Factory.php`, content);
|
|
98
101
|
}
|
|
102
|
+
/** Generate the FileFactory class (only when schemas use File type). */
|
|
103
|
+
export function generateFileFactory(reader, config) {
|
|
104
|
+
if (!reader.hasFileProperties())
|
|
105
|
+
return [];
|
|
106
|
+
const modelNamespace = config.models.namespace;
|
|
107
|
+
const factoryNamespace = config.factories.namespace;
|
|
108
|
+
const content = `<?php
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* File Factory
|
|
112
|
+
*
|
|
113
|
+
* SAFE TO EDIT - This file is never overwritten by Omnify.
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
namespace ${factoryNamespace};
|
|
117
|
+
|
|
118
|
+
use ${modelNamespace}\\File;
|
|
119
|
+
use ${modelNamespace}\\FileStatusEnum;
|
|
120
|
+
use Illuminate\\Database\\Eloquent\\Factories\\Factory;
|
|
121
|
+
use Illuminate\\Support\\Str;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @extends Factory<File>
|
|
125
|
+
*/
|
|
126
|
+
class FileFactory extends Factory
|
|
127
|
+
{
|
|
128
|
+
protected $model = File::class;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Define the model's default state.
|
|
132
|
+
*
|
|
133
|
+
* @return array<string, mixed>
|
|
134
|
+
*/
|
|
135
|
+
public function definition(): array
|
|
136
|
+
{
|
|
137
|
+
$extension = fake()->randomElement(['jpg', 'png', 'pdf', 'docx']);
|
|
138
|
+
$originalName = fake()->word() . '.' . $extension;
|
|
139
|
+
$mimeTypes = [
|
|
140
|
+
'jpg' => 'image/jpeg',
|
|
141
|
+
'png' => 'image/png',
|
|
142
|
+
'pdf' => 'application/pdf',
|
|
143
|
+
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
return [
|
|
147
|
+
'collection' => 'default',
|
|
148
|
+
'disk' => 'public',
|
|
149
|
+
'path' => 'files/' . now()->format('Y/m') . '/' . Str::uuid() . '.' . $extension,
|
|
150
|
+
'original_name' => $originalName,
|
|
151
|
+
'mime_type' => $mimeTypes[$extension] ?? 'application/octet-stream',
|
|
152
|
+
'size' => fake()->numberBetween(1024, 10485760),
|
|
153
|
+
'status' => FileStatusEnum::Temporary,
|
|
154
|
+
'expires_at' => now()->addHours(24),
|
|
155
|
+
'sort_order' => 0,
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Indicate that the file is permanent.
|
|
161
|
+
*/
|
|
162
|
+
public function permanent(): static
|
|
163
|
+
{
|
|
164
|
+
return $this->state(fn (array $attributes) => [
|
|
165
|
+
'status' => FileStatusEnum::Permanent,
|
|
166
|
+
'expires_at' => null,
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Indicate that the file has expired.
|
|
172
|
+
*/
|
|
173
|
+
public function expired(): static
|
|
174
|
+
{
|
|
175
|
+
return $this->state(fn (array $attributes) => [
|
|
176
|
+
'status' => FileStatusEnum::Temporary,
|
|
177
|
+
'expires_at' => now()->subHour(),
|
|
178
|
+
]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Set a specific collection.
|
|
183
|
+
*/
|
|
184
|
+
public function inCollection(string $collection): static
|
|
185
|
+
{
|
|
186
|
+
return $this->state(fn (array $attributes) => [
|
|
187
|
+
'collection' => $collection,
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
`;
|
|
192
|
+
return [
|
|
193
|
+
userFile(`${config.factories.path}/FileFactory.php`, content),
|
|
194
|
+
];
|
|
195
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the CleanupExpiredFiles artisan command.
|
|
3
|
+
* Only generated when fileConfig.tempFlow is enabled.
|
|
4
|
+
*/
|
|
5
|
+
import { SchemaReader } from './schema-reader.js';
|
|
6
|
+
import type { GeneratedFile, PhpConfig } from './types.js';
|
|
7
|
+
/** Generate the cleanup command (only when tempFlow is enabled). */
|
|
8
|
+
export declare function generateFileCleanup(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the CleanupExpiredFiles artisan command.
|
|
3
|
+
* Only generated when fileConfig.tempFlow is enabled.
|
|
4
|
+
*/
|
|
5
|
+
import { baseFile } from './types.js';
|
|
6
|
+
/** Generate the cleanup command (only when tempFlow is enabled). */
|
|
7
|
+
export function generateFileCleanup(reader, config) {
|
|
8
|
+
const fileConfig = reader.getFileConfig();
|
|
9
|
+
if (!fileConfig?.tempFlow)
|
|
10
|
+
return [];
|
|
11
|
+
const modelNamespace = config.models.namespace;
|
|
12
|
+
const content = `<?php
|
|
13
|
+
|
|
14
|
+
namespace App\\Console\\Commands;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
18
|
+
* Any changes will be overwritten on next generation.
|
|
19
|
+
*
|
|
20
|
+
* @generated by omnify
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
use Illuminate\\Console\\Command;
|
|
24
|
+
use Illuminate\\Support\\Facades\\Storage;
|
|
25
|
+
use ${modelNamespace}\\File;
|
|
26
|
+
|
|
27
|
+
class CleanupExpiredFiles extends Command
|
|
28
|
+
{
|
|
29
|
+
/**
|
|
30
|
+
* The name and signature of the console command.
|
|
31
|
+
*/
|
|
32
|
+
protected $signature = 'omnify:cleanup-expired-files
|
|
33
|
+
{--dry-run : Show what would be deleted without actually deleting}';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The console command description.
|
|
37
|
+
*/
|
|
38
|
+
protected $description = 'Delete expired temporary files';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Execute the console command.
|
|
42
|
+
*/
|
|
43
|
+
public function handle(): int
|
|
44
|
+
{
|
|
45
|
+
$query = File::expired();
|
|
46
|
+
$count = $query->count();
|
|
47
|
+
|
|
48
|
+
if ($count === 0) {
|
|
49
|
+
$this->info('No expired files found.');
|
|
50
|
+
return self::SUCCESS;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if ($this->option('dry-run')) {
|
|
54
|
+
$this->info("Would delete {$count} expired file(s).");
|
|
55
|
+
return self::SUCCESS;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
$deleted = 0;
|
|
59
|
+
$query->chunkById(100, function ($files) use (&$deleted) {
|
|
60
|
+
foreach ($files as $file) {
|
|
61
|
+
// Delete from storage
|
|
62
|
+
if ($file->path && Storage::disk($file->disk)->exists($file->path)) {
|
|
63
|
+
Storage::disk($file->disk)->delete($file->path);
|
|
64
|
+
}
|
|
65
|
+
// Soft delete the record
|
|
66
|
+
$file->delete();
|
|
67
|
+
$deleted++;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
$this->info("Deleted {$deleted} expired file(s).");
|
|
72
|
+
return self::SUCCESS;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
return [
|
|
77
|
+
baseFile('app/Console/Commands/CleanupExpiredFiles.php', content),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates File model infrastructure:
|
|
3
|
+
* - FileBaseModel.php (base, always overwritten)
|
|
4
|
+
* - File.php (user, created once)
|
|
5
|
+
* - FileStatusEnum.php (base, always overwritten)
|
|
6
|
+
* - FileLocales.php (base, always overwritten)
|
|
7
|
+
*/
|
|
8
|
+
import { SchemaReader } from './schema-reader.js';
|
|
9
|
+
import type { GeneratedFile, PhpConfig } from './types.js';
|
|
10
|
+
/** Generate all File model infrastructure files. */
|
|
11
|
+
export declare function generateFileModels(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates File model infrastructure:
|
|
3
|
+
* - FileBaseModel.php (base, always overwritten)
|
|
4
|
+
* - File.php (user, created once)
|
|
5
|
+
* - FileStatusEnum.php (base, always overwritten)
|
|
6
|
+
* - FileLocales.php (base, always overwritten)
|
|
7
|
+
*/
|
|
8
|
+
import { baseFile, userFile } from './types.js';
|
|
9
|
+
/** Generate all File model infrastructure files. */
|
|
10
|
+
export function generateFileModels(reader, config) {
|
|
11
|
+
if (!reader.hasFileProperties())
|
|
12
|
+
return [];
|
|
13
|
+
const files = [];
|
|
14
|
+
const locale = reader.getLocale();
|
|
15
|
+
const defaultDisk = reader.getFileConfig()?.defaultDisk ?? 'public';
|
|
16
|
+
files.push(generateFileBaseModel(config, defaultDisk));
|
|
17
|
+
files.push(generateFileUserModel(config));
|
|
18
|
+
files.push(generateFileStatusEnum(config));
|
|
19
|
+
if (locale) {
|
|
20
|
+
files.push(generateFileLocales(config, locale.locales));
|
|
21
|
+
}
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
function generateFileBaseModel(config, defaultDisk) {
|
|
25
|
+
const baseNamespace = config.models.baseNamespace;
|
|
26
|
+
const modelNamespace = config.models.namespace;
|
|
27
|
+
const content = `<?php
|
|
28
|
+
|
|
29
|
+
namespace ${baseNamespace};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
33
|
+
* Any changes will be overwritten on next generation.
|
|
34
|
+
*
|
|
35
|
+
* @generated by omnify
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
use Illuminate\\Database\\Eloquent\\Concerns\\HasUuids;
|
|
39
|
+
use Illuminate\\Database\\Eloquent\\Relations\\MorphTo;
|
|
40
|
+
use Illuminate\\Database\\Eloquent\\SoftDeletes;
|
|
41
|
+
use Illuminate\\Database\\Eloquent\\Builder;
|
|
42
|
+
use ${baseNamespace}\\Locales\\FileLocales;
|
|
43
|
+
use ${baseNamespace}\\Traits\\HasLocalizedDisplayName;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* FileBaseModel
|
|
47
|
+
*
|
|
48
|
+
* @property string $id
|
|
49
|
+
* @property string|null $organization_id
|
|
50
|
+
* @property string|null $fileable_type
|
|
51
|
+
* @property string|null $fileable_id
|
|
52
|
+
* @property string $collection
|
|
53
|
+
* @property string $disk
|
|
54
|
+
* @property string $path
|
|
55
|
+
* @property string $original_name
|
|
56
|
+
* @property string $mime_type
|
|
57
|
+
* @property int $size
|
|
58
|
+
* @property \\${modelNamespace}\\FileStatusEnum $status
|
|
59
|
+
* @property \\Illuminate\\Support\\Carbon|null $expires_at
|
|
60
|
+
* @property int $sort_order
|
|
61
|
+
*/
|
|
62
|
+
class FileBaseModel extends BaseModel
|
|
63
|
+
{
|
|
64
|
+
use HasUuids;
|
|
65
|
+
use SoftDeletes;
|
|
66
|
+
use HasLocalizedDisplayName;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The table associated with the model.
|
|
70
|
+
*/
|
|
71
|
+
protected $table = 'files';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The primary key for the model.
|
|
75
|
+
*/
|
|
76
|
+
protected $primaryKey = 'id';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The type of the primary key.
|
|
80
|
+
*/
|
|
81
|
+
protected $keyType = 'string';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Indicates if the IDs are auto-incrementing.
|
|
85
|
+
*/
|
|
86
|
+
public $incrementing = false;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Indicates if the model should be timestamped.
|
|
90
|
+
*/
|
|
91
|
+
public $timestamps = true;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Localized display names for this model.
|
|
95
|
+
*/
|
|
96
|
+
protected static array $localizedDisplayNames = FileLocales::DISPLAY_NAMES;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Localized display names for properties.
|
|
100
|
+
*/
|
|
101
|
+
protected static array $localizedPropertyDisplayNames = FileLocales::PROPERTY_DISPLAY_NAMES;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The attributes that are mass assignable.
|
|
105
|
+
*/
|
|
106
|
+
protected $fillable = [
|
|
107
|
+
'organization_id',
|
|
108
|
+
'fileable_type',
|
|
109
|
+
'fileable_id',
|
|
110
|
+
'collection',
|
|
111
|
+
'disk',
|
|
112
|
+
'path',
|
|
113
|
+
'original_name',
|
|
114
|
+
'mime_type',
|
|
115
|
+
'size',
|
|
116
|
+
'status',
|
|
117
|
+
'expires_at',
|
|
118
|
+
'sort_order',
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the attributes that should be cast.
|
|
123
|
+
*/
|
|
124
|
+
protected function casts(): array
|
|
125
|
+
{
|
|
126
|
+
return [
|
|
127
|
+
'size' => 'integer',
|
|
128
|
+
'sort_order' => 'integer',
|
|
129
|
+
'status' => \\${modelNamespace}\\FileStatusEnum::class,
|
|
130
|
+
'expires_at' => 'datetime',
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// -----------------------------------------------------------------------
|
|
135
|
+
// Relations
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get the parent fileable model.
|
|
140
|
+
*/
|
|
141
|
+
public function fileable(): MorphTo
|
|
142
|
+
{
|
|
143
|
+
return $this->morphTo();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// -----------------------------------------------------------------------
|
|
147
|
+
// Scopes
|
|
148
|
+
// -----------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Scope to a specific collection.
|
|
152
|
+
*/
|
|
153
|
+
public function scopeInCollection(Builder $query, string $collection): Builder
|
|
154
|
+
{
|
|
155
|
+
return $query->where('collection', $collection);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Scope to permanent files.
|
|
160
|
+
*/
|
|
161
|
+
public function scopePermanent(Builder $query): Builder
|
|
162
|
+
{
|
|
163
|
+
return $query->where('status', 'permanent');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Scope to temporary files.
|
|
168
|
+
*/
|
|
169
|
+
public function scopeTemporary(Builder $query): Builder
|
|
170
|
+
{
|
|
171
|
+
return $query->where('status', 'temporary');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Scope to expired files (temporary + past expires_at).
|
|
176
|
+
*/
|
|
177
|
+
public function scopeExpired(Builder $query): Builder
|
|
178
|
+
{
|
|
179
|
+
return $query->where('status', 'temporary')
|
|
180
|
+
->whereNotNull('expires_at')
|
|
181
|
+
->where('expires_at', '<', now());
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// -----------------------------------------------------------------------
|
|
185
|
+
// Helpers
|
|
186
|
+
// -----------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if the file is permanent.
|
|
190
|
+
*/
|
|
191
|
+
public function isPermanent(): bool
|
|
192
|
+
{
|
|
193
|
+
return $this->status === \\${modelNamespace}\\FileStatusEnum::Permanent;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if the file has expired.
|
|
198
|
+
*/
|
|
199
|
+
public function isExpired(): bool
|
|
200
|
+
{
|
|
201
|
+
return $this->status === \\${modelNamespace}\\FileStatusEnum::Temporary
|
|
202
|
+
&& $this->expires_at !== null
|
|
203
|
+
&& $this->expires_at->isPast();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Make the file permanent.
|
|
208
|
+
*/
|
|
209
|
+
public function makePermanent(): static
|
|
210
|
+
{
|
|
211
|
+
$this->update([
|
|
212
|
+
'status' => \\${modelNamespace}\\FileStatusEnum::Permanent,
|
|
213
|
+
'expires_at' => null,
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
return $this;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
`;
|
|
220
|
+
return baseFile(`${config.models.basePath}/FileBaseModel.php`, content);
|
|
221
|
+
}
|
|
222
|
+
function generateFileUserModel(config) {
|
|
223
|
+
const modelNamespace = config.models.namespace;
|
|
224
|
+
const baseNamespace = config.models.baseNamespace;
|
|
225
|
+
const factoryNamespace = config.factories.namespace;
|
|
226
|
+
const content = `<?php
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* File Model
|
|
230
|
+
*
|
|
231
|
+
* SAFE TO EDIT - This file is never overwritten by Omnify.
|
|
232
|
+
*/
|
|
233
|
+
|
|
234
|
+
namespace ${modelNamespace};
|
|
235
|
+
|
|
236
|
+
use ${baseNamespace}\\FileBaseModel;
|
|
237
|
+
use ${factoryNamespace}\\FileFactory;
|
|
238
|
+
use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* File — add project-specific model logic here.
|
|
242
|
+
*/
|
|
243
|
+
class File extends FileBaseModel
|
|
244
|
+
{
|
|
245
|
+
use HasFactory;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Create a new factory instance for the model.
|
|
249
|
+
*/
|
|
250
|
+
protected static function newFactory(): FileFactory
|
|
251
|
+
{
|
|
252
|
+
return FileFactory::new();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
//
|
|
256
|
+
}
|
|
257
|
+
`;
|
|
258
|
+
return userFile(`${config.models.path}/File.php`, content);
|
|
259
|
+
}
|
|
260
|
+
function generateFileStatusEnum(config) {
|
|
261
|
+
const modelNamespace = config.models.namespace;
|
|
262
|
+
const content = `<?php
|
|
263
|
+
|
|
264
|
+
namespace ${modelNamespace};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
268
|
+
* Any changes will be overwritten on next generation.
|
|
269
|
+
*
|
|
270
|
+
* @generated by omnify
|
|
271
|
+
*/
|
|
272
|
+
enum FileStatusEnum: string
|
|
273
|
+
{
|
|
274
|
+
case Temporary = 'temporary';
|
|
275
|
+
case Permanent = 'permanent';
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get human-readable label.
|
|
279
|
+
*/
|
|
280
|
+
public function label(): string
|
|
281
|
+
{
|
|
282
|
+
return match ($this) {
|
|
283
|
+
self::Temporary => __('Temporary'),
|
|
284
|
+
self::Permanent => __('Permanent'),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get all possible values.
|
|
290
|
+
*
|
|
291
|
+
* @return array<string>
|
|
292
|
+
*/
|
|
293
|
+
public static function values(): array
|
|
294
|
+
{
|
|
295
|
+
return array_column(self::cases(), 'value');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
`;
|
|
299
|
+
return baseFile(`${config.models.path}/FileStatusEnum.php`, content);
|
|
300
|
+
}
|
|
301
|
+
function generateFileLocales(config, locales) {
|
|
302
|
+
const baseNamespace = config.models.baseNamespace;
|
|
303
|
+
// Display names for the File model
|
|
304
|
+
const displayNames = {
|
|
305
|
+
ja: 'ファイル',
|
|
306
|
+
en: 'File',
|
|
307
|
+
vi: 'Tệp',
|
|
308
|
+
};
|
|
309
|
+
// Property display names
|
|
310
|
+
const propertyDisplayNames = {
|
|
311
|
+
collection: { ja: 'コレクション', en: 'Collection', vi: 'Bộ sưu tập' },
|
|
312
|
+
disk: { ja: 'ディスク', en: 'Disk', vi: 'Đĩa' },
|
|
313
|
+
path: { ja: 'パス', en: 'Path', vi: 'Đường dẫn' },
|
|
314
|
+
original_name: { ja: 'ファイル名', en: 'File Name', vi: 'Tên tệp' },
|
|
315
|
+
mime_type: { ja: 'MIMEタイプ', en: 'MIME Type', vi: 'Loại MIME' },
|
|
316
|
+
size: { ja: 'サイズ', en: 'Size', vi: 'Kích thước' },
|
|
317
|
+
status: { ja: 'ステータス', en: 'Status', vi: 'Trạng thái' },
|
|
318
|
+
expires_at: { ja: '有効期限', en: 'Expires At', vi: 'Hết hạn' },
|
|
319
|
+
sort_order: { ja: '並び順', en: 'Sort Order', vi: 'Thứ tự sắp xếp' },
|
|
320
|
+
};
|
|
321
|
+
// Filter to configured locales
|
|
322
|
+
const filteredDisplayNames = Object.fromEntries(locales.filter(l => displayNames[l]).map(l => [l, displayNames[l]]));
|
|
323
|
+
const displayNamesPhp = formatPhpArray(filteredDisplayNames);
|
|
324
|
+
const propertyNamesPhp = Object.entries(propertyDisplayNames)
|
|
325
|
+
.map(([prop, names]) => {
|
|
326
|
+
const filtered = Object.fromEntries(locales.filter(l => names[l]).map(l => [l, names[l]]));
|
|
327
|
+
return ` '${prop}' => ${formatPhpArray(filtered)}`;
|
|
328
|
+
})
|
|
329
|
+
.join(',\n');
|
|
330
|
+
const content = `<?php
|
|
331
|
+
|
|
332
|
+
namespace ${baseNamespace}\\Locales;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
336
|
+
* Any changes will be overwritten on next generation.
|
|
337
|
+
*
|
|
338
|
+
* @generated by omnify
|
|
339
|
+
*/
|
|
340
|
+
class FileLocales
|
|
341
|
+
{
|
|
342
|
+
public const DISPLAY_NAMES = ${displayNamesPhp};
|
|
343
|
+
|
|
344
|
+
public const PROPERTY_DISPLAY_NAMES = [
|
|
345
|
+
${propertyNamesPhp},
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
`;
|
|
349
|
+
return baseFile(`${config.models.basePath}/Locales/FileLocales.php`, content);
|
|
350
|
+
}
|
|
351
|
+
function formatPhpArray(obj) {
|
|
352
|
+
const entries = Object.entries(obj)
|
|
353
|
+
.map(([key, val]) => `'${key}' => '${escapePhp(val)}'`)
|
|
354
|
+
.join(', ');
|
|
355
|
+
return `[${entries}]`;
|
|
356
|
+
}
|
|
357
|
+
function escapePhp(s) {
|
|
358
|
+
return s.replace(/'/g, "\\'");
|
|
359
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the HasFiles trait for models with File-type properties.
|
|
3
|
+
*/
|
|
4
|
+
import { baseFile } from './types.js';
|
|
5
|
+
/** Generate the HasFiles trait. */
|
|
6
|
+
export function generateFileTrait(config) {
|
|
7
|
+
const baseNamespace = config.models.baseNamespace;
|
|
8
|
+
const modelNamespace = config.models.namespace;
|
|
9
|
+
const content = `<?php
|
|
10
|
+
|
|
11
|
+
namespace ${baseNamespace}\\Traits;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Trait for models with polymorphic file attachments.
|
|
15
|
+
*
|
|
16
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
17
|
+
* Any changes will be overwritten on next generation.
|
|
18
|
+
*
|
|
19
|
+
* @generated by omnify
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
use Illuminate\\Database\\Eloquent\\Relations\\MorphMany;
|
|
23
|
+
use ${modelNamespace}\\File;
|
|
24
|
+
use ${modelNamespace}\\FileStatusEnum;
|
|
25
|
+
|
|
26
|
+
trait HasFiles
|
|
27
|
+
{
|
|
28
|
+
/**
|
|
29
|
+
* Get all files attached to this model.
|
|
30
|
+
*/
|
|
31
|
+
public function files(): MorphMany
|
|
32
|
+
{
|
|
33
|
+
return $this->morphMany(File::class, 'fileable');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get files in a specific collection.
|
|
38
|
+
*/
|
|
39
|
+
public function filesInCollection(string $collection): MorphMany
|
|
40
|
+
{
|
|
41
|
+
return $this->files()->where('collection', $collection);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get all permanent files.
|
|
46
|
+
*/
|
|
47
|
+
public function permanentFiles(): MorphMany
|
|
48
|
+
{
|
|
49
|
+
return $this->files()->where('status', FileStatusEnum::Permanent);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Attach files by IDs to a collection, making them permanent.
|
|
54
|
+
*
|
|
55
|
+
* @param array<string> $fileIds
|
|
56
|
+
*/
|
|
57
|
+
public function attachFiles(array $fileIds, string $collection = 'default', int $startOrder = 0): void
|
|
58
|
+
{
|
|
59
|
+
if (empty($fileIds)) return;
|
|
60
|
+
|
|
61
|
+
File::whereIn('id', $fileIds)->update([
|
|
62
|
+
'fileable_type' => $this->getMorphClass(),
|
|
63
|
+
'fileable_id' => $this->getKey(),
|
|
64
|
+
'collection' => $collection,
|
|
65
|
+
'status' => FileStatusEnum::Permanent,
|
|
66
|
+
'expires_at' => null,
|
|
67
|
+
'sort_order' => \\Illuminate\\Support\\Facades\\DB::raw('sort_order + ' . $startOrder),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sync files for a collection (detach old, attach new).
|
|
73
|
+
*
|
|
74
|
+
* @param array<string> $fileIds
|
|
75
|
+
*/
|
|
76
|
+
public function syncFiles(array $fileIds, string $collection = 'default'): void
|
|
77
|
+
{
|
|
78
|
+
// Soft-delete files no longer in the list
|
|
79
|
+
$this->filesInCollection($collection)
|
|
80
|
+
->whereNotIn('id', $fileIds)
|
|
81
|
+
->delete();
|
|
82
|
+
|
|
83
|
+
// Attach new files
|
|
84
|
+
$this->attachFiles($fileIds, $collection);
|
|
85
|
+
|
|
86
|
+
// Update sort order based on array position
|
|
87
|
+
foreach ($fileIds as $index => $id) {
|
|
88
|
+
File::where('id', $id)->update(['sort_order' => $index]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
return [
|
|
94
|
+
baseFile(`${config.models.basePath}/Traits/HasFiles.php`, content),
|
|
95
|
+
];
|
|
96
|
+
}
|
package/ts-dist/php/index.js
CHANGED
|
@@ -18,10 +18,13 @@ import { generateLocales } from './locales-generator.js';
|
|
|
18
18
|
import { generateModels } from './model-generator.js';
|
|
19
19
|
import { generateRequests } from './request-generator.js';
|
|
20
20
|
import { generateResources } from './resource-generator.js';
|
|
21
|
-
import { generateFactories } from './factory-generator.js';
|
|
21
|
+
import { generateFactories, generateFileFactory } from './factory-generator.js';
|
|
22
22
|
import { generateServiceProvider } from './service-provider-generator.js';
|
|
23
23
|
import { generateTranslationModels } from './translation-model-generator.js';
|
|
24
24
|
import { generatePolicies } from './policy-generator.js';
|
|
25
|
+
import { generateFileModels } from './file-model-generator.js';
|
|
26
|
+
import { generateFileTrait } from './file-trait-generator.js';
|
|
27
|
+
import { generateFileCleanup } from './file-cleanup-generator.js';
|
|
25
28
|
export { derivePhpConfig } from './types.js';
|
|
26
29
|
/** Generate all PHP files from schemas.json data. */
|
|
27
30
|
export function generatePhp(data, overrides) {
|
|
@@ -32,6 +35,13 @@ export function generatePhp(data, overrides) {
|
|
|
32
35
|
files.push(...generateBaseModel(config));
|
|
33
36
|
files.push(...generateTrait(config));
|
|
34
37
|
files.push(...generateServiceProvider(reader, config));
|
|
38
|
+
// File infrastructure (only when schemas use File type)
|
|
39
|
+
if (reader.hasFileProperties()) {
|
|
40
|
+
files.push(...generateFileModels(reader, config));
|
|
41
|
+
files.push(...generateFileTrait(config));
|
|
42
|
+
files.push(...generateFileFactory(reader, config));
|
|
43
|
+
files.push(...generateFileCleanup(reader, config));
|
|
44
|
+
}
|
|
35
45
|
// Per-schema files
|
|
36
46
|
files.push(...generateLocales(reader, config));
|
|
37
47
|
files.push(...generateModels(reader, config));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Port of ModelGenerator.php — generates Eloquent model base + user classes.
|
|
3
3
|
*/
|
|
4
|
-
import { toPascalCase, toSnakeCase } from './naming-helper.js';
|
|
4
|
+
import { toPascalCase, toSnakeCase, toCamelCase } from './naming-helper.js';
|
|
5
5
|
import { toCast, toPhpDocType, isHiddenByDefault } from './type-mapper.js';
|
|
6
6
|
import { buildRelation } from './relation-builder.js';
|
|
7
7
|
import { baseFile, userFile } from './types.js';
|
|
@@ -54,17 +54,23 @@ function generateBaseModel(name, schema, reader, config) {
|
|
|
54
54
|
const isCompositeKey = primaryKey.includes(',');
|
|
55
55
|
const needsUuidTrait = !isCompositeKey && idType === 'Uuid';
|
|
56
56
|
const needsUlidTrait = !isCompositeKey && idType === 'Ulid';
|
|
57
|
-
|
|
57
|
+
// Detect File properties for HasFiles trait
|
|
58
|
+
const hasFiles = propertyOrder.some(p => {
|
|
59
|
+
const prop = properties[p];
|
|
60
|
+
return prop && prop['type'] === 'File';
|
|
61
|
+
});
|
|
62
|
+
const imports = buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable, hasTranslatable, properties, needsUuidTrait, needsUlidTrait, hasNestedSet, config.nestedset.namespace, hasFiles, modelNamespace);
|
|
58
63
|
const docProperties = buildDocProperties(properties, expandedProperties, propertyOrder);
|
|
59
64
|
const baseClass = isAuthenticatable ? 'Authenticatable' : 'BaseModel';
|
|
60
65
|
const implementsClause = hasTranslatable ? ' implements TranslatableContract' : '';
|
|
61
|
-
const traits = buildTraits(hasSoftDelete, isAuthenticatable, hasTranslatable, needsUuidTrait, needsUlidTrait, hasNestedSet);
|
|
66
|
+
const traits = buildTraits(hasSoftDelete, isAuthenticatable, hasTranslatable, needsUuidTrait, needsUlidTrait, hasNestedSet, hasFiles);
|
|
62
67
|
const fillable = buildFillable(properties, expandedProperties, propertyOrder);
|
|
63
68
|
const hidden = buildHidden(properties, expandedProperties, propertyOrder);
|
|
64
69
|
const appends = buildAppends(expandedProperties);
|
|
65
70
|
const casts = buildCasts(properties, expandedProperties, propertyOrder, reader);
|
|
66
71
|
const relations = buildRelations(name, properties, propertyOrder, modelNamespace, reader);
|
|
67
72
|
const accessors = buildAccessors(expandedProperties);
|
|
73
|
+
const fileAccessors = hasFiles ? buildFileAccessors(properties, propertyOrder, modelNamespace) : '';
|
|
68
74
|
const nestedSetMethod = buildNestedSetParentIdMethod(nestedSetParentColumn);
|
|
69
75
|
let keyTypeSection = '';
|
|
70
76
|
if (idType === 'Uuid' || idType === 'Ulid' || idType === 'String') {
|
|
@@ -162,7 +168,7 @@ ${appends} ];
|
|
|
162
168
|
return [
|
|
163
169
|
${casts} ];
|
|
164
170
|
}
|
|
165
|
-
${relations}${accessors}${nestedSetMethod}
|
|
171
|
+
${relations}${accessors}${fileAccessors}${nestedSetMethod}
|
|
166
172
|
}
|
|
167
173
|
`;
|
|
168
174
|
return baseFile(`${config.models.basePath}/${modelName}BaseModel.php`, content);
|
|
@@ -217,7 +223,7 @@ ${traits.join('\n')}
|
|
|
217
223
|
`;
|
|
218
224
|
return userFile(`${config.models.path}/${modelName}.php`, content);
|
|
219
225
|
}
|
|
220
|
-
function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable, hasTranslatable, properties, needsUuidTrait = false, needsUlidTrait = false, hasNestedSet = false, nestedSetNamespace = 'Aimeos\\Nestedset') {
|
|
226
|
+
function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable, hasTranslatable, properties, needsUuidTrait = false, needsUlidTrait = false, hasNestedSet = false, nestedSetNamespace = 'Aimeos\\Nestedset', hasFiles = false, modelNamespace = '') {
|
|
221
227
|
const lines = [];
|
|
222
228
|
if (isAuthenticatable) {
|
|
223
229
|
lines.push('use Illuminate\\Foundation\\Auth\\User as Authenticatable;');
|
|
@@ -237,6 +243,12 @@ function buildImports(baseNamespace, modelName, hasSoftDelete, isAuthenticatable
|
|
|
237
243
|
lines.push('use Illuminate\\Database\\Eloquent\\Collection as EloquentCollection;');
|
|
238
244
|
lines.push(`use ${baseNamespace}\\Traits\\HasLocalizedDisplayName;`);
|
|
239
245
|
lines.push(`use ${baseNamespace}\\Locales\\${modelName}Locales;`);
|
|
246
|
+
if (hasFiles) {
|
|
247
|
+
lines.push(`use ${baseNamespace}\\Traits\\HasFiles;`);
|
|
248
|
+
if (modelNamespace) {
|
|
249
|
+
lines.push(`use ${modelNamespace}\\File;`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
240
252
|
if (hasSoftDelete) {
|
|
241
253
|
lines.push('use Illuminate\\Database\\Eloquent\\SoftDeletes;');
|
|
242
254
|
}
|
|
@@ -283,9 +295,11 @@ function buildDocProperties(properties, expandedProperties, propertyOrder) {
|
|
|
283
295
|
}
|
|
284
296
|
return lines.length === 0 ? '' : lines.join('\n') + '\n';
|
|
285
297
|
}
|
|
286
|
-
function buildTraits(hasSoftDelete, isAuthenticatable, hasTranslatable, needsUuidTrait = false, needsUlidTrait = false, hasNestedSet = false) {
|
|
298
|
+
function buildTraits(hasSoftDelete, isAuthenticatable, hasTranslatable, needsUuidTrait = false, needsUlidTrait = false, hasNestedSet = false, hasFiles = false) {
|
|
287
299
|
const lines = [];
|
|
288
300
|
lines.push(' use HasLocalizedDisplayName;');
|
|
301
|
+
if (hasFiles)
|
|
302
|
+
lines.push(' use HasFiles;');
|
|
289
303
|
if (needsUuidTrait)
|
|
290
304
|
lines.push(' use HasUuids;');
|
|
291
305
|
if (needsUlidTrait)
|
|
@@ -535,6 +549,38 @@ function buildNestedSetParentIdMethod(parentColumn) {
|
|
|
535
549
|
}
|
|
536
550
|
`;
|
|
537
551
|
}
|
|
552
|
+
function buildFileAccessors(properties, propertyOrder, modelNamespace) {
|
|
553
|
+
const methods = [];
|
|
554
|
+
for (const propName of propertyOrder) {
|
|
555
|
+
const prop = properties[propName];
|
|
556
|
+
if (!prop || prop['type'] !== 'File')
|
|
557
|
+
continue;
|
|
558
|
+
const collection = prop['collection'] || toSnakeCase(propName);
|
|
559
|
+
const multiple = prop['multiple'] ?? false;
|
|
560
|
+
const methodName = toCamelCase(propName);
|
|
561
|
+
if (multiple) {
|
|
562
|
+
methods.push(`
|
|
563
|
+
/**
|
|
564
|
+
* Get ${propName} files (collection: ${collection}).
|
|
565
|
+
*/
|
|
566
|
+
public function ${methodName}(): MorphMany
|
|
567
|
+
{
|
|
568
|
+
return $this->filesInCollection('${collection}');
|
|
569
|
+
}`);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
methods.push(`
|
|
573
|
+
/**
|
|
574
|
+
* Get ${propName} file (collection: ${collection}).
|
|
575
|
+
*/
|
|
576
|
+
public function ${methodName}(): ?File
|
|
577
|
+
{
|
|
578
|
+
return $this->filesInCollection('${collection}')->first();
|
|
579
|
+
}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return methods.join('\n');
|
|
583
|
+
}
|
|
538
584
|
function fullNameAccessor(prefix, suffix, fields, separator) {
|
|
539
585
|
const methodName = 'get' + toPascalCase(prefix) + suffix + 'Attribute';
|
|
540
586
|
const fieldAccess = fields.map(f => `$this->${f}`).join(', ');
|
|
@@ -86,6 +86,29 @@ function generateBaseRequest(name, schema, reader, config, action) {
|
|
|
86
86
|
attributeKeys.push(snakeName);
|
|
87
87
|
continue;
|
|
88
88
|
}
|
|
89
|
+
// File type: generate array item rules for multiple files
|
|
90
|
+
if (type === 'File') {
|
|
91
|
+
const snakeName = toSnakeCase(propName);
|
|
92
|
+
const multiple = prop['multiple'] ?? false;
|
|
93
|
+
const rules = isUpdate
|
|
94
|
+
? toUpdateRules(prop, tableName, modelRouteParam)
|
|
95
|
+
: toStoreRules(prop, tableName);
|
|
96
|
+
rulesLines.push(` '${snakeName}' => ${formatRules(rules)},`);
|
|
97
|
+
attributeKeys.push(snakeName);
|
|
98
|
+
if (multiple) {
|
|
99
|
+
// Generate array item rules: field.* => ['file', 'mimes:...', 'max:...']
|
|
100
|
+
const itemRules = ['file'];
|
|
101
|
+
const accept = prop['accept'];
|
|
102
|
+
if (accept?.length)
|
|
103
|
+
itemRules.push(`mimes:${accept.join(',')}`);
|
|
104
|
+
const maxSize = prop['maxSize'];
|
|
105
|
+
if (maxSize)
|
|
106
|
+
itemRules.push(`max:${maxSize}`);
|
|
107
|
+
const formattedItemRules = `[${itemRules.map(r => `'${r}'`).join(', ')}]`;
|
|
108
|
+
rulesLines.push(` '${snakeName}.*' => ${formattedItemRules},`);
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
89
112
|
const snakeName = toSnakeCase(propName);
|
|
90
113
|
const rules = isUpdate
|
|
91
114
|
? toUpdateRules(prop, tableName, modelRouteParam)
|
|
@@ -47,6 +47,12 @@ 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
|
+
/** Check if any schema has File-type properties. */
|
|
51
|
+
hasFileProperties(): boolean;
|
|
52
|
+
/** Get the file config from schemas.json. */
|
|
53
|
+
getFileConfig(): import("../types.js").FileConfigExport | null;
|
|
54
|
+
/** Get all schemas that have File-type properties. */
|
|
55
|
+
getSchemasWithFileProperties(): Record<string, SchemaDefinition>;
|
|
50
56
|
/** Get translatable field names (snake_case) for a schema. */
|
|
51
57
|
getTranslatableFields(schemaName: string): string[];
|
|
52
58
|
}
|
|
@@ -158,6 +158,35 @@ export class SchemaReader {
|
|
|
158
158
|
const schema = this.getSchema(schemaName);
|
|
159
159
|
return schema?.tableName ?? '';
|
|
160
160
|
}
|
|
161
|
+
/** Check if any schema has File-type properties. */
|
|
162
|
+
hasFileProperties() {
|
|
163
|
+
for (const schema of Object.values(this.getObjectSchemas())) {
|
|
164
|
+
const props = schema.properties ?? {};
|
|
165
|
+
for (const prop of Object.values(props)) {
|
|
166
|
+
if (prop.type === 'File')
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
/** Get the file config from schemas.json. */
|
|
173
|
+
getFileConfig() {
|
|
174
|
+
return this.data.fileConfig ?? null;
|
|
175
|
+
}
|
|
176
|
+
/** Get all schemas that have File-type properties. */
|
|
177
|
+
getSchemasWithFileProperties() {
|
|
178
|
+
const result = {};
|
|
179
|
+
for (const [name, schema] of Object.entries(this.getObjectSchemas())) {
|
|
180
|
+
const props = schema.properties ?? {};
|
|
181
|
+
for (const prop of Object.values(props)) {
|
|
182
|
+
if (prop.type === 'File') {
|
|
183
|
+
result[name] = schema;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
161
190
|
/** Get translatable field names (snake_case) for a schema. */
|
|
162
191
|
getTranslatableFields(schemaName) {
|
|
163
192
|
const schema = this.getSchema(schemaName);
|
|
@@ -12,6 +12,10 @@ export function generateServiceProvider(reader, config) {
|
|
|
12
12
|
const modelName = toPascalCase(name);
|
|
13
13
|
morphEntries.push(` '${modelName}' => \\${modelNamespace}\\${modelName}::class,`);
|
|
14
14
|
}
|
|
15
|
+
// Add File to morph map when any schema uses File type
|
|
16
|
+
if (reader.hasFileProperties()) {
|
|
17
|
+
morphEntries.push(` 'File' => \\${modelNamespace}\\File::class,`);
|
|
18
|
+
}
|
|
15
19
|
morphEntries.sort();
|
|
16
20
|
const morphMapContent = morphEntries.join('\n');
|
|
17
21
|
// Build package migration loading lines
|
|
@@ -149,6 +149,25 @@ export function toStoreRules(property, tableName) {
|
|
|
149
149
|
}
|
|
150
150
|
break;
|
|
151
151
|
}
|
|
152
|
+
case 'File': {
|
|
153
|
+
const multiple = property['multiple'] ?? false;
|
|
154
|
+
if (multiple) {
|
|
155
|
+
rules.push('array');
|
|
156
|
+
const maxFiles = property['maxFiles'];
|
|
157
|
+
if (maxFiles)
|
|
158
|
+
rules.push(`max:${maxFiles}`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
rules.push('file');
|
|
162
|
+
const accept = property['accept'];
|
|
163
|
+
if (accept?.length)
|
|
164
|
+
rules.push(`mimes:${accept.join(',')}`);
|
|
165
|
+
const maxSize = property['maxSize'];
|
|
166
|
+
if (maxSize)
|
|
167
|
+
rules.push(`max:${maxSize}`);
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
152
171
|
default:
|
|
153
172
|
rules.push('string', 'max:255');
|
|
154
173
|
}
|
package/ts-dist/types.d.ts
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** Localized string — either a plain string or a locale map. */
|
|
7
7
|
export type LocalizedString = string | Record<string, string>;
|
|
8
|
+
/** File upload configuration. */
|
|
9
|
+
export interface FileConfigExport {
|
|
10
|
+
readonly tempFlow?: boolean;
|
|
11
|
+
readonly tempTtl?: string;
|
|
12
|
+
readonly cleanupSchedule?: string;
|
|
13
|
+
readonly defaultDisk?: string;
|
|
14
|
+
}
|
|
8
15
|
/** Top-level schemas.json structure. */
|
|
9
16
|
export interface SchemasJson {
|
|
10
17
|
readonly generatedAt: string;
|
|
@@ -22,6 +29,7 @@ export interface SchemasJson {
|
|
|
22
29
|
readonly simple: Record<string, SimpleTypeDefinition>;
|
|
23
30
|
readonly enums: Record<string, string[]>;
|
|
24
31
|
};
|
|
32
|
+
readonly fileConfig?: FileConfigExport;
|
|
25
33
|
readonly packages?: Record<string, PackageExportInfo>;
|
|
26
34
|
readonly schemas: Record<string, SchemaDefinition>;
|
|
27
35
|
}
|
|
@@ -189,6 +197,11 @@ export interface PropertyDefinition {
|
|
|
189
197
|
readonly pattern?: string;
|
|
190
198
|
readonly rules?: ValidationRules;
|
|
191
199
|
readonly enum?: string | readonly string[];
|
|
200
|
+
readonly multiple?: boolean;
|
|
201
|
+
readonly maxFiles?: number;
|
|
202
|
+
readonly accept?: readonly string[];
|
|
203
|
+
readonly maxSize?: number;
|
|
204
|
+
readonly collection?: string;
|
|
192
205
|
readonly relation?: string;
|
|
193
206
|
readonly target?: string;
|
|
194
207
|
readonly targets?: readonly string[];
|
package/ts-dist/zod-generator.js
CHANGED
|
@@ -227,8 +227,17 @@ function getZodSchemaForType(propDef, _fieldName, options) {
|
|
|
227
227
|
schema = 'z.number().int().positive()';
|
|
228
228
|
break;
|
|
229
229
|
case 'Association':
|
|
230
|
-
case 'File':
|
|
231
230
|
return '';
|
|
231
|
+
case 'File': {
|
|
232
|
+
const multiple = propDef.multiple ?? false;
|
|
233
|
+
if (multiple) {
|
|
234
|
+
schema = 'z.array(OmnifyFileSchema)';
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
schema = 'OmnifyFileSchema.nullable()';
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
232
241
|
default:
|
|
233
242
|
schema = 'z.string()';
|
|
234
243
|
}
|