1
import {EntityMetadata} from "../metadata/EntityMetadata";
2 10
import {MissingPrimaryColumnError} from "../error/MissingPrimaryColumnError";
3 10
import {CircularRelationsError} from "../error/CircularRelationsError";
4 10
import {DepGraph} from "../util/DepGraph";
5
import {Driver} from "../driver/Driver";
6 10
import {DataTypeNotSupportedError} from "../error/DataTypeNotSupportedError";
7
import {ColumnType} from "../driver/types/ColumnTypes";
8 10
import {MongoDriver} from "../driver/mongodb/MongoDriver";
9 10
import {SqlServerDriver} from "../driver/sqlserver/SqlServerDriver";
10 10
import {MysqlDriver} from "../driver/mysql/MysqlDriver";
11 10
import {NoConnectionOptionError} from "../error/NoConnectionOptionError";
12 10
import {InitializedRelationError} from "../error/InitializedRelationError";
13 10
import {AuroraDataApiDriver} from "../driver/aurora-data-api/AuroraDataApiDriver";
14

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

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

32
/**
33
 * Validates built entity metadatas.
34
 */
35 10
export class EntityMetadataValidator {
36

37
    // -------------------------------------------------------------------------
38
    // Public Methods
39
    // -------------------------------------------------------------------------
40

41
    /**
42
     * Validates all given entity metadatas.
43
     */
44 10
    validateMany(entityMetadatas: EntityMetadata[], driver: Driver) {
45 10
        entityMetadatas.forEach(entityMetadata => this.validate(entityMetadata, entityMetadatas, driver));
46 10
        this.validateDependencies(entityMetadatas);
47 10
        this.validateEagerRelations(entityMetadatas);
48
    }
49

50
    /**
51
     * Validates given entity metadata.
52
     */
53 10
    validate(entityMetadata: EntityMetadata, allEntityMetadatas: EntityMetadata[], driver: Driver) {
54

55
        // check if table metadata has an id
56 10
        if (!entityMetadata.primaryColumns.length && !entityMetadata.isJunction)
57 0
            throw new MissingPrimaryColumnError(entityMetadata);
58

59
        // validate if table is using inheritance it has a discriminator
60
        // also validate if discriminator values are not empty and not repeated
61 10
        if (entityMetadata.inheritancePattern === "STI" || entityMetadata.tableType === "entity-child") {
62 10
            if (!entityMetadata.discriminatorColumn)
63 0
                throw new Error(`Entity ${entityMetadata.name} using single-table inheritance, it should also have a discriminator column. Did you forget to put discriminator column options?`);
64

65 10
            if (typeof entityMetadata.discriminatorValue === "undefined")
66 0
                throw new Error(`Entity ${entityMetadata.name} has an undefined discriminator value. Discriminator value should be defined.`);
67

68 10
            const sameDiscriminatorValueEntityMetadata = allEntityMetadatas.find(metadata => {
69 10
                return metadata !== entityMetadata
70 10
                    && (metadata.inheritancePattern === "STI" || metadata.tableType === "entity-child")
71 10
                    && metadata.discriminatorValue === entityMetadata.discriminatorValue
72 10
                    && metadata.inheritanceTree.some(parent => entityMetadata.inheritanceTree.indexOf(parent) !== -1);
73
            });
74 10
            if (sameDiscriminatorValueEntityMetadata)
75 0
                throw new Error(`Entities ${entityMetadata.name} and ${sameDiscriminatorValueEntityMetadata.name} have the same discriminator values. Make sure they are different while using the @ChildEntity decorator.`);
76
        }
77

78 10
        entityMetadata.relationCounts.forEach(relationCount => {
79 10
            if (relationCount.relation.isManyToOne || relationCount.relation.isOneToOne)
80 10
                throw new Error(`Relation count can not be implemented on ManyToOne or OneToOne relations.`);
81
        });
82

83 10
        if (!(driver instanceof MongoDriver)) {
84 10
            entityMetadata.columns.forEach(column => {
85 10
                const normalizedColumn = driver.normalizeType(column) as ColumnType;
86 10
                if (driver.supportedDataTypes.indexOf(normalizedColumn) === -1)
87 0
                    throw new DataTypeNotSupportedError(column, normalizedColumn, driver.options.type);
88 10
                if (column.length && driver.withLengthColumnTypes.indexOf(normalizedColumn) === -1)
89 0
                    throw new Error(`Column ${column.propertyName} of Entity ${entityMetadata.name} does not support length property.`);
90
            });
91
        }
92

93 10
        if (driver instanceof MysqlDriver || driver instanceof AuroraDataApiDriver) {
94 10
            const generatedColumns = entityMetadata.columns.filter(column => column.isGenerated && column.generationStrategy !== "uuid");
95 10
            if (generatedColumns.length > 1)
96 0
                throw new Error(`Error in ${entityMetadata.name} entity. There can be only one auto-increment column in MySql table.`);
97
        }
98

99
        // for mysql we are able to not define a default selected database, instead all entities can have their database
100
        // defined in their decorators. To make everything work either all entities must have database define and we
101
        // can live without database set in the connection options, either database in the connection options must be set
102 10
        if (driver instanceof MysqlDriver) {
103 10
            const metadatasWithDatabase = allEntityMetadatas.filter(metadata => metadata.database);
104 10
            if (metadatasWithDatabase.length === 0 && !driver.database)
105 0
                throw new NoConnectionOptionError("database");
106
        }
107

108 10
        if (driver instanceof SqlServerDriver) {
109 6
            const charsetColumns = entityMetadata.columns.filter(column => column.charset);
110 6
            if (charsetColumns.length > 1)
111 0
                throw new Error(`Character set specifying is not supported in Sql Server`);
112
        }
113

114
        // check if relations are all without initialized properties
115 10
        const entityInstance = entityMetadata.create();
116 10
        entityMetadata.relations.forEach(relation => {
117 10
            if (relation.isManyToMany || relation.isOneToMany) {
118

119
                // we skip relations for which persistence is disabled since initialization in them cannot harm somehow
120 10
                if (relation.persistenceEnabled === false)
121 10
                    return;
122

123
                // get entity relation value and check if its an array
124 10
                const relationInitializedValue = relation.getEntityValue(entityInstance);
125 10
                if (Array.isArray(relationInitializedValue))
126 10
                    throw new InitializedRelationError(relation);
127
            }
128
        });
129

130
        // validate relations
131 10
        entityMetadata.relations.forEach(relation => {
132

133
            // check join tables:
134
            // using JoinTable is possible only on one side of the many-to-many relation
135
            // todo(dima): fix
136
            // if (relation.joinTable) {
137
            //     if (!relation.isManyToMany)
138
            //         throw new UsingJoinTableIsNotAllowedError(entityMetadata, relation);
139

140
            //     // if there is inverse side of the relation, then check if it does not have join table too
141
            //     if (relation.hasInverseSide && relation.inverseRelation.joinTable)
142
            //         throw new UsingJoinTableOnlyOnOneSideAllowedError(entityMetadata, relation);
143
            // }
144

145
            // check join columns:
146
            // using JoinColumn is possible only on one side of the relation and on one-to-one, many-to-one relation types
147
            // first check if relation is one-to-one or many-to-one
148
            // todo(dima): fix
149
            /*if (relation.joinColumn) {
150

151
                // join column can be applied only on one-to-one and many-to-one relations
152
                if (!relation.isOneToOne && !relation.isManyToOne)
153
                    throw new UsingJoinColumnIsNotAllowedError(entityMetadata, relation);
154

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

159
                // check if join column really has referenced column
160
                if (relation.joinColumn && !relation.joinColumn.referencedColumn)
161
                    throw new Error(`Join column does not have referenced column set`);
162

163
            }
164

165
            // if its a one-to-one relation and JoinColumn is missing on both sides of the relation
166
            // or its one-side relation without JoinColumn we should give an error
167
            if (!relation.joinColumn && relation.isOneToOne && (!relation.hasInverseSide || !relation.inverseRelation.joinColumn))
168
                throw new MissingJoinColumnError(entityMetadata, relation);*/
169

170
            // if its a many-to-many relation and JoinTable is missing on both sides of the relation
171
            // or its one-side relation without JoinTable we should give an error
172
            // todo(dima): fix it
173
            // if (!relation.joinTable && relation.isManyToMany && (!relation.hasInverseSide || !relation.inverseRelation.joinTable))
174
            //     throw new MissingJoinTableError(entityMetadata, relation);
175

176

177
            // todo: validate if its one-to-one and side which does not have join column MUST have inverse side
178
            // todo: validate if its many-to-many and side which does not have join table MUST have inverse side
179
            // todo: if there is a relation, and inverse side is specified only on one side, shall we give error
180
            // todo: with message like: "Inverse side is specified only on one side of the relationship. Specify on other side too to prevent confusion".
181
            // 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)
182
            // todo: check if there are multiple columns on the same column applied.
183
            // todo: check column type if is missing in relational databases (throw new Error(`Column type of ${type} cannot be determined.`);)
184
            // todo: include driver-specific checks. for example in mongodb empty prefixes are not allowed
185
            // todo: if multiple columns with same name - throw exception, including cases when columns are in embeds with same prefixes or without prefix at all
186
            // todo: if multiple primary key used, at least one of them must be unique or @Index decorator must be set on entity
187
            // todo: check if entity with duplicate names, some decorators exist
188

189

190
        });
191

192
        // make sure cascade remove is not set for both sides of relationships (can be set in OneToOne decorators)
193 10
        entityMetadata.relations.forEach(relation => {
194 10
            const isCircularCascadeRemove = relation.isCascadeRemove && relation.inverseRelation && relation.inverseRelation!.isCascadeRemove;
195 10
            if (isCircularCascadeRemove)
196 0
                throw new Error(`Relation ${entityMetadata.name}#${relation.propertyName} and ${relation.inverseRelation!.entityMetadata.name}#${relation.inverseRelation!.propertyName} both has cascade remove set. ` +
197
                    `This may lead to unexpected circular removals. Please set cascade remove only from one side of relationship.`);
198
        }); // todo: maybe better just deny removal from one to one relation without join column?
199

200 10
        entityMetadata.eagerRelations.forEach(relation => {
201

202
        });
203
    }
204

205
    /**
206
     * Validates dependencies of the entity metadatas.
207
     */
208 10
    protected validateDependencies(entityMetadatas: EntityMetadata[]) {
209

210 10
        const graph = new DepGraph();
211 10
        entityMetadatas.forEach(entityMetadata => {
212 10
            graph.addNode(entityMetadata.name);
213
        });
214 10
        entityMetadatas.forEach(entityMetadata => {
215 10
            entityMetadata.relationsWithJoinColumns
216 10
                .filter(relation => !relation.isNullable)
217 10
                .forEach(relation => {
218 10
                    graph.addDependency(entityMetadata.name, relation.inverseEntityMetadata.name);
219
                });
220
        });
221 10
        try {
222 10
            graph.overallOrder();
223

224
        } catch (err) {
225 10
            throw new CircularRelationsError(err.toString().replace("Error: Dependency Cycle Found: ", ""));
226
        }
227
    }
228

229
    /**
230
     * Validates eager relations to prevent circular dependency in them.
231
     */
232 10
    protected validateEagerRelations(entityMetadatas: EntityMetadata[]) {
233 10
        entityMetadatas.forEach(entityMetadata => {
234 10
            entityMetadata.eagerRelations.forEach(relation => {
235 10
                if (relation.inverseRelation && relation.inverseRelation.isEager)
236 10
                    throw new Error(`Circular eager relations are disallowed. ` +
237
                        `${entityMetadata.targetName}#${relation.propertyPath} contains "eager: true", and its inverse side ` +
238
                        `${relation.inverseEntityMetadata.targetName}#${relation.inverseRelation.propertyPath} contains "eager: true" as well.` +
239
                        ` Remove "eager: true" from one side of the relation.`);
240
            });
241
        });
242
    }
243

244 10
}

Read our documentation on viewing source code .

Loading