@omnifyjp/ts 8.0.2 → 8.0.3

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/cli.js CHANGED
@@ -31,7 +31,7 @@ import { generateTypeScript } from './generator.js';
31
31
  import { generatePhp, derivePhpConfig } from './php/index.js';
32
32
  import { evaluateShadowingConflicts } from './php/shadowing-conflicts.js';
33
33
  import { collectReferencedClasses, hasEditableSubclassForBase, pruneDisabledScaffoldLayerBases, pruneOrphanServiceFilesIfEnabled } from './php/orphan-cleanup.js';
34
- import { findSiblingWithSameBasename, findAppRootAncestor } from './php/sibling-skip.js';
34
+ import { findSiblingWithSameBasename, findAppRootAncestor, isOmnifyWrapper } from './php/sibling-skip.js';
35
35
  import { resolveInput, sniffInputKind } from './input-resolver.js';
36
36
  function resolveFromConfig(configPath) {
37
37
  const raw = readFileSync(configPath, 'utf-8');
@@ -295,7 +295,9 @@ program
295
295
  // files always regenerate.
296
296
  if (!file.overwrite) {
297
297
  const scanRoot = findAppRootAncestor(filePath) ?? dirname(filePath);
298
- const collision = findSiblingWithSameBasename(scanRoot, basename(filePath), filePath);
298
+ // #299: only an omnify wrapper replaces the stub, never an
299
+ // unrelated class that shares its name (Laravel's stock User.php).
300
+ const collision = findSiblingWithSameBasename(scanRoot, basename(filePath), filePath, (path) => isOmnifyWrapper(readFileSync(path, 'utf-8'), file.content));
299
301
  if (collision) {
300
302
  phpSkippedSibling++;
301
303
  if (skippedSiblingExamples.length < 3) {
@@ -39,7 +39,9 @@ namespace ${traitsNamespace};
39
39
  * Trait that records every Eloquent lifecycle event into the central
40
40
  * audits history table. Apply via \`use HasOmnifyAuditLog;\` on any model
41
41
  * whose schema has \`options.audit.log: true\` (or when the global
42
- * \`audit.log\` flag is enabled).
42
+ * \`audit.log\` flag is enabled). The using class declares
43
+ * \`protected static array $auditExclude\` and \`$auditTags\` (the generated
44
+ * base model always does).
43
45
  *
44
46
  * DO NOT EDIT — auto-generated by Omnify. Any manual changes are lost on
45
47
  * the next \`omnify generate\` run.
@@ -110,7 +112,7 @@ trait HasOmnifyAuditLog
110
112
 
111
113
  $exclude = array_unique(array_merge(
112
114
  (array) config('omnify.audit.logExclude', []),
113
- (array) (static::$auditExclude ?? [])
115
+ static::$auditExclude
114
116
  ));
115
117
  if (!empty($exclude)) {
116
118
  $flip = array_flip($exclude);
@@ -124,7 +126,7 @@ trait HasOmnifyAuditLog
124
126
 
125
127
  $request = function_exists('request') ? request() : null;
126
128
  $user = Auth::user();
127
- $tags = static::$auditTags ?? [];
129
+ $tags = static::$auditTags;
128
130
 
129
131
  $payload = [
130
132
  'auditable_type' => $this->getMorphClass(),
@@ -1205,8 +1205,8 @@ function buildNestedSetParentIdMethod(parentColumn) {
1205
1205
  * - `$auditTags` is the static tag list applied to every audit row
1206
1206
  * from this model; useful for grouping (e.g. ["billing"]).
1207
1207
  *
1208
- * Empty arrays are omitted entirely so the trait falls through to its
1209
- * own static defaults — keeps the generated PHP minimal.
1208
+ * Both are declared on every audited model, as `[]` when empty: the trait
1209
+ * reads them directly (#298).
1210
1210
  */
1211
1211
  function buildAuditLogProperties(schemaName, reader) {
1212
1212
  if (!reader.isAuditLogEnabled(schemaName))
@@ -1214,9 +1214,13 @@ function buildAuditLogProperties(schemaName, reader) {
1214
1214
  const excludes = reader.getAuditExcludesFor(schemaName);
1215
1215
  const tags = reader.getAuditTagsFor(schemaName);
1216
1216
  const parts = [];
1217
- if (excludes.length > 0) {
1218
- const lines = excludes.map((c) => ` ${JSON.stringify(c).replace(/"/g, "'")},`).join('\n');
1219
- parts.push(`
1217
+ // #298: both statics are always declared on an audited model, empty or not,
1218
+ // so HasOmnifyAuditLog reads them directly instead of `?? []` on a property
1219
+ // that may not exist (PHPStan nullCoalesce.property when it does).
1220
+ const phpList = (items) => items.length === 0
1221
+ ? '[]'
1222
+ : `[\n${items.map((c) => ` ${JSON.stringify(c).replace(/"/g, "'")},`).join('\n')}\n ]`;
1223
+ parts.push(`
1220
1224
  /**
1221
1225
  * Columns scrubbed from old_values / new_values BEFORE the audit row
1222
1226
  * is built. Merged at runtime with the global \`config('omnify.audit.logExclude')\`
@@ -1224,13 +1228,8 @@ function buildAuditLogProperties(schemaName, reader) {
1224
1228
  *
1225
1229
  * @var array<int, string>
1226
1230
  */
1227
- protected static array $auditExclude = [
1228
- ${lines}
1229
- ];`);
1230
- }
1231
- if (tags.length > 0) {
1232
- const lines = tags.map((t) => ` ${JSON.stringify(t).replace(/"/g, "'")},`).join('\n');
1233
- parts.push(`
1231
+ protected static array $auditExclude = ${phpList(excludes)};`);
1232
+ parts.push(`
1234
1233
  /**
1235
1234
  * Static tags applied to every audit row from this model. Surfaces
1236
1235
  * in the audits table's \`tags\` column for grouping in the activity
@@ -1238,11 +1237,8 @@ ${lines}
1238
1237
  *
1239
1238
  * @var array<int, string>
1240
1239
  */
1241
- protected static array $auditTags = [
1242
- ${lines}
1243
- ];`);
1244
- }
1245
- return parts.length === 0 ? '' : parts.join('\n') + '\n';
1240
+ protected static array $auditTags = ${phpList(tags)};`);
1241
+ return parts.join('\n') + '\n';
1246
1242
  }
1247
1243
  function buildAuditSection(options, reader, modelNamespace) {
1248
1244
  const opts = options;
@@ -361,6 +361,12 @@ function operandToPhp(operand, schema, reader, side) {
361
361
  if (prop?.relation === 'ManyToOne') {
362
362
  return `$record->${toFkColumnName(propName)}`;
363
363
  }
364
+ // #295: an EnumRef column is cast to its backed enum (same predicate as
365
+ // the model's casts), so `$record->status === 'published'` compares an
366
+ // enum instance with a string and is never true. Compare the value.
367
+ if (prop?.type === 'EnumRef' && typeof prop.enum === 'string' && reader.getSchema(prop.enum)?.kind === 'enum') {
368
+ return `$record->${toSnakeCase(propName)}${prop.nullable ? '?->' : '->'}value`;
369
+ }
364
370
  return `$record->${toSnakeCase(propName)}`;
365
371
  }
366
372
  case 'variable': {
@@ -62,7 +62,8 @@ export function getReturnType(relation) {
62
62
  case 'MorphOne': return 'MorphOne';
63
63
  case 'MorphMany': return 'MorphMany';
64
64
  case 'MorphToMany': return 'MorphToMany';
65
- case 'MorphedByMany': return 'MorphedByMany';
65
+ // Laravel has no MorphedByMany class: morphedByMany() returns MorphToMany (#297).
66
+ case 'MorphedByMany': return 'MorphToMany';
66
67
  default: return 'BelongsTo';
67
68
  }
68
69
  }
@@ -255,7 +256,7 @@ function morphedByMany(propName, property, target, ns) {
255
256
  return ` /**
256
257
  * The ${propName} that belong to this model.
257
258
  */
258
- public function ${methodName}(): MorphedByMany
259
+ public function ${methodName}(): MorphToMany
259
260
  {
260
261
  return $this->morphedByMany(${fqcn}::class, '${morphName}')
261
262
  ->withTimestamps();
@@ -95,7 +95,7 @@ class OmnifyServiceProvider extends ServiceProvider
95
95
  // connection via Schema::connection(), so a single migrate run suffices).
96
96
  $omnifyMigrations = database_path('migrations/omnify');
97
97
  $this->loadMigrationsFrom($omnifyMigrations);
98
- foreach (glob($omnifyMigrations.'/*', GLOB_ONLYDIR) as $connectionDir) {
98
+ foreach (glob($omnifyMigrations.'/*', GLOB_ONLYDIR) ?: [] as $connectionDir) {
99
99
  $this->loadMigrationsFrom($connectionDir);
100
100
  }
101
101
  ${packageMigrationsBlock}
@@ -62,4 +62,20 @@ export declare function findOmnifyRoot(filePath: string): string | null;
62
62
  * skip rule is "any directory whose name starts with `Omnify` is
63
63
  * codegen-owned", which matches `Omnify/`, `OmnifyBase/`, etc.
64
64
  */
65
- export declare function findSiblingWithSameBasename(rootDir: string, baseName: string, excludePath: string): string | null;
65
+ export declare function findSiblingWithSameBasename(rootDir: string, baseName: string, excludePath: string,
66
+ /** #299: a same-name file counts only when this accepts it; scanning continues otherwise. */
67
+ accept?: (path: string) => boolean): string | null;
68
+ /**
69
+ * Whether `siblingContent` is an omnify wrapper that may stand in for the stub
70
+ * whose content is `stubContent`. Issue #299.
71
+ *
72
+ * Skipping a stub is only safe on positive evidence: a wrongly skipped model
73
+ * stub leaves every relation, service and policy pointing at a class nothing
74
+ * declares, while a wrongly emitted one is a single unused file. Laravel's
75
+ * stock `app/Models/User.php` shares the User stub's basename and is neither.
76
+ *
77
+ * Evidence is either omnify's own marker (the stub header, `Omnify` /
78
+ * `OmnifyBase` namespaces of v4 wrappers, #99) or a reference to the class the
79
+ * stub extends, by its real short name (#98 domain subfolders).
80
+ */
81
+ export declare function isOmnifyWrapper(siblingContent: string, stubContent: string): boolean;
@@ -87,7 +87,9 @@ export function findOmnifyRoot(filePath) {
87
87
  * skip rule is "any directory whose name starts with `Omnify` is
88
88
  * codegen-owned", which matches `Omnify/`, `OmnifyBase/`, etc.
89
89
  */
90
- export function findSiblingWithSameBasename(rootDir, baseName, excludePath) {
90
+ export function findSiblingWithSameBasename(rootDir, baseName, excludePath,
91
+ /** #299: a same-name file counts only when this accepts it; scanning continues otherwise. */
92
+ accept = () => true) {
91
93
  // #178: only a file generated from the SAME omnify root can be a relocated
92
94
  // version of this stub. One from another root is a different class that
93
95
  // merely shares a filename, and skipping on it leaves the class nothing
@@ -135,9 +137,43 @@ export function findSiblingWithSameBasename(rootDir, baseName, excludePath) {
135
137
  else if (entry === baseName && fullPath !== excludePath) {
136
138
  if (findOmnifyRoot(fullPath) !== ownerRoot)
137
139
  continue;
140
+ if (!accept(fullPath))
141
+ continue;
138
142
  return fullPath;
139
143
  }
140
144
  }
141
145
  }
142
146
  return null;
143
147
  }
148
+ /**
149
+ * Whether `siblingContent` is an omnify wrapper that may stand in for the stub
150
+ * whose content is `stubContent`. Issue #299.
151
+ *
152
+ * Skipping a stub is only safe on positive evidence: a wrongly skipped model
153
+ * stub leaves every relation, service and policy pointing at a class nothing
154
+ * declares, while a wrongly emitted one is a single unused file. Laravel's
155
+ * stock `app/Models/User.php` shares the User stub's basename and is neither.
156
+ *
157
+ * Evidence is either omnify's own marker (the stub header, `Omnify` /
158
+ * `OmnifyBase` namespaces of v4 wrappers, #99) or a reference to the class the
159
+ * stub extends, by its real short name (#98 domain subfolders).
160
+ */
161
+ export function isOmnifyWrapper(siblingContent, stubContent) {
162
+ if (/omnify/i.test(siblingContent))
163
+ return true;
164
+ const stub = stubContent.match(/\bclass\s+(\w+)\s+extends\s+(\w+)/);
165
+ if (!stub)
166
+ return false;
167
+ const [, stubClass, extendsName] = stub;
168
+ let baseClass = extendsName;
169
+ for (const m of stubContent.matchAll(/^use\s+([\w\\]+?)(?:\s+as\s+(\w+))?\s*;/gm)) {
170
+ const short = m[1].slice(m[1].lastIndexOf('\\') + 1);
171
+ if ((m[2] ?? short) === extendsName)
172
+ baseClass = short;
173
+ }
174
+ // With flatBase the base shares the stub's short name, which every
175
+ // same-name file "references" — that says nothing.
176
+ if (baseClass === stubClass)
177
+ return false;
178
+ return new RegExp(`\\b${baseClass}\\b`).test(siblingContent);
179
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnifyjp/ts",
3
- "version": "8.0.2",
3
+ "version": "8.0.3",
4
4
  "description": "TypeScript model type generator from Omnify schemas.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",