@omnifyjp/ts 8.0.2 → 8.0.4
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-observer-generator.js +6 -0
- package/dist/php/audit-trait-generator.js +21 -12
- package/dist/php/controller-generator.js +61 -25
- package/dist/php/factory-generator.js +5 -2
- package/dist/php/faker-mapper.js +13 -7
- package/dist/php/file-model-generator.js +21 -1
- package/dist/php/file-trait-generator.js +12 -1
- package/dist/php/model-generator.js +45 -30
- package/dist/php/policy-generator.js +6 -0
- package/dist/php/relation-builder.js +3 -2
- package/dist/php/route-generator.js +11 -3
- 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) {
|
|
@@ -167,6 +167,8 @@ class Audit extends Model
|
|
|
167
167
|
/**
|
|
168
168
|
* The audited record. \`auditable_id\` is unconstrained so the audit
|
|
169
169
|
* row survives a hard-delete of the source row.
|
|
170
|
+
*
|
|
171
|
+
* @return MorphTo<\\Illuminate\\Database\\Eloquent\\Model, $this>
|
|
170
172
|
*/
|
|
171
173
|
public function auditable(): MorphTo
|
|
172
174
|
{
|
|
@@ -176,6 +178,8 @@ class Audit extends Model
|
|
|
176
178
|
/**
|
|
177
179
|
* The actor who triggered the change. Null \`user_*\` columns
|
|
178
180
|
* indicate a system-driven write (CLI, scheduled job, internal API).
|
|
181
|
+
*
|
|
182
|
+
* @return MorphTo<\\Illuminate\\Database\\Eloquent\\Model, $this>
|
|
179
183
|
*/
|
|
180
184
|
public function user(): MorphTo
|
|
181
185
|
{
|
|
@@ -192,6 +196,8 @@ class Audit extends Model
|
|
|
192
196
|
* - \`1y\` → 1 year
|
|
193
197
|
*
|
|
194
198
|
* Empty / unparseable retention → matches no rows (kept forever).
|
|
199
|
+
*
|
|
200
|
+
* @return Builder<static>
|
|
195
201
|
*/
|
|
196
202
|
public function prunable(): Builder
|
|
197
203
|
{
|
|
@@ -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.
|
|
@@ -48,6 +50,7 @@ namespace ${traitsNamespace};
|
|
|
48
50
|
*/
|
|
49
51
|
|
|
50
52
|
use ${modelNamespace}\\Audit;
|
|
53
|
+
use Illuminate\\Database\\Eloquent\\SoftDeletes;
|
|
51
54
|
use Illuminate\\Support\\Facades\\Auth;
|
|
52
55
|
use Illuminate\\Support\\Str;
|
|
53
56
|
|
|
@@ -82,16 +85,18 @@ trait HasOmnifyAuditLog
|
|
|
82
85
|
$model->writeOmnifyAudit('updated', $old, $dirty);
|
|
83
86
|
});
|
|
84
87
|
static::deleted(function ($model) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
// class_uses_recursive, not method_exists: PHPStan evaluates the
|
|
89
|
+
// trait per using class and reports method_exists() as always
|
|
90
|
+
// true (or false) there.
|
|
91
|
+
$softDeletes = in_array(SoftDeletes::class, class_uses_recursive($model), true);
|
|
92
|
+
$event = $softDeletes && ! $model->isForceDeleting() ? 'soft_deleted' : 'deleted';
|
|
88
93
|
$model->writeOmnifyAudit($event, $model->getOriginal(), null);
|
|
89
94
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
+
// \`restored\` only fires for SoftDeletes models; registering the
|
|
96
|
+
// listener on any other model is a no-op.
|
|
97
|
+
static::registerModelEvent('restored', function ($model) {
|
|
98
|
+
$model->writeOmnifyAudit('restored', null, $model->getAttributes());
|
|
99
|
+
});
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
/**
|
|
@@ -110,7 +115,7 @@ trait HasOmnifyAuditLog
|
|
|
110
115
|
|
|
111
116
|
$exclude = array_unique(array_merge(
|
|
112
117
|
(array) config('omnify.audit.logExclude', []),
|
|
113
|
-
|
|
118
|
+
static::$auditExclude
|
|
114
119
|
));
|
|
115
120
|
if (!empty($exclude)) {
|
|
116
121
|
$flip = array_flip($exclude);
|
|
@@ -124,7 +129,7 @@ trait HasOmnifyAuditLog
|
|
|
124
129
|
|
|
125
130
|
$request = function_exists('request') ? request() : null;
|
|
126
131
|
$user = Auth::user();
|
|
127
|
-
$tags = static::$auditTags
|
|
132
|
+
$tags = static::$auditTags;
|
|
128
133
|
|
|
129
134
|
$payload = [
|
|
130
135
|
'auditable_type' => $this->getMorphClass(),
|
|
@@ -185,7 +190,11 @@ trait HasOmnifyAuditLog
|
|
|
185
190
|
$this->writeOmnifyAudit($event, $oldValues, $newValues);
|
|
186
191
|
}
|
|
187
192
|
|
|
188
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Eloquent relation back to every audit row that targets this model.
|
|
195
|
+
*
|
|
196
|
+
* @return \\Illuminate\\Database\\Eloquent\\Relations\\MorphMany<Audit, $this>
|
|
197
|
+
*/
|
|
189
198
|
public function audits(): \\Illuminate\\Database\\Eloquent\\Relations\\MorphMany
|
|
190
199
|
{
|
|
191
200
|
return $this->morphMany(Audit::class, 'auditable');
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Generates base + editable controller classes for schemas with `options.api`.
|
|
3
3
|
*/
|
|
4
4
|
import { toPascalCase } from './naming-helper.js';
|
|
5
|
-
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup } from './types.js';
|
|
5
|
+
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup } from './types.js';
|
|
6
6
|
import { buildControllerMethodAttributes, openApiControllerImports } from './openapi-generator.js';
|
|
7
7
|
const DEFAULT_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
|
|
8
8
|
/** Generate controller classes for all schemas with api config. */
|
|
@@ -27,7 +27,14 @@ function generateBaseController(name, schema, reader, config) {
|
|
|
27
27
|
const actions = [...(api.actions ?? DEFAULT_ACTIONS)];
|
|
28
28
|
const lookup = api.lookup ?? true;
|
|
29
29
|
const bulkDelete = api.bulkDelete ?? false;
|
|
30
|
-
|
|
30
|
+
// Restoring needs soft deletes: without them the service has no restore()
|
|
31
|
+
// or withTrashed lookup, so `api.restore: true` alone cannot be honoured.
|
|
32
|
+
const softDelete = schema.options?.softDelete ?? false;
|
|
33
|
+
const restore = softDelete && (api.restore ?? true);
|
|
34
|
+
if (api.restore === true && !softDelete) {
|
|
35
|
+
console.warn(`[omnify-ts] ${name}.options.api.restore is ignored (#301) — ${name} has no ` +
|
|
36
|
+
`softDelete, so there is nothing to restore. No restore action or route is generated.`);
|
|
37
|
+
}
|
|
31
38
|
// Issue #98 v5.8.4 / v5.8.5: cross-layer imports must mirror where
|
|
32
39
|
// the imported file actually lives. BASE layers (Models, Services
|
|
33
40
|
// base, Controllers base) are ALWAYS group-nested in legacy structure.
|
|
@@ -51,24 +58,26 @@ function generateBaseController(name, schema, reader, config) {
|
|
|
51
58
|
// and every project-owned service / policy that takes the
|
|
52
59
|
// editable type fails with TypeError.
|
|
53
60
|
const modelNs = groupNestEditable(config.models.userEditableNamespace, config.models);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
61
|
+
// Each class the controller names must be one a generator actually writes,
|
|
62
|
+
// at the namespace and short name THAT generator uses: services and requests
|
|
63
|
+
// ship only their base (#101), resources also ship an editable stub (#114).
|
|
64
|
+
const service = baseClassRef(config, name, schema, 'services', 'Services', `${modelName}Service`);
|
|
65
|
+
const storeRequest = baseClassRef(config, name, schema, 'requests', 'Requests', `${modelName}StoreRequest`);
|
|
66
|
+
const updateRequest = baseClassRef(config, name, schema, 'requests', 'Requests', `${modelName}UpdateRequest`);
|
|
67
|
+
const resource = editableResourceRef(config, schema, modelName);
|
|
59
68
|
// Build imports
|
|
60
69
|
const imports = [
|
|
61
70
|
`use App\\Http\\Controllers\\Controller;`,
|
|
62
|
-
`use ${
|
|
71
|
+
`use ${service.fqn};`,
|
|
63
72
|
];
|
|
64
73
|
if (actions.includes('store')) {
|
|
65
|
-
imports.push(`use ${
|
|
74
|
+
imports.push(`use ${storeRequest.fqn};`);
|
|
66
75
|
}
|
|
67
76
|
if (actions.includes('update')) {
|
|
68
|
-
imports.push(`use ${
|
|
77
|
+
imports.push(`use ${updateRequest.fqn};`);
|
|
69
78
|
}
|
|
70
|
-
if (actions.includes('show') || actions.includes('
|
|
71
|
-
imports.push(`use ${
|
|
79
|
+
if (actions.includes('index') || lookup || actions.includes('show') || actions.includes('store') || actions.includes('update') || restore) {
|
|
80
|
+
imports.push(`use ${resource.fqn};`);
|
|
72
81
|
}
|
|
73
82
|
imports.push(`use ${modelNs}\\${modelName};`);
|
|
74
83
|
imports.push(`use Illuminate\\Http\\JsonResponse;`);
|
|
@@ -95,34 +104,34 @@ function generateBaseController(name, schema, reader, config) {
|
|
|
95
104
|
{
|
|
96
105
|
$result = $this->service->list($request->all());
|
|
97
106
|
|
|
98
|
-
return ${
|
|
107
|
+
return ${resource.className}::collection($result);
|
|
99
108
|
}`);
|
|
100
109
|
}
|
|
101
110
|
if (actions.includes('store')) {
|
|
102
111
|
methods.push(`${attrFor('store')}
|
|
103
|
-
public function store(${
|
|
112
|
+
public function store(${storeRequest.className} $request): JsonResponse
|
|
104
113
|
{
|
|
105
114
|
$model = $this->service->create($request->validated());
|
|
106
115
|
|
|
107
|
-
return (new ${
|
|
116
|
+
return (new ${resource.className}($model))
|
|
108
117
|
->response()
|
|
109
118
|
->setStatusCode(201);
|
|
110
119
|
}`);
|
|
111
120
|
}
|
|
112
121
|
if (actions.includes('show')) {
|
|
113
122
|
methods.push(`${attrFor('show')}
|
|
114
|
-
public function show(${modelName} $model): ${
|
|
123
|
+
public function show(${modelName} $model): ${resource.className}
|
|
115
124
|
{
|
|
116
|
-
return new ${
|
|
125
|
+
return new ${resource.className}($model);
|
|
117
126
|
}`);
|
|
118
127
|
}
|
|
119
128
|
if (actions.includes('update')) {
|
|
120
129
|
methods.push(`${attrFor('update')}
|
|
121
|
-
public function update(${
|
|
130
|
+
public function update(${updateRequest.className} $request, ${modelName} $model): ${resource.className}
|
|
122
131
|
{
|
|
123
132
|
$model = $this->service->update($model, $request->validated());
|
|
124
133
|
|
|
125
|
-
return new ${
|
|
134
|
+
return new ${resource.className}($model);
|
|
126
135
|
}`);
|
|
127
136
|
}
|
|
128
137
|
if (actions.includes('destroy')) {
|
|
@@ -140,25 +149,29 @@ function generateBaseController(name, schema, reader, config) {
|
|
|
140
149
|
{
|
|
141
150
|
$result = $this->service->lookup($request->all());
|
|
142
151
|
|
|
143
|
-
return ${
|
|
152
|
+
return ${resource.className}::collection($result);
|
|
144
153
|
}`);
|
|
145
154
|
}
|
|
146
155
|
if (bulkDelete) {
|
|
147
156
|
methods.push(`${attrFor('bulkDelete')}
|
|
148
157
|
public function bulkDelete(Request $request): JsonResponse
|
|
149
158
|
{
|
|
150
|
-
|
|
159
|
+
// The service has no bulk method: delete each record through it, so
|
|
160
|
+
// soft deletes, model events and auditing apply as for destroy().
|
|
161
|
+
foreach ((array) $request->input('ids', []) as $id) {
|
|
162
|
+
$this->service->delete($this->service->findById((string) $id));
|
|
163
|
+
}
|
|
151
164
|
|
|
152
165
|
return response()->json(null, 204);
|
|
153
166
|
}`);
|
|
154
167
|
}
|
|
155
168
|
if (restore) {
|
|
156
169
|
methods.push(`${attrFor('restore')}
|
|
157
|
-
public function restore(string $id): ${
|
|
170
|
+
public function restore(string $id): ${resource.className}
|
|
158
171
|
{
|
|
159
|
-
$model = $this->service->restore($id);
|
|
172
|
+
$model = $this->service->restore($this->service->findById($id, withTrashed: true));
|
|
160
173
|
|
|
161
|
-
return new ${
|
|
174
|
+
return new ${resource.className}($model);
|
|
162
175
|
}`);
|
|
163
176
|
}
|
|
164
177
|
const content = `<?php
|
|
@@ -177,7 +190,7 @@ ${imports.join('\n')}
|
|
|
177
190
|
abstract class ${modelName}ControllerBase extends Controller
|
|
178
191
|
{
|
|
179
192
|
public function __construct(
|
|
180
|
-
protected readonly ${
|
|
193
|
+
protected readonly ${service.className} $service,
|
|
181
194
|
) {}
|
|
182
195
|
${methods.join('\n')}
|
|
183
196
|
}
|
|
@@ -254,3 +267,26 @@ function legacyGroupedBase(config, className, group) {
|
|
|
254
267
|
const { namespace } = nestByGroup({ path: '', namespace: raw.namespace }, group);
|
|
255
268
|
return { className: raw.className, fqn: `${namespace}\\${raw.className}` };
|
|
256
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* The base class a scaffold generator writes for `schemaName`, as that
|
|
272
|
+
* generator names and places it: modular → `<Name>Base` in the module, legacy
|
|
273
|
+
* → `resolveBaseClass` (flatBase drops the suffix) nested under the group.
|
|
274
|
+
*/
|
|
275
|
+
function baseClassRef(config, name, schema, layer, modularFolder, className) {
|
|
276
|
+
if (config.structure === 'modular') {
|
|
277
|
+
const ns = resolveModularBaseNamespace(config, name, modularFolder, config[layer].baseNamespace);
|
|
278
|
+
return { className: `${className}Base`, fqn: `${ns}\\${className}Base` };
|
|
279
|
+
}
|
|
280
|
+
const raw = resolveBaseClass(config[layer], className, 'Base');
|
|
281
|
+
const { namespace } = nestByGroup({ path: '', namespace: raw.namespace }, schema.group);
|
|
282
|
+
return { className: raw.className, fqn: `${namespace}\\${raw.className}` };
|
|
283
|
+
}
|
|
284
|
+
/** The editable Resource stub the resource generator writes (#114). */
|
|
285
|
+
function editableResourceRef(config, schema, modelName) {
|
|
286
|
+
if (config.structure === 'modular') {
|
|
287
|
+
return { className: `${modelName}Resource`, fqn: `${config.resources.namespace}\\${modelName}Resource` };
|
|
288
|
+
}
|
|
289
|
+
const raw = resolveEditableClass(config.resources, modelName, 'Resource');
|
|
290
|
+
const { namespace } = nestEditableByGroup(config.resources, { path: raw.path, namespace: raw.namespace }, schema.group);
|
|
291
|
+
return { className: raw.className, fqn: `${namespace}\\${raw.className}` };
|
|
292
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { toPascalCase } from './naming-helper.js';
|
|
5
5
|
import { toFaker, compoundFaker, associationFaker } from './faker-mapper.js';
|
|
6
|
-
import { userFile } from './types.js';
|
|
6
|
+
import { userFile, resolveGlobalEnumNamespace } from './types.js';
|
|
7
7
|
/** Generate Factory classes for all project-owned visible object schemas. */
|
|
8
8
|
export function generateFactories(reader, config) {
|
|
9
9
|
const files = [];
|
|
@@ -148,6 +148,9 @@ export function generateFileFactory(reader, config) {
|
|
|
148
148
|
return [];
|
|
149
149
|
const modelNamespace = config.models.namespace;
|
|
150
150
|
const factoryNamespace = config.factories.namespace;
|
|
151
|
+
// FileStatusEnum is a global enum (file-model-generator casts it from here),
|
|
152
|
+
// not a class in the models namespace.
|
|
153
|
+
const enumNamespace = resolveGlobalEnumNamespace(config, config.models.namespace);
|
|
151
154
|
const content = `<?php
|
|
152
155
|
|
|
153
156
|
/**
|
|
@@ -159,7 +162,7 @@ export function generateFileFactory(reader, config) {
|
|
|
159
162
|
namespace ${factoryNamespace};
|
|
160
163
|
|
|
161
164
|
use ${modelNamespace}\\File;
|
|
162
|
-
use ${
|
|
165
|
+
use ${enumNamespace}\\FileStatusEnum;
|
|
163
166
|
use Illuminate\\Database\\Eloquent\\Factories\\Factory;
|
|
164
167
|
use Illuminate\\Support\\Str;
|
|
165
168
|
|
package/dist/php/faker-mapper.js
CHANGED
|
@@ -40,14 +40,17 @@ export function toFaker(propName, property, schemas = {}) {
|
|
|
40
40
|
/** Generate faker expressions for compound type expanded columns. */
|
|
41
41
|
export function compoundFaker(propName, expansion, sourceType) {
|
|
42
42
|
const snakeName = toSnakeCase(propName);
|
|
43
|
+
// Locale-only providers (ja_JP kana names, ward, en secondaryAddress) are
|
|
44
|
+
// called through format(): Faker's Generator declares no @method for them,
|
|
45
|
+
// so a direct call is "undefined method" to static analysis.
|
|
43
46
|
const jaFaker = "fake('ja_JP')";
|
|
44
47
|
switch (sourceType) {
|
|
45
48
|
case 'JapaneseName':
|
|
46
49
|
return [
|
|
47
50
|
`'${snakeName}_lastname' => ${jaFaker}->lastName(),`,
|
|
48
51
|
`'${snakeName}_firstname' => ${jaFaker}->firstName(),`,
|
|
49
|
-
`'${snakeName}_kana_lastname' => ${jaFaker}->lastKanaName
|
|
50
|
-
`'${snakeName}_kana_firstname' => ${jaFaker}->firstKanaName
|
|
52
|
+
`'${snakeName}_kana_lastname' => ${jaFaker}->format('lastKanaName'),`,
|
|
53
|
+
`'${snakeName}_kana_firstname' => ${jaFaker}->format('firstKanaName'),`,
|
|
51
54
|
];
|
|
52
55
|
case 'JapaneseAddress':
|
|
53
56
|
return japaneseAddressFaker(snakeName, expansion);
|
|
@@ -57,7 +60,7 @@ export function compoundFaker(propName, expansion, sourceType) {
|
|
|
57
60
|
`'${snakeName}_branch_code' => ${jaFaker}->regexify('[0-9]{3}'),`,
|
|
58
61
|
`'${snakeName}_account_type' => ${jaFaker}->randomElement(['1', '2', '4']),`,
|
|
59
62
|
`'${snakeName}_account_number' => ${jaFaker}->regexify('[0-9]{7}'),`,
|
|
60
|
-
`'${snakeName}_account_holder' => ${jaFaker}->lastKanaName
|
|
63
|
+
`'${snakeName}_account_holder' => ${jaFaker}->format('lastKanaName').' '.${jaFaker}->format('firstKanaName'),`,
|
|
61
64
|
];
|
|
62
65
|
default:
|
|
63
66
|
return genericCompoundFaker(snakeName, expansion);
|
|
@@ -73,7 +76,8 @@ export function associationFaker(propName, property, modelNamespace, currentSche
|
|
|
73
76
|
const nullable = property['nullable'] ?? false;
|
|
74
77
|
const fake = nullable
|
|
75
78
|
? `'${foreignKey}' => ${target}::query()->inRandomOrder()->first()?->id,`
|
|
76
|
-
|
|
79
|
+
// `->id ??`, not `?->id ??`: `??` already reads a property of null as null.
|
|
80
|
+
: `'${foreignKey}' => ${target}::query()->inRandomOrder()->first()->id ?? ${target}::factory()->create()->id,`;
|
|
77
81
|
let importLine = null;
|
|
78
82
|
if (target !== currentSchema) {
|
|
79
83
|
const targetNs = property['targetNamespace'] ?? modelNamespace;
|
|
@@ -136,8 +140,10 @@ function stringFakerExpr(propName, snakeName, property) {
|
|
|
136
140
|
if (lower.includes('code'))
|
|
137
141
|
return `fake()->unique()->regexify('[A-Z0-9]{8}')`;
|
|
138
142
|
const length = property['length'];
|
|
143
|
+
// text(), not words(3, true): words() is typed array|string, and every
|
|
144
|
+
// String column is wrapped in mb_substr(), which needs a string.
|
|
139
145
|
if (length != null && length <= 50)
|
|
140
|
-
return `fake()->
|
|
146
|
+
return `fake()->text(50)`;
|
|
141
147
|
return `fake()->sentence()`;
|
|
142
148
|
}
|
|
143
149
|
/** Largest value a `Decimal(precision, scale)` column accepts (all-9s at that precision/scale). */
|
|
@@ -221,9 +227,9 @@ function japaneseAddressFaker(snakeName, expansion) {
|
|
|
221
227
|
else {
|
|
222
228
|
lines.push(`'${snakeName}_prefecture' => fake()->randomElement(['hokkaido', 'aomori', 'iwate', 'miyagi', 'akita', 'yamagata', 'fukushima', 'ibaraki', 'tochigi', 'gunma', 'saitama', 'chiba', 'tokyo', 'kanagawa', 'niigata', 'toyama', 'ishikawa', 'fukui', 'yamanashi', 'nagano', 'gifu', 'shizuoka', 'aichi', 'mie', 'shiga', 'kyoto', 'osaka', 'hyogo', 'nara', 'wakayama', 'tottori', 'shimane', 'okayama', 'hiroshima', 'yamaguchi', 'tokushima', 'kagawa', 'ehime', 'kochi', 'fukuoka', 'saga', 'nagasaki', 'kumamoto', 'oita', 'miyazaki', 'kagoshima', 'okinawa']),`);
|
|
223
229
|
}
|
|
224
|
-
lines.push(`'${snakeName}_address1' => ${jaFaker}->city().${jaFaker}->ward
|
|
230
|
+
lines.push(`'${snakeName}_address1' => ${jaFaker}->city().${jaFaker}->format('ward'),`);
|
|
225
231
|
lines.push(`'${snakeName}_address2' => ${jaFaker}->streetAddress(),`);
|
|
226
|
-
lines.push(`'${snakeName}_address3' => ${jaFaker}->optional()->secondaryAddress
|
|
232
|
+
lines.push(`'${snakeName}_address3' => ${jaFaker}->optional()->format('secondaryAddress'),`);
|
|
227
233
|
return lines;
|
|
228
234
|
}
|
|
229
235
|
function genericCompoundFaker(snakeName, expansion) {
|
|
@@ -39,6 +39,7 @@ namespace ${baseNamespace};
|
|
|
39
39
|
use Illuminate\\Http\\Request;
|
|
40
40
|
use Illuminate\\Http\\Resources\\Json\\JsonResource;
|
|
41
41
|
|
|
42
|
+
/** @mixin \\${config.models.namespace}\\File */
|
|
42
43
|
class FileResourceBase extends JsonResource
|
|
43
44
|
{
|
|
44
45
|
/**
|
|
@@ -56,7 +57,7 @@ class FileResourceBase extends JsonResource
|
|
|
56
57
|
'original_name' => $this->original_name,
|
|
57
58
|
'mime_type' => $this->mime_type,
|
|
58
59
|
'size' => $this->size,
|
|
59
|
-
'status' => $this->status
|
|
60
|
+
'status' => $this->status->value,
|
|
60
61
|
'url' => $this->url ?? null,
|
|
61
62
|
'expires_at' => $this->expires_at?->toISOString(),
|
|
62
63
|
'sort_order' => $this->sort_order,
|
|
@@ -167,11 +168,15 @@ class FileBaseModel extends BaseModel
|
|
|
167
168
|
|
|
168
169
|
/**
|
|
169
170
|
* Localized display names for this model.
|
|
171
|
+
*
|
|
172
|
+
* @var array<string, string>
|
|
170
173
|
*/
|
|
171
174
|
protected static array $localizedDisplayNames = FileLocales::DISPLAY_NAMES;
|
|
172
175
|
|
|
173
176
|
/**
|
|
174
177
|
* Localized display names for properties.
|
|
178
|
+
*
|
|
179
|
+
* @var array<string, array<string, string>>
|
|
175
180
|
*/
|
|
176
181
|
protected static array $localizedPropertyDisplayNames = FileLocales::PROPERTY_DISPLAY_NAMES;
|
|
177
182
|
|
|
@@ -212,6 +217,8 @@ class FileBaseModel extends BaseModel
|
|
|
212
217
|
|
|
213
218
|
/**
|
|
214
219
|
* Get the parent fileable model.
|
|
220
|
+
*
|
|
221
|
+
* @return MorphTo<\\Illuminate\\Database\\Eloquent\\Model, $this>
|
|
215
222
|
*/
|
|
216
223
|
public function fileable(): MorphTo
|
|
217
224
|
{
|
|
@@ -224,6 +231,9 @@ class FileBaseModel extends BaseModel
|
|
|
224
231
|
|
|
225
232
|
/**
|
|
226
233
|
* Scope to a specific collection.
|
|
234
|
+
*
|
|
235
|
+
* @param Builder<self> $query
|
|
236
|
+
* @return Builder<self>
|
|
227
237
|
*/
|
|
228
238
|
public function scopeInCollection(Builder $query, string $collection): Builder
|
|
229
239
|
{
|
|
@@ -232,6 +242,9 @@ class FileBaseModel extends BaseModel
|
|
|
232
242
|
|
|
233
243
|
/**
|
|
234
244
|
* Scope to permanent files.
|
|
245
|
+
*
|
|
246
|
+
* @param Builder<self> $query
|
|
247
|
+
* @return Builder<self>
|
|
235
248
|
*/
|
|
236
249
|
public function scopePermanent(Builder $query): Builder
|
|
237
250
|
{
|
|
@@ -240,6 +253,9 @@ class FileBaseModel extends BaseModel
|
|
|
240
253
|
|
|
241
254
|
/**
|
|
242
255
|
* Scope to temporary files.
|
|
256
|
+
*
|
|
257
|
+
* @param Builder<self> $query
|
|
258
|
+
* @return Builder<self>
|
|
243
259
|
*/
|
|
244
260
|
public function scopeTemporary(Builder $query): Builder
|
|
245
261
|
{
|
|
@@ -248,6 +264,9 @@ class FileBaseModel extends BaseModel
|
|
|
248
264
|
|
|
249
265
|
/**
|
|
250
266
|
* Scope to expired files (temporary + past expires_at).
|
|
267
|
+
*
|
|
268
|
+
* @param Builder<self> $query
|
|
269
|
+
* @return Builder<self>
|
|
251
270
|
*/
|
|
252
271
|
public function scopeExpired(Builder $query): Builder
|
|
253
272
|
{
|
|
@@ -317,6 +336,7 @@ use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
|
|
|
317
336
|
*/
|
|
318
337
|
class File extends FileBaseModel
|
|
319
338
|
{
|
|
339
|
+
/** @use HasFactory<FileFactory> */
|
|
320
340
|
use HasFactory;
|
|
321
341
|
|
|
322
342
|
/**
|
|
@@ -30,6 +30,8 @@ trait HasFiles
|
|
|
30
30
|
{
|
|
31
31
|
/**
|
|
32
32
|
* Get all files attached to this model.
|
|
33
|
+
*
|
|
34
|
+
* @return MorphMany<File, $this>
|
|
33
35
|
*/
|
|
34
36
|
public function files(): MorphMany
|
|
35
37
|
{
|
|
@@ -38,6 +40,8 @@ trait HasFiles
|
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* Get files in a specific collection.
|
|
43
|
+
*
|
|
44
|
+
* @return MorphMany<File, $this>
|
|
41
45
|
*/
|
|
42
46
|
public function filesInCollection(string $collection): MorphMany
|
|
43
47
|
{
|
|
@@ -46,6 +50,8 @@ trait HasFiles
|
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* Get all permanent files.
|
|
53
|
+
*
|
|
54
|
+
* @return MorphMany<File, $this>
|
|
49
55
|
*/
|
|
50
56
|
public function permanentFiles(): MorphMany
|
|
51
57
|
{
|
|
@@ -69,8 +75,13 @@ trait HasFiles
|
|
|
69
75
|
'collection' => $collection,
|
|
70
76
|
'status' => FileStatusEnum::Permanent,
|
|
71
77
|
'expires_at' => null,
|
|
72
|
-
'sort_order' => \\Illuminate\\Support\\Facades\\DB::raw('sort_order + '.$startOrder),
|
|
73
78
|
]);
|
|
79
|
+
|
|
80
|
+
// increment(), not DB::raw('sort_order + '.$startOrder): raw SQL is
|
|
81
|
+
// typed literal-string, and a column offset needs no SQL of our own.
|
|
82
|
+
if ($startOrder !== 0) {
|
|
83
|
+
File::whereIn('id', $fileIds)->increment('sort_order', $startOrder);
|
|
84
|
+
}
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
/**
|
|
@@ -443,15 +443,20 @@ function generateUserModel(name, schema, reader, config, hasNestedSet = false) {
|
|
|
443
443
|
const baseImport = baseAlias === base.className
|
|
444
444
|
? `use ${base.fqn};`
|
|
445
445
|
: `use ${base.fqn} as ${baseAlias};`;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
446
|
+
// The factory generator skips hidden schemas, so a hidden schema's stub
|
|
447
|
+
// must not name a factory that is never written.
|
|
448
|
+
const hasFactory = !schema.options?.hidden;
|
|
449
|
+
const imports = hasFactory
|
|
450
|
+
? [
|
|
451
|
+
baseImport,
|
|
452
|
+
`use ${factoryNamespace}\\${modelName}Factory;`,
|
|
453
|
+
`use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;`,
|
|
454
|
+
]
|
|
455
|
+
: [baseImport];
|
|
451
456
|
if (hasNestedSet) {
|
|
452
457
|
imports.push(`use ${nestedSetNamespace}\\NodeTrait;`);
|
|
453
458
|
}
|
|
454
|
-
const traits = [` /** @use HasFactory<${modelName}Factory> */\n use HasFactory;`];
|
|
459
|
+
const traits = hasFactory ? [` /** @use HasFactory<${modelName}Factory> */\n use HasFactory;`] : [];
|
|
455
460
|
if (hasNestedSet) {
|
|
456
461
|
traits.push(' use NodeTrait;');
|
|
457
462
|
}
|
|
@@ -481,7 +486,7 @@ ${imports.join('\n')}
|
|
|
481
486
|
*/
|
|
482
487
|
class ${modelName} extends ${baseAlias}
|
|
483
488
|
{
|
|
484
|
-
${traits.join('\n')}
|
|
489
|
+
${hasFactory ? `${traits.join('\n')}
|
|
485
490
|
|
|
486
491
|
/**
|
|
487
492
|
* Create a new factory instance for the model.
|
|
@@ -491,7 +496,9 @@ ${traits.join('\n')}
|
|
|
491
496
|
return ${modelName}Factory::new();
|
|
492
497
|
}
|
|
493
498
|
|
|
494
|
-
|
|
499
|
+
` : traits.length > 0 ? `${traits.join('\n')}
|
|
500
|
+
|
|
501
|
+
` : ''} //
|
|
495
502
|
}
|
|
496
503
|
`;
|
|
497
504
|
// #171: this subclass is created once and then owned by the project. The
|
|
@@ -1087,19 +1094,24 @@ function buildAccessors(expandedProperties) {
|
|
|
1087
1094
|
for (const [propName, expansion] of Object.entries(expandedProperties)) {
|
|
1088
1095
|
const sourceType = expansion.sourceType ?? '';
|
|
1089
1096
|
const snakeName = toSnakeCase(propName);
|
|
1097
|
+
// A column absent from the expansion is treated as nullable (safe side).
|
|
1098
|
+
const nullable = (column) => {
|
|
1099
|
+
const col = expansion.columns?.find((c) => c.name === column);
|
|
1100
|
+
return col === undefined ? true : (col.nullable ?? false);
|
|
1101
|
+
};
|
|
1090
1102
|
switch (sourceType) {
|
|
1091
1103
|
case 'JapaneseName':
|
|
1092
1104
|
methods.push(fullNameAccessor(snakeName, 'FullName', [
|
|
1093
1105
|
`${snakeName}_lastname`, `${snakeName}_firstname`,
|
|
1094
|
-
], ' '));
|
|
1106
|
+
], ' ', nullable));
|
|
1095
1107
|
methods.push(fullNameAccessor(snakeName, 'FullNameKana', [
|
|
1096
1108
|
`${snakeName}_kana_lastname`, `${snakeName}_kana_firstname`,
|
|
1097
|
-
], ' '));
|
|
1109
|
+
], ' ', nullable));
|
|
1098
1110
|
break;
|
|
1099
1111
|
case 'JapaneseAddress':
|
|
1100
1112
|
methods.push(fullNameAccessor(snakeName, 'FullAddress', [
|
|
1101
1113
|
`${snakeName}_address1`, `${snakeName}_address2`, `${snakeName}_address3`,
|
|
1102
|
-
], ''));
|
|
1114
|
+
], '', nullable));
|
|
1103
1115
|
break;
|
|
1104
1116
|
}
|
|
1105
1117
|
}
|
|
@@ -1205,8 +1217,8 @@ function buildNestedSetParentIdMethod(parentColumn) {
|
|
|
1205
1217
|
* - `$auditTags` is the static tag list applied to every audit row
|
|
1206
1218
|
* from this model; useful for grouping (e.g. ["billing"]).
|
|
1207
1219
|
*
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
1220
|
+
* Both are declared on every audited model, as `[]` when empty: the trait
|
|
1221
|
+
* reads them directly (#298).
|
|
1210
1222
|
*/
|
|
1211
1223
|
function buildAuditLogProperties(schemaName, reader) {
|
|
1212
1224
|
if (!reader.isAuditLogEnabled(schemaName))
|
|
@@ -1214,9 +1226,13 @@ function buildAuditLogProperties(schemaName, reader) {
|
|
|
1214
1226
|
const excludes = reader.getAuditExcludesFor(schemaName);
|
|
1215
1227
|
const tags = reader.getAuditTagsFor(schemaName);
|
|
1216
1228
|
const parts = [];
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1229
|
+
// #298: both statics are always declared on an audited model, empty or not,
|
|
1230
|
+
// so HasOmnifyAuditLog reads them directly instead of `?? []` on a property
|
|
1231
|
+
// that may not exist (PHPStan nullCoalesce.property when it does).
|
|
1232
|
+
const phpList = (items) => items.length === 0
|
|
1233
|
+
? '[]'
|
|
1234
|
+
: `[\n${items.map((c) => ` ${JSON.stringify(c).replace(/"/g, "'")},`).join('\n')}\n ]`;
|
|
1235
|
+
parts.push(`
|
|
1220
1236
|
/**
|
|
1221
1237
|
* Columns scrubbed from old_values / new_values BEFORE the audit row
|
|
1222
1238
|
* is built. Merged at runtime with the global \`config('omnify.audit.logExclude')\`
|
|
@@ -1224,13 +1240,8 @@ function buildAuditLogProperties(schemaName, reader) {
|
|
|
1224
1240
|
*
|
|
1225
1241
|
* @var array<int, string>
|
|
1226
1242
|
*/
|
|
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(`
|
|
1243
|
+
protected static array $auditExclude = ${phpList(excludes)};`);
|
|
1244
|
+
parts.push(`
|
|
1234
1245
|
/**
|
|
1235
1246
|
* Static tags applied to every audit row from this model. Surfaces
|
|
1236
1247
|
* in the audits table's \`tags\` column for grouping in the activity
|
|
@@ -1238,11 +1249,8 @@ ${lines}
|
|
|
1238
1249
|
*
|
|
1239
1250
|
* @var array<int, string>
|
|
1240
1251
|
*/
|
|
1241
|
-
protected static array $auditTags =
|
|
1242
|
-
|
|
1243
|
-
];`);
|
|
1244
|
-
}
|
|
1245
|
-
return parts.length === 0 ? '' : parts.join('\n') + '\n';
|
|
1252
|
+
protected static array $auditTags = ${phpList(tags)};`);
|
|
1253
|
+
return parts.join('\n') + '\n';
|
|
1246
1254
|
}
|
|
1247
1255
|
function buildAuditSection(options, reader, modelNamespace) {
|
|
1248
1256
|
const opts = options;
|
|
@@ -1351,6 +1359,8 @@ function buildFileAccessors(properties, propertyOrder, modelNamespace) {
|
|
|
1351
1359
|
methods.push(`
|
|
1352
1360
|
/**
|
|
1353
1361
|
* Get ${propName} files (collection: ${collection}).
|
|
1362
|
+
*
|
|
1363
|
+
* @return MorphMany<File, $this>
|
|
1354
1364
|
*/
|
|
1355
1365
|
public function ${methodName}(): MorphMany
|
|
1356
1366
|
{
|
|
@@ -1361,6 +1371,8 @@ function buildFileAccessors(properties, propertyOrder, modelNamespace) {
|
|
|
1361
1371
|
methods.push(`
|
|
1362
1372
|
/**
|
|
1363
1373
|
* Get ${propName} file (collection: ${collection}).
|
|
1374
|
+
*
|
|
1375
|
+
* @return MorphOne<File, $this>
|
|
1364
1376
|
*/
|
|
1365
1377
|
public function ${methodName}(): MorphOne
|
|
1366
1378
|
{
|
|
@@ -1371,17 +1383,20 @@ function buildFileAccessors(properties, propertyOrder, modelNamespace) {
|
|
|
1371
1383
|
}
|
|
1372
1384
|
return methods.join('\n');
|
|
1373
1385
|
}
|
|
1374
|
-
function fullNameAccessor(prefix, suffix, fields, separator) {
|
|
1386
|
+
function fullNameAccessor(prefix, suffix, fields, separator, isNullable) {
|
|
1375
1387
|
const methodName = 'get' + toPascalCase(prefix) + suffix + 'Attribute';
|
|
1376
1388
|
const fieldAccess = fields.map(f => `$this->${f}`).join(', ');
|
|
1377
1389
|
const separatorArg = separator === '' ? "''" : `'${separator}'`;
|
|
1390
|
+
// `$v !== null` on parts that are all non-nullable columns is dead code
|
|
1391
|
+
// (PHPStan notIdentical.alwaysTrue); keep it only when a part can be null.
|
|
1392
|
+
const keep = fields.some(isNullable) ? "$v !== null && $v !== ''" : "$v !== ''";
|
|
1378
1393
|
return `
|
|
1379
1394
|
/**
|
|
1380
1395
|
* Get the ${prefix} ${suffix} attribute.
|
|
1381
1396
|
*/
|
|
1382
1397
|
public function ${methodName}(): ?string
|
|
1383
1398
|
{
|
|
1384
|
-
$parts = array_filter([${fieldAccess}], fn($v) => $
|
|
1399
|
+
$parts = array_filter([${fieldAccess}], fn($v) => ${keep});
|
|
1385
1400
|
return count($parts) > 0 ? implode(${separatorArg}, $parts) : null;
|
|
1386
1401
|
}`;
|
|
1387
1402
|
}
|
|
@@ -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();
|
|
@@ -23,7 +23,10 @@ function generateRouteFile(name, schema, config) {
|
|
|
23
23
|
const actions = [...(api.actions ?? DEFAULT_ACTIONS)];
|
|
24
24
|
const lookup = api.lookup ?? true;
|
|
25
25
|
const bulkDelete = api.bulkDelete ?? false;
|
|
26
|
-
|
|
26
|
+
// Restoring needs soft deletes: without them the service has no restore()
|
|
27
|
+
// or withTrashed lookup, so `api.restore: true` alone cannot be honoured.
|
|
28
|
+
const softDelete = schema.options?.softDelete ?? false;
|
|
29
|
+
const restore = softDelete && (api.restore ?? true);
|
|
27
30
|
const middleware = api.middleware ?? [];
|
|
28
31
|
// Route prefix: use api.prefix or default to snake_case plural
|
|
29
32
|
const prefix = api.prefix ?? pluralize(toSnakeCase(name));
|
|
@@ -31,9 +34,14 @@ function generateRouteFile(name, schema, config) {
|
|
|
31
34
|
const routeParam = toCamelCase(name);
|
|
32
35
|
// Controller namespace
|
|
33
36
|
const group = schema.group ? toPascalCase(schema.group) : null;
|
|
37
|
+
// The editable controller lives where generateUserController writes it:
|
|
38
|
+
// under userEditableNamespace (legacy), not the base layer's namespace.
|
|
39
|
+
const controllerRoot = config.structure === 'modular'
|
|
40
|
+
? config.controllers.namespace
|
|
41
|
+
: config.controllers.userEditableNamespace;
|
|
34
42
|
const controllerNs = group
|
|
35
|
-
? `${
|
|
36
|
-
: `${
|
|
43
|
+
? `${controllerRoot}\\Api\\V1\\${group}`
|
|
44
|
+
: `${controllerRoot}\\Api\\V1`;
|
|
37
45
|
// Build use statements
|
|
38
46
|
const useStatements = [
|
|
39
47
|
`use ${controllerNs}\\${modelName}Controller;`,
|
|
@@ -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
|
+
}
|