@omnifyjp/ts 6.0.6 → 6.2.0
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 +5 -0
- package/dist/interface-generator.d.ts +0 -1
- package/dist/interface-generator.js +25 -4
- package/dist/php/model-generator.js +173 -16
- package/dist/php/relation-builder.js +8 -4
- package/dist/php/resource-generator.js +46 -3
- package/dist/php/types.d.ts +18 -1
- package/dist/php/types.js +2 -2
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -233,6 +233,11 @@ program
|
|
|
233
233
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
234
234
|
if (!file.overwrite && existsSync(filePath)) {
|
|
235
235
|
phpSkipped++;
|
|
236
|
+
// The file predates something the template now needs. Say so once,
|
|
237
|
+
// naming the file and the line, rather than rewriting user code.
|
|
238
|
+
if (file.expects && !readFileSync(filePath, 'utf-8').includes(file.expects.needle)) {
|
|
239
|
+
console.warn(`[omnify-ts] ${relative(configDir, filePath)}: ${file.expects.hint}`);
|
|
240
|
+
}
|
|
236
241
|
continue;
|
|
237
242
|
}
|
|
238
243
|
// Issue #98 v5.8.18 / #99 v5.8.19: skip user-editable stub
|
|
@@ -17,7 +17,6 @@ export declare function toSnakeCase(str: string): string;
|
|
|
17
17
|
* unconditionally produced `table_session_id_id` (issue #149).
|
|
18
18
|
*/
|
|
19
19
|
export declare function toFkColumnName(propertyName: string): string;
|
|
20
|
-
/** Gets TypeScript type for a property. */
|
|
21
20
|
export declare function getPropertyType(property: PropertyDefinition, allSchemas: Record<string, SchemaDefinition>): string;
|
|
22
21
|
/**
|
|
23
22
|
* Convert a property to TSProperty array.
|
|
@@ -63,12 +63,29 @@ function getIdType(options) {
|
|
|
63
63
|
return 'BigInt';
|
|
64
64
|
}
|
|
65
65
|
/** Gets TypeScript type for a property. */
|
|
66
|
+
/**
|
|
67
|
+
* The interface name to write for a relation target, or `unknown` when the
|
|
68
|
+
* target lives outside the generated schema set (external `targetNamespace`
|
|
69
|
+
* models). Imports are collected from the same schema set, so anything absent
|
|
70
|
+
* here would be referenced without ever being imported.
|
|
71
|
+
*/
|
|
72
|
+
function resolveTargetName(target, allSchemas) {
|
|
73
|
+
if (!target || !(target in allSchemas)) {
|
|
74
|
+
return 'unknown';
|
|
75
|
+
}
|
|
76
|
+
return target;
|
|
77
|
+
}
|
|
66
78
|
export function getPropertyType(property, allSchemas) {
|
|
67
79
|
if (property.type === 'File') {
|
|
68
80
|
return property.multiple ? 'OmnifyFile[]' : 'OmnifyFile | null';
|
|
69
81
|
}
|
|
70
82
|
if (property.type === 'Association') {
|
|
71
|
-
|
|
83
|
+
// A target outside the schema set (an external model reached through
|
|
84
|
+
// `targetNamespace`, e.g. the app's own User) has no generated interface
|
|
85
|
+
// and therefore no import. Naming it anyway emits `user?: User` against an
|
|
86
|
+
// identifier nothing declares — TS2304 in every consumer. `unknown` is the
|
|
87
|
+
// honest type for a shape this generator cannot see.
|
|
88
|
+
const targetName = resolveTargetName(property.target, allSchemas);
|
|
72
89
|
switch (property.relation) {
|
|
73
90
|
case 'OneToOne':
|
|
74
91
|
case 'ManyToOne':
|
|
@@ -78,7 +95,9 @@ export function getPropertyType(property, allSchemas) {
|
|
|
78
95
|
return `${targetName}[]`;
|
|
79
96
|
case 'MorphTo':
|
|
80
97
|
if (property.targets && property.targets.length > 0) {
|
|
81
|
-
return property.targets
|
|
98
|
+
return property.targets
|
|
99
|
+
.map(target => resolveTargetName(target, allSchemas))
|
|
100
|
+
.join(' | ');
|
|
82
101
|
}
|
|
83
102
|
return 'unknown';
|
|
84
103
|
case 'MorphOne':
|
|
@@ -162,7 +181,9 @@ export function propertyToTSProperties(propertyName, property, schema, allSchema
|
|
|
162
181
|
// Handle Association: MorphTo — creates _type, _id, and relation properties
|
|
163
182
|
if (property.type === 'Association' && property.relation === 'MorphTo' && property.targets && property.targets.length > 0) {
|
|
164
183
|
const targetUnion = property.targets.map(t => `'${t}'`).join(' | ');
|
|
165
|
-
const relationUnion = property.targets
|
|
184
|
+
const relationUnion = property.targets
|
|
185
|
+
.map(target => resolveTargetName(target, allSchemas))
|
|
186
|
+
.join(' | ');
|
|
166
187
|
return [
|
|
167
188
|
{
|
|
168
189
|
name: `${propertyName}_type`,
|
|
@@ -186,7 +207,7 @@ export function propertyToTSProperties(propertyName, property, schema, allSchema
|
|
|
186
207
|
}
|
|
187
208
|
// Handle Association: ManyToOne / OneToOne (owning side) — creates FK _id and relation
|
|
188
209
|
if (property.type === 'Association' && (property.relation === 'ManyToOne' || property.relation === 'OneToOne') && !property.mappedBy) {
|
|
189
|
-
const targetName = property.target
|
|
210
|
+
const targetName = resolveTargetName(property.target, allSchemas);
|
|
190
211
|
// Determine FK type from target schema
|
|
191
212
|
let fkType = 'number';
|
|
192
213
|
const targetSchema = property.target ? allSchemas[property.target] : undefined;
|
|
@@ -15,15 +15,105 @@ export function generateModels(reader, config) {
|
|
|
15
15
|
for (const [name, schema] of Object.entries(reader.getProjectObjectSchemas())) {
|
|
16
16
|
files.push(...generateForSchema(name, schema, reader, config));
|
|
17
17
|
}
|
|
18
|
-
// Package schemas: user model only (extends package model)
|
|
18
|
+
// Package schemas: user model only (extends package model), plus a trait
|
|
19
|
+
// carrying anything a project-level `kind: extend` added to them (#164).
|
|
19
20
|
for (const [name, schema] of Object.entries(reader.getPackageObjectSchemas())) {
|
|
20
21
|
const pkgNs = reader.resolveModelNamespace(name, config.models.namespace);
|
|
21
22
|
if (pkgNs !== config.models.namespace) {
|
|
22
|
-
|
|
23
|
+
const extended = extendedPropertyNames(schema);
|
|
24
|
+
if (extended.length > 0) {
|
|
25
|
+
files.push(generateExtendAttributesTrait(name, schema, extended, reader, config));
|
|
26
|
+
}
|
|
27
|
+
files.push(generatePackageUserModel(name, pkgNs, config, extended.length > 0));
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
30
|
return files;
|
|
26
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The properties a project-level `kind: extend` added to a schema, in merge
|
|
34
|
+
* order, keeping only the ones the schema still declares.
|
|
35
|
+
*/
|
|
36
|
+
function extendedPropertyNames(schema) {
|
|
37
|
+
const declared = schema.properties ?? {};
|
|
38
|
+
return (schema.extendedProperties ?? []).filter(name => declared[name] !== undefined);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A trait carrying the columns a project-level `kind: extend` added to a
|
|
42
|
+
* PACKAGE-owned schema (#164).
|
|
43
|
+
*
|
|
44
|
+
* The package's own generate run writes that model from the package's schemas
|
|
45
|
+
* alone; it cannot see a consumer's extend, so `$fillable` and `casts()` there
|
|
46
|
+
* are missing the added columns. The migration has them — the column exists —
|
|
47
|
+
* so `$model->new_column = x` is silently dropped by mass-assignment guarding
|
|
48
|
+
* and a Json column reads back as a raw string. The extend is only half
|
|
49
|
+
* applied, and nothing says so.
|
|
50
|
+
*
|
|
51
|
+
* It is written here, in the CONSUMER's own namespace, rather than back into
|
|
52
|
+
* the package's model. Writing into the package directory would work in a
|
|
53
|
+
* monorepo and silently do nothing once that package is installed under
|
|
54
|
+
* `vendor/`, where the next `composer install` removes it.
|
|
55
|
+
*
|
|
56
|
+
* `initialize<TraitName>()` is Laravel's own hook: `Model::__construct` calls
|
|
57
|
+
* `initializeTraits()`, which invokes it on every used trait. `mergeFillable` /
|
|
58
|
+
* `mergeCasts` add to what the parent declares instead of replacing it, which a
|
|
59
|
+
* redeclared `protected $fillable` in the child would do.
|
|
60
|
+
*/
|
|
61
|
+
function generateExtendAttributesTrait(name, schema, extended, reader, config) {
|
|
62
|
+
const modelName = toPascalCase(name);
|
|
63
|
+
const traitName = extendAttributesTraitName(name);
|
|
64
|
+
const properties = (schema.properties ?? {});
|
|
65
|
+
const expandedProperties = reader.getExpandedProperties(name);
|
|
66
|
+
// The same two builders the BaseModel uses, over the extended properties
|
|
67
|
+
// only. Sharing them is the point: a column the BaseModel would have made
|
|
68
|
+
// fillable, or cast, is made fillable and cast the same way here.
|
|
69
|
+
const fillable = buildFillable(properties, expandedProperties, extended)
|
|
70
|
+
.split('\n')
|
|
71
|
+
.filter(line => line.trim() !== '')
|
|
72
|
+
.map(line => ` ${line.trim()}`)
|
|
73
|
+
.join('\n');
|
|
74
|
+
const casts = buildCasts(properties, expandedProperties, extended, reader, config)
|
|
75
|
+
.split('\n')
|
|
76
|
+
.filter(line => line.trim() !== '')
|
|
77
|
+
.map(line => ` ${line.trim()}`)
|
|
78
|
+
.join('\n');
|
|
79
|
+
const body = [];
|
|
80
|
+
if (fillable !== '') {
|
|
81
|
+
body.push(` $this->mergeFillable([\n${fillable}\n ]);`);
|
|
82
|
+
}
|
|
83
|
+
if (casts !== '') {
|
|
84
|
+
body.push(` $this->mergeCasts([\n${casts}\n ]);`);
|
|
85
|
+
}
|
|
86
|
+
const content = `<?php
|
|
87
|
+
|
|
88
|
+
namespace ${config.models.namespace}\\Concerns;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
92
|
+
* Any changes will be overwritten on next generation.
|
|
93
|
+
*
|
|
94
|
+
* @generated by omnify
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Columns this project's \`kind: extend\` added to the ${modelName} schema.
|
|
99
|
+
*
|
|
100
|
+
* ${modelName} is owned by a package, so the package's own model cannot know
|
|
101
|
+
* about them. Used by ${config.models.namespace}\\${modelName}.
|
|
102
|
+
*/
|
|
103
|
+
trait ${traitName}
|
|
104
|
+
{
|
|
105
|
+
/** Called by Laravel's Model::initializeTraits() from the constructor. */
|
|
106
|
+
public function initialize${traitName}(): void
|
|
107
|
+
{
|
|
108
|
+
${body.join('\n\n')}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
return baseFile(`${config.models.path}/Concerns/${traitName}.php`, content);
|
|
113
|
+
}
|
|
114
|
+
function extendAttributesTraitName(name) {
|
|
115
|
+
return `${toPascalCase(name)}ExtendAttributes`;
|
|
116
|
+
}
|
|
27
117
|
function generateForSchema(name, schema, reader, config) {
|
|
28
118
|
const properties = (schema.properties ?? {});
|
|
29
119
|
const options = schema.options ?? {};
|
|
@@ -104,9 +194,22 @@ function generateBaseModel(name, schema, reader, config) {
|
|
|
104
194
|
const nestedSetParentColumn = null;
|
|
105
195
|
const translatableFields = reader.getTranslatableFields(name);
|
|
106
196
|
const hasTranslatable = translatableFields.length > 0;
|
|
107
|
-
|
|
197
|
+
// A schema may name its primary key three mutually-exclusive ways: the
|
|
198
|
+
// `id:` shortcut (always column "id"), `options.primaryKey` (composite),
|
|
199
|
+
// or a property carrying `primary: true` (custom-named single-column PK
|
|
200
|
+
// — omnify-go#167). The schema validator rejects any schema declaring
|
|
201
|
+
// more than one of these, so checking them in this order is safe.
|
|
202
|
+
const primaryKeyOption = options['primaryKey'];
|
|
203
|
+
const customPrimaryEntry = primaryKeyOption
|
|
204
|
+
? undefined
|
|
205
|
+
: propertyOrder
|
|
206
|
+
.map((p) => [p, properties[p]])
|
|
207
|
+
.find(([, prop]) => prop?.['primary'] === true);
|
|
208
|
+
const primaryKey = primaryKeyOption ?? (customPrimaryEntry ? toSnakeCase(customPrimaryEntry[0]) : 'id');
|
|
108
209
|
const rawId = options.id;
|
|
109
|
-
const idType =
|
|
210
|
+
const idType = customPrimaryEntry
|
|
211
|
+
? (customPrimaryEntry[1]['type'] ?? 'String')
|
|
212
|
+
: (typeof rawId === 'string' ? rawId : 'BigInt');
|
|
110
213
|
const isCompositeKey = primaryKey.includes(',');
|
|
111
214
|
const needsUuidTrait = !isCompositeKey && idType === 'Uuid';
|
|
112
215
|
const needsUlidTrait = !isCompositeKey && idType === 'Ulid';
|
|
@@ -168,6 +271,17 @@ function generateBaseModel(name, schema, reader, config) {
|
|
|
168
271
|
*/
|
|
169
272
|
protected $keyType = 'string';
|
|
170
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Indicates if the IDs are auto-incrementing.
|
|
276
|
+
*/
|
|
277
|
+
public $incrementing = false;`;
|
|
278
|
+
}
|
|
279
|
+
else if (customPrimaryEntry && customPrimaryEntry[1]['autoIncrement'] !== true) {
|
|
280
|
+
// Integer-typed custom primary key not declared `autoIncrement: true`
|
|
281
|
+
// (omnify-go#167). Eloquent's Model default is $incrementing = true,
|
|
282
|
+
// which makes it omit the column on insert and read the value back via
|
|
283
|
+
// lastInsertId() — wrong for a key the caller assigns itself.
|
|
284
|
+
keyTypeSection = `
|
|
171
285
|
/**
|
|
172
286
|
* Indicates if the IDs are auto-incrementing.
|
|
173
287
|
*/
|
|
@@ -448,7 +562,11 @@ function buildDocProperties(properties, expandedProperties, propertyOrder, reade
|
|
|
448
562
|
if (type === 'Association') {
|
|
449
563
|
const relation = prop['relation'] ?? '';
|
|
450
564
|
if (relation === 'ManyToOne') {
|
|
451
|
-
|
|
565
|
+
// omnify-go#166: honor the `column:` override (ForeignColumn) the
|
|
566
|
+
// migration generator already applies — falling back to the
|
|
567
|
+
// {property}_id convention here documents a column the table
|
|
568
|
+
// doesn't actually have.
|
|
569
|
+
const snakeName = prop['column'] || toFkColumnName(propName);
|
|
452
570
|
const nullable = prop['nullable'] ?? false;
|
|
453
571
|
const phpType = nullable ? 'int|null' : 'int';
|
|
454
572
|
lines.push(` * @property ${phpType} $${snakeName}`);
|
|
@@ -525,7 +643,8 @@ function buildFillable(properties, expandedProperties, propertyOrder) {
|
|
|
525
643
|
if (type === 'Association') {
|
|
526
644
|
const relation = prop['relation'] ?? '';
|
|
527
645
|
if (relation === 'ManyToOne') {
|
|
528
|
-
|
|
646
|
+
// omnify-go#166: honor the `column:` override (ForeignColumn).
|
|
647
|
+
fields.push(prop['column'] || toFkColumnName(propName));
|
|
529
648
|
}
|
|
530
649
|
else if (relation === 'MorphTo') {
|
|
531
650
|
const morphName = prop['morphName'] || toSnakeCase(propName);
|
|
@@ -633,7 +752,8 @@ function buildCasts(properties, expandedProperties, propertyOrder, reader, confi
|
|
|
633
752
|
? targetSchema.options.id
|
|
634
753
|
: 'BigInt';
|
|
635
754
|
if (targetIdType !== 'Uuid' && targetIdType !== 'Ulid') {
|
|
636
|
-
|
|
755
|
+
// omnify-go#166: honor the `column:` override (ForeignColumn).
|
|
756
|
+
casts.push(`'${prop['column'] || toFkColumnName(propName)}' => 'integer',`);
|
|
637
757
|
}
|
|
638
758
|
}
|
|
639
759
|
continue;
|
|
@@ -678,7 +798,9 @@ function buildCasts(properties, expandedProperties, propertyOrder, reader, confi
|
|
|
678
798
|
* 4. Default — flat editable namespace (Laravel-canonical
|
|
679
799
|
* `App\Models\<Name>`).
|
|
680
800
|
*/
|
|
681
|
-
function resolveRelatedModelNamespace(target, modelNamespace, config, reader
|
|
801
|
+
function resolveRelatedModelNamespace(target, modelNamespace, config, reader,
|
|
802
|
+
/** The property's `targetNamespace`, for a target with no schema. */
|
|
803
|
+
externalNamespace) {
|
|
682
804
|
// Package schemas use their own model namespace (we don't generate
|
|
683
805
|
// editable stubs for them — relations dispatch to the package class).
|
|
684
806
|
const packageNs = reader.resolveModelNamespace(target, modelNamespace);
|
|
@@ -689,8 +811,16 @@ function resolveRelatedModelNamespace(target, modelNamespace, config, reader) {
|
|
|
689
811
|
return config.models.namespace;
|
|
690
812
|
}
|
|
691
813
|
const targetSchema = reader.getSchema(target);
|
|
692
|
-
if (!targetSchema)
|
|
693
|
-
|
|
814
|
+
if (!targetSchema) {
|
|
815
|
+
// A target outside the schema set is an external model, and
|
|
816
|
+
// `targetNamespace` is the documented way to say where it lives
|
|
817
|
+
// ("Target model namespace for external packages"). Ignoring it and
|
|
818
|
+
// falling back to this project's editable namespace emits
|
|
819
|
+
// `\App\Models\User` for a model that is not there — the relation
|
|
820
|
+
// resolves to a class that does not exist. The fallback still applies
|
|
821
|
+
// when the field is absent, which is every schema that does not use it.
|
|
822
|
+
return externalNamespace || config.models.userEditableNamespace;
|
|
823
|
+
}
|
|
694
824
|
const editableNs = config.models.userEditableNamespace;
|
|
695
825
|
if (targetSchema.kind === 'pivot') {
|
|
696
826
|
return `${editableNs}\\Pivot`;
|
|
@@ -717,7 +847,9 @@ function buildRelations(schemaName, properties, propertyOrder, modelNamespace, r
|
|
|
717
847
|
// Issue #98 v5.8.6: also nest by target's group / Pivot/ so the
|
|
718
848
|
// FQCN matches the target file's actual location.
|
|
719
849
|
const target = prop['target'] ?? '';
|
|
720
|
-
const ns = target
|
|
850
|
+
const ns = target
|
|
851
|
+
? resolveRelatedModelNamespace(target, modelNamespace, config, reader, prop['targetNamespace'])
|
|
852
|
+
: modelNamespace;
|
|
721
853
|
const targetTableName = target ? reader.getTableName(target) : '';
|
|
722
854
|
// For ManyToMany, look up the explicit `kind: pivot` schema by pivotFor
|
|
723
855
|
// pair (user-authored filenames may predate the sorted-name convention)
|
|
@@ -845,7 +977,11 @@ function buildInverseRelations(parentName, modelNamespace, reader, declaredMetho
|
|
|
845
977
|
// generate a child-side accessor, not a parent-side one.
|
|
846
978
|
// Here we're emitting the *parent* accessor, so the child's ManyToOne
|
|
847
979
|
// (or owning OneToOne) is what we react to.
|
|
848
|
-
|
|
980
|
+
// omnify-go#166: honor the child property's `column:` override
|
|
981
|
+
// (ForeignColumn) — the physical FK column lives on the child's
|
|
982
|
+
// table, so it must match whatever the child's own migration/model
|
|
983
|
+
// used, not the {property}_id convention.
|
|
984
|
+
const fkColumn = childProp['column'] || toFkColumnName(childPropName);
|
|
849
985
|
// Issue #98 v5.8.6: nest the inverse-side FQCN by the CHILD's
|
|
850
986
|
// group / Pivot/ so the relation resolves to the child's actual
|
|
851
987
|
// file location (which lives in `<childGroup>/<Child>.php` or
|
|
@@ -890,16 +1026,28 @@ function buildAccessors(expandedProperties) {
|
|
|
890
1026
|
return methods.join('\n');
|
|
891
1027
|
}
|
|
892
1028
|
/** Generate a user model for a package schema that extends the package's model class. */
|
|
893
|
-
function generatePackageUserModel(name, packageNamespace, config) {
|
|
1029
|
+
function generatePackageUserModel(name, packageNamespace, config, hasExtendAttributes = false) {
|
|
894
1030
|
const modelName = toPascalCase(name);
|
|
895
1031
|
const modelNamespace = config.models.namespace;
|
|
896
1032
|
const packageModelClass = `${packageNamespace}\\${modelName}`;
|
|
1033
|
+
const traitName = extendAttributesTraitName(name);
|
|
1034
|
+
// This file is created once and never rewritten, so the `use` line can only
|
|
1035
|
+
// be put here for a model that does not exist yet. A project whose model
|
|
1036
|
+
// predates its extend keeps the file it has, and `generateModels` warns with
|
|
1037
|
+
// the one line to add — overwriting somebody's model to add an import is the
|
|
1038
|
+
// worse of the two.
|
|
1039
|
+
const traitImport = hasExtendAttributes
|
|
1040
|
+
? `use ${modelNamespace}\\Concerns\\${traitName};\n`
|
|
1041
|
+
: '';
|
|
1042
|
+
const traitUse = hasExtendAttributes
|
|
1043
|
+
? ` use ${traitName};\n\n`
|
|
1044
|
+
: '';
|
|
897
1045
|
const content = `<?php
|
|
898
1046
|
|
|
899
1047
|
namespace ${modelNamespace};
|
|
900
1048
|
|
|
901
1049
|
use ${packageModelClass} as Package${modelName};
|
|
902
|
-
|
|
1050
|
+
${traitImport}
|
|
903
1051
|
/**
|
|
904
1052
|
* ${modelName} Model (extends package model)
|
|
905
1053
|
*
|
|
@@ -908,10 +1056,19 @@ use ${packageModelClass} as Package${modelName};
|
|
|
908
1056
|
*/
|
|
909
1057
|
class ${modelName} extends Package${modelName}
|
|
910
1058
|
{
|
|
911
|
-
// Add your custom methods here
|
|
1059
|
+
${traitUse} // Add your custom methods here
|
|
912
1060
|
}
|
|
913
1061
|
`;
|
|
914
|
-
return userFile(`${config.models.path}/${modelName}.php`, content
|
|
1062
|
+
return userFile(`${config.models.path}/${modelName}.php`, content, hasExtendAttributes
|
|
1063
|
+
? {
|
|
1064
|
+
needle: `use ${traitName};`,
|
|
1065
|
+
hint: `this project's \`kind: extend\` adds columns to the package-owned `
|
|
1066
|
+
+ `${modelName} schema, but this model predates the trait that carries `
|
|
1067
|
+
+ `them, so those columns are still not fillable or cast. Add `
|
|
1068
|
+
+ `\`use ${config.models.namespace}\\Concerns\\${traitName};\` and `
|
|
1069
|
+
+ `\`use ${traitName};\` inside the class (#164).`,
|
|
1070
|
+
}
|
|
1071
|
+
: undefined);
|
|
915
1072
|
}
|
|
916
1073
|
/**
|
|
917
1074
|
* Detect whether a schema uses nested set (tree structure).
|
|
@@ -10,7 +10,7 @@ export function buildRelation(propName, property, modelNamespace, context) {
|
|
|
10
10
|
let method = null;
|
|
11
11
|
switch (relation) {
|
|
12
12
|
case 'ManyToOne':
|
|
13
|
-
method = belongsTo(propName, target, modelNamespace);
|
|
13
|
+
method = belongsTo(propName, property, target, modelNamespace);
|
|
14
14
|
break;
|
|
15
15
|
case 'OneToOne':
|
|
16
16
|
method = oneToOne(propName, property, target, modelNamespace);
|
|
@@ -60,8 +60,12 @@ export function getReturnType(relation) {
|
|
|
60
60
|
default: return 'BelongsTo';
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
function belongsTo(propName, target, ns) {
|
|
64
|
-
|
|
63
|
+
function belongsTo(propName, property, target, ns) {
|
|
64
|
+
// omnify-go#166: honor the `column:` override (ForeignColumn) — the
|
|
65
|
+
// migration generator already names the physical column from it, so
|
|
66
|
+
// falling back to the {property}_id convention here points the relation
|
|
67
|
+
// at a column the table doesn't have.
|
|
68
|
+
const foreignKey = property['column'] || toFkColumnName(propName);
|
|
65
69
|
const methodName = toCamelCase(propName);
|
|
66
70
|
const fqcn = `\\${ns}\\${target}`;
|
|
67
71
|
return ` /**
|
|
@@ -86,7 +90,7 @@ function oneToOne(propName, property, target, ns) {
|
|
|
86
90
|
return $this->hasOne(${fqcn}::class, '${foreignKey}');
|
|
87
91
|
}`;
|
|
88
92
|
}
|
|
89
|
-
return belongsTo(propName, target, ns);
|
|
93
|
+
return belongsTo(propName, property, target, ns);
|
|
90
94
|
}
|
|
91
95
|
function hasMany(propName, property, target, ns) {
|
|
92
96
|
const inverse = property['mappedBy'] ?? property['inversedBy'] ?? null;
|
|
@@ -18,13 +18,41 @@ import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace
|
|
|
18
18
|
export function generateResources(reader, config) {
|
|
19
19
|
const files = [];
|
|
20
20
|
for (const [name, schema] of Object.entries(reader.getProjectVisibleObjectSchemas())) {
|
|
21
|
-
if (schema
|
|
21
|
+
if (!generatesOwnResource(schema))
|
|
22
22
|
continue;
|
|
23
23
|
files.push(generateBaseResource(name, schema, reader, config));
|
|
24
24
|
files.push(generateUserResource(name, schema, config));
|
|
25
25
|
}
|
|
26
26
|
return files;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether THIS generator writes a Resource for a project-owned schema.
|
|
30
|
+
*
|
|
31
|
+
* The loop above and the cross-resource reference below have to agree about
|
|
32
|
+
* this, and when they were two separate conditions they did not: the loop
|
|
33
|
+
* skipped `kind: extend`, and the reference did not ask at all — so a relation
|
|
34
|
+
* pointing at one named a class the loop had just declined to write.
|
|
35
|
+
*/
|
|
36
|
+
function generatesOwnResource(schema) {
|
|
37
|
+
return schema.kind !== 'extend' && schema.options?.hidden !== true;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether a Resource class exists for an association target — ours or a
|
|
41
|
+
* package's. `false` means there is no class to name, and the caller must fall
|
|
42
|
+
* back to a bare `whenLoaded()`.
|
|
43
|
+
*/
|
|
44
|
+
function hasResourceClass(target, reader) {
|
|
45
|
+
const schema = reader.getSchema(target);
|
|
46
|
+
// Not a schema at all: an external model reached through `targetNamespace`,
|
|
47
|
+
// e.g. the app's own User. Omnify generates nothing for it.
|
|
48
|
+
if (!schema)
|
|
49
|
+
return false;
|
|
50
|
+
// A package ships its own resources; whether they are reachable is the
|
|
51
|
+
// namespace question, which the caller answers separately.
|
|
52
|
+
if (schema.package != null)
|
|
53
|
+
return true;
|
|
54
|
+
return generatesOwnResource(schema);
|
|
55
|
+
}
|
|
28
56
|
function generateBaseResource(name, schema, reader, config) {
|
|
29
57
|
const modelName = toPascalCase(name);
|
|
30
58
|
// Issue #98 v5.8.2: nest namespace by group in legacy structure so the
|
|
@@ -242,8 +270,23 @@ function addAssociationFields(propName, prop, fields, resourceNamespace, modelNa
|
|
|
242
270
|
targetResNs = editableNs;
|
|
243
271
|
}
|
|
244
272
|
}
|
|
245
|
-
//
|
|
246
|
-
|
|
273
|
+
// The namespace resolving is not the same question as the class existing.
|
|
274
|
+
// `resolveResourceNamespace()` only returns null for a PACKAGE schema whose
|
|
275
|
+
// package declares no resource namespace — a target that is not a schema at
|
|
276
|
+
// all (an external model reached through `targetNamespace`, e.g. the app's
|
|
277
|
+
// own User), or one this generator declines to write a Resource for, falls
|
|
278
|
+
// through and is treated as project-owned. The reference then names
|
|
279
|
+
// `\<projectResourceNs>\<Target>Resource`, a class nothing declares, and
|
|
280
|
+
// `whenLoaded(..., fn () => new UserResource(...))` fatals the moment that
|
|
281
|
+
// relation is loaded and the resource is rendered.
|
|
282
|
+
//
|
|
283
|
+
// This is the same rule `interface-generator.ts` already applies on the TS
|
|
284
|
+
// side (`resolveTargetName` → `unknown` for a target outside the schema set),
|
|
285
|
+
// arriving late on the PHP side.
|
|
286
|
+
const targetHasResource = target ? hasResourceClass(target, reader) : false;
|
|
287
|
+
// If target is a package schema without a resource namespace, or a model no
|
|
288
|
+
// Resource is written for, use simple whenLoaded
|
|
289
|
+
if ((!targetResNs || !targetHasResource) && relation !== 'MorphTo') {
|
|
247
290
|
if (relation === 'ManyToOne') {
|
|
248
291
|
const snakeName = toFkColumnName(propName);
|
|
249
292
|
fields.push(` '${snakeName}' => $this->${snakeName},`);
|
package/dist/php/types.d.ts
CHANGED
|
@@ -7,11 +7,28 @@ export interface GeneratedFile {
|
|
|
7
7
|
readonly content: string;
|
|
8
8
|
readonly overwrite: boolean;
|
|
9
9
|
readonly type: 'base' | 'user';
|
|
10
|
+
/**
|
|
11
|
+
* For a user file only: something the file has to contain that a version
|
|
12
|
+
* written by an EARLIER omnify would not have.
|
|
13
|
+
*
|
|
14
|
+
* A user file is created once and never rewritten, so a template that grows a
|
|
15
|
+
* new line reaches new projects and silently misses every existing one. The
|
|
16
|
+
* writer checks this against the file it skipped and prints `hint` when it is
|
|
17
|
+
* absent — telling the one project that needs the edit, rather than warning
|
|
18
|
+
* everybody on every run or overwriting somebody's code to add a line.
|
|
19
|
+
*/
|
|
20
|
+
readonly expects?: {
|
|
21
|
+
readonly needle: string;
|
|
22
|
+
readonly hint: string;
|
|
23
|
+
};
|
|
10
24
|
}
|
|
11
25
|
/** Create a base file (always overwritten). */
|
|
12
26
|
export declare function baseFile(path: string, content: string): GeneratedFile;
|
|
13
27
|
/** Create a user file (created once, skip if exists). */
|
|
14
|
-
export declare function userFile(path: string, content: string
|
|
28
|
+
export declare function userFile(path: string, content: string, expects?: {
|
|
29
|
+
needle: string;
|
|
30
|
+
hint: string;
|
|
31
|
+
}): GeneratedFile;
|
|
15
32
|
/** Category of base file for modular path resolution. */
|
|
16
33
|
export type BaseCategory = 'Models' | 'Controllers' | 'Requests' | 'Resources' | 'Services' | 'Enums' | 'Traits' | 'Locales' | 'Policies';
|
|
17
34
|
/**
|
package/dist/php/types.js
CHANGED
|
@@ -6,8 +6,8 @@ export function baseFile(path, content) {
|
|
|
6
6
|
return { path, content, overwrite: true, type: 'base' };
|
|
7
7
|
}
|
|
8
8
|
/** Create a user file (created once, skip if exists). */
|
|
9
|
-
export function userFile(path, content) {
|
|
10
|
-
return { path, content, overwrite: false, type: 'user' };
|
|
9
|
+
export function userFile(path, content, expects) {
|
|
10
|
+
return { path, content, overwrite: false, type: 'user', ...(expects ? { expects } : {}) };
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Resolve base file path for a schema based on structure.
|
package/dist/types.d.ts
CHANGED
|
@@ -240,6 +240,14 @@ export interface SchemaDefinition {
|
|
|
240
240
|
readonly properties?: Record<string, PropertyDefinition>;
|
|
241
241
|
readonly propertyOrder?: readonly string[];
|
|
242
242
|
readonly expandedProperties?: Record<string, ExpandedProperty>;
|
|
243
|
+
/**
|
|
244
|
+
* Properties a project-level `kind: extend` merged into this schema, in
|
|
245
|
+
* merge order. The Go loader folds an extend into its target and deletes
|
|
246
|
+
* the extend, so this is the only remaining trace of where they came from
|
|
247
|
+
* — which a PACKAGE-owned target needs, because the package's own generate
|
|
248
|
+
* run never saw them (#164).
|
|
249
|
+
*/
|
|
250
|
+
readonly extendedProperties?: readonly string[];
|
|
243
251
|
readonly values?: readonly EnumValueDefinition[];
|
|
244
252
|
readonly pivotFor?: readonly string[];
|
|
245
253
|
readonly policies?: readonly PolicyDefinition[];
|