• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

typeorm / typeorm / 19549987525

20 Nov 2025 08:11PM UTC coverage: 80.769% (+4.3%) from 76.433%
19549987525

push

github

web-flow
ci: run tests on commits to master and next (#11783)

Co-authored-by: Oleg "OSA413" Sokolov <OSA413@users.noreply.github.com>

26500 of 32174 branches covered (82.36%)

Branch coverage included in aggregate %.

91252 of 113615 relevant lines covered (80.32%)

88980.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.05
/src/metadata-builder/EntityMetadataValidator.ts
1
import { EntityMetadata } from "../metadata/EntityMetadata"
26✔
2
import { MissingPrimaryColumnError } from "../error/MissingPrimaryColumnError"
26✔
3
import { CircularRelationsError } from "../error/CircularRelationsError"
26✔
4
import { DepGraph } from "../util/DepGraph"
26✔
5
import { Driver } from "../driver/Driver"
26✔
6
import { DataTypeNotSupportedError } from "../error/DataTypeNotSupportedError"
26✔
7
import { ColumnType } from "../driver/types/ColumnTypes"
26✔
8
import { NoConnectionOptionError } from "../error/NoConnectionOptionError"
26✔
9
import { InitializedRelationError } from "../error/InitializedRelationError"
26✔
10
import { TypeORMError } from "../error"
26✔
11
import { DriverUtils } from "../driver/DriverUtils"
26✔
12

26✔
13
/// todo: add check if there are multiple tables with the same name
26✔
14
/// todo: add checks when generated column / table names are too long for the specific driver
26✔
15
// todo: type in function validation, inverse side function validation
26✔
16
// todo: check on build for duplicate names, since naming checking was removed from MetadataStorage
26✔
17
// todo: duplicate name checking for: table, relation, column, index, naming strategy, join tables/columns?
26✔
18
// todo: check if multiple tree parent metadatas in validator
26✔
19
// todo: tree decorators can be used only on closure table (validation)
26✔
20
// todo: throw error if parent tree metadata was not specified in a closure table
26✔
21

26✔
22
// todo: MetadataArgsStorage: type in function validation, inverse side function validation
26✔
23
// todo: MetadataArgsStorage: check on build for duplicate names, since naming checking was removed from MetadataStorage
26✔
24
// todo: MetadataArgsStorage: duplicate name checking for: table, relation, column, index, naming strategy, join tables/columns?
26✔
25
// todo: MetadataArgsStorage: check for duplicate targets too since this check has been removed too
26✔
26
// todo: check if relation decorator contains primary: true and nullable: true
26✔
27
// todo: check column length, precision. scale
26✔
28
// todo: MySQL index can be unique or spatial or fulltext
26✔
29

26✔
30
/**
26✔
31
 * Validates built entity metadatas.
26✔
32
 */
26✔
33
export class EntityMetadataValidator {
26✔
34
    // -------------------------------------------------------------------------
26✔
35
    // Public Methods
26✔
36
    // -------------------------------------------------------------------------
26✔
37

26✔
38
    /**
26✔
39
     * Validates all given entity metadatas.
26✔
40
     */
26✔
41
    validateMany(entityMetadatas: EntityMetadata[], driver: Driver) {
26✔
42
        entityMetadatas.forEach((entityMetadata) =>
14,569✔
43
            this.validate(entityMetadata, entityMetadatas, driver),
14,569✔
44
        )
14,569✔
45
        this.validateDependencies(entityMetadatas)
14,569✔
46
        this.validateEagerRelations(entityMetadatas)
14,569✔
47
    }
14,569✔
48

26✔
49
    /**
26✔
50
     * Validates given entity metadata.
26✔
51
     */
26✔
52
    validate(
26✔
53
        entityMetadata: EntityMetadata,
36,328✔
54
        allEntityMetadatas: EntityMetadata[],
36,328✔
55
        driver: Driver,
36,328✔
56
    ) {
36,328✔
57
        // check if table metadata has an id
36,328✔
58
        if (!entityMetadata.primaryColumns.length && !entityMetadata.isJunction)
36,328!
59
            throw new MissingPrimaryColumnError(entityMetadata)
36,328!
60

36,328✔
61
        // if entity has multiple primary keys and uses custom constraint name,
36,328✔
62
        // then all primary keys should have the same constraint name
36,328✔
63
        if (entityMetadata.primaryColumns.length > 1) {
36,328✔
64
            const areConstraintNamesEqual = entityMetadata.primaryColumns.every(
5,496✔
65
                (columnMetadata, i, columnMetadatas) =>
5,496✔
66
                    columnMetadata.primaryKeyConstraintName ===
12,147✔
67
                    columnMetadatas[0].primaryKeyConstraintName,
5,496✔
68
            )
5,496✔
69
            if (!areConstraintNamesEqual) {
5,496!
70
                throw new TypeORMError(
×
71
                    `Entity ${entityMetadata.name} has multiple primary columns with different constraint names. Constraint names should be the equal.`,
×
72
                )
×
73
            }
×
74
        }
5,496✔
75

36,328✔
76
        // validate if table is using inheritance it has a discriminator
36,328✔
77
        // also validate if discriminator values are not empty and not repeated
36,328✔
78
        if (
36,328✔
79
            entityMetadata.inheritancePattern === "STI" ||
36,328✔
80
            entityMetadata.tableType === "entity-child"
34,226✔
81
        ) {
36,328!
82
            if (!entityMetadata.discriminatorColumn)
2,102✔
83
                throw new TypeORMError(
2,102!
84
                    `Entity ${entityMetadata.name} using single-table inheritance, it should also have a discriminator column. Did you forget to put discriminator column options?`,
×
85
                )
×
86

2,102✔
87
            if (typeof entityMetadata.discriminatorValue === "undefined")
2,102✔
88
                throw new TypeORMError(
2,102!
89
                    `Entity ${entityMetadata.name} has an undefined discriminator value. Discriminator value should be defined.`,
×
90
                )
×
91

2,102✔
92
            const sameDiscriminatorValueEntityMetadata =
2,102✔
93
                allEntityMetadatas.find((metadata) => {
2,102✔
94
                    return (
11,522✔
95
                        metadata !== entityMetadata &&
11,522✔
96
                        (metadata.inheritancePattern === "STI" ||
9,420✔
97
                            metadata.tableType === "entity-child") &&
11,522✔
98
                        metadata.tableName === entityMetadata.tableName &&
11,522✔
99
                        metadata.discriminatorValue ===
4,936✔
100
                            entityMetadata.discriminatorValue &&
11,522✔
101
                        metadata.inheritanceTree.some(
24✔
102
                            (parent) =>
24✔
103
                                entityMetadata.inheritanceTree.indexOf(
48✔
104
                                    parent,
48✔
105
                                ) !== -1,
24✔
106
                        )
11,522✔
107
                    )
11,522✔
108
                })
2,102✔
109
            if (sameDiscriminatorValueEntityMetadata)
2,102✔
110
                throw new TypeORMError(
2,102✔
111
                    `Entities ${entityMetadata.name} and ${sameDiscriminatorValueEntityMetadata.name} have the same discriminator values. Make sure they are different while using the @ChildEntity decorator.`,
24✔
112
                )
24✔
113
        }
2,102✔
114

36,304✔
115
        entityMetadata.relationCounts.forEach((relationCount) => {
36,304✔
116
            if (
334✔
117
                relationCount.relation.isManyToOne ||
334✔
118
                relationCount.relation.isOneToOne
334✔
119
            )
334✔
120
                throw new TypeORMError(
334✔
121
                    `Relation count can not be implemented on ManyToOne or OneToOne relations.`,
26✔
122
                )
26✔
123
        })
36,304✔
124

36,304✔
125
        if (!(driver.options.type === "mongodb")) {
36,328✔
126
            entityMetadata.columns
36,212✔
127
                .filter((column) => !column.isVirtualProperty)
36,212✔
128
                .forEach((column) => {
36,212✔
129
                    const normalizedColumn = driver.normalizeType(
113,506✔
130
                        column,
113,506✔
131
                    ) as ColumnType
113,506✔
132
                    if (!driver.supportedDataTypes.includes(normalizedColumn))
113,506✔
133
                        throw new DataTypeNotSupportedError(
113,506!
134
                            column,
×
135
                            normalizedColumn,
×
136
                            driver.options.type,
×
137
                        )
×
138
                    if (
113,506✔
139
                        column.length &&
113,506!
140
                        !driver.withLengthColumnTypes.includes(normalizedColumn)
2,217✔
141
                    )
113,506✔
142
                        throw new TypeORMError(
113,506!
143
                            `Column ${column.propertyName} of Entity ${entityMetadata.name} does not support length property.`,
6✔
144
                        )
6✔
145
                    if (
113,500✔
146
                        column.type === "enum" &&
113,500!
147
                        !column.enum &&
113,506!
148
                        !column.enumName
4✔
149
                    )
113,506✔
150
                        throw new TypeORMError(
113,506!
151
                            `Column "${column.propertyName}" of Entity "${entityMetadata.name}" is defined as enum, but missing "enum" or "enumName" properties.`,
×
152
                        )
×
153
                })
36,212✔
154
        }
36,212✔
155

36,272✔
156
        if (
36,272✔
157
            DriverUtils.isMySQLFamily(driver) ||
36,272!
158
            driver.options.type === "aurora-mysql"
25,490✔
159
        ) {
36,328✔
160
            const generatedColumns = entityMetadata.columns.filter(
10,782✔
161
                (column) =>
10,782✔
162
                    column.isGenerated && column.generationStrategy !== "uuid",
10,782✔
163
            )
10,782✔
164
            if (generatedColumns.length > 1)
10,782✔
165
                throw new TypeORMError(
10,782!
166
                    `Error in ${entityMetadata.name} entity. There can be only one auto-increment column in MySql table.`,
×
167
                )
×
168
        }
10,782✔
169

36,272✔
170
        // for mysql we are able to not define a default selected database, instead all entities can have their database
36,272✔
171
        // defined in their decorators. To make everything work either all entities must have database define and we
36,272✔
172
        // can live without database set in the connection options, either database in the connection options must be set
36,272✔
173
        if (DriverUtils.isMySQLFamily(driver)) {
36,314✔
174
            const metadatasWithDatabase = allEntityMetadatas.filter(
10,782✔
175
                (metadata) => metadata.database,
10,782✔
176
            )
10,782✔
177
            if (metadatasWithDatabase.length === 0 && !driver.database)
10,782✔
178
                throw new NoConnectionOptionError("database")
10,782!
179
        }
10,782✔
180

36,272✔
181
        if (driver.options.type === "mssql") {
36,328!
182
            const charsetColumns = entityMetadata.columns.filter(
2,508✔
183
                (column) => column.charset,
2,508✔
184
            )
2,508✔
185
            if (charsetColumns.length > 1)
2,508✔
186
                throw new TypeORMError(
2,508!
187
                    `Character set specifying is not supported in Sql Server`,
×
188
                )
×
189
        }
2,508✔
190

36,272✔
191
        // Postgres supports only STORED generated columns.
36,272✔
192
        if (driver.options.type === "postgres") {
36,328!
193
            const virtualColumn = entityMetadata.columns.find(
6,300✔
194
                (column) =>
6,300✔
195
                    column.asExpression &&
20,584✔
196
                    (!column.generatedType ||
16✔
197
                        column.generatedType === "VIRTUAL"),
6,300✔
198
            )
6,300✔
199
            if (virtualColumn)
6,300✔
200
                throw new TypeORMError(
6,300!
201
                    `Column "${virtualColumn.propertyName}" of Entity "${entityMetadata.name}" is defined as VIRTUAL, but Postgres supports only STORED generated columns.`,
×
202
                )
×
203
        }
6,300✔
204

36,272✔
205
        // check if relations are all without initialized properties
36,272✔
206
        const entityInstance = entityMetadata.create(undefined, {
36,272✔
207
            fromDeserializer: true,
36,272✔
208
        })
36,272✔
209
        entityMetadata.relations.forEach((relation) => {
36,272✔
210
            if (relation.isManyToMany || relation.isOneToMany) {
23,486✔
211
                // we skip relations for which persistence is disabled since initialization in them cannot harm somehow
10,961✔
212
                if (relation.persistenceEnabled === false) return
10,961✔
213

10,935✔
214
                // get entity relation value and check if its an array
10,935✔
215
                const relationInitializedValue =
10,935✔
216
                    relation.getEntityValue(entityInstance)
10,935✔
217
                if (Array.isArray(relationInitializedValue))
10,935✔
218
                    throw new InitializedRelationError(relation)
10,959✔
219
            }
10,961✔
220
        })
36,272✔
221

36,272✔
222
        // validate relations
36,272✔
223
        entityMetadata.relations.forEach((relation) => {
36,272✔
224
            // check OnDeleteTypes
23,408✔
225
            if (
23,408✔
226
                driver.supportedOnDeleteTypes &&
23,408!
227
                relation.onDelete &&
23,408!
228
                !driver.supportedOnDeleteTypes.includes(relation.onDelete)
20✔
229
            ) {
23,408!
230
                throw new TypeORMError(
2✔
231
                    `OnDeleteType "${relation.onDelete}" is not supported for ${driver.options.type}!`,
2✔
232
                )
2✔
233
            }
2✔
234

23,406✔
235
            // check OnUpdateTypes
23,406✔
236
            if (
23,406✔
237
                driver.supportedOnUpdateTypes &&
23,406!
238
                relation.onUpdate &&
23,408!
239
                !driver.supportedOnUpdateTypes.includes(relation.onUpdate)
6✔
240
            ) {
23,408!
241
                throw new TypeORMError(
×
242
                    `OnUpdateType "${relation.onUpdate}" is not valid for ${driver.options.type}!`,
×
243
                )
×
244
            }
×
245

23,408✔
246
            // check join tables:
23,408✔
247
            // using JoinTable is possible only on one side of the many-to-many relation
23,408✔
248
            // todo(dima): fix
23,408✔
249
            // if (relation.joinTable) {
23,408✔
250
            //     if (!relation.isManyToMany)
23,408✔
251
            //         throw new UsingJoinTableIsNotAllowedError(entityMetadata, relation);
23,408✔
252
            //     // if there is inverse side of the relation, then check if it does not have join table too
23,408✔
253
            //     if (relation.hasInverseSide && relation.inverseRelation.joinTable)
23,408✔
254
            //         throw new UsingJoinTableOnlyOnOneSideAllowedError(entityMetadata, relation);
23,408✔
255
            // }
23,408✔
256
            // check join columns:
23,408✔
257
            // using JoinColumn is possible only on one side of the relation and on one-to-one, many-to-one relation types
23,408✔
258
            // first check if relation is one-to-one or many-to-one
23,408✔
259
            // todo(dima): fix
23,408✔
260
            /*if (relation.joinColumn) {
23,408✔
261

23,408✔
262
                // join column can be applied only on one-to-one and many-to-one relations
23,408✔
263
                if (!relation.isOneToOne && !relation.isManyToOne)
23,408✔
264
                    throw new UsingJoinColumnIsNotAllowedError(entityMetadata, relation);
23,408✔
265

23,408✔
266
                // if there is inverse side of the relation, then check if it does not have join table too
23,408✔
267
                if (relation.hasInverseSide && relation.inverseRelation.joinColumn && relation.isOneToOne)
23,408✔
268
                    throw new UsingJoinColumnOnlyOnOneSideAllowedError(entityMetadata, relation);
23,408✔
269

23,408✔
270
                // check if join column really has referenced column
23,408✔
271
                if (relation.joinColumn && !relation.joinColumn.referencedColumn)
23,408✔
272
                    throw new TypeORMError(`Join column does not have referenced column set`);
23,408✔
273

23,408✔
274
            }
23,408✔
275

23,408✔
276
            // if its a one-to-one relation and JoinColumn is missing on both sides of the relation
23,408✔
277
            // or its one-side relation without JoinColumn we should give an error
23,408✔
278
            if (!relation.joinColumn && relation.isOneToOne && (!relation.hasInverseSide || !relation.inverseRelation.joinColumn))
23,408✔
279
                throw new MissingJoinColumnError(entityMetadata, relation);*/
23,408✔
280
            // if its a many-to-many relation and JoinTable is missing on both sides of the relation
23,408✔
281
            // or its one-side relation without JoinTable we should give an error
23,408✔
282
            // todo(dima): fix it
23,408✔
283
            // if (!relation.joinTable && relation.isManyToMany && (!relation.hasInverseSide || !relation.inverseRelation.joinTable))
23,408✔
284
            //     throw new MissingJoinTableError(entityMetadata, relation);
23,408✔
285
            // todo: validate if its one-to-one and side which does not have join column MUST have inverse side
23,408✔
286
            // todo: validate if its many-to-many and side which does not have join table MUST have inverse side
23,408✔
287
            // todo: if there is a relation, and inverse side is specified only on one side, shall we give error
23,408✔
288
            // todo: with message like: "Inverse side is specified only on one side of the relationship. Specify on other side too to prevent confusion".
23,408✔
289
            // todo: add validation if there two entities with the same target, and show error message with description of the problem (maybe file was renamed/moved but left in output directory)
23,408✔
290
            // todo: check if there are multiple columns on the same column applied.
23,408✔
291
            // todo: check column type if is missing in relational databases (throw new TypeORMError(`Column type of ${type} cannot be determined.`);)
23,408✔
292
            // todo: include driver-specific checks. for example in mongodb empty prefixes are not allowed
23,408✔
293
            // todo: if multiple columns with same name - throw exception, including cases when columns are in embeds with same prefixes or without prefix at all
23,408✔
294
            // todo: if multiple primary key used, at least one of them must be unique or @Index decorator must be set on entity
23,408✔
295
            // todo: check if entity with duplicate names, some decorators exist
23,408✔
296
        })
36,272✔
297

36,272✔
298
        // make sure cascade remove is not set for both sides of relationships (can be set in OneToOne decorators)
36,272✔
299
        entityMetadata.relations.forEach((relation) => {
36,272✔
300
            const isCircularCascadeRemove =
23,406✔
301
                relation.isCascadeRemove &&
23,406!
302
                relation.inverseRelation &&
23,406!
303
                relation.inverseRelation!.isCascadeRemove
1,352✔
304
            if (isCircularCascadeRemove)
23,406✔
305
                throw new TypeORMError(
23,406!
306
                    `Relation ${entityMetadata.name}#${
×
307
                        relation.propertyName
×
308
                    } and ${relation.inverseRelation!.entityMetadata.name}#${
×
309
                        relation.inverseRelation!.propertyName
×
310
                    } both has cascade remove set. ` +
×
311
                        `This may lead to unexpected circular removals. Please set cascade remove only from one side of relationship.`,
×
312
                )
×
313
        }) // todo: maybe better just deny removal from one to one relation without join column?
36,272✔
314
    }
36,272✔
315

26✔
316
    /**
26✔
317
     * Validates dependencies of the entity metadatas.
26✔
318
     */
26✔
319
    protected validateDependencies(entityMetadatas: EntityMetadata[]) {
26✔
320
        const graph = new DepGraph()
14,441✔
321
        entityMetadatas.forEach((entityMetadata) => {
14,441✔
322
            graph.addNode(entityMetadata.name)
36,092✔
323
        })
14,441✔
324
        entityMetadatas.forEach((entityMetadata) => {
14,441✔
325
            entityMetadata.relationsWithJoinColumns
36,092✔
326
                .filter((relation) => !relation.isNullable)
36,092✔
327
                .forEach((relation) => {
36,092✔
328
                    graph.addDependency(
473✔
329
                        entityMetadata.name,
473✔
330
                        relation.inverseEntityMetadata.name,
473✔
331
                    )
473✔
332
                })
36,092✔
333
        })
14,441✔
334
        try {
14,441✔
335
            graph.overallOrder()
14,441✔
336
        } catch (err) {
14,441✔
337
            throw new CircularRelationsError(
26✔
338
                err.toString().replace("Error: Dependency Cycle Found: ", ""),
26✔
339
            )
26✔
340
        }
26✔
341
    }
14,441✔
342

26✔
343
    /**
26✔
344
     * Validates eager relations to prevent circular dependency in them.
26✔
345
     */
26✔
346
    protected validateEagerRelations(entityMetadatas: EntityMetadata[]) {
26✔
347
        entityMetadatas.forEach((entityMetadata) => {
14,415✔
348
            entityMetadata.eagerRelations.forEach((relation) => {
35,936✔
349
                if (
1,410✔
350
                    relation.inverseRelation &&
1,410✔
351
                    relation.inverseRelation.isEager
838✔
352
                )
1,410✔
353
                    throw new TypeORMError(
1,410✔
354
                        `Circular eager relations are disallowed. ` +
26✔
355
                            `${entityMetadata.targetName}#${relation.propertyPath} contains "eager: true", and its inverse side ` +
26✔
356
                            `${relation.inverseEntityMetadata.targetName}#${relation.inverseRelation.propertyPath} contains "eager: true" as well.` +
26✔
357
                            ` Remove "eager: true" from one side of the relation.`,
26✔
358
                    )
26✔
359
            })
35,936✔
360
        })
14,415✔
361
    }
14,415✔
362
}
26✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc