@omnifyjp/ts 6.7.14 → 6.7.16
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/php/file-cleanup-generator.js +40 -4
- package/dist/php/model-generator.js +14 -9
- package/dist/php/relation-builder.d.ts +12 -0
- package/dist/php/relation-builder.js +43 -3
- package/dist/php/request-generator.js +2 -20
- package/dist/php/type-mapper.d.ts +14 -0
- package/dist/php/type-mapper.js +60 -4
- package/package.json +1 -1
|
@@ -122,18 +122,48 @@ class CleanupFiles extends Command
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
if ($dryRun) {
|
|
125
|
+
$skippedShared = 0;
|
|
126
|
+
// Each row is counted independently; the real run force-deletes sequentially so
|
|
127
|
+
// earlier rows stop counting as references — this total is an upper bound.
|
|
128
|
+
$query->chunkById(100, function ($files) use (&$skippedShared) {
|
|
129
|
+
foreach ($files as $file) {
|
|
130
|
+
if ($file->path && File::withTrashed()
|
|
131
|
+
->where('disk', $file->disk)
|
|
132
|
+
->where('path', $file->path)
|
|
133
|
+
->where('id', '!=', $file->id)
|
|
134
|
+
->exists()) {
|
|
135
|
+
$skippedShared++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
125
139
|
$this->info("[Phase 2] Would purge {$count} file(s) older than {$hours}h.");
|
|
140
|
+
if ($skippedShared > 0) {
|
|
141
|
+
$this->info("[Phase 2] Would skip up to {$skippedShared} physical delete(s) — object may still be referenced by another row.");
|
|
142
|
+
}
|
|
126
143
|
return self::SUCCESS;
|
|
127
144
|
}
|
|
128
145
|
|
|
129
146
|
$purged = 0;
|
|
130
147
|
$errors = 0;
|
|
148
|
+
$skippedShared = 0;
|
|
131
149
|
|
|
132
|
-
$query->chunkById(100, function ($files) use (&$purged, &$errors) {
|
|
150
|
+
$query->chunkById(100, function ($files) use (&$purged, &$errors, &$skippedShared) {
|
|
133
151
|
foreach ($files as $file) {
|
|
134
152
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
// Rows force-deleted earlier in this batch no longer appear in withTrashed(),
|
|
154
|
+
// so the last row sharing a disk+path deletes the object without extra bookkeeping.
|
|
155
|
+
if ($file->path) {
|
|
156
|
+
$pathReferencedElsewhere = File::withTrashed()
|
|
157
|
+
->where('disk', $file->disk)
|
|
158
|
+
->where('path', $file->path)
|
|
159
|
+
->where('id', '!=', $file->id)
|
|
160
|
+
->exists();
|
|
161
|
+
|
|
162
|
+
if (! $pathReferencedElsewhere && Storage::disk($file->disk)->exists($file->path)) {
|
|
163
|
+
Storage::disk($file->disk)->delete($file->path);
|
|
164
|
+
} elseif ($pathReferencedElsewhere) {
|
|
165
|
+
$skippedShared++;
|
|
166
|
+
}
|
|
137
167
|
}
|
|
138
168
|
$file->forceDelete();
|
|
139
169
|
$purged++;
|
|
@@ -144,7 +174,13 @@ class CleanupFiles extends Command
|
|
|
144
174
|
}
|
|
145
175
|
});
|
|
146
176
|
|
|
147
|
-
$
|
|
177
|
+
$purgeMsg = "[Phase 2] Purged {$purged} file(s).";
|
|
178
|
+
if ($skippedShared > 0) {
|
|
179
|
+
$purgeMsg .= " Skipped {$skippedShared} physical delete(s) — object still referenced by another row.";
|
|
180
|
+
} else {
|
|
181
|
+
$purgeMsg .= ' Physical files removed.';
|
|
182
|
+
}
|
|
183
|
+
$this->info($purgeMsg);
|
|
148
184
|
if ($errors > 0) {
|
|
149
185
|
$this->warn("{$errors} file(s) failed to purge.");
|
|
150
186
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { toPascalCase, toSnakeCase, toCamelCase, toFkColumnName, pluralize } from './naming-helper.js';
|
|
5
5
|
import { resolveTimestamps } from './timestamps.js';
|
|
6
|
-
import { toCast, toPhpDocType, isHiddenByDefault } from './type-mapper.js';
|
|
6
|
+
import { toCast, toPhpDocType, isHiddenByDefault, resolveFkCast, resolveFkPhpDocType } from './type-mapper.js';
|
|
7
7
|
import { buildRelation } from './relation-builder.js';
|
|
8
8
|
import { shadowingPropertyConflicts } from './shadowing-conflicts.js';
|
|
9
9
|
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveSharedBaseNamespace, resolveGlobalTraitNamespace, resolveGlobalEnumNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup, } from './types.js';
|
|
@@ -614,7 +614,8 @@ function buildDocProperties(properties, expandedProperties, propertyOrder, reade
|
|
|
614
614
|
// doesn't actually have.
|
|
615
615
|
const snakeName = prop['column'] || toFkColumnName(propName);
|
|
616
616
|
const nullable = prop['nullable'] ?? false;
|
|
617
|
-
const
|
|
617
|
+
const target = prop['target'] ?? '';
|
|
618
|
+
const phpType = resolveFkPhpDocType(prop, target, reader, nullable);
|
|
618
619
|
lines.push(` * @property ${phpType} $${snakeName}`);
|
|
619
620
|
}
|
|
620
621
|
else if (relation === 'MorphTo') {
|
|
@@ -808,13 +809,10 @@ function buildCasts(properties, expandedProperties, propertyOrder, reader, confi
|
|
|
808
809
|
const relation = prop['relation'] ?? '';
|
|
809
810
|
if (relation === 'ManyToOne') {
|
|
810
811
|
const target = prop['target'] ?? '';
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
? targetSchema.options.id
|
|
814
|
-
: 'BigInt';
|
|
815
|
-
if (targetIdType !== 'Uuid' && targetIdType !== 'Ulid') {
|
|
812
|
+
const fkCast = resolveFkCast(prop, target, reader);
|
|
813
|
+
if (fkCast) {
|
|
816
814
|
// omnify-go#166: honor the `column:` override (ForeignColumn).
|
|
817
|
-
casts.push(`'${prop['column'] || toFkColumnName(propName)}' => '
|
|
815
|
+
casts.push(`'${prop['column'] || toFkColumnName(propName)}' => '${fkCast}',`);
|
|
818
816
|
}
|
|
819
817
|
}
|
|
820
818
|
continue;
|
|
@@ -934,7 +932,14 @@ function buildRelations(schemaName, properties, propertyOrder, modelNamespace, r
|
|
|
934
932
|
: toSnakeCase(propName));
|
|
935
933
|
}
|
|
936
934
|
}
|
|
937
|
-
const result = buildRelation(propName, prop, ns, {
|
|
935
|
+
const result = buildRelation(propName, prop, ns, {
|
|
936
|
+
sourceTableName,
|
|
937
|
+
targetTableName,
|
|
938
|
+
pivotProperties,
|
|
939
|
+
pivotTableName,
|
|
940
|
+
reader,
|
|
941
|
+
sourceSchemaName: schemaName,
|
|
942
|
+
});
|
|
938
943
|
if (result) {
|
|
939
944
|
methods.push('\n' + result.method);
|
|
940
945
|
declaredMethods.add(toCamelCase(propName));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Port of RelationBuilder.php — generates Eloquent relation methods.
|
|
3
3
|
*/
|
|
4
|
+
import type { SchemaReader } from './schema-reader.js';
|
|
4
5
|
interface RelationResult {
|
|
5
6
|
method: string;
|
|
6
7
|
imports: string[];
|
|
@@ -8,6 +9,9 @@ interface RelationResult {
|
|
|
8
9
|
interface RelationContext {
|
|
9
10
|
sourceTableName: string;
|
|
10
11
|
targetTableName: string;
|
|
12
|
+
/** Owning schema name (PascalCase) for OneToMany FK resolution. Issue #237. */
|
|
13
|
+
sourceSchemaName?: string;
|
|
14
|
+
reader?: SchemaReader;
|
|
11
15
|
/** Non-conventional physical table resolved from an explicit pivot schema.
|
|
12
16
|
* Omitted when Laravel can infer the joining table from model basenames. */
|
|
13
17
|
pivotTableName?: string;
|
|
@@ -24,6 +28,14 @@ export declare function buildRelation(propName: string, property: Record<string,
|
|
|
24
28
|
export declare function generatePivotTableName(sourceModel: string, targetModel: string): string;
|
|
25
29
|
/** Get the return type hint for a relation. */
|
|
26
30
|
export declare function getReturnType(relation: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the child-table FK column for a OneToMany when `mappedBy` / `inversedBy`
|
|
33
|
+
* is absent. Prefer the single inverse ManyToOne on the target (omnify-go#166
|
|
34
|
+
* `column:` override included); if zero matches, fall back to the owning schema
|
|
35
|
+
* name (external targets). If two or more matches, throw — omnify-go#242.
|
|
36
|
+
* Issue #237.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveHasManyForeignKey(propName: string, target: string, context?: RelationContext): string;
|
|
27
39
|
/**
|
|
28
40
|
* Build a chained `->orderBy('col', 'dir')` PHP fragment from an OrderByList.
|
|
29
41
|
* Returns an empty string if no ordering is configured. Issue #40.
|
|
@@ -16,7 +16,7 @@ export function buildRelation(propName, property, modelNamespace, context) {
|
|
|
16
16
|
method = oneToOne(propName, property, target, modelNamespace);
|
|
17
17
|
break;
|
|
18
18
|
case 'OneToMany':
|
|
19
|
-
method = hasMany(propName, property, target, modelNamespace);
|
|
19
|
+
method = hasMany(propName, property, target, modelNamespace, context);
|
|
20
20
|
break;
|
|
21
21
|
case 'ManyToMany':
|
|
22
22
|
method = belongsToMany(propName, property, target, modelNamespace, context);
|
|
@@ -97,9 +97,11 @@ function oneToOne(propName, property, target, ns) {
|
|
|
97
97
|
}
|
|
98
98
|
return belongsTo(propName, property, target, ns);
|
|
99
99
|
}
|
|
100
|
-
function hasMany(propName, property, target, ns) {
|
|
100
|
+
function hasMany(propName, property, target, ns, context) {
|
|
101
101
|
const inverse = property['mappedBy'] ?? property['inversedBy'] ?? null;
|
|
102
|
-
const foreignKey = inverse
|
|
102
|
+
const foreignKey = inverse
|
|
103
|
+
? toFkColumnName(inverse)
|
|
104
|
+
: resolveHasManyForeignKey(propName, target, context);
|
|
103
105
|
const methodName = toCamelCase(propName);
|
|
104
106
|
const fqcn = `\\${ns}\\${target}`;
|
|
105
107
|
const orderChain = formatOrderByChain(property['orderBy']);
|
|
@@ -111,6 +113,44 @@ function hasMany(propName, property, target, ns) {
|
|
|
111
113
|
return $this->hasMany(${fqcn}::class, '${foreignKey}')${orderChain};
|
|
112
114
|
}`;
|
|
113
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve the child-table FK column for a OneToMany when `mappedBy` / `inversedBy`
|
|
118
|
+
* is absent. Prefer the single inverse ManyToOne on the target (omnify-go#166
|
|
119
|
+
* `column:` override included); if zero matches, fall back to the owning schema
|
|
120
|
+
* name (external targets). If two or more matches, throw — omnify-go#242.
|
|
121
|
+
* Issue #237.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveHasManyForeignKey(propName, target, context) {
|
|
124
|
+
const reader = context?.reader;
|
|
125
|
+
const owningSchemaName = context?.sourceSchemaName;
|
|
126
|
+
if (reader && owningSchemaName && target) {
|
|
127
|
+
const matches = findInverseManyToOneFkColumns(reader, target, owningSchemaName);
|
|
128
|
+
if (matches.length === 1) {
|
|
129
|
+
return matches[0];
|
|
130
|
+
}
|
|
131
|
+
if (matches.length >= 2) {
|
|
132
|
+
const columnList = matches.join(', ');
|
|
133
|
+
throw new Error(`${owningSchemaName}.${propName}: target ${target} has ${matches.length} ManyToOne associations back to ${owningSchemaName} (${columnList}) — set mappedBy: on the OneToMany to pick one`);
|
|
134
|
+
}
|
|
135
|
+
return toFkColumnName(owningSchemaName);
|
|
136
|
+
}
|
|
137
|
+
return toFkColumnName(propName);
|
|
138
|
+
}
|
|
139
|
+
function findInverseManyToOneFkColumns(reader, targetSchemaName, owningSchemaName) {
|
|
140
|
+
const targetSchema = reader.getSchema(targetSchemaName);
|
|
141
|
+
const props = (targetSchema?.properties ?? {});
|
|
142
|
+
const columns = [];
|
|
143
|
+
for (const [childPropName, childProp] of Object.entries(props)) {
|
|
144
|
+
if (childProp['type'] !== 'Association')
|
|
145
|
+
continue;
|
|
146
|
+
if (childProp['relation'] !== 'ManyToOne')
|
|
147
|
+
continue;
|
|
148
|
+
if (childProp['target'] !== owningSchemaName)
|
|
149
|
+
continue;
|
|
150
|
+
columns.push(childProp['column'] || toFkColumnName(childPropName));
|
|
151
|
+
}
|
|
152
|
+
return columns;
|
|
153
|
+
}
|
|
114
154
|
/**
|
|
115
155
|
* Build a chained `->orderBy('col', 'dir')` PHP fragment from an OrderByList.
|
|
116
156
|
* Returns an empty string if no ordering is configured. Issue #40.
|
|
@@ -2,26 +2,8 @@
|
|
|
2
2
|
* Port of RequestGenerator.php — generates Store/Update request classes.
|
|
3
3
|
*/
|
|
4
4
|
import { toPascalCase, toSnakeCase, toCamelCase, toFkColumnName } from './naming-helper.js';
|
|
5
|
-
import { toStoreRules, toUpdateRules, formatRules } from './type-mapper.js';
|
|
5
|
+
import { toStoreRules, toUpdateRules, formatRules, resolveFkRuleType } from './type-mapper.js';
|
|
6
6
|
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace, resolveBaseClass, resolveEditableClass, nestByGroup, nestEditableByGroup } from './types.js';
|
|
7
|
-
/**
|
|
8
|
-
* Resolve the Laravel validation rule type ('integer', 'uuid', or 'string') for
|
|
9
|
-
* a foreign key column based on the target schema's primary key type.
|
|
10
|
-
*
|
|
11
|
-
* Mirrors the FK cast logic in `model-generator.ts` so validation rules and
|
|
12
|
-
* Eloquent casts agree about the wire format. Issue #36.
|
|
13
|
-
*/
|
|
14
|
-
function resolveFkRuleType(target, reader) {
|
|
15
|
-
if (!target)
|
|
16
|
-
return 'integer';
|
|
17
|
-
const targetSchema = reader.getSchema(target);
|
|
18
|
-
const id = targetSchema?.options?.id;
|
|
19
|
-
if (id === 'Uuid')
|
|
20
|
-
return 'uuid';
|
|
21
|
-
if (id === 'Ulid' || id === 'String')
|
|
22
|
-
return 'string';
|
|
23
|
-
return 'integer';
|
|
24
|
-
}
|
|
25
7
|
/** Generate Store/Update request classes for all project-owned visible object schemas. */
|
|
26
8
|
export function generateRequests(reader, config) {
|
|
27
9
|
const files = [];
|
|
@@ -105,7 +87,7 @@ function generateBaseRequest(name, schema, reader, config, action) {
|
|
|
105
87
|
if (relation === 'ManyToOne') {
|
|
106
88
|
const snakeName = toFkColumnName(propName);
|
|
107
89
|
const target = prop['target'] ?? '';
|
|
108
|
-
const targetIdType = resolveFkRuleType(target, reader);
|
|
90
|
+
const targetIdType = resolveFkRuleType(prop, target, reader);
|
|
109
91
|
const rules = isUpdate
|
|
110
92
|
? toUpdateRules(prop, tableName, modelRouteParam, targetIdType)
|
|
111
93
|
: toStoreRules(prop, tableName, targetIdType);
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Port of TypeMapper.php — type mappings for PHP casts, rules, resources.
|
|
3
3
|
*/
|
|
4
|
+
import type { SchemaReader } from './schema-reader.js';
|
|
4
5
|
type Rule = string | ['rule_in', string[]] | ['rule_unique_ignore', string, string];
|
|
6
|
+
/**
|
|
7
|
+
* Effective primary-key type for a ManyToOne foreign key column.
|
|
8
|
+
* Property-level `idType:` wins (required for targets outside the schema set),
|
|
9
|
+
* then the target schema's `options.id`, then an integer-family default.
|
|
10
|
+
* Issue #237.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveFkIdType(property: Record<string, unknown>, target: string, reader: SchemaReader): string;
|
|
13
|
+
/** Laravel validation rule type for a ManyToOne FK (Issue #36, #237). */
|
|
14
|
+
export declare function resolveFkRuleType(property: Record<string, unknown>, target: string, reader: SchemaReader): 'integer' | 'uuid' | 'string';
|
|
15
|
+
/** Eloquent cast for a ManyToOne FK column. */
|
|
16
|
+
export declare function resolveFkCast(property: Record<string, unknown>, target: string, reader: SchemaReader): 'integer' | 'string' | null;
|
|
17
|
+
/** `@property` doc type for a ManyToOne FK column. */
|
|
18
|
+
export declare function resolveFkPhpDocType(property: Record<string, unknown>, target: string, reader: SchemaReader, nullable: boolean): string;
|
|
5
19
|
/** Map Omnify property type to PHP cast. */
|
|
6
20
|
export declare function toCast(type: string, property?: Record<string, unknown>): string | null;
|
|
7
21
|
/** Map Omnify property type to PHP doc type. */
|
package/dist/php/type-mapper.js
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
* Port of TypeMapper.php — type mappings for PHP casts, rules, resources.
|
|
3
3
|
*/
|
|
4
4
|
import { toTableName } from './naming-helper.js';
|
|
5
|
+
/**
|
|
6
|
+
* Effective primary-key type for a ManyToOne foreign key column.
|
|
7
|
+
* Property-level `idType:` wins (required for targets outside the schema set),
|
|
8
|
+
* then the target schema's `options.id`, then an integer-family default.
|
|
9
|
+
* Issue #237.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveFkIdType(property, target, reader) {
|
|
12
|
+
const override = property['idType'];
|
|
13
|
+
if (typeof override === 'string' && override !== '') {
|
|
14
|
+
return override;
|
|
15
|
+
}
|
|
16
|
+
if (!target)
|
|
17
|
+
return 'BigInt';
|
|
18
|
+
const targetSchema = reader.getSchema(target);
|
|
19
|
+
const id = targetSchema?.options?.id;
|
|
20
|
+
if (typeof id === 'string')
|
|
21
|
+
return id;
|
|
22
|
+
return 'BigInt';
|
|
23
|
+
}
|
|
24
|
+
/** Laravel validation rule type for a ManyToOne FK (Issue #36, #237). */
|
|
25
|
+
export function resolveFkRuleType(property, target, reader) {
|
|
26
|
+
const idType = resolveFkIdType(property, target, reader);
|
|
27
|
+
if (idType === 'Uuid')
|
|
28
|
+
return 'uuid';
|
|
29
|
+
if (idType === 'Ulid' || idType === 'String')
|
|
30
|
+
return 'string';
|
|
31
|
+
return 'integer';
|
|
32
|
+
}
|
|
33
|
+
/** Eloquent cast for a ManyToOne FK column. */
|
|
34
|
+
export function resolveFkCast(property, target, reader) {
|
|
35
|
+
const idType = resolveFkIdType(property, target, reader);
|
|
36
|
+
if (idType === 'Uuid' || idType === 'Ulid' || idType === 'String')
|
|
37
|
+
return 'string';
|
|
38
|
+
if (idType === 'Int' || idType === 'BigInt' || idType === 'TinyInt')
|
|
39
|
+
return 'integer';
|
|
40
|
+
return 'integer';
|
|
41
|
+
}
|
|
42
|
+
/** `@property` doc type for a ManyToOne FK column. */
|
|
43
|
+
export function resolveFkPhpDocType(property, target, reader, nullable) {
|
|
44
|
+
const idType = resolveFkIdType(property, target, reader);
|
|
45
|
+
const phpType = idType === 'Int' || idType === 'BigInt' || idType === 'TinyInt' ? 'int' : 'string';
|
|
46
|
+
return nullable ? `${phpType}|null` : phpType;
|
|
47
|
+
}
|
|
5
48
|
/** Map Omnify property type to PHP cast. */
|
|
6
49
|
export function toCast(type, property = {}) {
|
|
7
50
|
switch (type) {
|
|
@@ -85,11 +128,24 @@ export function toStoreRules(property, tableName, fkRuleType = 'integer') {
|
|
|
85
128
|
const unique = property['unique'] ?? false;
|
|
86
129
|
const vr = property['rules'];
|
|
87
130
|
const rules = [];
|
|
88
|
-
// required/nullable — rules.required can override
|
|
89
|
-
const
|
|
90
|
-
|
|
131
|
+
// required/nullable/sometimes — rules.required can override; non-nullable + default → sometimes (#235)
|
|
132
|
+
const explicitRequired = vr?.['required'];
|
|
133
|
+
const hasExplicitRequired = explicitRequired !== undefined;
|
|
134
|
+
const isRequired = hasExplicitRequired
|
|
135
|
+
? explicitRequired
|
|
91
136
|
: !nullable;
|
|
92
|
-
|
|
137
|
+
// `default: null` on a non-nullable column is contradictory YAML — treat it
|
|
138
|
+
// as no default so the column stays 'required' rather than silently optional.
|
|
139
|
+
const hasDefault = property['default'] !== undefined && property['default'] !== null;
|
|
140
|
+
if (!isRequired) {
|
|
141
|
+
rules.push('nullable');
|
|
142
|
+
}
|
|
143
|
+
else if (!hasExplicitRequired && !nullable && hasDefault) {
|
|
144
|
+
rules.push('sometimes');
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
rules.push('required');
|
|
148
|
+
}
|
|
93
149
|
switch (type) {
|
|
94
150
|
case 'String':
|
|
95
151
|
case 'Slug':
|