ormbridge 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +459 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -0
- package/dist/cliRunner.d.ts +20 -0
- package/dist/cliRunner.js +363 -0
- package/dist/cliRunner.js.map +1 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +220 -0
- package/dist/config.js.map +1 -0
- package/dist/convert.d.ts +20 -0
- package/dist/convert.js +61 -0
- package/dist/convert.js.map +1 -0
- package/dist/emitters/django.d.ts +4 -0
- package/dist/emitters/django.js +501 -0
- package/dist/emitters/django.js.map +1 -0
- package/dist/emitters/doctrine.d.ts +20 -0
- package/dist/emitters/doctrine.js +1134 -0
- package/dist/emitters/doctrine.js.map +1 -0
- package/dist/emitters/gorm.d.ts +38 -0
- package/dist/emitters/gorm.js +1507 -0
- package/dist/emitters/gorm.js.map +1 -0
- package/dist/emitters/graphene.d.ts +4 -0
- package/dist/emitters/graphene.js +431 -0
- package/dist/emitters/graphene.js.map +1 -0
- package/dist/emitters/laravel.d.ts +33 -0
- package/dist/emitters/laravel.js +1695 -0
- package/dist/emitters/laravel.js.map +1 -0
- package/dist/emitters/prisma.d.ts +21 -0
- package/dist/emitters/prisma.js +1058 -0
- package/dist/emitters/prisma.js.map +1 -0
- package/dist/emitters/typeorm.d.ts +9 -0
- package/dist/emitters/typeorm.js +838 -0
- package/dist/emitters/typeorm.js.map +1 -0
- package/dist/emitters/typescriptInterfaces.d.ts +15 -0
- package/dist/emitters/typescriptInterfaces.js +350 -0
- package/dist/emitters/typescriptInterfaces.js.map +1 -0
- package/dist/formats.d.ts +82 -0
- package/dist/formats.js +238 -0
- package/dist/formats.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +48 -0
- package/dist/io.js +343 -0
- package/dist/io.js.map +1 -0
- package/dist/ir.d.ts +197 -0
- package/dist/ir.js +10 -0
- package/dist/ir.js.map +1 -0
- package/dist/naming.d.ts +11 -0
- package/dist/naming.js +88 -0
- package/dist/naming.js.map +1 -0
- package/dist/parsers/django.d.ts +14 -0
- package/dist/parsers/django.js +1161 -0
- package/dist/parsers/django.js.map +1 -0
- package/dist/parsers/doctrine.d.ts +12 -0
- package/dist/parsers/doctrine.js +1984 -0
- package/dist/parsers/doctrine.js.map +1 -0
- package/dist/parsers/goSyntax.d.ts +105 -0
- package/dist/parsers/goSyntax.js +435 -0
- package/dist/parsers/goSyntax.js.map +1 -0
- package/dist/parsers/gorm.d.ts +16 -0
- package/dist/parsers/gorm.js +1857 -0
- package/dist/parsers/gorm.js.map +1 -0
- package/dist/parsers/laravel.d.ts +17 -0
- package/dist/parsers/laravel.js +2824 -0
- package/dist/parsers/laravel.js.map +1 -0
- package/dist/parsers/phpSyntax.d.ts +124 -0
- package/dist/parsers/phpSyntax.js +429 -0
- package/dist/parsers/phpSyntax.js.map +1 -0
- package/dist/parsers/prisma.d.ts +12 -0
- package/dist/parsers/prisma.js +964 -0
- package/dist/parsers/prisma.js.map +1 -0
- package/dist/parsers/pythonSyntax.d.ts +38 -0
- package/dist/parsers/pythonSyntax.js +120 -0
- package/dist/parsers/pythonSyntax.js.map +1 -0
- package/dist/parsers/typeorm.d.ts +12 -0
- package/dist/parsers/typeorm.js +1614 -0
- package/dist/parsers/typeorm.js.map +1 -0
- package/dist/parsers/typescriptSyntax.d.ts +57 -0
- package/dist/parsers/typescriptSyntax.js +200 -0
- package/dist/parsers/typescriptSyntax.js.map +1 -0
- package/dist/prismaOnlyConstructs.d.ts +7 -0
- package/dist/prismaOnlyConstructs.js +76 -0
- package/dist/prismaOnlyConstructs.js.map +1 -0
- package/dist/result.d.ts +20 -0
- package/dist/result.js +18 -0
- package/dist/result.js.map +1 -0
- package/dist/transforms.d.ts +13 -0
- package/dist/transforms.js +142 -0
- package/dist/transforms.js.map +1 -0
- package/package.json +83 -4
|
@@ -0,0 +1,1984 @@
|
|
|
1
|
+
import { toCamelCase, toSnakeCase } from '../naming.js';
|
|
2
|
+
import { err, ok } from '../result.js';
|
|
3
|
+
import { attributesOf, collectDeclarations, evaluateNode, getPhpParser, lastNameSegment, readEnumDeclaration, resolveClassName, typeOf, } from './phpSyntax.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Static tables
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const MAPPING_NAMESPACE = 'Doctrine\\ORM\\Mapping\\';
|
|
8
|
+
/** Doctrine DBAL type names mapped to the IR scalar type. */
|
|
9
|
+
const DBAL_TYPES = {
|
|
10
|
+
string: 'string',
|
|
11
|
+
ascii_string: 'string',
|
|
12
|
+
text: 'text',
|
|
13
|
+
integer: 'int',
|
|
14
|
+
smallint: 'int',
|
|
15
|
+
bigint: 'bigInt',
|
|
16
|
+
boolean: 'boolean',
|
|
17
|
+
decimal: 'decimal',
|
|
18
|
+
number: 'decimal',
|
|
19
|
+
float: 'float',
|
|
20
|
+
smallfloat: 'float',
|
|
21
|
+
datetime: 'dateTime',
|
|
22
|
+
datetime_immutable: 'dateTime',
|
|
23
|
+
datetimetz: 'dateTime',
|
|
24
|
+
datetimetz_immutable: 'dateTime',
|
|
25
|
+
date: 'date',
|
|
26
|
+
date_immutable: 'date',
|
|
27
|
+
time: 'time',
|
|
28
|
+
time_immutable: 'time',
|
|
29
|
+
dateinterval: 'duration',
|
|
30
|
+
json: 'json',
|
|
31
|
+
json_array: 'json',
|
|
32
|
+
jsonb: 'json',
|
|
33
|
+
array: 'json',
|
|
34
|
+
object: 'json',
|
|
35
|
+
simple_array: 'text',
|
|
36
|
+
binary: 'bytes',
|
|
37
|
+
blob: 'bytes',
|
|
38
|
+
guid: 'uuid',
|
|
39
|
+
uuid: 'uuid',
|
|
40
|
+
ulid: 'string',
|
|
41
|
+
enum: 'string',
|
|
42
|
+
};
|
|
43
|
+
/** DBAL types whose storage differs from the IR type in a way worth telling the user about. */
|
|
44
|
+
const APPROXIMATED_TYPES = {
|
|
45
|
+
array: 'is stored as PHP-serialized text; the IR has no equivalent, so it was converted to json',
|
|
46
|
+
object: 'is stored as PHP-serialized text; the IR has no equivalent, so it was converted to json',
|
|
47
|
+
simple_array: 'is stored as comma-separated text; the IR has no array type, so it was converted to text',
|
|
48
|
+
ulid: 'is stored as a 26 character string; it was converted to a string column',
|
|
49
|
+
};
|
|
50
|
+
/** Constants of `Doctrine\DBAL\Types\Types`, mapped to the type name they hold. */
|
|
51
|
+
const TYPE_CONSTANTS = {
|
|
52
|
+
STRING: 'string',
|
|
53
|
+
ASCII_STRING: 'ascii_string',
|
|
54
|
+
TEXT: 'text',
|
|
55
|
+
INTEGER: 'integer',
|
|
56
|
+
SMALLINT: 'smallint',
|
|
57
|
+
BIGINT: 'bigint',
|
|
58
|
+
BOOLEAN: 'boolean',
|
|
59
|
+
DECIMAL: 'decimal',
|
|
60
|
+
NUMBER: 'number',
|
|
61
|
+
FLOAT: 'float',
|
|
62
|
+
SMALLFLOAT: 'smallfloat',
|
|
63
|
+
DATETIME_MUTABLE: 'datetime',
|
|
64
|
+
DATETIME_IMMUTABLE: 'datetime_immutable',
|
|
65
|
+
DATETIMETZ_MUTABLE: 'datetimetz',
|
|
66
|
+
DATETIMETZ_IMMUTABLE: 'datetimetz_immutable',
|
|
67
|
+
DATE_MUTABLE: 'date',
|
|
68
|
+
DATE_IMMUTABLE: 'date_immutable',
|
|
69
|
+
TIME_MUTABLE: 'time',
|
|
70
|
+
TIME_IMMUTABLE: 'time_immutable',
|
|
71
|
+
DATEINTERVAL: 'dateinterval',
|
|
72
|
+
JSON: 'json',
|
|
73
|
+
ARRAY: 'array',
|
|
74
|
+
SIMPLE_ARRAY: 'simple_array',
|
|
75
|
+
OBJECT: 'object',
|
|
76
|
+
BINARY: 'binary',
|
|
77
|
+
BLOB: 'blob',
|
|
78
|
+
GUID: 'guid',
|
|
79
|
+
ENUM: 'enum',
|
|
80
|
+
// Constants of the legacy `Doctrine\DBAL\Types\Type` class.
|
|
81
|
+
DATETIME: 'datetime',
|
|
82
|
+
DATETIMETZ: 'datetimetz',
|
|
83
|
+
TARRAY: 'array',
|
|
84
|
+
JSON_ARRAY: 'json_array',
|
|
85
|
+
};
|
|
86
|
+
/** Type names Doctrine infers from a PHP property type when `#[ORM\Column]` omits `type`. */
|
|
87
|
+
const PHP_TYPE_TO_DBAL = {
|
|
88
|
+
int: 'integer',
|
|
89
|
+
string: 'string',
|
|
90
|
+
bool: 'boolean',
|
|
91
|
+
float: 'float',
|
|
92
|
+
array: 'json',
|
|
93
|
+
datetime: 'datetime',
|
|
94
|
+
datetimeimmutable: 'datetime_immutable',
|
|
95
|
+
dateinterval: 'dateinterval',
|
|
96
|
+
};
|
|
97
|
+
const ON_DELETE_MAP = {
|
|
98
|
+
CASCADE: 'cascade',
|
|
99
|
+
'SET NULL': 'setNull',
|
|
100
|
+
RESTRICT: 'restrict',
|
|
101
|
+
'NO ACTION': 'noAction',
|
|
102
|
+
'SET DEFAULT': 'setDefault',
|
|
103
|
+
};
|
|
104
|
+
const NOW_SQL = /^(now\(\)|current_timestamp(\(\d*\))?|getdate\(\)|sysdate|systimestamp|localtimestamp(\(\d*\))?|datetime\('now'\)|\(datetime\('now'\)\))$/i;
|
|
105
|
+
const UUID_SQL = /^(uuid_generate_v[14]\(\)|gen_random_uuid\(\)|uuid\(\)|newid\(\)|newsequentialid\(\)|sys_guid\(\))$/i;
|
|
106
|
+
const RELATION_ATTRIBUTES = new Set([
|
|
107
|
+
'ManyToOne',
|
|
108
|
+
'OneToOne',
|
|
109
|
+
'OneToMany',
|
|
110
|
+
'ManyToMany',
|
|
111
|
+
]);
|
|
112
|
+
/** Positional parameter order of each attribute constructor (Doctrine ORM 3). */
|
|
113
|
+
const PARAMETER_ORDER = {
|
|
114
|
+
Entity: ['repositoryClass', 'readOnly'],
|
|
115
|
+
Table: ['name', 'schema', 'indexes', 'uniqueConstraints', 'options'],
|
|
116
|
+
Index: ['name', 'columns', 'fields', 'flags', 'options'],
|
|
117
|
+
UniqueConstraint: ['name', 'columns', 'fields', 'options'],
|
|
118
|
+
Column: [
|
|
119
|
+
'name',
|
|
120
|
+
'type',
|
|
121
|
+
'length',
|
|
122
|
+
'precision',
|
|
123
|
+
'scale',
|
|
124
|
+
'unique',
|
|
125
|
+
'nullable',
|
|
126
|
+
'insertable',
|
|
127
|
+
'updatable',
|
|
128
|
+
'enumType',
|
|
129
|
+
'options',
|
|
130
|
+
'columnDefinition',
|
|
131
|
+
'generated',
|
|
132
|
+
'index',
|
|
133
|
+
],
|
|
134
|
+
GeneratedValue: ['strategy'],
|
|
135
|
+
CustomIdGenerator: ['class'],
|
|
136
|
+
SequenceGenerator: ['sequenceName', 'allocationSize', 'initialValue'],
|
|
137
|
+
JoinColumn: [
|
|
138
|
+
'name',
|
|
139
|
+
'referencedColumnName',
|
|
140
|
+
'deferrable',
|
|
141
|
+
'unique',
|
|
142
|
+
'nullable',
|
|
143
|
+
'onDelete',
|
|
144
|
+
'columnDefinition',
|
|
145
|
+
'fieldName',
|
|
146
|
+
'foreignKeyName',
|
|
147
|
+
'options',
|
|
148
|
+
],
|
|
149
|
+
JoinTable: [
|
|
150
|
+
'name',
|
|
151
|
+
'schema',
|
|
152
|
+
'foreignKeyName',
|
|
153
|
+
'inverseForeignKeyName',
|
|
154
|
+
'options',
|
|
155
|
+
],
|
|
156
|
+
ManyToOne: ['targetEntity', 'cascade', 'fetch', 'inversedBy'],
|
|
157
|
+
OneToOne: [
|
|
158
|
+
'targetEntity',
|
|
159
|
+
'mappedBy',
|
|
160
|
+
'inversedBy',
|
|
161
|
+
'cascade',
|
|
162
|
+
'fetch',
|
|
163
|
+
'orphanRemoval',
|
|
164
|
+
],
|
|
165
|
+
OneToMany: [
|
|
166
|
+
'mappedBy',
|
|
167
|
+
'targetEntity',
|
|
168
|
+
'cascade',
|
|
169
|
+
'fetch',
|
|
170
|
+
'orphanRemoval',
|
|
171
|
+
'indexBy',
|
|
172
|
+
],
|
|
173
|
+
ManyToMany: [
|
|
174
|
+
'targetEntity',
|
|
175
|
+
'mappedBy',
|
|
176
|
+
'inversedBy',
|
|
177
|
+
'cascade',
|
|
178
|
+
'fetch',
|
|
179
|
+
'orphanRemoval',
|
|
180
|
+
'indexBy',
|
|
181
|
+
],
|
|
182
|
+
Embedded: ['class', 'columnPrefix'],
|
|
183
|
+
InheritanceType: ['value'],
|
|
184
|
+
DiscriminatorColumn: [
|
|
185
|
+
'name',
|
|
186
|
+
'type',
|
|
187
|
+
'length',
|
|
188
|
+
'columnDefinition',
|
|
189
|
+
'enumType',
|
|
190
|
+
'options',
|
|
191
|
+
],
|
|
192
|
+
DiscriminatorMap: ['value'],
|
|
193
|
+
};
|
|
194
|
+
/** Mapping attributes that carry no schema information and are skipped without a warning. */
|
|
195
|
+
const IGNORED_ATTRIBUTES = new Set([
|
|
196
|
+
'HasLifecycleCallbacks',
|
|
197
|
+
'ChangeTrackingPolicy',
|
|
198
|
+
'EntityListeners',
|
|
199
|
+
'EntityListener',
|
|
200
|
+
'Cache',
|
|
201
|
+
'OrderBy',
|
|
202
|
+
'PrePersist',
|
|
203
|
+
'PostPersist',
|
|
204
|
+
'PreUpdate',
|
|
205
|
+
'PostUpdate',
|
|
206
|
+
'PreRemove',
|
|
207
|
+
'PostRemove',
|
|
208
|
+
'PostLoad',
|
|
209
|
+
'PreFlush',
|
|
210
|
+
'ReadOnly',
|
|
211
|
+
]);
|
|
212
|
+
/** Attributes that define schema we cannot represent; each produces a warning. */
|
|
213
|
+
const UNSUPPORTED_ATTRIBUTES = {
|
|
214
|
+
AttributeOverride: 'attribute overrides are not supported',
|
|
215
|
+
AttributeOverrides: 'attribute overrides are not supported',
|
|
216
|
+
AssociationOverride: 'association overrides are not supported',
|
|
217
|
+
AssociationOverrides: 'association overrides are not supported',
|
|
218
|
+
NamedQuery: 'named queries are not part of the schema',
|
|
219
|
+
NamedNativeQuery: 'named queries are not part of the schema',
|
|
220
|
+
SqlResultSetMapping: 'result set mappings are not part of the schema',
|
|
221
|
+
};
|
|
222
|
+
/** Column options handled elsewhere or known to have no IR equivalent. */
|
|
223
|
+
const UNSUPPORTED_COLUMN_OPTIONS = {
|
|
224
|
+
unsigned: 'unsigned integers',
|
|
225
|
+
comment: 'a column comment',
|
|
226
|
+
collation: 'a collation',
|
|
227
|
+
charset: 'a character set',
|
|
228
|
+
fixed: 'a fixed-length (CHAR) column',
|
|
229
|
+
check: 'a column check constraint',
|
|
230
|
+
autoincrement: 'an explicit autoincrement flag',
|
|
231
|
+
version: 'a version flag',
|
|
232
|
+
};
|
|
233
|
+
/** Symfony UID id generators that map onto IR defaults. */
|
|
234
|
+
const UUID_GENERATORS = new Set([
|
|
235
|
+
'Symfony\\Bridge\\Doctrine\\IdGenerator\\UuidGenerator',
|
|
236
|
+
'Ramsey\\Uuid\\Doctrine\\UuidGenerator',
|
|
237
|
+
]);
|
|
238
|
+
const ULID_GENERATORS = new Set([
|
|
239
|
+
'Symfony\\Bridge\\Doctrine\\IdGenerator\\UlidGenerator',
|
|
240
|
+
]);
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Public entry point
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
/** Parses Doctrine ORM entity files (PHP 8 attributes) into the shared IR using tree-sitter. */
|
|
245
|
+
export async function parseDoctrine(sources, options) {
|
|
246
|
+
const parserResult = await getPhpParser();
|
|
247
|
+
if (!parserResult.ok) {
|
|
248
|
+
return parserResult;
|
|
249
|
+
}
|
|
250
|
+
const parser = parserResult.value;
|
|
251
|
+
const warnings = [];
|
|
252
|
+
const state = { enums: new Map(), warnings };
|
|
253
|
+
const parsedFiles = [];
|
|
254
|
+
let annotationFiles = [];
|
|
255
|
+
for (const source of sources) {
|
|
256
|
+
const tree = parser.parse(source.text);
|
|
257
|
+
if (tree.rootNode.hasError) {
|
|
258
|
+
warnings.push(`${source.path}: the file contains PHP syntax errors; some entities or columns may be missing from the output.`);
|
|
259
|
+
}
|
|
260
|
+
if (usesDocblockAnnotations(tree.rootNode)) {
|
|
261
|
+
annotationFiles = [...annotationFiles, source.path];
|
|
262
|
+
warnings.push(`${source.path}: Doctrine docblock annotations (such as @ORM\\Entity) are not supported yet; ` +
|
|
263
|
+
`convert them to PHP 8 attributes (#[ORM\\Entity]). Annotated classes were skipped.`);
|
|
264
|
+
}
|
|
265
|
+
parsedFiles.push({
|
|
266
|
+
source,
|
|
267
|
+
declarations: collectDeclarations(tree.rootNode, source.path),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// Enums are collected from every file first, so a column can reference an enum from another file.
|
|
271
|
+
for (const { declarations } of parsedFiles) {
|
|
272
|
+
for (const { ctx, node } of declarations) {
|
|
273
|
+
if (node.type === 'enum_declaration') {
|
|
274
|
+
registerEnum(node, ctx, state);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const classes = new Map();
|
|
279
|
+
for (const { declarations } of parsedFiles) {
|
|
280
|
+
for (const { ctx, node } of declarations) {
|
|
281
|
+
if (node.type !== 'class_declaration' &&
|
|
282
|
+
node.type !== 'trait_declaration') {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const rawClass = parseClass(node, ctx, state);
|
|
286
|
+
if (rawClass === undefined) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (classes.has(rawClass.fqn)) {
|
|
290
|
+
warnings.push(`Duplicate class name "${rawClass.fqn}" (${ctx.path}); only the first definition was converted.`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
classes.set(rawClass.fqn, rawClass);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const schema = buildSchema(classes, state, options);
|
|
297
|
+
if (schema.models.length === 0) {
|
|
298
|
+
const checkedPaths = sources
|
|
299
|
+
.map((source) => source.path)
|
|
300
|
+
.join(', ');
|
|
301
|
+
const annotationHint = annotationFiles.length > 0
|
|
302
|
+
? ` Docblock annotations (@ORM\\Entity) were found in ${annotationFiles.join(', ')}; annotation mapping is not supported yet, so convert them to PHP 8 attributes.`
|
|
303
|
+
: '';
|
|
304
|
+
return err('NO_MODELS_FOUND', `No Doctrine entities were found in: ${checkedPaths}. An entity is a class marked with the #[ORM\\Entity] attribute ` +
|
|
305
|
+
`(XML and YAML mappings are not supported).${annotationHint}`);
|
|
306
|
+
}
|
|
307
|
+
return ok(schema);
|
|
308
|
+
}
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// File structure: namespaces, imports, declarations
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
function usesDocblockAnnotations(root) {
|
|
313
|
+
return root
|
|
314
|
+
.descendantsOfType('comment')
|
|
315
|
+
.some((comment) => /@(ORM\\\w+|(?:Entity|MappedSuperclass|Embeddable)\b)/.test(comment.text));
|
|
316
|
+
}
|
|
317
|
+
/** The Doctrine mapping attribute name (for example "Column") for an attribute, or undefined for other attributes. */
|
|
318
|
+
function mappingName(attribute, ctx) {
|
|
319
|
+
const fqn = resolveClassName(attribute.rawName, ctx);
|
|
320
|
+
if (fqn.startsWith(MAPPING_NAMESPACE)) {
|
|
321
|
+
return fqn.slice(MAPPING_NAMESPACE.length);
|
|
322
|
+
}
|
|
323
|
+
// A snippet that uses the conventional alias without importing it.
|
|
324
|
+
if (attribute.rawName.startsWith('ORM\\') && !ctx.uses.has('orm')) {
|
|
325
|
+
return attribute.rawName.slice('ORM\\'.length);
|
|
326
|
+
}
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
function toMappingAttributes(attributes, ctx) {
|
|
330
|
+
const mapped = [];
|
|
331
|
+
for (const attribute of attributes) {
|
|
332
|
+
const name = mappingName(attribute, ctx);
|
|
333
|
+
if (name !== undefined) {
|
|
334
|
+
mapped.push({
|
|
335
|
+
name,
|
|
336
|
+
attribute,
|
|
337
|
+
args: mergeArguments(name, attribute.args, attribute.named),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return mapped;
|
|
342
|
+
}
|
|
343
|
+
function mergeArguments(name, positional, named) {
|
|
344
|
+
const merged = { ...named };
|
|
345
|
+
const order = PARAMETER_ORDER[name] ?? [];
|
|
346
|
+
positional.forEach((value, index) => {
|
|
347
|
+
const key = order[index];
|
|
348
|
+
if (key !== undefined && merged[key] === undefined) {
|
|
349
|
+
merged[key] = value;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
return merged;
|
|
353
|
+
}
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// Value helpers
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
function stringValue(value) {
|
|
358
|
+
return value !== undefined && value.kind === 'string'
|
|
359
|
+
? value.value
|
|
360
|
+
: undefined;
|
|
361
|
+
}
|
|
362
|
+
function boolValue(value) {
|
|
363
|
+
return value !== undefined && value.kind === 'bool' ? value.value : undefined;
|
|
364
|
+
}
|
|
365
|
+
function numberValue(value) {
|
|
366
|
+
if (value === undefined) {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
if (value.kind === 'number') {
|
|
370
|
+
return value.value;
|
|
371
|
+
}
|
|
372
|
+
if (value.kind === 'string' && /^\d+$/.test(value.value)) {
|
|
373
|
+
return Number(value.value);
|
|
374
|
+
}
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
/** The elements of an array value, or an empty list for anything else. */
|
|
378
|
+
function arrayItems(value) {
|
|
379
|
+
return value !== undefined && value.kind === 'array'
|
|
380
|
+
? value.items.map((item) => item.value)
|
|
381
|
+
: [];
|
|
382
|
+
}
|
|
383
|
+
/** The string elements of an array value; undefined when any element is not a plain string. */
|
|
384
|
+
function stringList(value) {
|
|
385
|
+
if (value === undefined) {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
if (value.kind === 'string') {
|
|
389
|
+
return [value.value];
|
|
390
|
+
}
|
|
391
|
+
if (value.kind !== 'array') {
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
const strings = value.items.flatMap((item) => item.value.kind === 'string' ? [item.value.value] : []);
|
|
395
|
+
return strings.length === value.items.length ? strings : undefined;
|
|
396
|
+
}
|
|
397
|
+
/** The entries of a `['key' => value]` array whose keys are strings. */
|
|
398
|
+
function arrayEntries(value) {
|
|
399
|
+
const entries = {};
|
|
400
|
+
if (value !== undefined && value.kind === 'array') {
|
|
401
|
+
for (const item of value.items) {
|
|
402
|
+
if (item.key !== undefined && item.key.kind === 'string') {
|
|
403
|
+
entries[item.key.value] = item.value;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return entries;
|
|
408
|
+
}
|
|
409
|
+
/** Reads `'IDENTITY'` or `ClassMetadata::GENERATOR_TYPE_IDENTITY` as upper case text. */
|
|
410
|
+
function keywordValue(value, prefix) {
|
|
411
|
+
if (value === undefined) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
if (value.kind === 'string') {
|
|
415
|
+
return value.value.toUpperCase();
|
|
416
|
+
}
|
|
417
|
+
if (value.kind === 'constant' && value.name.startsWith(prefix)) {
|
|
418
|
+
return value.name.slice(prefix.length);
|
|
419
|
+
}
|
|
420
|
+
if (value.kind === 'constant' || value.kind === 'name') {
|
|
421
|
+
const text = value.kind === 'name' ? value.value : value.name;
|
|
422
|
+
return text.toUpperCase();
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
/** A class name written as `Foo::class`, `'Foo'` or `'\App\Foo'`, resolved against the current file and class. */
|
|
427
|
+
function entityReference(value, ctx, currentFqn) {
|
|
428
|
+
if (value === undefined) {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
if (value.kind === 'classRef') {
|
|
432
|
+
const lowered = value.name.toLowerCase();
|
|
433
|
+
if (lowered === 'self' || lowered === 'static') {
|
|
434
|
+
return currentFqn;
|
|
435
|
+
}
|
|
436
|
+
return resolveClassName(value.name, ctx);
|
|
437
|
+
}
|
|
438
|
+
if (value.kind === 'string') {
|
|
439
|
+
// Doctrine treats a string without a namespace as relative to the entity's own namespace.
|
|
440
|
+
if (value.value.startsWith('\\')) {
|
|
441
|
+
return value.value.slice(1);
|
|
442
|
+
}
|
|
443
|
+
return value.value.includes('\\') || ctx.namespace === ''
|
|
444
|
+
? value.value
|
|
445
|
+
: `${ctx.namespace}\\${value.value}`;
|
|
446
|
+
}
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
function describeValue(value) {
|
|
450
|
+
switch (value.kind) {
|
|
451
|
+
case 'other':
|
|
452
|
+
return value.text;
|
|
453
|
+
case 'name':
|
|
454
|
+
return value.value;
|
|
455
|
+
case 'constant':
|
|
456
|
+
return `${value.owner}::${value.name}`;
|
|
457
|
+
case 'classRef':
|
|
458
|
+
return `${value.name}::class`;
|
|
459
|
+
default:
|
|
460
|
+
return value.kind;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// Doctrine naming strategy (UnderscoreNamingStrategy)
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
/** Doctrine's `underscore()`: an underscore before an upper case letter that follows a lower case letter or digit. */
|
|
467
|
+
function underscore(name) {
|
|
468
|
+
return name.replace(/(?<=[a-z0-9])([A-Z])/g, '_$1').toLowerCase();
|
|
469
|
+
}
|
|
470
|
+
function stripQuotes(columnName) {
|
|
471
|
+
return columnName.replace(/^`(.*)`$/, '$1');
|
|
472
|
+
}
|
|
473
|
+
function upperFirst(text) {
|
|
474
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
475
|
+
}
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
// Enums
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
function registerEnum(node, ctx, state) {
|
|
480
|
+
const info = readEnumDeclaration(node, ctx, state.warnings);
|
|
481
|
+
if (info === undefined) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (state.enums.has(info.fqn)) {
|
|
485
|
+
state.warnings.push(`Duplicate enum name "${info.fqn}" (${ctx.path}); only the first definition was used.`);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
state.enums.set(info.fqn, info);
|
|
489
|
+
}
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
// Class parsing
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
function parseClass(node, ctx, state) {
|
|
494
|
+
const nameNode = node.childForFieldName('name');
|
|
495
|
+
const bodyNode = node.childForFieldName('body');
|
|
496
|
+
if (nameNode === null || bodyNode === null) {
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
const className = nameNode.text;
|
|
500
|
+
const fqn = ctx.namespace === '' ? className : `${ctx.namespace}\\${className}`;
|
|
501
|
+
const mappings = toMappingAttributes(attributesOf(node), ctx);
|
|
502
|
+
const rawClass = {
|
|
503
|
+
name: className,
|
|
504
|
+
fqn,
|
|
505
|
+
ctx,
|
|
506
|
+
isTrait: node.type === 'trait_declaration',
|
|
507
|
+
isAbstract: node.namedChildren.some((child) => child.type === 'abstract_modifier'),
|
|
508
|
+
role: 'plain',
|
|
509
|
+
traitFqns: [],
|
|
510
|
+
members: [],
|
|
511
|
+
indexes: [],
|
|
512
|
+
discriminatorMapSize: 0,
|
|
513
|
+
hasMappedProperties: false,
|
|
514
|
+
initializers: new Map(),
|
|
515
|
+
constructorAssignments: new Map(),
|
|
516
|
+
preUpdateProperties: new Set(),
|
|
517
|
+
prePersistProperties: new Set(),
|
|
518
|
+
};
|
|
519
|
+
const baseClause = node.namedChildren.find((child) => child.type === 'base_clause');
|
|
520
|
+
const baseName = baseClause?.namedChildren[0];
|
|
521
|
+
if (baseName !== undefined) {
|
|
522
|
+
rawClass.parentFqn = resolveClassName(baseName.text.replace(/\s+/g, ''), ctx);
|
|
523
|
+
}
|
|
524
|
+
parseClassAttributes(rawClass, mappings, state);
|
|
525
|
+
for (const member of bodyNode.namedChildren) {
|
|
526
|
+
if (member.type === 'use_declaration') {
|
|
527
|
+
for (const used of member.namedChildren) {
|
|
528
|
+
if (used.type === 'name' || used.type === 'qualified_name') {
|
|
529
|
+
rawClass.traitFqns.push(resolveClassName(used.text.replace(/\s+/g, ''), ctx));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else if (member.type === 'property_declaration') {
|
|
534
|
+
parsePropertyDeclaration(member, rawClass, state);
|
|
535
|
+
}
|
|
536
|
+
else if (member.type === 'method_declaration') {
|
|
537
|
+
parseConstructor(member, rawClass, state);
|
|
538
|
+
scanMethodBehaviour(member, rawClass, state);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
applyBehaviour(rawClass, state);
|
|
542
|
+
if (rawClass.role === 'plain' &&
|
|
543
|
+
!rawClass.isTrait &&
|
|
544
|
+
rawClass.hasMappedProperties) {
|
|
545
|
+
state.warnings.push(`${className}: the class has Doctrine mapping attributes on its properties but is not marked with #[ORM\\Entity], ` +
|
|
546
|
+
`#[ORM\\MappedSuperclass] or #[ORM\\Embeddable]; it was skipped.`);
|
|
547
|
+
}
|
|
548
|
+
return rawClass;
|
|
549
|
+
}
|
|
550
|
+
function parseClassAttributes(rawClass, mappings, state) {
|
|
551
|
+
const className = rawClass.name;
|
|
552
|
+
for (const mapping of mappings) {
|
|
553
|
+
const args = mapping.args;
|
|
554
|
+
switch (mapping.name) {
|
|
555
|
+
case 'Entity':
|
|
556
|
+
rawClass.role = 'entity';
|
|
557
|
+
if (boolValue(args['readOnly']) === true) {
|
|
558
|
+
state.warnings.push(`${className}: the readOnly option of #[ORM\\Entity] has no equivalent and was ignored.`);
|
|
559
|
+
}
|
|
560
|
+
break;
|
|
561
|
+
case 'MappedSuperclass':
|
|
562
|
+
rawClass.role = 'mappedSuperclass';
|
|
563
|
+
break;
|
|
564
|
+
case 'Embeddable':
|
|
565
|
+
rawClass.role = 'embeddable';
|
|
566
|
+
break;
|
|
567
|
+
case 'Table':
|
|
568
|
+
parseTableAttribute(rawClass, args, state);
|
|
569
|
+
break;
|
|
570
|
+
case 'Index':
|
|
571
|
+
case 'UniqueConstraint': {
|
|
572
|
+
const index = parseIndexAttribute(mapping.name, args, undefined, className, rawClass.ctx, state);
|
|
573
|
+
if (index !== undefined) {
|
|
574
|
+
rawClass.indexes.push(index);
|
|
575
|
+
}
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case 'InheritanceType': {
|
|
579
|
+
const kind = keywordValue(args['value'], 'INHERITANCE_TYPE_');
|
|
580
|
+
if (kind === 'SINGLE_TABLE' || kind === 'JOINED' || kind === 'NONE') {
|
|
581
|
+
rawClass.inheritanceType = kind;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
state.warnings.push(`${className}: the inheritance type "${kind ?? 'unknown'}" is not supported; ` +
|
|
585
|
+
`only SINGLE_TABLE and JOINED are converted.`);
|
|
586
|
+
}
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
case 'DiscriminatorColumn':
|
|
590
|
+
rawClass.discriminatorName = stringValue(args['name']);
|
|
591
|
+
rawClass.discriminatorType = stringValue(args['type']);
|
|
592
|
+
{
|
|
593
|
+
const length = numberValue(args['length']);
|
|
594
|
+
if (length !== undefined) {
|
|
595
|
+
rawClass.discriminatorLength = length;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (args['columnDefinition'] !== undefined) {
|
|
599
|
+
state.warnings.push(`${className}: the columnDefinition of #[ORM\\DiscriminatorColumn] has no equivalent and was ignored.`);
|
|
600
|
+
}
|
|
601
|
+
break;
|
|
602
|
+
case 'DiscriminatorMap':
|
|
603
|
+
rawClass.discriminatorMapSize = Object.keys(arrayEntries(args['value'])).length;
|
|
604
|
+
break;
|
|
605
|
+
default: {
|
|
606
|
+
const reason = UNSUPPORTED_ATTRIBUTES[mapping.name];
|
|
607
|
+
if (reason !== undefined) {
|
|
608
|
+
state.warnings.push(`${className}: #[ORM\\${mapping.name}] was skipped; ${reason}.`);
|
|
609
|
+
}
|
|
610
|
+
else if (!IGNORED_ATTRIBUTES.has(mapping.name)) {
|
|
611
|
+
state.warnings.push(`${className}: the attribute #[ORM\\${mapping.name}] is not supported and was ignored.`);
|
|
612
|
+
}
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function parseTableAttribute(rawClass, args, state) {
|
|
619
|
+
const className = rawClass.name;
|
|
620
|
+
const tableName = stringValue(args['name']);
|
|
621
|
+
if (tableName !== undefined) {
|
|
622
|
+
rawClass.tableName = stripQuotes(tableName);
|
|
623
|
+
}
|
|
624
|
+
const schemaName = stringValue(args['schema']);
|
|
625
|
+
if (schemaName !== undefined) {
|
|
626
|
+
state.warnings.push(`${className}: the schema "${schemaName}" given to #[ORM\\Table] was ignored; only the table name is converted.`);
|
|
627
|
+
}
|
|
628
|
+
if (args['options'] !== undefined) {
|
|
629
|
+
state.warnings.push(`${className}: the options of #[ORM\\Table] (engine, charset and similar) have no equivalent and were ignored.`);
|
|
630
|
+
}
|
|
631
|
+
for (const key of ['indexes', 'uniqueConstraints']) {
|
|
632
|
+
for (const item of arrayItems(args[key])) {
|
|
633
|
+
const nested = nestedAttribute(item, rawClass.ctx);
|
|
634
|
+
if (nested === undefined ||
|
|
635
|
+
(nested.name !== 'Index' && nested.name !== 'UniqueConstraint')) {
|
|
636
|
+
state.warnings.push(`${className}: an entry of the "${key}" option of #[ORM\\Table] could not be read statically and was skipped.`);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const index = parseIndexAttribute(nested.name, nested.args, undefined, className, rawClass.ctx, state);
|
|
640
|
+
if (index !== undefined) {
|
|
641
|
+
rawClass.indexes.push(index);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/** Reads `new ORM\Index(...)` into the attribute name and its merged arguments. */
|
|
647
|
+
function nestedAttribute(value, ctx) {
|
|
648
|
+
if (value.kind !== 'new') {
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
const created = value;
|
|
652
|
+
const name = mappingName({ rawName: created.className, args: created.args, named: created.named }, ctx);
|
|
653
|
+
return name === undefined
|
|
654
|
+
? undefined
|
|
655
|
+
: { name, args: mergeArguments(name, created.args, created.named) };
|
|
656
|
+
}
|
|
657
|
+
/** Parses `#[ORM\Index]` or `#[ORM\UniqueConstraint]`. A property attribute passes the property name. */
|
|
658
|
+
function parseIndexAttribute(attributeName, args, propertyName, location, _ctx, state) {
|
|
659
|
+
const columns = stringList(args['columns']);
|
|
660
|
+
const fields = stringList(args['fields']);
|
|
661
|
+
let refs = columns ?? fields;
|
|
662
|
+
if (refs === undefined && propertyName !== undefined) {
|
|
663
|
+
refs = [propertyName];
|
|
664
|
+
}
|
|
665
|
+
if (refs === undefined || refs.length === 0) {
|
|
666
|
+
state.warnings.push(`${location}: could not read the column list of #[ORM\\${attributeName}]; the ${attributeName === 'UniqueConstraint' ? 'unique constraint' : 'index'} was skipped.`);
|
|
667
|
+
return undefined;
|
|
668
|
+
}
|
|
669
|
+
const flags = (stringList(args['flags']) ?? []).map((flag) => flag.toLowerCase());
|
|
670
|
+
const options = arrayEntries(args['options']);
|
|
671
|
+
for (const option of Object.keys(options)) {
|
|
672
|
+
state.warnings.push(`${location}: the "${option}" option of #[ORM\\${attributeName}] has no equivalent and was ignored.`);
|
|
673
|
+
}
|
|
674
|
+
for (const flag of flags) {
|
|
675
|
+
if (flag !== 'fulltext' && flag !== 'clustered') {
|
|
676
|
+
state.warnings.push(`${location}: the "${flag}" flag of #[ORM\\${attributeName}] has no equivalent and was ignored.`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const name = stringValue(args['name']);
|
|
680
|
+
return {
|
|
681
|
+
refs: refs.map(stripQuotes),
|
|
682
|
+
isUnique: attributeName === 'UniqueConstraint',
|
|
683
|
+
flags,
|
|
684
|
+
...(name === undefined ? {} : { name }),
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
// ---------------------------------------------------------------------------
|
|
688
|
+
// Property parsing
|
|
689
|
+
// ---------------------------------------------------------------------------
|
|
690
|
+
function parsePropertyDeclaration(node, rawClass, state) {
|
|
691
|
+
if (node.namedChildren.some((child) => child.type === 'static_modifier')) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const attributes = attributesOf(node);
|
|
695
|
+
const type = typeOf(node);
|
|
696
|
+
for (const element of node.namedChildren) {
|
|
697
|
+
if (element.type !== 'property_element') {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const variable = element.namedChildren.find((child) => child.type === 'variable_name');
|
|
701
|
+
if (variable === undefined) {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const initializer = element.namedChildren.find((child) => child.type === 'property_initializer');
|
|
705
|
+
const initialValue = initializer?.namedChildren[0];
|
|
706
|
+
if (initialValue !== undefined) {
|
|
707
|
+
rawClass.initializers.set(variable.text.replace(/^\$/, ''), initialValue);
|
|
708
|
+
}
|
|
709
|
+
parseProperty({ name: variable.text.replace(/^\$/, ''), attributes, type }, rawClass, state);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/** Constructor promotion: `public function __construct(#[ORM\Column] private string $name)`. */
|
|
713
|
+
function parseConstructor(node, rawClass, state) {
|
|
714
|
+
const nameNode = node.childForFieldName('name');
|
|
715
|
+
if (nameNode === null || nameNode.text.toLowerCase() !== '__construct') {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const parameters = node.childForFieldName('parameters');
|
|
719
|
+
if (parameters === null) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
for (const parameter of parameters.namedChildren) {
|
|
723
|
+
if (parameter.type !== 'property_promotion_parameter') {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
const variable = parameter.childForFieldName('name');
|
|
727
|
+
if (variable === null) {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
parseProperty({
|
|
731
|
+
name: variable.text.replace(/^\$/, ''),
|
|
732
|
+
attributes: attributesOf(parameter),
|
|
733
|
+
type: typeOf(parameter),
|
|
734
|
+
}, rawClass, state);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function parseProperty(info, rawClass, state) {
|
|
738
|
+
const mappings = toMappingAttributes(info.attributes, rawClass.ctx);
|
|
739
|
+
if (mappings.length === 0) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
rawClass.hasMappedProperties = true;
|
|
743
|
+
const location = `${rawClass.name}.${info.name}`;
|
|
744
|
+
const has = (name) => mappings.some((mapping) => mapping.name === name);
|
|
745
|
+
const relation = mappings.find((mapping) => RELATION_ATTRIBUTES.has(mapping.name));
|
|
746
|
+
const embedded = mappings.find((mapping) => mapping.name === 'Embedded');
|
|
747
|
+
if (embedded !== undefined) {
|
|
748
|
+
parseEmbedded(embedded, info, location, rawClass, state);
|
|
749
|
+
}
|
|
750
|
+
else if (relation !== undefined) {
|
|
751
|
+
parseRelation(relation, mappings, info, location, rawClass, state);
|
|
752
|
+
}
|
|
753
|
+
else if (has('Column') || has('Id')) {
|
|
754
|
+
if (!has('Column')) {
|
|
755
|
+
state.warnings.push(`${location}: #[ORM\\Id] was used without #[ORM\\Column]; Doctrine needs both, so the column was mapped from the property type.`);
|
|
756
|
+
}
|
|
757
|
+
parseColumn(mappings, info, location, rawClass, state);
|
|
758
|
+
}
|
|
759
|
+
for (const mapping of mappings) {
|
|
760
|
+
if (mapping.name === 'Index' || mapping.name === 'UniqueConstraint') {
|
|
761
|
+
const index = parseIndexAttribute(mapping.name, mapping.args, info.name, location, rawClass.ctx, state);
|
|
762
|
+
if (index !== undefined) {
|
|
763
|
+
rawClass.indexes.push(index);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
else if (mapping.name !== 'Column' &&
|
|
767
|
+
mapping.name !== 'Id' &&
|
|
768
|
+
mapping.name !== 'Version' &&
|
|
769
|
+
mapping.name !== 'GeneratedValue' &&
|
|
770
|
+
mapping.name !== 'CustomIdGenerator' &&
|
|
771
|
+
mapping.name !== 'SequenceGenerator' &&
|
|
772
|
+
mapping.name !== 'JoinColumn' &&
|
|
773
|
+
mapping.name !== 'InverseJoinColumn' &&
|
|
774
|
+
mapping.name !== 'JoinTable' &&
|
|
775
|
+
mapping.name !== 'Embedded' &&
|
|
776
|
+
!RELATION_ATTRIBUTES.has(mapping.name) &&
|
|
777
|
+
!IGNORED_ATTRIBUTES.has(mapping.name)) {
|
|
778
|
+
state.warnings.push(`${location}: the attribute #[ORM\\${mapping.name}] is not supported and was ignored.`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/** The DBAL type name written in `type:`, as text; undefined when absent or not a plain name. */
|
|
783
|
+
function declaredTypeName(value) {
|
|
784
|
+
if (value === undefined) {
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
if (value.kind === 'string') {
|
|
788
|
+
return value.value.toLowerCase();
|
|
789
|
+
}
|
|
790
|
+
if (value.kind === 'constant' &&
|
|
791
|
+
['Types', 'Type'].includes(lastNameSegment(value.owner))) {
|
|
792
|
+
return TYPE_CONSTANTS[value.name];
|
|
793
|
+
}
|
|
794
|
+
return undefined;
|
|
795
|
+
}
|
|
796
|
+
function findEnum(fqn, state) {
|
|
797
|
+
const exact = state.enums.get(fqn);
|
|
798
|
+
if (exact !== undefined) {
|
|
799
|
+
return exact;
|
|
800
|
+
}
|
|
801
|
+
const short = lastNameSegment(fqn);
|
|
802
|
+
const candidates = [...state.enums.values()].filter((candidate) => candidate.name === short);
|
|
803
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
804
|
+
}
|
|
805
|
+
function resolveColumnType(args, type, location, rawClass, state) {
|
|
806
|
+
const ctx = rawClass.ctx;
|
|
807
|
+
const typeValue = args['type'];
|
|
808
|
+
const declared = declaredTypeName(typeValue);
|
|
809
|
+
if (typeValue !== undefined && declared === undefined) {
|
|
810
|
+
state.warnings.push(`${location}: the column type (${describeValue(typeValue)}) cannot be read statically; it was converted to a string.`);
|
|
811
|
+
return { type: 'string', dbalType: undefined };
|
|
812
|
+
}
|
|
813
|
+
// Enum-backed column: an explicit enumType, or a PHP type that is an enum.
|
|
814
|
+
const enumValue = args['enumType'];
|
|
815
|
+
let enumFqn = entityReference(enumValue, ctx, rawClass.fqn);
|
|
816
|
+
const phpName = type?.names[0];
|
|
817
|
+
if (enumFqn === undefined &&
|
|
818
|
+
phpName !== undefined &&
|
|
819
|
+
(declared === undefined || declared === 'enum')) {
|
|
820
|
+
const candidate = resolveClassName(phpName, ctx);
|
|
821
|
+
if (findEnum(candidate, state) !== undefined) {
|
|
822
|
+
enumFqn = candidate;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (enumFqn !== undefined) {
|
|
826
|
+
const info = findEnum(enumFqn, state);
|
|
827
|
+
if (info === undefined) {
|
|
828
|
+
state.warnings.push(`${location}: the enum "${lastNameSegment(enumFqn)}" was not found in the input files; the column was converted to a string. ` +
|
|
829
|
+
`Add the file that defines it to --input.`);
|
|
830
|
+
return { type: 'string', dbalType: declared };
|
|
831
|
+
}
|
|
832
|
+
if (info.backing === 'none') {
|
|
833
|
+
state.warnings.push(`${location}: the enum "${info.name}" is not a backed enum, which Doctrine cannot store; the column was converted to a string.`);
|
|
834
|
+
return { type: 'string', dbalType: declared };
|
|
835
|
+
}
|
|
836
|
+
if (info.backing === 'int') {
|
|
837
|
+
state.warnings.push(`${location}: the enum "${info.name}" has integer values, which the shared model cannot represent; the column was converted to an int.`);
|
|
838
|
+
return { type: 'int', dbalType: declared ?? 'integer' };
|
|
839
|
+
}
|
|
840
|
+
return { type: 'string', dbalType: declared ?? 'string', enumInfo: info };
|
|
841
|
+
}
|
|
842
|
+
if (declared !== undefined) {
|
|
843
|
+
const mapped = DBAL_TYPES[declared];
|
|
844
|
+
if (mapped === undefined) {
|
|
845
|
+
state.warnings.push(`${location}: column type "${declared}" has no equivalent in the shared model (custom DBAL types are not resolved); it was converted to a string.`);
|
|
846
|
+
return { type: 'string', dbalType: declared };
|
|
847
|
+
}
|
|
848
|
+
const note = APPROXIMATED_TYPES[declared];
|
|
849
|
+
if (note !== undefined) {
|
|
850
|
+
state.warnings.push(`${location}: column type "${declared}" ${note}.`);
|
|
851
|
+
}
|
|
852
|
+
if (declared === 'enum') {
|
|
853
|
+
state.warnings.push(`${location}: an enum column was declared without a readable enumType; it was converted to a string.`);
|
|
854
|
+
}
|
|
855
|
+
return { type: mapped, dbalType: declared };
|
|
856
|
+
}
|
|
857
|
+
// No explicit type: Doctrine infers it from the PHP property type.
|
|
858
|
+
if (phpName === undefined || (type?.names.length ?? 0) > 1) {
|
|
859
|
+
state.warnings.push(`${location}: no column type could be inferred (add a property type or an explicit type); it was converted to a string.`);
|
|
860
|
+
return { type: 'string', dbalType: undefined };
|
|
861
|
+
}
|
|
862
|
+
const inferred = PHP_TYPE_TO_DBAL[lastNameSegment(phpName).toLowerCase()];
|
|
863
|
+
if (inferred !== undefined) {
|
|
864
|
+
return { type: DBAL_TYPES[inferred] ?? 'string', dbalType: inferred };
|
|
865
|
+
}
|
|
866
|
+
state.warnings.push(`${location}: the PHP type "${type?.text ?? phpName}" cannot be mapped to a column type; add an explicit type. It was converted to a string.`);
|
|
867
|
+
return { type: 'string', dbalType: undefined };
|
|
868
|
+
}
|
|
869
|
+
function parseColumn(mappings, info, location, rawClass, state) {
|
|
870
|
+
const column = mappings.find((mapping) => mapping.name === 'Column');
|
|
871
|
+
const args = column?.args ?? {};
|
|
872
|
+
const has = (name) => mappings.some((mapping) => mapping.name === name);
|
|
873
|
+
const find = (name) => mappings.find((mapping) => mapping.name === name);
|
|
874
|
+
const isPrimaryKey = has('Id');
|
|
875
|
+
const resolved = resolveColumnType(args, info.type, location, rawClass, state);
|
|
876
|
+
let irType = resolved.type;
|
|
877
|
+
const columnNameOption = stringValue(args['name']);
|
|
878
|
+
const columnName = columnNameOption === undefined
|
|
879
|
+
? underscore(info.name)
|
|
880
|
+
: stripQuotes(columnNameOption);
|
|
881
|
+
const isNullable = !isPrimaryKey && boolValue(args['nullable']) === true;
|
|
882
|
+
const field = {
|
|
883
|
+
name: info.name,
|
|
884
|
+
columnName,
|
|
885
|
+
type: irType,
|
|
886
|
+
isPrimaryKey,
|
|
887
|
+
isUnique: boolValue(args['unique']) === true && !isPrimaryKey,
|
|
888
|
+
isNullable,
|
|
889
|
+
isAutoUpdated: false,
|
|
890
|
+
...(resolved.enumInfo === undefined
|
|
891
|
+
? {}
|
|
892
|
+
: { enumName: resolved.enumInfo.name }),
|
|
893
|
+
};
|
|
894
|
+
// Length and precision ---------------------------------------------------
|
|
895
|
+
const length = numberValue(args['length']);
|
|
896
|
+
if (irType === 'string' && resolved.enumInfo === undefined) {
|
|
897
|
+
field.maxLength = length ?? 255;
|
|
898
|
+
}
|
|
899
|
+
else if (irType === 'string' && length !== undefined) {
|
|
900
|
+
field.maxLength = length;
|
|
901
|
+
}
|
|
902
|
+
if (irType === 'decimal') {
|
|
903
|
+
field.maxDigits = numberValue(args['precision']) ?? 10;
|
|
904
|
+
field.decimalPlaces = numberValue(args['scale']) ?? 0;
|
|
905
|
+
}
|
|
906
|
+
// Id generation ------------------------------------------------------------
|
|
907
|
+
const generated = find('GeneratedValue');
|
|
908
|
+
const customGenerator = find('CustomIdGenerator');
|
|
909
|
+
let defaultValue;
|
|
910
|
+
if (generated !== undefined || customGenerator !== undefined) {
|
|
911
|
+
if (!isPrimaryKey) {
|
|
912
|
+
state.warnings.push(`${location}: #[ORM\\GeneratedValue] was used on a property without #[ORM\\Id]; it was ignored.`);
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
const generatedDefault = convertGenerator(generated, customGenerator, irType, location, rawClass, state);
|
|
916
|
+
defaultValue = generatedDefault.value;
|
|
917
|
+
if (generatedDefault.type !== undefined) {
|
|
918
|
+
irType = generatedDefault.type;
|
|
919
|
+
field.type = irType;
|
|
920
|
+
delete field.maxLength;
|
|
921
|
+
if (irType === 'string') {
|
|
922
|
+
field.maxLength = 26;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (find('SequenceGenerator') !== undefined) {
|
|
928
|
+
state.warnings.push(`${location}: #[ORM\\SequenceGenerator] settings (sequence name, allocation size) have no equivalent and were ignored.`);
|
|
929
|
+
}
|
|
930
|
+
// Defaults -----------------------------------------------------------------
|
|
931
|
+
const columnOptions = arrayEntries(args['options']);
|
|
932
|
+
const optionDefault = columnOptions['default'];
|
|
933
|
+
if (optionDefault !== undefined && defaultValue === undefined) {
|
|
934
|
+
defaultValue = convertDefault(optionDefault, field, resolved.enumInfo, location, state);
|
|
935
|
+
}
|
|
936
|
+
// Version columns ----------------------------------------------------------
|
|
937
|
+
if (has('Version')) {
|
|
938
|
+
state.warnings.push(`${location}: #[ORM\\Version] was converted to a plain column${irType === 'int' ? ' defaulting to 1' : ''}; ` +
|
|
939
|
+
`Doctrine's optimistic locking is not represented.`);
|
|
940
|
+
if (irType === 'int' && defaultValue === undefined) {
|
|
941
|
+
defaultValue = { kind: 'literal', value: 1 };
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (defaultValue !== undefined) {
|
|
945
|
+
field.default = defaultValue;
|
|
946
|
+
}
|
|
947
|
+
// Options with no equivalent ----------------------------------------------
|
|
948
|
+
for (const key of Object.keys(columnOptions)) {
|
|
949
|
+
if (key === 'default' || key === 'jsonb') {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const description = UNSUPPORTED_COLUMN_OPTIONS[key];
|
|
953
|
+
state.warnings.push(description === undefined
|
|
954
|
+
? `${location}: the column option "${key}" has no equivalent and was ignored.`
|
|
955
|
+
: `${location}: ${description} (option "${key}") has no equivalent and was ignored.`);
|
|
956
|
+
}
|
|
957
|
+
if (args['columnDefinition'] !== undefined) {
|
|
958
|
+
state.warnings.push(`${location}: columnDefinition has no equivalent (it is raw DDL) and was ignored; the column type was derived from the declared type.`);
|
|
959
|
+
}
|
|
960
|
+
for (const key of ['insertable', 'updatable']) {
|
|
961
|
+
if (boolValue(args[key]) === false) {
|
|
962
|
+
state.warnings.push(`${location}: ${key}: false has no equivalent and was ignored.`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (args['generated'] !== undefined) {
|
|
966
|
+
state.warnings.push(`${location}: the generated option (a database-generated column) has no equivalent and was ignored.`);
|
|
967
|
+
}
|
|
968
|
+
if (boolValue(args['index']) === true) {
|
|
969
|
+
rawClass.indexes.push({ refs: [info.name], isUnique: false, flags: [] });
|
|
970
|
+
}
|
|
971
|
+
rawClass.members.push({ kind: 'field', field });
|
|
972
|
+
}
|
|
973
|
+
/** Maps `#[ORM\GeneratedValue]` and `#[ORM\CustomIdGenerator]` to an IR default. */
|
|
974
|
+
function convertGenerator(generated, custom, type, location, rawClass, state) {
|
|
975
|
+
const strategy = keywordValue(generated?.args['strategy'], 'GENERATOR_TYPE_') ??
|
|
976
|
+
(generated === undefined ? 'CUSTOM' : 'AUTO');
|
|
977
|
+
if (strategy === 'CUSTOM' || custom !== undefined) {
|
|
978
|
+
const generatorClass = entityReference(custom?.args['class'], rawClass.ctx, rawClass.fqn);
|
|
979
|
+
if (generatorClass !== undefined && UUID_GENERATORS.has(generatorClass)) {
|
|
980
|
+
return { value: { kind: 'uuid' }, type: 'uuid' };
|
|
981
|
+
}
|
|
982
|
+
if (generatorClass !== undefined && ULID_GENERATORS.has(generatorClass)) {
|
|
983
|
+
state.warnings.push(`${location}: the ULID id generator was converted to a client-generated string key (26 characters).`);
|
|
984
|
+
return {
|
|
985
|
+
value: { kind: 'clientGenerated', generator: 'ulid' },
|
|
986
|
+
type: 'string',
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
state.warnings.push(`${location}: the custom id generator ${generatorClass === undefined ? '(unreadable)' : `"${lastNameSegment(generatorClass)}"`} ` +
|
|
990
|
+
`has no equivalent; the key has no generated default.`);
|
|
991
|
+
return {};
|
|
992
|
+
}
|
|
993
|
+
switch (strategy) {
|
|
994
|
+
case 'NONE':
|
|
995
|
+
return {};
|
|
996
|
+
case 'UUID':
|
|
997
|
+
return { value: { kind: 'uuid' }, type: 'uuid' };
|
|
998
|
+
case 'SEQUENCE':
|
|
999
|
+
state.warnings.push(`${location}: the SEQUENCE generation strategy was converted to an auto-increment key.`);
|
|
1000
|
+
return type === 'int' || type === 'bigInt'
|
|
1001
|
+
? { value: { kind: 'autoIncrement' } }
|
|
1002
|
+
: {};
|
|
1003
|
+
case 'AUTO':
|
|
1004
|
+
case 'IDENTITY':
|
|
1005
|
+
if (type === 'uuid') {
|
|
1006
|
+
return { value: { kind: 'uuid' } };
|
|
1007
|
+
}
|
|
1008
|
+
if (type === 'int' || type === 'bigInt') {
|
|
1009
|
+
return { value: { kind: 'autoIncrement' } };
|
|
1010
|
+
}
|
|
1011
|
+
state.warnings.push(`${location}: the ${strategy} generation strategy on a ${type} key has no equivalent; the key has no generated default.`);
|
|
1012
|
+
return {};
|
|
1013
|
+
default:
|
|
1014
|
+
state.warnings.push(`${location}: the generation strategy "${strategy}" is not supported; the key has no generated default.`);
|
|
1015
|
+
return {};
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function convertDefault(value, field, enumInfo, location, state) {
|
|
1019
|
+
switch (value.kind) {
|
|
1020
|
+
case 'null':
|
|
1021
|
+
return undefined;
|
|
1022
|
+
case 'bool':
|
|
1023
|
+
return { kind: 'literal', value: value.value };
|
|
1024
|
+
case 'number':
|
|
1025
|
+
return field.type === 'string' || field.type === 'text'
|
|
1026
|
+
? { kind: 'literal', value: String(value.value) }
|
|
1027
|
+
: { kind: 'literal', value: value.value };
|
|
1028
|
+
case 'string':
|
|
1029
|
+
return convertStringDefault(value.value, field, enumInfo, location, state);
|
|
1030
|
+
case 'constant': {
|
|
1031
|
+
if (enumInfo !== undefined &&
|
|
1032
|
+
lastNameSegment(value.owner) === enumInfo.name) {
|
|
1033
|
+
const member = enumInfo.values.find((candidate) => candidate.name === value.name);
|
|
1034
|
+
if (member !== undefined) {
|
|
1035
|
+
return { kind: 'enumValue', value: member.name };
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
default:
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
state.warnings.push(`${location}: the default value (${describeValue(value)}) cannot be evaluated statically and was skipped.`);
|
|
1044
|
+
return undefined;
|
|
1045
|
+
}
|
|
1046
|
+
function convertStringDefault(text, field, enumInfo, location, state) {
|
|
1047
|
+
const trimmed = text.trim();
|
|
1048
|
+
if (enumInfo !== undefined) {
|
|
1049
|
+
const member = enumInfo.values.find((candidate) => candidate.dbValue === text);
|
|
1050
|
+
if (member !== undefined) {
|
|
1051
|
+
return { kind: 'enumValue', value: member.name };
|
|
1052
|
+
}
|
|
1053
|
+
state.warnings.push(`${location}: the default "${text}" is not a value of the enum "${enumInfo.name}"; it was kept as a literal.`);
|
|
1054
|
+
return { kind: 'literal', value: text };
|
|
1055
|
+
}
|
|
1056
|
+
switch (field.type) {
|
|
1057
|
+
case 'dateTime':
|
|
1058
|
+
case 'date':
|
|
1059
|
+
case 'time':
|
|
1060
|
+
if (NOW_SQL.test(trimmed) || /^current_(date|time)$/i.test(trimmed)) {
|
|
1061
|
+
return { kind: 'now' };
|
|
1062
|
+
}
|
|
1063
|
+
break;
|
|
1064
|
+
case 'uuid':
|
|
1065
|
+
case 'string':
|
|
1066
|
+
if (UUID_SQL.test(trimmed)) {
|
|
1067
|
+
return { kind: 'uuid' };
|
|
1068
|
+
}
|
|
1069
|
+
break;
|
|
1070
|
+
case 'boolean':
|
|
1071
|
+
if (/^(1|true)$/i.test(trimmed)) {
|
|
1072
|
+
return { kind: 'literal', value: true };
|
|
1073
|
+
}
|
|
1074
|
+
if (/^(0|false)$/i.test(trimmed)) {
|
|
1075
|
+
return { kind: 'literal', value: false };
|
|
1076
|
+
}
|
|
1077
|
+
break;
|
|
1078
|
+
case 'int':
|
|
1079
|
+
case 'bigInt':
|
|
1080
|
+
case 'float':
|
|
1081
|
+
case 'decimal':
|
|
1082
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
1083
|
+
return { kind: 'literal', value: Number(trimmed) };
|
|
1084
|
+
}
|
|
1085
|
+
break;
|
|
1086
|
+
default:
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
return { kind: 'literal', value: text };
|
|
1090
|
+
}
|
|
1091
|
+
/** Reads a `$this->name = value;` statement. */
|
|
1092
|
+
function thisAssignment(statement) {
|
|
1093
|
+
if (statement.type !== 'expression_statement') {
|
|
1094
|
+
return undefined;
|
|
1095
|
+
}
|
|
1096
|
+
const assignment = statement.namedChildren[0];
|
|
1097
|
+
if (assignment === undefined || assignment.type !== 'assignment_expression') {
|
|
1098
|
+
return undefined;
|
|
1099
|
+
}
|
|
1100
|
+
const target = assignment.childForFieldName('left');
|
|
1101
|
+
const value = assignment.childForFieldName('right');
|
|
1102
|
+
if (target === null ||
|
|
1103
|
+
value === null ||
|
|
1104
|
+
target.type !== 'member_access_expression') {
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
const owner = target.childForFieldName('object');
|
|
1108
|
+
const name = target.childForFieldName('name');
|
|
1109
|
+
if (owner === null || name === null || owner.text !== '$this') {
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
1112
|
+
return { property: name.text, value };
|
|
1113
|
+
}
|
|
1114
|
+
/** True for `new DateTime()`, `new \DateTimeImmutable()` and `new DateTimeImmutable('now')`. */
|
|
1115
|
+
function isNowExpression(value) {
|
|
1116
|
+
if (value.type !== 'object_creation_expression') {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
const evaluated = evaluateNode(value);
|
|
1120
|
+
if (evaluated.kind !== 'new') {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
const className = lastNameSegment(evaluated.className);
|
|
1124
|
+
if (className !== 'DateTime' && className !== 'DateTimeImmutable') {
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
if (evaluated.args.length === 0) {
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
const first = evaluated.args[0];
|
|
1131
|
+
return (evaluated.args.length === 1 &&
|
|
1132
|
+
first !== undefined &&
|
|
1133
|
+
first.kind === 'string' &&
|
|
1134
|
+
first.value.trim().toLowerCase() === 'now');
|
|
1135
|
+
}
|
|
1136
|
+
/** `self::generateUuid()` (as written by the emitter) and the common `Uuid::v4()` spellings. */
|
|
1137
|
+
const UUID_CALL = /^(?:(?:self|static|\$this)(?:::|->)generateUuid|\\?(?:[A-Za-z_]+\\)*Uuid::(?:uuid[47]|v[47]))\(\)(?:->(?:toRfc4122|toString)\(\))?$/i;
|
|
1138
|
+
/** Reads the lifecycle callbacks and `__construct` assignments of a method. */
|
|
1139
|
+
function scanMethodBehaviour(node, rawClass, state) {
|
|
1140
|
+
const nameNode = node.childForFieldName('name');
|
|
1141
|
+
const body = node.childForFieldName('body');
|
|
1142
|
+
if (nameNode === null || body === null) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const methodName = nameNode.text;
|
|
1146
|
+
if (methodName.toLowerCase() === '__construct') {
|
|
1147
|
+
for (const statement of body.namedChildren) {
|
|
1148
|
+
const assignment = thisAssignment(statement);
|
|
1149
|
+
if (assignment !== undefined) {
|
|
1150
|
+
rawClass.constructorAssignments.set(assignment.property, assignment.value);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const callbacks = toMappingAttributes(attributesOf(node), rawClass.ctx)
|
|
1156
|
+
.map((mapping) => mapping.name)
|
|
1157
|
+
.filter((name) => name === 'PreUpdate' || name === 'PrePersist');
|
|
1158
|
+
if (callbacks.length === 0) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
for (const statement of body.namedChildren) {
|
|
1162
|
+
if (statement.type === 'comment') {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
const assignment = thisAssignment(statement);
|
|
1166
|
+
if (assignment === undefined || !isNowExpression(assignment.value)) {
|
|
1167
|
+
state.warnings.push(`${rawClass.name}::${methodName}: the lifecycle callback statement \`${statement.text.replace(/\s+/g, ' ')}\` ` +
|
|
1168
|
+
`is not a plain timestamp assignment (\`$this->property = new \\DateTimeImmutable();\`); it was ignored.`);
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
for (const callback of callbacks) {
|
|
1172
|
+
(callback === 'PreUpdate'
|
|
1173
|
+
? rawClass.preUpdateProperties
|
|
1174
|
+
: rawClass.prePersistProperties).add(assignment.property);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
/** Converts a statically known PHP value to a JSON value; undefined when part of it is dynamic. */
|
|
1179
|
+
function phpValueToJson(value) {
|
|
1180
|
+
switch (value.kind) {
|
|
1181
|
+
case 'string':
|
|
1182
|
+
case 'number':
|
|
1183
|
+
case 'bool':
|
|
1184
|
+
return value.value;
|
|
1185
|
+
case 'null':
|
|
1186
|
+
return null;
|
|
1187
|
+
case 'array': {
|
|
1188
|
+
const isList = value.items.every((item) => item.key === undefined);
|
|
1189
|
+
if (isList) {
|
|
1190
|
+
const list = value.items.map((item) => phpValueToJson(item.value));
|
|
1191
|
+
return list.includes(undefined) ? undefined : list;
|
|
1192
|
+
}
|
|
1193
|
+
const record = {};
|
|
1194
|
+
for (const item of value.items) {
|
|
1195
|
+
const key = item.key;
|
|
1196
|
+
const converted = phpValueToJson(item.value);
|
|
1197
|
+
if (key === undefined ||
|
|
1198
|
+
(key.kind !== 'string' && key.kind !== 'number') ||
|
|
1199
|
+
converted === undefined) {
|
|
1200
|
+
return undefined;
|
|
1201
|
+
}
|
|
1202
|
+
record[String(key.value)] = converted;
|
|
1203
|
+
}
|
|
1204
|
+
return record;
|
|
1205
|
+
}
|
|
1206
|
+
default:
|
|
1207
|
+
return undefined;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
/** Converts the value a property starts with (initialiser or constructor) to an IR default. */
|
|
1211
|
+
function convertInitialValue(node, field, enumInfo, location, state) {
|
|
1212
|
+
const compact = node.text.replace(/\s+/g, '');
|
|
1213
|
+
if (UUID_CALL.test(compact)) {
|
|
1214
|
+
return field.type === 'uuid' || field.type === 'string'
|
|
1215
|
+
? { kind: 'uuid' }
|
|
1216
|
+
: undefined;
|
|
1217
|
+
}
|
|
1218
|
+
if (isNowExpression(node)) {
|
|
1219
|
+
return field.type === 'dateTime' ||
|
|
1220
|
+
field.type === 'date' ||
|
|
1221
|
+
field.type === 'time'
|
|
1222
|
+
? { kind: 'now' }
|
|
1223
|
+
: undefined;
|
|
1224
|
+
}
|
|
1225
|
+
const value = evaluateNode(node);
|
|
1226
|
+
if (value.kind === 'null') {
|
|
1227
|
+
return undefined;
|
|
1228
|
+
}
|
|
1229
|
+
if (value.kind === 'new') {
|
|
1230
|
+
const className = lastNameSegment(value.className);
|
|
1231
|
+
const first = value.args[0];
|
|
1232
|
+
if ((className === 'DateTime' || className === 'DateTimeImmutable') &&
|
|
1233
|
+
first !== undefined &&
|
|
1234
|
+
first.kind === 'string') {
|
|
1235
|
+
return { kind: 'literal', value: first.value };
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (value.kind === 'array') {
|
|
1239
|
+
const json = phpValueToJson(value);
|
|
1240
|
+
if ((field.type === 'json' || field.arrayDepth !== undefined) &&
|
|
1241
|
+
json !== undefined) {
|
|
1242
|
+
return { kind: 'literal', value: JSON.stringify(json) };
|
|
1243
|
+
}
|
|
1244
|
+
state.warnings.push(`${location}: the initial value ${describeValue(value)} cannot be evaluated statically and was skipped.`);
|
|
1245
|
+
return undefined;
|
|
1246
|
+
}
|
|
1247
|
+
return convertDefault(value, field, enumInfo, location, state);
|
|
1248
|
+
}
|
|
1249
|
+
/** Applies lifecycle callbacks, property initialisers and constructor assignments to the fields of a class. */
|
|
1250
|
+
function applyBehaviour(rawClass, state) {
|
|
1251
|
+
for (const member of rawClass.members) {
|
|
1252
|
+
if (member.kind !== 'field') {
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
const field = member.field;
|
|
1256
|
+
const location = `${rawClass.name}.${field.name}`;
|
|
1257
|
+
if (rawClass.preUpdateProperties.has(field.name)) {
|
|
1258
|
+
field.isAutoUpdated = true;
|
|
1259
|
+
}
|
|
1260
|
+
else if (rawClass.prePersistProperties.has(field.name) &&
|
|
1261
|
+
field.default === undefined) {
|
|
1262
|
+
// Set once on insert, which is what a "now" default means.
|
|
1263
|
+
field.default = { kind: 'now' };
|
|
1264
|
+
}
|
|
1265
|
+
if (field.default !== undefined) {
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
const initial = rawClass.constructorAssignments.get(field.name) ??
|
|
1269
|
+
rawClass.initializers.get(field.name);
|
|
1270
|
+
if (initial === undefined) {
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
const enumInfo = field.enumName === undefined
|
|
1274
|
+
? undefined
|
|
1275
|
+
: [...state.enums.values()].find((candidate) => candidate.name === field.enumName);
|
|
1276
|
+
const converted = convertInitialValue(initial, field, enumInfo, location, state);
|
|
1277
|
+
if (converted !== undefined) {
|
|
1278
|
+
field.default = converted;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
// ---------------------------------------------------------------------------
|
|
1283
|
+
// Embedded
|
|
1284
|
+
// ---------------------------------------------------------------------------
|
|
1285
|
+
function parseEmbedded(embedded, info, location, rawClass, state) {
|
|
1286
|
+
const args = embedded.args;
|
|
1287
|
+
let classFqn = entityReference(args['class'], rawClass.ctx, rawClass.fqn);
|
|
1288
|
+
if (classFqn === undefined && info.type?.names[0] !== undefined) {
|
|
1289
|
+
classFqn = resolveClassName(info.type.names[0], rawClass.ctx);
|
|
1290
|
+
}
|
|
1291
|
+
if (classFqn === undefined) {
|
|
1292
|
+
state.warnings.push(`${location}: the embeddable class of #[ORM\\Embedded] could not be resolved, so the columns were skipped.`);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const prefixValue = args['columnPrefix'];
|
|
1296
|
+
let prefix;
|
|
1297
|
+
if (prefixValue !== undefined && prefixValue.kind === 'string') {
|
|
1298
|
+
prefix = prefixValue.value;
|
|
1299
|
+
}
|
|
1300
|
+
else if (prefixValue !== undefined && prefixValue.kind === 'bool') {
|
|
1301
|
+
prefix = prefixValue.value ? undefined : false;
|
|
1302
|
+
}
|
|
1303
|
+
rawClass.members.push({
|
|
1304
|
+
kind: 'embedded',
|
|
1305
|
+
property: info.name,
|
|
1306
|
+
classFqn,
|
|
1307
|
+
prefix,
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
// ---------------------------------------------------------------------------
|
|
1311
|
+
// Relations
|
|
1312
|
+
// ---------------------------------------------------------------------------
|
|
1313
|
+
function parseRelation(relationAttribute, mappings, info, location, rawClass, state) {
|
|
1314
|
+
const args = relationAttribute.args;
|
|
1315
|
+
const attributeName = relationAttribute.name;
|
|
1316
|
+
let targetFqn = entityReference(args['targetEntity'], rawClass.ctx, rawClass.fqn);
|
|
1317
|
+
if (targetFqn === undefined &&
|
|
1318
|
+
(attributeName === 'ManyToOne' || attributeName === 'OneToOne')) {
|
|
1319
|
+
// Doctrine infers the target of a to-one relation from the property type.
|
|
1320
|
+
const typeName = info.type?.names[0];
|
|
1321
|
+
if (typeName !== undefined) {
|
|
1322
|
+
const lowered = typeName.toLowerCase();
|
|
1323
|
+
targetFqn =
|
|
1324
|
+
lowered === 'self' || lowered === 'static'
|
|
1325
|
+
? rawClass.fqn
|
|
1326
|
+
: resolveClassName(typeName, rawClass.ctx);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (targetFqn === undefined) {
|
|
1330
|
+
state.warnings.push(`${location}: the target of #[ORM\\${attributeName}] could not be resolved (add targetEntity: Foo::class); the relation was skipped.`);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const mappedBy = stringValue(args['mappedBy']);
|
|
1334
|
+
const inversedBy = stringValue(args['inversedBy']);
|
|
1335
|
+
const cascade = (stringList(args['cascade']) ?? []).map((item) => item.toLowerCase());
|
|
1336
|
+
if (cascade.includes('remove') ||
|
|
1337
|
+
cascade.includes('all') ||
|
|
1338
|
+
boolValue(args['orphanRemoval']) === true) {
|
|
1339
|
+
state.warnings.push(`${location}: ORM-level cascade remove / orphanRemoval is not represented; ` +
|
|
1340
|
+
`use onDelete: 'CASCADE' on the join column for a database-level cascade.`);
|
|
1341
|
+
}
|
|
1342
|
+
const isOwner = attributeName === 'ManyToOne' ||
|
|
1343
|
+
((attributeName === 'OneToOne' || attributeName === 'ManyToMany') &&
|
|
1344
|
+
mappedBy === undefined);
|
|
1345
|
+
if (!isOwner) {
|
|
1346
|
+
rawClass.members.push({
|
|
1347
|
+
kind: 'inverse',
|
|
1348
|
+
property: info.name,
|
|
1349
|
+
targetFqn,
|
|
1350
|
+
...(mappedBy === undefined ? {} : { mappedBy }),
|
|
1351
|
+
relationKind: attributeName === 'OneToMany'
|
|
1352
|
+
? 'oneToMany'
|
|
1353
|
+
: attributeName === 'ManyToMany'
|
|
1354
|
+
? 'manyToMany'
|
|
1355
|
+
: 'oneToOne',
|
|
1356
|
+
});
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
const kind = attributeName === 'ManyToMany'
|
|
1360
|
+
? 'manyToMany'
|
|
1361
|
+
: attributeName === 'OneToOne'
|
|
1362
|
+
? 'oneToOne'
|
|
1363
|
+
: 'foreignKey';
|
|
1364
|
+
// Join columns ------------------------------------------------------------
|
|
1365
|
+
const joinColumns = mappings
|
|
1366
|
+
.filter((mapping) => mapping.name === 'JoinColumn')
|
|
1367
|
+
.map((mapping) => mapping.args);
|
|
1368
|
+
if (kind !== 'manyToMany' && joinColumns.length > 1) {
|
|
1369
|
+
state.warnings.push(`${location}: composite foreign keys (several join columns) are not supported; the relation was skipped.`);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const joinColumn = joinColumns[0];
|
|
1373
|
+
if (kind === 'manyToMany') {
|
|
1374
|
+
// Doctrine 2 passes joinColumns / inverseJoinColumns to JoinTable; Doctrine 3 uses #[ORM\JoinColumn] and #[ORM\InverseJoinColumn].
|
|
1375
|
+
const joinTable = mappings.find((mapping) => mapping.name === 'JoinTable');
|
|
1376
|
+
const customized = [
|
|
1377
|
+
...['name', 'schema', 'joinColumns', 'inverseJoinColumns'].filter((key) => joinTable?.args[key] !== undefined),
|
|
1378
|
+
...(joinColumns.length > 0 ? ['JoinColumn'] : []),
|
|
1379
|
+
...(mappings.some((mapping) => mapping.name === 'InverseJoinColumn')
|
|
1380
|
+
? ['InverseJoinColumn']
|
|
1381
|
+
: []),
|
|
1382
|
+
];
|
|
1383
|
+
if (customized.length > 0) {
|
|
1384
|
+
state.warnings.push(`${location}: custom join table settings (${customized.join(', ')}) are not preserved; ` +
|
|
1385
|
+
`the join table name and columns are derived from the models.`);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
let explicitColumn;
|
|
1389
|
+
let referencedColumn;
|
|
1390
|
+
let nullableOption;
|
|
1391
|
+
let onDelete = 'noAction';
|
|
1392
|
+
if (joinColumn !== undefined && kind !== 'manyToMany') {
|
|
1393
|
+
explicitColumn = stringValue(joinColumn['name']);
|
|
1394
|
+
referencedColumn = stringValue(joinColumn['referencedColumnName']);
|
|
1395
|
+
nullableOption = boolValue(joinColumn['nullable']);
|
|
1396
|
+
if (joinColumn['columnDefinition'] !== undefined) {
|
|
1397
|
+
state.warnings.push(`${location}: the columnDefinition of the join column has no equivalent and was ignored.`);
|
|
1398
|
+
}
|
|
1399
|
+
const onDeleteValue = stringValue(joinColumn['onDelete']);
|
|
1400
|
+
if (onDeleteValue !== undefined) {
|
|
1401
|
+
const mapped = ON_DELETE_MAP[onDeleteValue.toUpperCase()];
|
|
1402
|
+
if (mapped === undefined) {
|
|
1403
|
+
state.warnings.push(`${location}: onDelete "${onDeleteValue}" has no equivalent and was converted to NoAction.`);
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
onDelete = mapped;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (kind === 'manyToMany') {
|
|
1411
|
+
onDelete = 'cascade';
|
|
1412
|
+
}
|
|
1413
|
+
const isPrimaryKey = mappings.some((mapping) => mapping.name === 'Id');
|
|
1414
|
+
const relation = {
|
|
1415
|
+
name: info.name,
|
|
1416
|
+
kind,
|
|
1417
|
+
targetModel: targetFqn,
|
|
1418
|
+
columnName: explicitColumn === undefined
|
|
1419
|
+
? `${underscore(info.name)}_id`
|
|
1420
|
+
: stripQuotes(explicitColumn),
|
|
1421
|
+
// Doctrine join columns are nullable unless nullable: false is given.
|
|
1422
|
+
isNullable: !isPrimaryKey && nullableOption !== false,
|
|
1423
|
+
onDelete,
|
|
1424
|
+
...(inversedBy === undefined ? {} : { relatedName: inversedBy }),
|
|
1425
|
+
...(referencedColumn === undefined || referencedColumn === 'id'
|
|
1426
|
+
? {}
|
|
1427
|
+
: { toField: stripQuotes(referencedColumn) }),
|
|
1428
|
+
...(isPrimaryKey ? { isPrimaryKey: true } : {}),
|
|
1429
|
+
};
|
|
1430
|
+
rawClass.members.push({
|
|
1431
|
+
kind: 'relation',
|
|
1432
|
+
relation,
|
|
1433
|
+
hasExplicitNullable: nullableOption !== undefined,
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
// ---------------------------------------------------------------------------
|
|
1437
|
+
// Schema assembly (inheritance, embeddables, inverse sides)
|
|
1438
|
+
// ---------------------------------------------------------------------------
|
|
1439
|
+
function buildSchema(classes, state, options) {
|
|
1440
|
+
const warnings = state.warnings;
|
|
1441
|
+
const entities = [...classes.values()].filter((rawClass) => rawClass.role === 'entity');
|
|
1442
|
+
// Inheritance: how each entity relates to the entity it extends. -----------
|
|
1443
|
+
const rootOf = (rawClass) => {
|
|
1444
|
+
let current = rawClass;
|
|
1445
|
+
const seen = new Set([current.fqn]);
|
|
1446
|
+
for (;;) {
|
|
1447
|
+
const parent = nearestEntityParent(current, classes);
|
|
1448
|
+
if (parent === undefined || seen.has(parent.fqn)) {
|
|
1449
|
+
return current;
|
|
1450
|
+
}
|
|
1451
|
+
seen.add(parent.fqn);
|
|
1452
|
+
current = parent;
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
const rootsWithChildren = new Set(entities.flatMap((entity) => nearestEntityParent(entity, classes) === undefined
|
|
1456
|
+
? []
|
|
1457
|
+
: [rootOf(entity).fqn]));
|
|
1458
|
+
const inheritanceOf = (rawClass) => {
|
|
1459
|
+
const root = rootOf(rawClass);
|
|
1460
|
+
if (!rootsWithChildren.has(root.fqn)) {
|
|
1461
|
+
return 'NONE';
|
|
1462
|
+
}
|
|
1463
|
+
// Doctrine rejects an entity hierarchy without an inheritance type; single table is the closest reading.
|
|
1464
|
+
return root.inheritanceType === undefined || root.inheritanceType === 'NONE'
|
|
1465
|
+
? 'SINGLE_TABLE'
|
|
1466
|
+
: root.inheritanceType;
|
|
1467
|
+
};
|
|
1468
|
+
const modelNames = new Map();
|
|
1469
|
+
const seenNames = new Set();
|
|
1470
|
+
const modelClasses = [];
|
|
1471
|
+
for (const entity of entities) {
|
|
1472
|
+
const parent = nearestEntityParent(entity, classes);
|
|
1473
|
+
const kind = inheritanceOf(entity);
|
|
1474
|
+
if (parent !== undefined) {
|
|
1475
|
+
const root = rootOf(entity);
|
|
1476
|
+
if (root.inheritanceType === undefined) {
|
|
1477
|
+
warnings.push(`${entity.name}: the entity extends the entity "${parent.name}" but "${root.name}" has no #[ORM\\InheritanceType]; ` +
|
|
1478
|
+
`single-table inheritance was assumed.`);
|
|
1479
|
+
}
|
|
1480
|
+
if (kind === 'SINGLE_TABLE') {
|
|
1481
|
+
modelNames.set(entity.fqn, root.name);
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (seenNames.has(entity.name)) {
|
|
1486
|
+
warnings.push(`Duplicate entity name "${entity.name}" (${entity.ctx.path}); only the first definition was converted.`);
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
seenNames.add(entity.name);
|
|
1490
|
+
modelNames.set(entity.fqn, entity.name);
|
|
1491
|
+
modelClasses.push(entity);
|
|
1492
|
+
}
|
|
1493
|
+
const models = [];
|
|
1494
|
+
const inverseSides = [];
|
|
1495
|
+
const joinedChildren = [];
|
|
1496
|
+
for (const entity of modelClasses) {
|
|
1497
|
+
const collected = collectMembers(entity, classes, state, new Set());
|
|
1498
|
+
const kind = inheritanceOf(entity);
|
|
1499
|
+
const parent = nearestEntityParent(entity, classes);
|
|
1500
|
+
const isRoot = parent === undefined;
|
|
1501
|
+
const isJoinedChild = parent !== undefined && kind === 'JOINED';
|
|
1502
|
+
const descendants = isRoot && kind === 'SINGLE_TABLE'
|
|
1503
|
+
? entities.filter((candidate) => candidate !== entity &&
|
|
1504
|
+
rootOf(candidate) === entity &&
|
|
1505
|
+
modelNames.get(candidate.fqn) === entity.name)
|
|
1506
|
+
: [];
|
|
1507
|
+
const descendantMembers = descendants.map((descendant) => collectMembers(descendant, classes, state, new Set()));
|
|
1508
|
+
const model = finalizeModel(entity, collected, descendantMembers, modelNames, state, options, isJoinedChild);
|
|
1509
|
+
if (isRoot && (kind === 'SINGLE_TABLE' || kind === 'JOINED')) {
|
|
1510
|
+
addDiscriminator(model, entity, state);
|
|
1511
|
+
}
|
|
1512
|
+
if (descendants.length > 0) {
|
|
1513
|
+
warnings.push(`${entity.name}: single-table inheritance was flattened into the table "${model.tableName}"; the columns of ` +
|
|
1514
|
+
`${descendants.map((descendant) => descendant.name).join(', ')} were merged as nullable columns ` +
|
|
1515
|
+
`and the discriminator values are not preserved.`);
|
|
1516
|
+
}
|
|
1517
|
+
if (isJoinedChild && parent !== undefined) {
|
|
1518
|
+
joinedChildren.push({ model, parentModel: parent.name });
|
|
1519
|
+
warnings.push(`${entity.name}: joined-table inheritance from "${parent.name}" was converted to a one-to-one primary key ` +
|
|
1520
|
+
`named "${joinedPointerName(parent.name)}".`);
|
|
1521
|
+
}
|
|
1522
|
+
models.push(model);
|
|
1523
|
+
for (const inverse of collected.inverses) {
|
|
1524
|
+
inverseSides.push({ model, inverse });
|
|
1525
|
+
}
|
|
1526
|
+
for (const descendant of descendantMembers) {
|
|
1527
|
+
for (const inverse of descendant.inverses) {
|
|
1528
|
+
inverseSides.push({ model, inverse });
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
resolveInverseSides(models, inverseSides, modelNames, classes, warnings);
|
|
1533
|
+
resolveTargets(models, modelNames, classes, warnings, options);
|
|
1534
|
+
patchJoinedKeys(models, joinedChildren);
|
|
1535
|
+
normalizeReferencedColumns(models);
|
|
1536
|
+
return { models, enums: pruneEnums(state, models), warnings };
|
|
1537
|
+
}
|
|
1538
|
+
function joinedPointerName(parentName) {
|
|
1539
|
+
return `${toSnakeCase(parentName)}_ptr`;
|
|
1540
|
+
}
|
|
1541
|
+
/** The closest ancestor that is an entity, skipping mapped superclasses and other plain classes. */
|
|
1542
|
+
function nearestEntityParent(rawClass, classes) {
|
|
1543
|
+
const seen = new Set([rawClass.fqn]);
|
|
1544
|
+
let current = rawClass;
|
|
1545
|
+
for (;;) {
|
|
1546
|
+
if (current.parentFqn === undefined) {
|
|
1547
|
+
return undefined;
|
|
1548
|
+
}
|
|
1549
|
+
const parent = classes.get(current.parentFqn);
|
|
1550
|
+
if (parent === undefined || seen.has(parent.fqn)) {
|
|
1551
|
+
return undefined;
|
|
1552
|
+
}
|
|
1553
|
+
if (parent.role === 'entity') {
|
|
1554
|
+
return parent;
|
|
1555
|
+
}
|
|
1556
|
+
seen.add(parent.fqn);
|
|
1557
|
+
current = parent;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function emptyCollected() {
|
|
1561
|
+
return { fields: [], relations: [], inverses: [], indexes: [] };
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Gathers the members a class contributes: its traits, then (through mapped superclasses only)
|
|
1565
|
+
* its parents, then its own properties. Embedded properties are flattened into prefixed fields.
|
|
1566
|
+
*/
|
|
1567
|
+
function collectMembers(rawClass, classes, state, visiting) {
|
|
1568
|
+
const inherited = emptyCollected();
|
|
1569
|
+
visiting.add(rawClass.fqn);
|
|
1570
|
+
if (rawClass.parentFqn !== undefined) {
|
|
1571
|
+
const parent = classes.get(rawClass.parentFqn);
|
|
1572
|
+
if (parent === undefined) {
|
|
1573
|
+
if (rawClass.role === 'entity' || rawClass.role === 'mappedSuperclass') {
|
|
1574
|
+
state.warnings.push(`${rawClass.name}: base class "${lastNameSegment(rawClass.parentFqn)}" was not found in the input files, so any columns it defines ` +
|
|
1575
|
+
`are missing. Add the file that defines it to --input.`);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
else if (visiting.has(parent.fqn)) {
|
|
1579
|
+
state.warnings.push(`${rawClass.name}: circular inheritance through "${parent.name}" was ignored.`);
|
|
1580
|
+
}
|
|
1581
|
+
else if (parent.role === 'mappedSuperclass') {
|
|
1582
|
+
mergeCollected(inherited, collectMembers(parent, classes, state, new Set(visiting)));
|
|
1583
|
+
}
|
|
1584
|
+
else if (parent.role !== 'entity' && parent.hasMappedProperties) {
|
|
1585
|
+
state.warnings.push(`${rawClass.name}: the base class "${parent.name}" is not marked #[ORM\\MappedSuperclass], so Doctrine ignores its mapped properties.`);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
for (const traitFqn of rawClass.traitFqns) {
|
|
1589
|
+
const trait = classes.get(traitFqn);
|
|
1590
|
+
if (trait === undefined) {
|
|
1591
|
+
state.warnings.push(`${rawClass.name}: the trait "${lastNameSegment(traitFqn)}" was not found in the input files, so any mapped properties it declares are missing. ` +
|
|
1592
|
+
`Add the file that defines it to --input.`);
|
|
1593
|
+
}
|
|
1594
|
+
else if (!visiting.has(trait.fqn)) {
|
|
1595
|
+
mergeCollected(inherited, collectMembers(trait, classes, state, new Set(visiting)));
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const own = emptyCollected();
|
|
1599
|
+
for (const member of rawClass.members) {
|
|
1600
|
+
switch (member.kind) {
|
|
1601
|
+
case 'field':
|
|
1602
|
+
own.fields.push(member.field);
|
|
1603
|
+
break;
|
|
1604
|
+
case 'relation':
|
|
1605
|
+
own.relations.push(member);
|
|
1606
|
+
break;
|
|
1607
|
+
case 'inverse':
|
|
1608
|
+
own.inverses.push(member);
|
|
1609
|
+
break;
|
|
1610
|
+
case 'embedded':
|
|
1611
|
+
own.fields.push(...expandEmbedded(member, `${rawClass.name}.${member.property}`, classes, state, new Set([rawClass.fqn])));
|
|
1612
|
+
break;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
own.indexes.push(...rawClass.indexes);
|
|
1616
|
+
const ownFieldNames = new Set(own.fields.map((field) => field.name));
|
|
1617
|
+
const ownRelationNames = new Set(own.relations.map((member) => member.relation.name));
|
|
1618
|
+
const ownInverseNames = new Set(own.inverses.map((member) => member.property));
|
|
1619
|
+
return {
|
|
1620
|
+
fields: [
|
|
1621
|
+
...inherited.fields.filter((field) => !ownFieldNames.has(field.name)),
|
|
1622
|
+
...own.fields,
|
|
1623
|
+
],
|
|
1624
|
+
relations: [
|
|
1625
|
+
...inherited.relations.filter((member) => !ownRelationNames.has(member.relation.name)),
|
|
1626
|
+
...own.relations,
|
|
1627
|
+
],
|
|
1628
|
+
inverses: [
|
|
1629
|
+
...inherited.inverses.filter((member) => !ownInverseNames.has(member.property)),
|
|
1630
|
+
...own.inverses,
|
|
1631
|
+
],
|
|
1632
|
+
indexes: [...inherited.indexes, ...own.indexes],
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function mergeCollected(target, source) {
|
|
1636
|
+
target.fields.push(...source.fields);
|
|
1637
|
+
target.relations.push(...source.relations);
|
|
1638
|
+
target.inverses.push(...source.inverses);
|
|
1639
|
+
target.indexes.push(...source.indexes);
|
|
1640
|
+
}
|
|
1641
|
+
/** Flattens an embeddable into prefixed fields, the way Doctrine's underscore naming strategy names them. */
|
|
1642
|
+
function expandEmbedded(embedded, location, classes, state, visiting) {
|
|
1643
|
+
const embeddable = classes.get(embedded.classFqn);
|
|
1644
|
+
if (embeddable === undefined) {
|
|
1645
|
+
state.warnings.push(`${location}: the embeddable "${lastNameSegment(embedded.classFqn)}" was not found in the input files, so its columns are missing. ` +
|
|
1646
|
+
`Add the file that defines it to --input.`);
|
|
1647
|
+
return [];
|
|
1648
|
+
}
|
|
1649
|
+
if (visiting.has(embeddable.fqn)) {
|
|
1650
|
+
state.warnings.push(`${location}: a recursive embeddable was ignored.`);
|
|
1651
|
+
return [];
|
|
1652
|
+
}
|
|
1653
|
+
if (embeddable.role !== 'embeddable') {
|
|
1654
|
+
state.warnings.push(`${location}: "${embeddable.name}" is not marked #[ORM\\Embeddable]; Doctrine would reject it, but its columns were embedded anyway.`);
|
|
1655
|
+
}
|
|
1656
|
+
const collected = collectMembers(embeddable, classes, state, new Set([...visiting, embeddable.fqn]));
|
|
1657
|
+
const columnPrefix = embedded.prefix === false
|
|
1658
|
+
? ''
|
|
1659
|
+
: (embedded.prefix ?? `${underscore(embedded.property)}_`);
|
|
1660
|
+
for (const relation of collected.relations) {
|
|
1661
|
+
state.warnings.push(`${location}.${relation.relation.name}: relations inside embeddables are not supported and were skipped.`);
|
|
1662
|
+
}
|
|
1663
|
+
for (const inverse of collected.inverses) {
|
|
1664
|
+
state.warnings.push(`${location}.${inverse.property}: relations inside embeddables are not supported and were skipped.`);
|
|
1665
|
+
}
|
|
1666
|
+
if (collected.indexes.length > 0) {
|
|
1667
|
+
state.warnings.push(`${location}: indexes declared inside the embeddable "${embeddable.name}" were skipped.`);
|
|
1668
|
+
}
|
|
1669
|
+
return collected.fields.map((field) => ({
|
|
1670
|
+
...field,
|
|
1671
|
+
name: toCamelCase(`${embedded.property}_${upperFirst(field.name)}`),
|
|
1672
|
+
columnName: `${columnPrefix}${field.columnName}`,
|
|
1673
|
+
isPrimaryKey: false,
|
|
1674
|
+
}));
|
|
1675
|
+
}
|
|
1676
|
+
function finalizeModel(rawClass, collected, descendants, modelNames, state, options, isJoinedChild) {
|
|
1677
|
+
const warnings = state.warnings;
|
|
1678
|
+
const fields = collected.fields.map((field) => ({
|
|
1679
|
+
...field,
|
|
1680
|
+
}));
|
|
1681
|
+
const relationMembers = [...collected.relations];
|
|
1682
|
+
const indexes = [...collected.indexes];
|
|
1683
|
+
// Columns of single-table subclasses are optional for rows of other subclasses.
|
|
1684
|
+
for (const descendant of descendants) {
|
|
1685
|
+
for (const field of descendant.fields) {
|
|
1686
|
+
if (!fields.some((existing) => existing.name === field.name)) {
|
|
1687
|
+
fields.push({ ...field, isNullable: true, isPrimaryKey: false });
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
for (const member of descendant.relations) {
|
|
1691
|
+
if (!relationMembers.some((existing) => existing.relation.name === member.relation.name)) {
|
|
1692
|
+
relationMembers.push({
|
|
1693
|
+
...member,
|
|
1694
|
+
relation: { ...member.relation, isNullable: true },
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
indexes.push(...descendant.indexes);
|
|
1699
|
+
}
|
|
1700
|
+
const relations = [];
|
|
1701
|
+
/** Scalar foreign-key properties merged into relations: property name -> relation name. */
|
|
1702
|
+
const renamed = new Map();
|
|
1703
|
+
for (const member of relationMembers) {
|
|
1704
|
+
const relation = { ...member.relation };
|
|
1705
|
+
if (relation.kind !== 'manyToMany') {
|
|
1706
|
+
const backing = fields.findIndex((field) => field.columnName === relation.columnName);
|
|
1707
|
+
const backingField = fields[backing];
|
|
1708
|
+
if (backingField !== undefined) {
|
|
1709
|
+
// The entity also declares the foreign key column as a plain property
|
|
1710
|
+
// (Doctrine requires insertable/updatable: false on it); the relation already represents it.
|
|
1711
|
+
fields.splice(backing, 1);
|
|
1712
|
+
renamed.set(backingField.name, relation.name);
|
|
1713
|
+
if (!member.hasExplicitNullable) {
|
|
1714
|
+
relation.isNullable = backingField.isNullable;
|
|
1715
|
+
}
|
|
1716
|
+
if (backingField.isPrimaryKey) {
|
|
1717
|
+
relation.isPrimaryKey = true;
|
|
1718
|
+
relation.isNullable = false;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
relations.push(relation);
|
|
1723
|
+
}
|
|
1724
|
+
const irIndexes = [];
|
|
1725
|
+
for (const raw of indexes) {
|
|
1726
|
+
const names = [];
|
|
1727
|
+
const missing = [];
|
|
1728
|
+
for (const ref of raw.refs) {
|
|
1729
|
+
const resolved = resolveIndexReference(ref, fields, relations, renamed);
|
|
1730
|
+
if (resolved === undefined) {
|
|
1731
|
+
missing.push(ref);
|
|
1732
|
+
}
|
|
1733
|
+
else {
|
|
1734
|
+
names.push(resolved);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (missing.length > 0) {
|
|
1738
|
+
warnings.push(`${rawClass.name}: the ${raw.isUnique ? 'unique constraint' : 'index'} on (${raw.refs.join(', ')}) refers to ` +
|
|
1739
|
+
`"${missing.join('", "')}", which is not a column or relation of the entity; it was skipped.`);
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
const singleField = names.length === 1
|
|
1743
|
+
? fields.find((field) => field.name === names[0])
|
|
1744
|
+
: undefined;
|
|
1745
|
+
if (raw.isUnique &&
|
|
1746
|
+
raw.name === undefined &&
|
|
1747
|
+
singleField !== undefined &&
|
|
1748
|
+
!singleField.isPrimaryKey) {
|
|
1749
|
+
singleField.isUnique = true;
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
irIndexes.push({
|
|
1753
|
+
fields: names,
|
|
1754
|
+
isUnique: raw.isUnique,
|
|
1755
|
+
...(raw.name === undefined ? {} : { name: raw.name }),
|
|
1756
|
+
...(raw.flags.includes('fulltext') ? { kind: 'fulltext' } : {}),
|
|
1757
|
+
...(raw.flags.includes('clustered') ? { clustered: true } : {}),
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
// Primary key -------------------------------------------------------------
|
|
1761
|
+
const primaryFields = fields.filter((field) => field.isPrimaryKey);
|
|
1762
|
+
const primaryRelations = relations.filter((relation) => relation.isPrimaryKey === true);
|
|
1763
|
+
const keyCount = primaryFields.length + primaryRelations.length;
|
|
1764
|
+
let compositePrimaryKey;
|
|
1765
|
+
if (isJoinedChild) {
|
|
1766
|
+
if (keyCount > 0) {
|
|
1767
|
+
warnings.push(`${rawClass.name}: a joined-table child inherits its identifier; its own #[ORM\\Id] properties were kept as ordinary columns.`);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
else if (keyCount === 0) {
|
|
1771
|
+
warnings.push(`${rawClass.name}: the entity has no identifier; Doctrine requires a property marked #[ORM\\Id].`);
|
|
1772
|
+
}
|
|
1773
|
+
else if (keyCount > 1) {
|
|
1774
|
+
compositePrimaryKey = [
|
|
1775
|
+
...primaryFields.map((field) => field.name),
|
|
1776
|
+
...primaryRelations.map((relation) => relation.name),
|
|
1777
|
+
];
|
|
1778
|
+
for (const field of primaryFields) {
|
|
1779
|
+
field.isPrimaryKey = false;
|
|
1780
|
+
field.isUnique = false;
|
|
1781
|
+
}
|
|
1782
|
+
for (const relation of primaryRelations) {
|
|
1783
|
+
delete relation.isPrimaryKey;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (isJoinedChild) {
|
|
1787
|
+
const parentFqn = rawClass.parentFqn;
|
|
1788
|
+
const parentName = parentFqn === undefined
|
|
1789
|
+
? 'Parent'
|
|
1790
|
+
: (modelNames.get(parentFqn) ?? lastNameSegment(parentFqn));
|
|
1791
|
+
for (const field of fields) {
|
|
1792
|
+
field.isPrimaryKey = false;
|
|
1793
|
+
}
|
|
1794
|
+
relations.unshift({
|
|
1795
|
+
name: joinedPointerName(parentName),
|
|
1796
|
+
kind: 'oneToOne',
|
|
1797
|
+
targetModel: parentFqn ?? parentName,
|
|
1798
|
+
columnName: 'id',
|
|
1799
|
+
isNullable: false,
|
|
1800
|
+
onDelete: 'cascade',
|
|
1801
|
+
isPrimaryKey: true,
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
return {
|
|
1805
|
+
name: rawClass.name,
|
|
1806
|
+
tableName: rawClass.tableName ?? underscore(rawClass.name),
|
|
1807
|
+
appLabel: options.appLabel,
|
|
1808
|
+
fields,
|
|
1809
|
+
relations,
|
|
1810
|
+
indexes: irIndexes,
|
|
1811
|
+
...(compositePrimaryKey === undefined ? {} : { compositePrimaryKey }),
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
/** Maps an index column (or property) reference to the IR field or relation name. */
|
|
1815
|
+
function resolveIndexReference(ref, fields, relations, renamed) {
|
|
1816
|
+
const byColumn = fields.find((field) => field.columnName === ref);
|
|
1817
|
+
if (byColumn !== undefined) {
|
|
1818
|
+
return byColumn.name;
|
|
1819
|
+
}
|
|
1820
|
+
const relationByColumn = relations.find((relation) => relation.kind !== 'manyToMany' && relation.columnName === ref);
|
|
1821
|
+
if (relationByColumn !== undefined) {
|
|
1822
|
+
return relationByColumn.name;
|
|
1823
|
+
}
|
|
1824
|
+
const renamedTo = renamed.get(ref);
|
|
1825
|
+
if (renamedTo !== undefined) {
|
|
1826
|
+
return renamedTo;
|
|
1827
|
+
}
|
|
1828
|
+
if (fields.some((field) => field.name === ref) ||
|
|
1829
|
+
relations.some((relation) => relation.name === ref)) {
|
|
1830
|
+
return ref;
|
|
1831
|
+
}
|
|
1832
|
+
return undefined;
|
|
1833
|
+
}
|
|
1834
|
+
function addDiscriminator(model, rawClass, state) {
|
|
1835
|
+
const name = rawClass.discriminatorName ?? 'dtype';
|
|
1836
|
+
if (model.fields.some((field) => field.name === name || field.columnName === name)) {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
const type = (rawClass.discriminatorType ?? 'string').toLowerCase();
|
|
1840
|
+
const isInteger = type === 'integer' || type === 'smallint';
|
|
1841
|
+
if (!isInteger && type !== 'string') {
|
|
1842
|
+
state.warnings.push(`${rawClass.name}: the discriminator column type "${type}" is not supported; a string column was used.`);
|
|
1843
|
+
}
|
|
1844
|
+
model.fields.push({
|
|
1845
|
+
name,
|
|
1846
|
+
columnName: name,
|
|
1847
|
+
type: isInteger ? 'int' : 'string',
|
|
1848
|
+
isPrimaryKey: false,
|
|
1849
|
+
isUnique: false,
|
|
1850
|
+
isNullable: false,
|
|
1851
|
+
isAutoUpdated: false,
|
|
1852
|
+
...(isInteger ? {} : { maxLength: rawClass.discriminatorLength ?? 255 }),
|
|
1853
|
+
});
|
|
1854
|
+
if (rawClass.discriminatorMapSize > 0) {
|
|
1855
|
+
state.warnings.push(`${rawClass.name}: the discriminator map (${rawClass.discriminatorMapSize} entries) maps values to subclasses and is not preserved; ` +
|
|
1856
|
+
`the discriminator column "${name}" is kept as a plain ${isInteger ? 'integer' : 'string'} column.`);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
/** Links inverse-side attributes (OneToMany, mappedBy) to the owning relation. */
|
|
1860
|
+
function resolveInverseSides(models, inverseSides, modelNames, classes, warnings) {
|
|
1861
|
+
const byName = new Map(models.map((model) => [model.name, model]));
|
|
1862
|
+
for (const { model, inverse } of inverseSides) {
|
|
1863
|
+
const location = `${model.name}.${inverse.property}`;
|
|
1864
|
+
const targetName = modelNames.get(inverse.targetFqn) ??
|
|
1865
|
+
findByShortName(inverse.targetFqn, modelNames);
|
|
1866
|
+
const target = targetName === undefined ? undefined : byName.get(targetName);
|
|
1867
|
+
if (target === undefined) {
|
|
1868
|
+
if (!classes.has(inverse.targetFqn)) {
|
|
1869
|
+
warnings.push(`${location}: the target "${lastNameSegment(inverse.targetFqn)}" is not an entity in the input, so the reverse accessor is not linked.`);
|
|
1870
|
+
}
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
if (inverse.mappedBy === undefined) {
|
|
1874
|
+
warnings.push(`${location}: this inverse side has no mappedBy, so it could not be linked to "${target.name}".`);
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
const owner = target.relations.find((relation) => relation.name === inverse.mappedBy);
|
|
1878
|
+
if (owner === undefined) {
|
|
1879
|
+
warnings.push(`${location}: the owning side "${target.name}.${inverse.mappedBy}" was not found; the reverse accessor is not included.`);
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
if (owner.relatedName === undefined) {
|
|
1883
|
+
owner.relatedName = inverse.property;
|
|
1884
|
+
}
|
|
1885
|
+
else if (owner.relatedName !== inverse.property) {
|
|
1886
|
+
warnings.push(`${location}: the owning side "${target.name}.${inverse.mappedBy}" names its reverse accessor ` +
|
|
1887
|
+
`"${owner.relatedName}"; "${inverse.property}" was ignored.`);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
function findByShortName(fqn, modelNames) {
|
|
1892
|
+
const short = lastNameSegment(fqn);
|
|
1893
|
+
const matches = [...modelNames.entries()]
|
|
1894
|
+
.filter(([key]) => lastNameSegment(key) === short)
|
|
1895
|
+
.map(([, name]) => name);
|
|
1896
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
1897
|
+
}
|
|
1898
|
+
/** Replaces the fully qualified targets stored on relations with model names, adding stubs for unknown classes. */
|
|
1899
|
+
function resolveTargets(models, modelNames, classes, warnings, options) {
|
|
1900
|
+
const known = new Set(models.map((model) => model.name));
|
|
1901
|
+
const stubs = [];
|
|
1902
|
+
for (const model of models) {
|
|
1903
|
+
for (const relation of model.relations) {
|
|
1904
|
+
const fqn = relation.targetModel;
|
|
1905
|
+
const mapped = modelNames.get(fqn) ?? findByShortName(fqn, modelNames);
|
|
1906
|
+
if (mapped !== undefined) {
|
|
1907
|
+
relation.targetModel = mapped;
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
const shortName = lastNameSegment(fqn);
|
|
1911
|
+
relation.targetModel = shortName;
|
|
1912
|
+
if (known.has(shortName)) {
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
known.add(shortName);
|
|
1916
|
+
const nonEntity = classes.get(fqn);
|
|
1917
|
+
stubs.push({
|
|
1918
|
+
name: shortName,
|
|
1919
|
+
tableName: underscore(shortName),
|
|
1920
|
+
appLabel: options.appLabel,
|
|
1921
|
+
fields: [
|
|
1922
|
+
{
|
|
1923
|
+
name: 'id',
|
|
1924
|
+
columnName: 'id',
|
|
1925
|
+
type: 'int',
|
|
1926
|
+
isPrimaryKey: true,
|
|
1927
|
+
isUnique: false,
|
|
1928
|
+
isNullable: false,
|
|
1929
|
+
isAutoUpdated: false,
|
|
1930
|
+
default: { kind: 'autoIncrement' },
|
|
1931
|
+
},
|
|
1932
|
+
],
|
|
1933
|
+
relations: [],
|
|
1934
|
+
indexes: [],
|
|
1935
|
+
});
|
|
1936
|
+
warnings.push(`${model.name}.${relation.name} references "${shortName}", which is not ${nonEntity === undefined ? 'an entity in the input' : 'marked #[ORM\\Entity]'}. ` +
|
|
1937
|
+
`A stub model with an auto-increment id was generated; replace it with the real definition.`);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
models.push(...stubs);
|
|
1941
|
+
}
|
|
1942
|
+
/** Joined-table children reuse the key column of their parent. */
|
|
1943
|
+
function patchJoinedKeys(models, joinedChildren) {
|
|
1944
|
+
const byName = new Map(models.map((model) => [model.name, model]));
|
|
1945
|
+
for (const { model, parentModel } of joinedChildren) {
|
|
1946
|
+
const parent = byName.get(parentModel);
|
|
1947
|
+
const pointer = model.relations.find((relation) => relation.isPrimaryKey === true);
|
|
1948
|
+
if (parent === undefined || pointer === undefined) {
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
const key = parent.fields.find((field) => field.isPrimaryKey)?.columnName ??
|
|
1952
|
+
parent.relations.find((relation) => relation.isPrimaryKey === true)?.columnName;
|
|
1953
|
+
if (key !== undefined) {
|
|
1954
|
+
pointer.columnName = key;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
/** A referencedColumnName that names the target's own key is the default and is dropped. */
|
|
1959
|
+
function normalizeReferencedColumns(models) {
|
|
1960
|
+
const byName = new Map(models.map((model) => [model.name, model]));
|
|
1961
|
+
for (const model of models) {
|
|
1962
|
+
for (const relation of model.relations) {
|
|
1963
|
+
if (relation.toField === undefined) {
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
const target = byName.get(relation.targetModel);
|
|
1967
|
+
const key = target?.fields.find((field) => field.isPrimaryKey);
|
|
1968
|
+
if (key !== undefined && key.columnName === relation.toField) {
|
|
1969
|
+
delete relation.toField;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
/** Keeps only enums that a converted field refers to. */
|
|
1975
|
+
function pruneEnums(state, models) {
|
|
1976
|
+
const used = new Set(models.flatMap((model) => model.fields.flatMap((field) => field.enumName === undefined ? [] : [field.enumName])));
|
|
1977
|
+
return [...state.enums.values()]
|
|
1978
|
+
.filter((info) => info.backing === 'string' && used.has(info.name))
|
|
1979
|
+
.map((info) => ({
|
|
1980
|
+
name: info.name,
|
|
1981
|
+
values: info.values,
|
|
1982
|
+
}));
|
|
1983
|
+
}
|
|
1984
|
+
//# sourceMappingURL=doctrine.js.map
|