@omnifyjp/ts 6.7.13 → 6.7.14

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.
@@ -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
- ?? $labels[array_key_first($labels) ?? 'en']
67
- ?? $this->value;
70
+ ?? ${labelThirdFallback};
68
71
  }
69
72
 
70
73
  /**
@@ -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 = [' use HasFactory;'];
454
+ const traits = [` /** @use HasFactory<${modelName}Factory> */\n use HasFactory;`];
455
455
  if (hasNestedSet) {
456
456
  traits.push(' use NodeTrait;');
457
457
  }
@@ -1054,6 +1054,7 @@ function buildInverseRelations(parentName, modelNamespace, reader, declaredMetho
1054
1054
  methods.push(`
1055
1055
  /**
1056
1056
  * Auto-generated inverse of ${childName}.${childPropName} (${relation}).
1057
+ * @return ${returnType}<${fqcn}, $this>
1057
1058
  */
1058
1059
  public function ${methodName}(): ${returnType}
1059
1060
  {
@@ -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. */
@@ -180,6 +180,58 @@ function generateBaseRequest(name, schema, reader, config, action) {
180
180
  const localesClass = `${modelLocalesNamespace}\\${modelName}Locales`;
181
181
  const attributeKeysList = attributeKeys.map(k => ` '${k}',`).join('\n');
182
182
  const needsRuleImport = rulesContent.includes('Rule::');
183
+ // WHICH KEYS THE LOCALES CLASS WILL ACTUALLY HOLD, which is not the same
184
+ // question as which keys exist. `locales-generator` skips every Association
185
+ // outright, while this generator pushes each ManyToOne's FK column into
186
+ // `attributeKeys` — so a schema whose properties are ALL associations gets a
187
+ // non-empty key list and an EMPTY PROPERTY_DISPLAY_NAMES, and the loop below
188
+ // then reads offsets off `array{}`: ten unfixable PHPStan errors per such schema.
189
+ //
190
+ // Measured twice. `PlanItem` hit it first and was dropped for an unrelated
191
+ // reason, which hid it; `PlanMembership` hit it again and could not be
192
+ // dropped, because a self-referential many-to-many is REFUSED as a pivot —
193
+ // omnify itself says "declare a regular `kind: object` schema with two
194
+ // ManyToOne associations". So it is a shape the generator prescribes and
195
+ // then cannot generate for.
196
+ //
197
+ // Fixing the LOCALES side instead — emitting association display names
198
+ // under their FK column — was tried and reverted: it is better semantically
199
+ // (a validation error would say 計画課題 rather than `plan_issue_id`) but it
200
+ // populates the constant for every schema in the repo, which makes PHPStan
201
+ // prove the `??` fallback chain dead and turns 0 errors into 317. Worth
202
+ // doing upstream, where the generated body can change with it. Not here.
203
+ const hasNamedAttributes = Object.values(properties).some((candidate) => (candidate?.type ?? 'String') !== 'Association');
204
+ const attributesBody = attributeKeys.length === 0 || !hasNamedAttributes
205
+ ? [
206
+ ' protected function schemaAttributes(): array',
207
+ ' {',
208
+ ' return [];',
209
+ ' }',
210
+ ].join('\n')
211
+ : [
212
+ ' protected function schemaAttributes(): array',
213
+ ' {',
214
+ " $locale = app()->getLocale();",
215
+ " $fallback = config('app.fallback_locale', 'en');",
216
+ ` $propertyNames = ${modelName}Locales::PROPERTY_DISPLAY_NAMES;`,
217
+ ' $attributes = [];',
218
+ '',
219
+ ' $keys = [',
220
+ attributeKeysList,
221
+ ' ];',
222
+ '',
223
+ ' foreach ($keys as $key) {',
224
+ ' $names = $propertyNames[$key] ?? [];',
225
+ " $attributes[$key] = $names[$locale] ?? $names[$fallback] ?? $names[array_key_first($names) ?? 'en'] ?? $key;",
226
+ ' }',
227
+ '',
228
+ ' return $attributes;',
229
+ ' }',
230
+ ].join('\n');
231
+ // Only the non-empty body references the Locales class.
232
+ const localesImport = attributeKeys.length === 0 || !hasNamedAttributes
233
+ ? ''
234
+ : `\nuse ${localesClass};`;
183
235
  const ruleImport = needsRuleImport ? '\nuse Illuminate\\Validation\\Rule;' : '';
184
236
  const content = `<?php
185
237
 
@@ -192,8 +244,7 @@ namespace ${baseNamespace};
192
244
  * @generated by omnify
193
245
  */
194
246
 
195
- use Illuminate\\Foundation\\Http\\FormRequest;${ruleImport}
196
- use ${localesClass};
247
+ use Illuminate\\Foundation\\Http\\FormRequest;${ruleImport}${localesImport}
197
248
 
198
249
  class ${modelName}${action}RequestBase extends FormRequest
199
250
  {
@@ -229,7 +280,7 @@ class ${modelName}${action}RequestBase extends FormRequest
229
280
  /**
230
281
  * Get the schema-defined validation rules.
231
282
  *
232
- * @return array<string, array>
283
+ * @return array<string, array<mixed>>
233
284
  */
234
285
  protected function schemaRules(): array
235
286
  {
@@ -244,24 +295,7 @@ ${rulesContent}
244
295
  *
245
296
  * @return array<string, string>
246
297
  */
247
- protected function schemaAttributes(): array
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
- }
298
+ ${attributesBody}
265
299
  }
266
300
  `;
267
301
  // Issue #96 + #98 v5.8+: legacy honors flatBase + group-by-folder.
@@ -165,6 +165,7 @@ namespace ${baseNamespace};
165
165
 
166
166
  ${useBlock}
167
167
 
168
+ /** @mixin \\${modelNamespace}${schema.kind === 'pivot' ? '\\Pivot' : ''}\\${modelName} */
168
169
  class ${modelName}ResourceBase extends JsonResource
169
170
  {
170
171
  /**
@@ -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
- const sortUnion = allowedSortColumns.length > 0
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 ` $query->when(isset($filters['${f.colName}']), fn ($q) => $q->whereTranslation('${f.colName}', $filters['${f.colName}']));`;
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 ` $query->when(isset($filters['${f.colName}']), fn ($q) => $q->where('${f.colName}', $filters['${f.colName}']));`;
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 ?? [] as $property => $displayNames) {
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']
@@ -56,7 +56,7 @@ export function toPhpDocType(type, nullable = false) {
56
56
  phpType = 'mixed';
57
57
  break;
58
58
  case 'Json':
59
- phpType = 'array';
59
+ phpType = 'array<array-key, mixed>';
60
60
  break;
61
61
  default:
62
62
  phpType = 'mixed';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "6.7.13",
3
+ "version": "6.7.14",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",