@omnifyjp/omnify 3.21.2 → 3.22.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/omnify",
3
- "version": "3.21.2",
3
+ "version": "3.22.1",
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": "3.21.2",
40
- "@omnifyjp/omnify-darwin-x64": "3.21.2",
41
- "@omnifyjp/omnify-linux-x64": "3.21.2",
42
- "@omnifyjp/omnify-linux-arm64": "3.21.2",
43
- "@omnifyjp/omnify-win32-x64": "3.21.2"
39
+ "@omnifyjp/omnify-darwin-arm64": "3.22.1",
40
+ "@omnifyjp/omnify-darwin-x64": "3.22.1",
41
+ "@omnifyjp/omnify-linux-x64": "3.22.1",
42
+ "@omnifyjp/omnify-linux-arm64": "3.22.1",
43
+ "@omnifyjp/omnify-win32-x64": "3.22.1"
44
44
  }
45
45
  }
@@ -26,6 +26,7 @@ import { generateFileModels } from './file-model-generator.js';
26
26
  import { generateFileTrait } from './file-trait-generator.js';
27
27
  import { generateFileCleanup } from './file-cleanup-generator.js';
28
28
  import { generateSchemaConfig } from './schema-config-generator.js';
29
+ import { generateTranslatableConfig } from './translatable-config-generator.js';
29
30
  import { baseFile } from './types.js';
30
31
  import { generateControllers } from './controller-generator.js';
31
32
  import { generateServices } from './service-generator.js';
@@ -73,6 +74,12 @@ export function generatePhp(data, overrides) {
73
74
  }
74
75
  // Issue #34: every `kind: enum` schema becomes a global PHP enum class.
75
76
  files.push(...generateEnums(reader, config));
77
+ // Astrotomic translatable config (only when at least one schema has
78
+ // translatable fields). Closes the silent footgun where the vendor
79
+ // default config shipped with Astrotomic only supports ['en', 'fr', 'es']
80
+ // and the generator's flat `attr:locale` form would be dropped for any
81
+ // locale outside that list.
82
+ files.push(...generateTranslatableConfig(reader, config));
76
83
  // Per-schema files
77
84
  files.push(...generateLocales(reader, config));
78
85
  files.push(...generateModels(reader, config));
@@ -524,11 +524,42 @@ function buildRelations(schemaName, properties, propertyOrder, modelNamespace, r
524
524
  }
525
525
  return methods.join('\n');
526
526
  }
527
+ /**
528
+ * Method names that belong to Laravel trait authors and will break PHP's
529
+ * method-declaration rules if a generated relation shadows them. Caught in
530
+ * practice when `NotificationExtend.yaml` added a reverse relation called
531
+ * `notifications()` on User, which collides with `Notifiable::notifications()`
532
+ * — the class fails to load with a "Declaration must be compatible" fatal.
533
+ *
534
+ * Keep this tight — only names we've actually seen collide in shipped
535
+ * Laravel traits. False positives here would silently drop valid relations,
536
+ * so err on the side of under-reserving.
537
+ */
538
+ const LARAVEL_RESERVED_RELATION_NAMES = new Set([
539
+ // Illuminate\Notifications\Notifiable
540
+ 'notifications',
541
+ 'readNotifications',
542
+ 'unreadNotifications',
543
+ // Laravel Sanctum / Passport
544
+ 'tokens',
545
+ // Laravel Scout (rare but would conflict on searchable models)
546
+ 'searchableAs',
547
+ // Spatie Permission (common enough to pre-reserve)
548
+ 'roles',
549
+ 'permissions',
550
+ ]);
527
551
  /**
528
552
  * Discover child schemas that point at `parentName` via ManyToOne/OneToOne and
529
553
  * generate the corresponding HasMany/HasOne accessor on the parent. Skips any
530
554
  * inverse relation whose derived method name collides with a user-declared
531
555
  * relation. Issue #39.
556
+ *
557
+ * Also skips + warns when the derived method name collides with a well-known
558
+ * Laravel trait method (e.g. Notifiable::notifications), since silently
559
+ * shadowing those produces a PHP fatal at class load time. Users who need
560
+ * the relation should declare it explicitly in the editable {Name}.php with
561
+ * a unique method name, or add `mappedBy: <customName>` to the child schema
562
+ * so the derived method name changes.
532
563
  */
