@omnifyjp/ts 6.7.13 → 6.7.15
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/enum-generator.js +6 -3
- package/dist/php/file-cleanup-generator.js +40 -4
- package/dist/php/model-generator.js +16 -10
- package/dist/php/relation-builder.d.ts +11 -0
- package/dist/php/relation-builder.js +43 -3
- package/dist/php/request-generator.js +57 -41
- package/dist/php/resource-generator.js +1 -0
- package/dist/php/service-generator.js +14 -12
- package/dist/php/trait-generator.js +3 -3
- package/dist/php/type-mapper.d.ts +14 -0
- package/dist/php/type-mapper.js +61 -5
- package/package.json +1 -1
|
@@ -32,6 +32,10 @@ function generateEnumClass(name, schema, config) {
|
|
|
32
32
|
const values = (schema.values ?? []);
|
|
33
33
|
const cases = values.map(v => buildCase(v));
|
|
34
34
|
const labelsConst = buildLabelsConstant(values);
|
|
35
|
+
const allValuesHaveLabels = values.every((value) => typeof value.label === 'string' || Object.keys(value.label ?? {}).length > 0);
|
|
36
|
+
const labelThirdFallback = allValuesHaveLabels
|
|
37
|
+
? '$labels[array_key_first($labels)]'
|
|
38
|
+
: '(array_values($labels)[0] ?? $this->value)';
|
|
35
39
|
const content = `<?php
|
|
36
40
|
|
|
37
41
|
namespace ${namespace};
|
|
@@ -59,12 +63,11 @@ ${labelsConst}
|
|
|
59
63
|
public function label(?string $locale = null): string
|
|
60
64
|
{
|
|
61
65
|
$locale = $locale ?? app()->getLocale();
|
|
62
|
-
$labels = self::LABELS[$this->value]
|
|
66
|
+
$labels = self::LABELS[$this->value];
|
|
63
67
|
|
|
64
68
|
return $labels[$locale]
|
|
65
69
|
?? $labels[config('app.fallback_locale', 'en')]
|
|
66
|
-
?? $
|
|
67
|
-
?? $this->value;
|
|
70
|
+
?? ${labelThirdFallback};
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
/**
|
|
@@ -122,18 +122,48 @@ class CleanupFiles extends Command
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
if ($dryRun) {
|
|
125
|
+
$skippedShared = 0;
|
|
126
|
+
// Each row is counted independently; the real run force-deletes sequentially so
|
|
127
|
+
// earlier rows stop counting as references — this total is an upper bound.
|
|
128
|
+
$query->chunkById(100, function ($files) use (&$skippedShared) {
|
|
129
|
+
foreach ($files as $file) {
|
|
130
|
+
if ($file->path && File::withTrashed()
|
|
131
|
+
->where('disk', $file->disk)
|
|
132
|
+
->where('path', $file->path)
|
|
133
|
+
->where('id', '!=', $file->id)
|
|
134
|
+
->exists()) {
|
|
135
|
+
$skippedShared++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
125
139
|
$this->info("[Phase 2] Would purge {$count} file(s) older than {$hours}h.");
|
|
140
|
+
if ($skippedShared > 0) {
|
|
141
|
+
$this->info("[Phase 2] Would skip up to {$skippedShared} physical delete(s) — object may still be referenced by another row.");
|
|
142
|
+
}
|
|
126
143
|
return self::SUCCESS;
|
|
127
144
|
}
|
|
128
145
|
|
|
129
146
|
$purged = 0;
|
|
130
147
|
$errors = 0;
|
|
148
|
+
$skippedShared = 0;
|
|
131
149
|
|
|
132
|
-
$query->chunkById(100, function ($files) use (&$purged, &$errors) {
|
|
150
|
+
$query->chunkById(100, function ($files) use (&$purged, &$errors, &$skippedShared) {
|
|
133
151
|
foreach ($files as $file) {
|
|
134
152
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
// Rows force-deleted earlier in this batch no longer appear in withTrashed(),
|
|
154
|
+
// so the last row sharing a disk+path deletes the object without extra bookkeeping.
|
|
155
|
+
if ($file->path) {
|
|
156
|
+
$pathReferencedElsewhere = File::withTrashed()
|
|
157
|
+
->where('disk', $file->disk)
|
|
158
|
+
->where('path', $file->path)
|
|
159
|
+
->where('id', '!=', $file->id)
|
|
160
|
+
->exists();
|
|
161
|
+
|
|
162
|
+
if (! $pathReferencedElsewhere && Storage::disk($file->disk)->exists($file->path)) {
|
|
163
|
+
Storage::disk($file->disk)->delete($file->path);
|
|
164
|
+
} elseif ($pathReferencedElsewhere) {
|
|
165
|
+
$skippedShared++;
|
|
166
|
+
}
|
|
137
167
|
}
|
|
138
168
|
$file->forceDelete();
|
|
139
169
|
$purged++;
|
|
@@ -144,7 +174,13 @@ class CleanupFiles extends Command
|
|
|
144
174
|
}
|
|
145
175
|
});
|
|
146
176
|
|
|
147
|
-
$
|
|
177
|
+
$purgeMsg = "[Phase 2] Purged {$purged} file(s).";
|
|
178
|
+
if ($skippedShared > 0) {
|
|
179
|
+
$purgeMsg .= " Skipped {$skippedShared} physical delete(s) — object still referenced by another row.";
|
|
180
|
+
} else {
|
|
181
|
+
$purgeMsg .= ' Physical files removed.';
|
|
182
|
+
}
|
|
183
|
+
$this->info($purgeMsg);
|
|
148
184
|
if ($errors > 0) {
|
|
149
185
|
$this->warn("{$errors} file(s) failed to purge.");
|
|
150
186
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { toPascalCase, toSnakeCase, toCamelCase, toFkColumnName, pluralize } from './naming-helper.js';
|
|
5
5
|
import { resolveTimestamps } from './timestamps.js';
|
|
6
|
-
import { toCast, toPhpDocType, isHiddenByDefault } from './type-mapper.js';
|
|
6
|
+
import { toCast, toPhpDocType, isHiddenByDefault, resolveFkCast, resolveFkPhpDocType } from './type-mapper.js';
|
|
7
7
|
import { buildRelation } from './relation-builder.js';
|
|
8
8
|
import { shadowingPropertyConflicts } from './shadowing-conflicts.js';
|
|
9
9
|
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup, } from './types.js';
|
|
@@ -451,7 +451,7 @@ function generateUserModel(name, schema, reader, config, hasNestedSet = false) {
|
|
|
451
451
|
if (hasNestedSet) {
|
|
452
452
|
imports.push(`use ${nestedSetNamespace}\\NodeTrait;`);
|
|
453
453
|
}
|
|
454
|
-
const traits = [
|
|
454
|
+
const traits = [` /** @use HasFactory<${modelName}Factory> */\n use HasFactory;`];
|
|
455
455
|
if (hasNestedSet) {
|
|
456
456
|
traits.push(' use NodeTrait;');
|
|
457
457
|
}
|
|
@@ -614,7 +614,8 @@ function buildDocProperties(properties, expandedProperties, propertyOrder, reade
|
|
|
614
614
|
// doesn't actually have.
|
|
615
615
|
const snakeName = prop['column'] || toFkColumnName(propName);
|
|
616
616
|
const nullable = prop['nullable'] ?? false;
|
|
617
|
-
const
|
|
617
|
+
const target = prop['target'] ?? '';
|
|
618
|
+
const phpType = resolveFkPhpDocType(prop, target, reader, nullable);
|
|
618
619
|
lines.push(` * @property ${phpType} $${snakeName}`);
|
|
619
620
|
}
|
|
620
621
|
else if (relation === 'MorphTo') {
|
|
@@ -808,13 +809,10 @@ function buildCasts(properties, expandedProperties, propertyOrder, reader, confi
|
|
|
808
809
|
const relation = prop['relation'] ?? '';
|
|
809
810
|
if (relation === 'ManyToOne') {
|
|
810
811
|
const target = prop['target'] ?? '';
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
? targetSchema.options.id
|
|
814
|
-
: 'BigInt';
|
|
815
|
-
if (targetIdType !== 'Uuid' && targetIdType !== 'Ulid') {
|
|
812
|
+
const fkCast = resolveFkCast(prop, target, reader);
|
|
813
|
+
if (fkCast) {
|
|
816
814
|
// omnify-go#166: honor the `column:` override (ForeignColumn).
|
|
817
|
-
casts.push(`'${prop['column'] || toFkColumnName(propName)}' => '
|
|
815
|
+
casts.push(`'${prop['column'] || toFkColumnName(propName)}' => '${fkCast}',`);
|
|
818
816
|
}
|
|
819
817
|
}
|
|
820
818
|
continue;
|
|
@@ -934,7 +932,14 @@ function buildRelations(schemaName, properties, propertyOrder, modelNamespace, r
|
|
|
934
932
|
: toSnakeCase(propName));
|
|
935
933
|
}
|
|
936
934
|
}
|
|
937
|
-
const result = buildRelation(propName, prop, ns, {
|
|
935
|
+
const result = buildRelation(propName, prop, ns, {
|
|
936
|
+
sourceTableName,
|
|
937
|
+
targetTableName,
|
|
938
|
+
pivotProperties,
|
|
939
|
+
pivotTableName,
|
|
940
|
+
reader,
|
|
941
|
+
sourceSchemaName: schemaName,
|
|
942
|
+
});
|
|
938
943
|
if (result) {
|
|
939
944
|
methods.push('\n' + result.method);
|
|
940
945
|
declaredMethods.add(toCamelCase(propName));
|
|
@@ -1054,6 +1059,7 @@ function buildInverseRelations(parentName, modelNamespace, reader, declaredMetho
|
|
|
1054
1059
|
methods.push(`
|
|
1055
1060
|
/**
|
|
1056
1061
|
* Auto-generated inverse of ${childName}.${childPropName} (${relation}).
|
|
1062
|
+
* @return ${returnType}<${fqcn}, $this>
|
|
1057
1063
|
*/
|
|
1058
1064
|
public function ${methodName}(): ${returnType}
|
|
1059
1065
|
{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Port of RelationBuilder.php — generates Eloquent relation methods.
|
|
3
3
|
*/
|
|
4
|
+
import type { SchemaReader } from './schema-reader.js';
|
|
4
5
|
interface RelationResult {
|
|
5
6
|
method: string;
|
|
6
7
|
imports: string[];
|
|
@@ -8,6 +9,9 @@ interface RelationResult {
|
|
|
8
9
|
interface RelationContext {
|
|
9
10
|
sourceTableName: string;
|
|
10
11
|
targetTableName: string;
|
|
12
|
+
/** Owning schema name (PascalCase) for OneToMany FK resolution. Issue #237. */
|
|
13
|
+
sourceSchemaName?: string;
|
|
14
|
+
reader?: SchemaReader;
|
|
11
15
|
/** Non-conventional physical table resolved from an explicit pivot schema.
|
|
12
16
|
* Omitted when Laravel can infer the joining table from model basenames. */
|
|
13
17
|
pivotTableName?: string;
|
|
@@ -24,6 +28,13 @@ export declare function buildRelation(propName: string, property: Record<string,
|
|
|
24
28
|
export declare function generatePivotTableName(sourceModel: string, targetModel: string): string;
|
|
25
29
|
/** Get the return type hint for a relation. */
|
|
26
30
|
export declare function getReturnType(relation: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the child-table FK column for a OneToMany when `mappedBy` / `inversedBy`
|
|
33
|
+
* is absent. Prefer the single inverse ManyToOne on the target (omnify-go#166
|
|
34
|
+
* `column:` override included); if zero or multiple matches, fall back to the
|
|
35
|
+
* owning schema name — not the plural property name. Issue #237.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveHasManyForeignKey(_propName: string, target: string, context?: RelationContext): string;
|
|
27
38
|
/**
|
|
28
39
|
* Build a chained `->orderBy('col', 'dir')` PHP fragment from an OrderByList.
|
|
29
40
|
* Returns an empty string if no ordering is configured. Issue #40.
|
|
@@ -16,7 +16,7 @@ export function buildRelation(propName, property, modelNamespace, context) {
|
|
|
16
16
|
method = oneToOne(propName, property, target, modelNamespace);
|
|
17
17
|
break;
|
|
18
18
|
case 'OneToMany':
|
|
19
|
-
method = hasMany(propName, property, target, modelNamespace);
|
|
19
|
+
method = hasMany(propName, property, target, modelNamespace, context);
|
|
20
20
|
break;
|
|
21
21
|
case 'ManyToMany':
|
|
22
22
|
method = belongsToMany(propName, property, target, modelNamespace, context);
|
|
@@ -39,6 +39,11 @@ export function buildRelation(propName, property, modelNamespace, context) {
|
|
|
39
39
|
}
|
|
40
40
|
if (method === null)
|
|
41
41
|
return null;
|
|
42
|
+
const type = getReturnType(relation);
|
|
43
|
+
const related = relation === 'MorphTo'
|
|
44
|
+
? '\\Illuminate\\Database\\Eloquent\\Model'
|
|
45
|
+
: '\\' + modelNamespace + '\\' + target;
|
|
46
|
+
method = method.replace(' */', ` * @return ${type}<${related}, $this>\n */`);
|
|
42
47
|
return { method, imports };
|
|
43
48
|
}
|
|
44
49
|
/** Generate the table name Laravel's joiningTable() infers from model basenames. */
|
|
@@ -92,9 +97,11 @@ function oneToOne(propName, property, target, ns) {
|
|
|
92
97
|
}
|
|
93
98
|
return belongsTo(propName, property, target, ns);
|
|
94
99
|
}
|
|
95
|
-
function hasMany(propName, property, target, ns) {
|
|
100
|
+
function hasMany(propName, property, target, ns, context) {
|
|
96
101
|
const inverse = property['mappedBy'] ?? property['inversedBy'] ?? null;
|
|
97
|
-
const foreignKey = inverse
|
|
102
|
+
const foreignKey = inverse
|
|
103
|
+
? toFkColumnName(inverse)
|
|
104
|
+
: resolveHasManyForeignKey(propName, target, context);
|
|
98
105
|
const methodName = toCamelCase(propName);
|
|
99
106
|
const fqcn = `\\${ns}\\${target}`;
|
|
100
107
|
const orderChain = formatOrderByChain(property['orderBy']);
|
|
@@ -106,6 +113,39 @@ function hasMany(propName, property, target, ns) {
|
|
|
106
113
|
return $this->hasMany(${fqcn}::class, '${foreignKey}')${orderChain};
|
|
107
114
|
}`;
|
|
108
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve the child-table FK column for a OneToMany when `mappedBy` / `inversedBy`
|
|
118
|
+
* is absent. Prefer the single inverse ManyToOne on the target (omnify-go#166
|
|
119
|
+
* `column:` override included); if zero or multiple matches, fall back to the
|
|
120
|
+
* owning schema name — not the plural property name. Issue #237.
|
|
121
|
+
*/
|
|
122
|
+
export function resolveHasManyForeignKey(_propName, target, context) {
|
|
123
|
+
const reader = context?.reader;
|
|
124
|
+
const owningSchemaName = context?.sourceSchemaName;
|
|
125
|
+
if (reader && owningSchemaName && target) {
|
|
126
|
+
const matches = findInverseManyToOneFkColumns(reader, target, owningSchemaName);
|
|
127
|
+
if (matches.length === 1) {
|
|
128
|
+
return matches[0];
|
|
129
|
+
}
|
|
130
|
+
return toFkColumnName(owningSchemaName);
|
|
131
|
+
}
|
|
132
|
+
return toFkColumnName(_propName);
|
|
133
|
+
}
|
|
134
|
+
function findInverseManyToOneFkColumns(reader, targetSchemaName, owningSchemaName) {
|
|
135
|
+
const targetSchema = reader.getSchema(targetSchemaName);
|
|
136
|
+
const props = (targetSchema?.properties ?? {});
|
|
137
|
+
const columns = [];
|
|
138
|
+
for (const [childPropName, childProp] of Object.entries(props)) {
|
|
139
|
+
if (childProp['type'] !== 'Association')
|
|
140
|
+
continue;
|
|
141
|
+
if (childProp['relation'] !== 'ManyToOne')
|
|
142
|
+
continue;
|
|
143
|
+
if (childProp['target'] !== owningSchemaName)
|
|
144
|
+
continue;
|
|
145
|
+
columns.push(childProp['column'] || toFkColumnName(childPropName));
|
|
146
|
+
}
|
|
147
|
+
return columns;
|
|
148
|
+
}
|
|
109
149
|
/**
|
|
110
150
|
* Build a chained `->orderBy('col', 'dir')` PHP fragment from an OrderByList.
|
|
111
151
|
* Returns an empty string if no ordering is configured. Issue #40.
|
|
@@ -2,26 +2,8 @@
|
|
|
2
2
|
* Port of RequestGenerator.php — generates Store/Update request classes.
|
|
3
3
|
*/
|
|
4
4
|
import { toPascalCase, toSnakeCase, toCamelCase, toFkColumnName } from './naming-helper.js';
|
|
5
|
-
import { toStoreRules, toUpdateRules, formatRules } from './type-mapper.js';
|
|
5
|
+
import { toStoreRules, toUpdateRules, formatRules, resolveFkRuleType } from './type-mapper.js';
|
|
6
6
|
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup } from './types.js';
|
|
7
|
-
/**
|
|
8
|
-
* Resolve the Laravel validation rule type ('integer', 'uuid', or 'string') for
|
|
9
|
-
* a foreign key column based on the target schema's primary key type.
|
|
10
|
-
*
|
|
11
|
-
* Mirrors the FK cast logic in `model-generator.ts` so validation rules and
|
|
12
|
-
* Eloquent casts agree about the wire format. Issue #36.
|
|
13
|
-
*/
|
|
14
|
-
function resolveFkRuleType(target, reader) {
|
|
15
|
-
if (!target)
|
|
16
|
-
return 'integer';
|
|
17
|
-
const targetSchema = reader.getSchema(target);
|
|
18
|
-
const id = targetSchema?.options?.id;
|
|
19
|
-
if (id === 'Uuid')
|
|
20
|
-
return 'uuid';
|
|
21
|
-
if (id === 'Ulid' || id === 'String')
|
|
22
|
-
return 'string';
|
|
23
|
-
return 'integer';
|
|
24
|
-
}
|
|
25
7
|
/** Generate Store/Update request classes for all project-owned visible object schemas. */
|
|
26
8
|
export function generateRequests(reader, config) {
|
|
27
9
|
const files = [];
|
|
@@ -105,7 +87,7 @@ function generateBaseRequest(name, schema, reader, config, action) {
|
|
|
105
87
|
if (relation === 'ManyToOne') {
|
|
106
88
|
const snakeName = toFkColumnName(propName);
|
|
107
89
|
const target = prop['target'] ?? '';
|
|
108
|
-
const targetIdType = resolveFkRuleType(target, reader);
|
|
90
|
+
const targetIdType = resolveFkRuleType(prop, target, reader);
|
|
109
91
|
const rules = isUpdate
|
|
110
92
|
? toUpdateRules(prop, tableName, modelRouteParam, targetIdType)
|
|
111
93
|
: toStoreRules(prop, tableName, targetIdType);
|
|
@@ -180,6 +162,58 @@ function generateBaseRequest(name, schema, reader, config, action) {
|
|
|
180
162
|
const localesClass = `${modelLocalesNamespace}\\${modelName}Locales`;
|
|
181
163
|
const attributeKeysList = attributeKeys.map(k => ` '${k}',`).join('\n');
|
|
182
164
|
const needsRuleImport = rulesContent.includes('Rule::');
|
|
165
|
+
// WHICH KEYS THE LOCALES CLASS WILL ACTUALLY HOLD, which is not the same
|
|
166
|
+
// question as which keys exist. `locales-generator` skips every Association
|
|
167
|
+
// outright, while this generator pushes each ManyToOne's FK column into
|
|
168
|
+
// `attributeKeys` — so a schema whose properties are ALL associations gets a
|
|
169
|
+
// non-empty key list and an EMPTY PROPERTY_DISPLAY_NAMES, and the loop below
|
|
170
|
+
// then reads offsets off `array{}`: ten unfixable PHPStan errors per such schema.
|
|
171
|
+
//
|
|
172
|
+
// Measured twice. `PlanItem` hit it first and was dropped for an unrelated
|
|
173
|
+
// reason, which hid it; `PlanMembership` hit it again and could not be
|
|
174
|
+
// dropped, because a self-referential many-to-many is REFUSED as a pivot —
|
|
175
|
+
// omnify itself says "declare a regular `kind: object` schema with two
|
|
176
|
+
// ManyToOne associations". So it is a shape the generator prescribes and
|
|
177
|
+
// then cannot generate for.
|
|
178
|
+
//
|
|
179
|
+
// Fixing the LOCALES side instead — emitting association display names
|
|
180
|
+
// under their FK column — was tried and reverted: it is better semantically
|
|
181
|
+
// (a validation error would say 計画課題 rather than `plan_issue_id`) but it
|
|
182
|
+
// populates the constant for every schema in the repo, which makes PHPStan
|
|
183
|
+
// prove the `??` fallback chain dead and turns 0 errors into 317. Worth
|
|
184
|
+
// doing upstream, where the generated body can change with it. Not here.
|
|
185
|
+
const hasNamedAttributes = Object.values(properties).some((candidate) => (candidate?.type ?? 'String') !== 'Association');
|
|
186
|
+
const attributesBody = attributeKeys.length === 0 || !hasNamedAttributes
|
|
187
|
+
? [
|
|
188
|
+
' protected function schemaAttributes(): array',
|
|
189
|
+
' {',
|
|
190
|
+
' return [];',
|
|
191
|
+
' }',
|
|
192
|
+
].join('\n')
|
|
193
|
+
: [
|
|
194
|
+
' protected function schemaAttributes(): array',
|
|
195
|
+
' {',
|
|
196
|
+
" $locale = app()->getLocale();",
|
|
197
|
+
" $fallback = config('app.fallback_locale', 'en');",
|
|
198
|
+
` $propertyNames = ${modelName}Locales::PROPERTY_DISPLAY_NAMES;`,
|
|
199
|
+
' $attributes = [];',
|
|
200
|
+
'',
|
|
201
|
+
' $keys = [',
|
|
202
|
+
attributeKeysList,
|
|
203
|
+
' ];',
|
|
204
|
+
'',
|
|
205
|
+
' foreach ($keys as $key) {',
|
|
206
|
+
' $names = $propertyNames[$key] ?? [];',
|
|
207
|
+
" $attributes[$key] = $names[$locale] ?? $names[$fallback] ?? $names[array_key_first($names) ?? 'en'] ?? $key;",
|
|
208
|
+
' }',
|
|
209
|
+
'',
|
|
210
|
+
' return $attributes;',
|
|
211
|
+
' }',
|
|
212
|
+
].join('\n');
|
|
213
|
+
// Only the non-empty body references the Locales class.
|
|
214
|
+
const localesImport = attributeKeys.length === 0 || !hasNamedAttributes
|
|
215
|
+
? ''
|
|
216
|
+
: `\nuse ${localesClass};`;
|
|
183
217
|
const ruleImport = needsRuleImport ? '\nuse Illuminate\\Validation\\Rule;' : '';
|
|
184
218
|
const content = `<?php
|
|
185
219
|
|
|
@@ -192,8 +226,7 @@ namespace ${baseNamespace};
|
|
|
192
226
|
* @generated by omnify
|
|
193
227
|
*/
|
|
194
228
|
|
|
195
|
-
use Illuminate\\Foundation\\Http\\FormRequest;${ruleImport}
|
|
196
|
-
use ${localesClass};
|
|
229
|
+
use Illuminate\\Foundation\\Http\\FormRequest;${ruleImport}${localesImport}
|
|
197
230
|
|
|
198
231
|
class ${modelName}${action}RequestBase extends FormRequest
|
|
199
232
|
{
|
|
@@ -229,7 +262,7 @@ class ${modelName}${action}RequestBase extends FormRequest
|
|
|
229
262
|
/**
|
|
230
263
|
* Get the schema-defined validation rules.
|
|
231
264
|
*
|
|
232
|
-
* @return array<string, array
|
|
265
|
+
* @return array<string, array<mixed>>
|
|
233
266
|
*/
|
|
234
267
|
protected function schemaRules(): array
|
|
235
268
|
{
|
|
@@ -244,24 +277,7 @@ ${rulesContent}
|
|
|
244
277
|
*
|
|
245
278
|
* @return array<string, string>
|
|
246
279
|
*/
|
|
247
|
-
|
|
248
|
-
{
|
|
249
|
-
$locale = app()->getLocale();
|
|
250
|
-
$fallback = config('app.fallback_locale', 'en');
|
|
251
|
-
$propertyNames = ${modelName}Locales::PROPERTY_DISPLAY_NAMES;
|
|
252
|
-
$attributes = [];
|
|
253
|
-
|
|
254
|
-
$keys = [
|
|
255
|
-
${attributeKeysList}
|
|
256
|
-
];
|
|
257
|
-
|
|
258
|
-
foreach ($keys as $key) {
|
|
259
|
-
$names = $propertyNames[$key] ?? [];
|
|
260
|
-
$attributes[$key] = $names[$locale] ?? $names[$fallback] ?? $names[array_key_first($names) ?? 'en'] ?? $key;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return $attributes;
|
|
264
|
-
}
|
|
280
|
+
${attributesBody}
|
|
265
281
|
}
|
|
266
282
|
`;
|
|
267
283
|
// Issue #96 + #98 v5.8+: legacy honors flatBase + group-by-folder.
|
|
@@ -554,7 +554,7 @@ function generateBaseService(name, schema, reader, config) {
|
|
|
554
554
|
// ── Build class body ──────────────────────────────────────────────────────
|
|
555
555
|
const sections = [];
|
|
556
556
|
// Model property
|
|
557
|
-
sections.push(` protected string $model = ${modelName}::class;`);
|
|
557
|
+
sections.push(` /** @var class-string<${modelName}> */\n protected string $model = ${modelName}::class;`);
|
|
558
558
|
// ── Query section ─────────────────────────────────────────────────────────
|
|
559
559
|
sections.push('');
|
|
560
560
|
sections.push(buildSectionComment('Query'));
|
|
@@ -595,7 +595,7 @@ function generateBaseService(name, schema, reader, config) {
|
|
|
595
595
|
// ── Eager Load Hooks (#81) ────────────────────────────────────────────────
|
|
596
596
|
sections.push('');
|
|
597
597
|
sections.push(buildSectionComment('Eager Load Hooks (#81)'));
|
|
598
|
-
sections.push(buildEagerLoadHooks());
|
|
598
|
+
sections.push(buildEagerLoadHooks(modelName));
|
|
599
599
|
// ── Translatable helpers ──────────────────────────────────────────────────
|
|
600
600
|
if (hasTranslatable) {
|
|
601
601
|
sections.push('');
|
|
@@ -675,14 +675,14 @@ function buildSectionComment(title) {
|
|
|
675
675
|
* config will be removed in a future major release per the #81
|
|
676
676
|
* deprecation path.
|
|
677
677
|
*/
|
|
678
|
-
function buildEagerLoadHooks() {
|
|
678
|
+
function buildEagerLoadHooks(modelName) {
|
|
679
679
|
return `
|
|
680
680
|
/**
|
|
681
681
|
* Override in the editable sibling service to eager-load relations
|
|
682
682
|
* on the list() query. Default: no-op. Legacy schema-level eagerLoad
|
|
683
683
|
* configuration still applies before this hook runs.
|
|
684
684
|
*
|
|
685
|
-
* @param \\Illuminate\\Database\\Eloquent\\Builder $query
|
|
685
|
+
* @param \\Illuminate\\Database\\Eloquent\\Builder<${modelName}> $query
|
|
686
686
|
*/
|
|
687
687
|
protected function applyListEagerLoads($query): void
|
|
688
688
|
{
|
|
@@ -694,7 +694,7 @@ function buildEagerLoadHooks() {
|
|
|
694
694
|
* Override in the editable sibling service to eager-load relations on
|
|
695
695
|
* the findById() query. Default: no-op.
|
|
696
696
|
*
|
|
697
|
-
* @param \\Illuminate\\Database\\Eloquent\\Builder $query
|
|
697
|
+
* @param \\Illuminate\\Database\\Eloquent\\Builder<${modelName}> $query
|
|
698
698
|
*/
|
|
699
699
|
protected function applyFindByIdEagerLoads($query): void
|
|
700
700
|
{
|
|
@@ -711,7 +711,7 @@ function buildEagerLoadHooks() {
|
|
|
711
711
|
* the lookup() query. Default: no-op. Most lookup endpoints should
|
|
712
712
|
* stay lean — eager loading defeats the point of a dropdown projection.
|
|
713
713
|
*
|
|
714
|
-
* @param \\Illuminate\\Database\\Eloquent\\Builder $query
|
|
714
|
+
* @param \\Illuminate\\Database\\Eloquent\\Builder<${modelName}> $query
|
|
715
715
|
*/
|
|
716
716
|
protected function applyLookupEagerLoads($query): void
|
|
717
717
|
{
|
|
@@ -875,10 +875,7 @@ function buildListFilterShape(filterableFields, allowedSortColumns, hasSoftDelet
|
|
|
875
875
|
lines.push(` * ${f.colName}?: ${phpType},`);
|
|
876
876
|
}
|
|
877
877
|
lines.push(` * search?: string,`);
|
|
878
|
-
|
|
879
|
-
? allowedSortColumns.flatMap(c => [`'${c}'`, `'-${c}'`]).join('|')
|
|
880
|
-
: 'string';
|
|
881
|
-
lines.push(` * sort?: ${sortUnion},`);
|
|
878
|
+
lines.push(` * sort?: string,`);
|
|
882
879
|
lines.push(` * per_page?: int,`);
|
|
883
880
|
if (hasSoftDelete) {
|
|
884
881
|
lines.push(` * with_trashed?: bool,`);
|
|
@@ -1106,6 +1103,7 @@ function buildListMethod(modelName, searchableFields, filterableFields, sortable
|
|
|
1106
1103
|
* @param array{
|
|
1107
1104
|
${filterShape}
|
|
1108
1105
|
* } $filters
|
|
1106
|
+
* @return LengthAwarePaginator<int, ${modelName}>
|
|
1109
1107
|
*
|
|
1110
1108
|
${listExample}
|
|
1111
1109
|
*/
|
|
@@ -1188,11 +1186,15 @@ function resolvePhpFilterType(type) {
|
|
|
1188
1186
|
}
|
|
1189
1187
|
function buildFilterLine(f) {
|
|
1190
1188
|
if (f.translatable) {
|
|
1191
|
-
return `
|
|
1189
|
+
return ` if (isset($filters['${f.colName}'])) {
|
|
1190
|
+
$query->whereTranslation('${f.colName}', $filters['${f.colName}']);
|
|
1191
|
+
}`;
|
|
1192
1192
|
}
|
|
1193
1193
|
switch (f.type) {
|
|
1194
1194
|
case 'Boolean':
|
|
1195
|
-
return `
|
|
1195
|
+
return ` if (isset($filters['${f.colName}'])) {
|
|
1196
|
+
$query->where('${f.colName}', $filters['${f.colName}']);
|
|
1197
|
+
}`;
|
|
1196
1198
|
default:
|
|
1197
1199
|
return ` $query->when($filters['${f.colName}'] ?? null, fn ($q, $v) => $q->where('${f.colName}', $v));`;
|
|
1198
1200
|
}
|
|
@@ -31,7 +31,7 @@ trait HasLocalizedDisplayName
|
|
|
31
31
|
public static function displayName(?string $locale = null): string
|
|
32
32
|
{
|
|
33
33
|
$locale = $locale ?? app()->getLocale();
|
|
34
|
-
$displayNames = static::$localizedDisplayNames
|
|
34
|
+
$displayNames = static::$localizedDisplayNames;
|
|
35
35
|
|
|
36
36
|
return $displayNames[$locale]
|
|
37
37
|
?? $displayNames[config('app.fallback_locale', 'en')]
|
|
@@ -46,7 +46,7 @@ trait HasLocalizedDisplayName
|
|
|
46
46
|
*/
|
|
47
47
|
public static function allDisplayNames(): array
|
|
48
48
|
{
|
|
49
|
-
return static::$localizedDisplayNames
|
|
49
|
+
return static::$localizedDisplayNames;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
@@ -88,7 +88,7 @@ trait HasLocalizedDisplayName
|
|
|
88
88
|
$locale = $locale ?? app()->getLocale();
|
|
89
89
|
$result = [];
|
|
90
90
|
|
|
91
|
-
foreach (static::$localizedPropertyDisplayNames
|
|
91
|
+
foreach (static::$localizedPropertyDisplayNames as $property => $displayNames) {
|
|
92
92
|
$result[$property] = $displayNames[$locale]
|
|
93
93
|
?? $displayNames[config('app.fallback_locale', 'en')]
|
|
94
94
|
?? $displayNames[array_key_first($displayNames) ?? 'en']
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Port of TypeMapper.php — type mappings for PHP casts, rules, resources.
|
|
3
3
|
*/
|
|
4
|
+
import type { SchemaReader } from './schema-reader.js';
|
|
4
5
|
type Rule = string | ['rule_in', string[]] | ['rule_unique_ignore', string, string];
|
|
6
|
+
/**
|
|
7
|
+
* Effective primary-key type for a ManyToOne foreign key column.
|
|
8
|
+
* Property-level `idType:` wins (required for targets outside the schema set),
|
|
9
|
+
* then the target schema's `options.id`, then an integer-family default.
|
|
10
|
+
* Issue #237.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveFkIdType(property: Record<string, unknown>, target: string, reader: SchemaReader): string;
|
|
13
|
+
/** Laravel validation rule type for a ManyToOne FK (Issue #36, #237). */
|
|
14
|
+
export declare function resolveFkRuleType(property: Record<string, unknown>, target: string, reader: SchemaReader): 'integer' | 'uuid' | 'string';
|
|
15
|
+
/** Eloquent cast for a ManyToOne FK column. */
|
|
16
|
+
export declare function resolveFkCast(property: Record<string, unknown>, target: string, reader: SchemaReader): 'integer' | 'string' | null;
|
|
17
|
+
/** `@property` doc type for a ManyToOne FK column. */
|
|
18
|
+
export declare function resolveFkPhpDocType(property: Record<string, unknown>, target: string, reader: SchemaReader, nullable: boolean): string;
|
|
5
19
|
/** Map Omnify property type to PHP cast. */
|
|
6
20
|
export declare function toCast(type: string, property?: Record<string, unknown>): string | null;
|
|
7
21
|
/** Map Omnify property type to PHP doc type. */
|
package/dist/php/type-mapper.js
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
* Port of TypeMapper.php — type mappings for PHP casts, rules, resources.
|
|
3
3
|
*/
|
|
4
4
|
import { toTableName } from './naming-helper.js';
|
|
5
|
+
/**
|
|
6
|
+
* Effective primary-key type for a ManyToOne foreign key column.
|
|
7
|
+
* Property-level `idType:` wins (required for targets outside the schema set),
|
|
8
|
+
* then the target schema's `options.id`, then an integer-family default.
|
|
9
|
+
* Issue #237.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveFkIdType(property, target, reader) {
|
|
12
|
+
const override = property['idType'];
|
|
13
|
+
if (typeof override === 'string' && override !== '') {
|
|
14
|
+
return override;
|
|
15
|
+
}
|
|
16
|
+
if (!target)
|
|
17
|
+
return 'BigInt';
|
|
18
|
+
const targetSchema = reader.getSchema(target);
|
|
19
|
+
const id = targetSchema?.options?.id;
|
|
20
|
+
if (typeof id === 'string')
|
|
21
|
+
return id;
|
|
22
|
+
return 'BigInt';
|
|
23
|
+
}
|
|
24
|
+
/** Laravel validation rule type for a ManyToOne FK (Issue #36, #237). */
|
|
25
|
+
export function resolveFkRuleType(property, target, reader) {
|
|
26
|
+
const idType = resolveFkIdType(property, target, reader);
|
|
27
|
+
if (idType === 'Uuid')
|
|
28
|
+
return 'uuid';
|
|
29
|
+
if (idType === 'Ulid' || idType === 'String')
|
|
30
|
+
return 'string';
|
|
31
|
+
return 'integer';
|
|
32
|
+
}
|
|
33
|
+
/** Eloquent cast for a ManyToOne FK column. */
|
|
34
|
+
export function resolveFkCast(property, target, reader) {
|
|
35
|
+
const idType = resolveFkIdType(property, target, reader);
|
|
36
|
+
if (idType === 'Uuid' || idType === 'Ulid' || idType === 'String')
|
|
37
|
+
return 'string';
|
|
38
|
+
if (idType === 'Int' || idType === 'BigInt' || idType === 'TinyInt')
|
|
39
|
+
return 'integer';
|
|
40
|
+
return 'integer';
|
|
41
|
+
}
|
|
42
|
+
/** `@property` doc type for a ManyToOne FK column. */
|
|
43
|
+
export function resolveFkPhpDocType(property, target, reader, nullable) {
|
|
44
|
+
const idType = resolveFkIdType(property, target, reader);
|
|
45
|
+
const phpType = idType === 'Int' || idType === 'BigInt' || idType === 'TinyInt' ? 'int' : 'string';
|
|
46
|
+
return nullable ? `${phpType}|null` : phpType;
|
|
47
|
+
}
|
|
5
48
|
/** Map Omnify property type to PHP cast. */
|
|
6
49
|
export function toCast(type, property = {}) {
|
|
7
50
|
switch (type) {
|
|
@@ -56,7 +99,7 @@ export function toPhpDocType(type, nullable = false) {
|
|
|
56
99
|
phpType = 'mixed';
|
|
57
100
|
break;
|
|
58
101
|
case 'Json':
|
|
59
|
-
phpType = 'array';
|
|
102
|
+
phpType = 'array<array-key, mixed>';
|
|
60
103
|
break;
|
|
61
104
|
default:
|
|
62
105
|
phpType = 'mixed';
|
|
@@ -85,11 +128,24 @@ export function toStoreRules(property, tableName, fkRuleType = 'integer') {
|
|
|
85
128
|
const unique = property['unique'] ?? false;
|
|
86
129
|
const vr = property['rules'];
|
|
87
130
|
const rules = [];
|
|
88
|
-
// required/nullable — rules.required can override
|
|
89
|
-
const
|
|
90
|
-
|
|
131
|
+
// required/nullable/sometimes — rules.required can override; non-nullable + default → sometimes (#235)
|
|
132
|
+
const explicitRequired = vr?.['required'];
|
|
133
|
+
const hasExplicitRequired = explicitRequired !== undefined;
|
|
134
|
+
const isRequired = hasExplicitRequired
|
|
135
|
+
? explicitRequired
|
|
91
136
|
: !nullable;
|
|
92
|
-
|
|
137
|
+
// `default: null` on a non-nullable column is contradictory YAML — treat it
|
|
138
|
+
// as no default so the column stays 'required' rather than silently optional.
|
|
139
|
+
const hasDefault = property['default'] !== undefined && property['default'] !== null;
|
|
140
|
+
if (!isRequired) {
|
|
141
|
+
rules.push('nullable');
|
|
142
|
+
}
|
|
143
|
+
else if (!hasExplicitRequired && !nullable && hasDefault) {
|
|
144
|
+
rules.push('sometimes');
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
rules.push('required');
|
|
148
|
+
}
|
|
93
149
|
switch (type) {
|
|
94
150
|
case 'String':
|
|
95
151
|
case 'Slug':
|