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

typeorm / typeorm / 15800765516

21 Jun 2025 11:43PM UTC coverage: 76.418% (+0.04%) from 76.383%
15800765516

push

github

web-flow
feat(sap): add support for REAL_VECTOR and HALF_VECTOR data types in SAP HANA Cloud (#11526)

* feat(sap): add support for REAL_VECTOR data type

* feat(sap): add support for HALF_VECTOR data type

9294 of 12871 branches covered (72.21%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

19015 of 24174 relevant lines covered (78.66%)

119179.99 hits per line

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

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

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

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

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

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

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

61
        // if entity has multiple primary keys and uses custom constraint name,
62
        // then all primary keys should have the same constraint name
63
        if (entityMetadata.primaryColumns.length > 1) {
30,610✔
64
            const areConstraintNamesEqual = entityMetadata.primaryColumns.every(
4,704✔
65
                (columnMetadata, i, columnMetadatas) =>
66
                    columnMetadata.primaryKeyConstraintName ===
10,399✔
67
                    columnMetadatas[0].primaryKeyConstraintName,
68
            )
69
            if (!areConstraintNamesEqual) {
4,704!
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
        }
75

76
        // validate if table is using inheritance it has a discriminator
77
        // also validate if discriminator values are not empty and not repeated
78
        if (
30,610✔
79
            entityMetadata.inheritancePattern === "STI" ||
59,424✔
80
            entityMetadata.tableType === "entity-child"
81
        ) {
82
            if (!entityMetadata.discriminatorColumn)
1,796!
83
                throw new TypeORMError(
×
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

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

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

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

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

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

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

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

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

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

214
                // get entity relation value and check if its an array
215
                const relationInitializedValue =
216
                    relation.getEntityValue(entityInstance)
9,096✔
217
                if (Array.isArray(relationInitializedValue))
9,096✔
218
                    throw new InitializedRelationError(relation)
48✔
219
            }
220
        })
221

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

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

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

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

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

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

274
            }
275

276
            // if its a one-to-one relation and JoinColumn is missing on both sides of the relation
277
            // or its one-side relation without JoinColumn we should give an error
278
            if (!relation.joinColumn && relation.isOneToOne && (!relation.hasInverseSide || !relation.inverseRelation.joinColumn))
279
                throw new MissingJoinColumnError(entityMetadata, relation);*/
280
            // if its a many-to-many relation and JoinTable is missing on both sides of the relation
281
            // or its one-side relation without JoinTable we should give an error
282
            // todo(dima): fix it
283
            // if (!relation.joinTable && relation.isManyToMany && (!relation.hasInverseSide || !relation.inverseRelation.joinTable))
284
            //     throw new MissingJoinTableError(entityMetadata, relation);
285
            // todo: validate if its one-to-one and side which does not have join column MUST have inverse side
286
            // todo: validate if its many-to-many and side which does not have join table MUST have inverse side
287
            // todo: if there is a relation, and inverse side is specified only on one side, shall we give error
288
            // todo: with message like: "Inverse side is specified only on one side of the relationship. Specify on other side too to prevent confusion".
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)
290
            // todo: check if there are multiple columns on the same column applied.
291
            // todo: check column type if is missing in relational databases (throw new TypeORMError(`Column type of ${type} cannot be determined.`);)
292
            // todo: include driver-specific checks. for example in mongodb empty prefixes are not allowed
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
294
            // todo: if multiple primary key used, at least one of them must be unique or @Index decorator must be set on entity
295
            // todo: check if entity with duplicate names, some decorators exist
296
        })
297

298
        // make sure cascade remove is not set for both sides of relationships (can be set in OneToOne decorators)
299
        entityMetadata.relations.forEach((relation) => {
30,492✔
300
            const isCircularCascadeRemove =
301
                relation.isCascadeRemove &&
19,614✔
302
                relation.inverseRelation &&
303
                relation.inverseRelation!.isCascadeRemove
304
            if (isCircularCascadeRemove)
19,614!
305
                throw new TypeORMError(
×
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?
314
    }
315

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

343
    /**
344
     * Validates eager relations to prevent circular dependency in them.
345
     */
346
    protected validateEagerRelations(entityMetadatas: EntityMetadata[]) {
347
        entityMetadatas.forEach((entityMetadata) => {
12,175✔
348
            entityMetadata.eagerRelations.forEach((relation) => {
30,254✔
349
                if (
1,196✔
350
                    relation.inverseRelation &&
1,912✔
351
                    relation.inverseRelation.isEager
352
                )
353
                    throw new TypeORMError(
24✔
354
                        `Circular eager relations are disallowed. ` +
355
                            `${entityMetadata.targetName}#${relation.propertyPath} contains "eager: true", and its inverse side ` +
356
                            `${relation.inverseEntityMetadata.targetName}#${relation.inverseRelation.propertyPath} contains "eager: true" as well.` +
357
                            ` Remove "eager: true" from one side of the relation.`,
358
                    )
359
            })
360
        })
361
    }
362
}
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