@omnifyjp/ts 8.0.1 → 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 +4 -2
- package/dist/php/audit-trait-generator.js +5 -3
- package/dist/php/file-model-generator.js +10 -0
- package/dist/php/locales-generator.js +10 -0
- package/dist/php/model-generator.js +13 -17
- package/dist/php/policy-generator.js +6 -0
- package/dist/php/relation-builder.js +3 -2
- package/dist/php/request-generator.js +1 -1
- package/dist/php/service-generator.js +1 -1
- package/dist/php/service-provider-generator.js +1 -1
- package/dist/php/sibling-skip.d.ts +17 -1
- package/dist/php/sibling-skip.js +37 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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(),
|
|
@@ -422,6 +422,16 @@ class FileLocales
|
|
|
422
422
|
public const PROPERTY_DISPLAY_NAMES = [
|
|
423
423
|
${propertyNamesPhp},
|
|
424
424
|
];
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Localized display names for properties, widened to the declared shape.
|
|
428
|
+
*
|
|
429
|
+
* @return array<string, array<string, string>>
|
|
430
|
+
*/
|
|
431
|
+
public static function propertyDisplayNames(): array
|
|
432
|
+
{
|
|
433
|
+
return self::PROPERTY_DISPLAY_NAMES;
|
|
434
|
+
}
|
|
425
435
|
}
|
|
426
436
|
`;
|
|
427
437
|
return baseFile(resolveModularBasePath(config, 'File', 'Locales', 'FileLocales.php', config.models.basePath + '/Locales'), content);
|
|
@@ -47,6 +47,16 @@ class ${modelName}Locales
|
|
|
47
47
|
* @var array<string, array<string, string>>
|
|
48
48
|
*/
|
|
49
49
|
public const PROPERTY_DISPLAY_NAMES = ${propertyDisplayNamesPhp};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Localized display names for properties, widened to the declared shape.
|
|
53
|
+
*
|
|
54
|
+
* @return array<string, array<string, string>>
|
|
55
|
+
*/
|
|
56
|
+
public static function propertyDisplayNames(): array
|
|
57
|
+
{
|
|
58
|
+
return self::PROPERTY_DISPLAY_NAMES;
|
|
59
|
+
}
|
|
50
60
|
}
|
|
51
61
|
`;
|
|
52
62
|
return baseFile(resolveModularBasePath(config, name, 'Locales', `${modelName}Locales.php`, config.models.basePath + '/Locales'), content);
|
|
@@ -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
|
-
*
|
|
1209
|
-
*
|
|
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
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}():
|
|
259
|
+
public function ${methodName}(): MorphToMany
|
|
259
260
|
{
|
|
260
261
|
return $this->morphedByMany(${fqcn}::class, '${morphName}')
|
|
261
262
|
->withTimestamps();
|
|
@@ -244,7 +244,7 @@ function generateBaseRequest(name, schema, reader, config, action) {
|
|
|
244
244
|
' {',
|
|
245
245
|
" $locale = app()->getLocale();",
|
|
246
246
|
" $fallback = config('app.fallback_locale', 'en');",
|
|
247
|
-
` $propertyNames = ${modelName}Locales::
|
|
247
|
+
` $propertyNames = ${modelName}Locales::propertyDisplayNames();`,
|
|
248
248
|
' $attributes = [];',
|
|
249
249
|
'',
|
|
250
250
|
' $keys = [',
|
|
@@ -1158,7 +1158,7 @@ ${listExample}
|
|
|
1158
1158
|
// disable LIMIT (whole-table dump) and a huge one is unbounded.
|
|
1159
1159
|
lines.push(`
|
|
1160
1160
|
$perPage = filter_var($filters['per_page'] ?? null, FILTER_VALIDATE_INT);
|
|
1161
|
-
$perPage = $perPage === false
|
|
1161
|
+
$perPage = $perPage === false ? ${perPage} : $perPage;
|
|
1162
1162
|
$perPage = max(1, min($perPage, ${maxPerPage}));
|
|
1163
1163
|
|
|
1164
1164
|
return $query->paginate($perPage);
|
|
@@ -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
|
|
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;
|
package/dist/php/sibling-skip.js
CHANGED
|
@@ -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
|
+
}
|