533
564
  function buildInverseRelations(parentName, modelNamespace, reader, declaredMethods, explicitInverses) {
534
565
  const methods = [];
@@ -561,6 +592,17 @@ function buildInverseRelations(parentName, modelNamespace, reader, declaredMetho
561
592
  : toCamelCase(pluralize(childName));
562
593
  if (declaredMethods.has(methodName))
563
594
  continue;
595
+ // Reserved-name collision guard: Laravel trait methods like
596
+ // Notifiable::notifications() cannot be safely shadowed. Skip + warn.
597
+ if (LARAVEL_RESERVED_RELATION_NAMES.has(methodName)) {
598
+ console.warn(`[omnify-ts] ${parentName}: skipping auto-generated inverse relation ` +
599
+ `'${methodName}()' from ${childName}.${childPropName} — name collides ` +
600
+ `with a Laravel trait method (e.g. Notifiable::${methodName}). ` +
601
+ `Fix: add 'mappedBy: <customName>' to ${childName}.${childPropName} ` +
602
+ `in your schema so the derived method name changes, or declare the ` +
603
+ `relation manually in the editable ${parentName}.php with a unique name.`);
604
+ continue;
605
+ }
564
606
  declaredMethods.add(methodName);
565
607
  // OneToOne with mappedBy is the reverse side already — skip; it would
566
608
  // generate a child-side accessor, not a parent-side one.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Generates `config/translatable.php` from the project's omnify locale config.
3
+ *
4
+ * Addresses a gap caught while writing the #75–#79 service base E2E tests
5
+ * in l12: Astrotomic's vendor default config only ships `['en', 'fr', 'es']`
6
+ * as supported locales. Consumers using `ja` / `vi` / `ko` / etc. hit a
7
+ * silent footgun — `Translatable::fill(['title:ja' => '...'])` drops the key
8
+ * because `getLocalesHelper()->has('ja')` returns false, producing NULL
9
+ * translations at runtime with no error.
10
+ *
11
+ * This generator emits a ready-to-commit `config/translatable.php` wired to
12
+ * the locales declared in `omnify.yaml` under `locale.locales`. It's only
13
+ * written when the project actually has a schema with translatable fields
14
+ * (no point in rewriting the config if nothing would consume it).
15
+ *
16
+ * The file is marked `overwrite: true` — it's a base file in Omnify's sense:
17
+ * when locales change in omnify.yaml, the config should follow automatically.
18
+ * Consumers that need per-project overrides can copy it to a different name
19
+ * or extend it via Laravel's config merging.
20
+ */
21
+ import type { SchemaReader } from './schema-reader.js';
22
+ import type { GeneratedFile, PhpConfig } from './types.js';
23
+ export declare function generateTranslatableConfig(reader: SchemaReader, _config: PhpConfig): GeneratedFile[];
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Generates `config/translatable.php` from the project's omnify locale config.
3
+ *
4
+ * Addresses a gap caught while writing the #75–#79 service base E2E tests
5
+ * in l12: Astrotomic's vendor default config only ships `['en', 'fr', 'es']`
6
+ * as supported locales. Consumers using `ja` / `vi` / `ko` / etc. hit a
7
+ * silent footgun — `Translatable::fill(['title:ja' => '...'])` drops the key
8
+ * because `getLocalesHelper()->has('ja')` returns false, producing NULL
9
+ * translations at runtime with no error.
10
+ *
11
+ * This generator emits a ready-to-commit `config/translatable.php` wired to
12
+ * the locales declared in `omnify.yaml` under `locale.locales`. It's only
13
+ * written when the project actually has a schema with translatable fields
14
+ * (no point in rewriting the config if nothing would consume it).
15
+ *
16
+ * The file is marked `overwrite: true` — it's a base file in Omnify's sense:
17
+ * when locales change in omnify.yaml, the config should follow automatically.
18
+ * Consumers that need per-project overrides can copy it to a different name
19
+ * or extend it via Laravel's config merging.
20
+ */
21
+ import { baseFile } from './types.js';
22
+ export function generateTranslatableConfig(reader, _config) {
23
+ // Only emit when at least one schema has translatable fields — otherwise
24
+ // the file would just be dead weight in projects that don't use i18n.
25
+ const objectSchemas = reader.getProjectObjectSchemas();
26
+ let hasAnyTranslatable = false;
27
+ for (const name of Object.keys(objectSchemas)) {
28
+ if (reader.getTranslatableFields(name).length > 0) {
29
+ hasAnyTranslatable = true;
30
+ break;
31
+ }
32
+ }
33
+ if (!hasAnyTranslatable) {
34
+ return [];
35
+ }
36
+ const locales = reader.getLocales();
37
+ if (locales.length === 0) {
38
+ // Nothing we can honestly write — fall back to Astrotomic's own default.
39
+ return [];
40
+ }
41
+ const defaultLocale = reader.getDefaultLocale();
42
+ // Astrotomic has a separate fallback_locale config — prefer the omnify
43
+ // `fallbackLocale` if set, otherwise fall back to the same defaultLocale.
44
+ const fallbackLocale = reader.getLocale()?.fallbackLocale ?? defaultLocale;
45
+ const localesPhp = locales.map(l => ` '${l}',`).join('\n');
46
+ const content = `<?php
47
+
48
+ /**
49
+ * DO NOT EDIT - This file is auto-generated by Omnify from omnify.yaml's
50
+ * \`locale.locales\` config. Any manual changes will be overwritten on the
51
+ * next \`omnify generate\`.
52
+ *
53
+ * Consumers that need per-project overrides should use Laravel's config
54
+ * merging or a separate config file.
55
+ *
56
+ * @generated by omnify
57
+ */
58
+
59
+ return [
60
+
61
+ /*
62
+ |--------------------------------------------------------------------------
63
+ | Application Locales
64
+ |--------------------------------------------------------------------------
65
+ |
66
+ | Emitted from \`locale.locales\` in omnify.yaml. Adding or removing
67
+ | locales here without editing omnify.yaml first will be overwritten
68
+ | on the next generation.
69
+ |
70
+ */
71
+ 'locales' => [
72
+ ${localesPhp}
73
+ ],
74
+
75
+ /*
76
+ |--------------------------------------------------------------------------
77
+ | Locale separator
78
+ |--------------------------------------------------------------------------
79
+ */
80
+ 'locale_separator' => '-',
81
+
82
+ /*
83
+ |--------------------------------------------------------------------------
84
+ | Default locale
85
+ |--------------------------------------------------------------------------
86
+ */
87
+ 'default_locale' => '${defaultLocale}',
88
+
89
+ /*
90
+ |--------------------------------------------------------------------------
91
+ | Translator
92
+ |--------------------------------------------------------------------------
93
+ */
94
+ 'translator' => null,
95
+
96
+ /*
97
+ |--------------------------------------------------------------------------
98
+ | Use fallback
99
+ |--------------------------------------------------------------------------
100
+ |
101
+ | Enabled so that missing translations transparently fall back to the
102
+ | configured fallback_locale. Without this, attributesToArray() returns
103
+ | NULL for untranslated attributes instead of using the fallback row —
104
+ | which would surface as blank dropdown labels in multi-locale projects.
105
+ |
106
+ */
107
+ 'use_fallback' => true,
108
+
109
+ /*
110
+ |--------------------------------------------------------------------------
111
+ | Use property fallback
112
+ |--------------------------------------------------------------------------
113
+ */
114
+ 'use_property_fallback' => true,
115
+
116
+ /*
117
+ |--------------------------------------------------------------------------
118
+ | Fallback locale
119
+ |--------------------------------------------------------------------------
120
+ */
121
+ 'fallback_locale' => '${fallbackLocale}',
122
+
123
+ /*
124
+ |--------------------------------------------------------------------------
125
+ | Translation model namespace
126
+ |--------------------------------------------------------------------------
127
+ */
128
+ 'translation_model_namespace' => null,
129
+
130
+ /*
131
+ |--------------------------------------------------------------------------
132
+ | Translation suffix
133
+ |--------------------------------------------------------------------------
134
+ */
135
+ 'translation_suffix' => 'Translation',
136
+
137
+ /*
138
+ |--------------------------------------------------------------------------
139
+ | Locale key
140
+ |--------------------------------------------------------------------------
141
+ */
142
+ 'locale_key' => 'locale',
143
+
144
+ /*
145
+ |--------------------------------------------------------------------------
146
+ | To array always loads translations
147
+ |--------------------------------------------------------------------------
148
+ |
149
+ | When true, calling toArray() on a model auto-loads its translations.
150
+ | Keeps paginator results self-contained for JSON API responses.
151
+ |
152
+ */
153
+ 'to_array_always_loads_translations' => true,
154
+
155
+ /*
156
+ |--------------------------------------------------------------------------
157
+ | Rule factory
158
+ |--------------------------------------------------------------------------
159
+ */
160
+ 'rule_factory' => [
161
+ 'format' => 0, // Astrotomic\Translatable\Validation\RuleFactory::FORMAT_ARRAY
162
+ 'prefix' => '%',
163
+ 'suffix' => '%',
164
+ ],
165
+ ];
166
+ `;
167
+ return [baseFile('config/translatable.php', content)];
168
+ }