@contember/schema-utils 1.2.0-alpha.17 → 1.2.0-alpha.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/model/NamingHelper.js +2 -2
- package/dist/src/model/NamingHelper.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/src/type-schema/index.d.ts +1 -0
- package/dist/src/type-schema/index.d.ts.map +1 -1
- package/dist/src/type-schema/index.js +1 -0
- package/dist/src/type-schema/index.js.map +1 -1
- package/dist/src/type-schema/settings.d.ts +10 -0
- package/dist/src/type-schema/settings.d.ts.map +1 -0
- package/dist/src/type-schema/settings.js +32 -0
- package/dist/src/type-schema/settings.js.map +1 -0
- package/dist/src/validation/AclValidator.d.ts.map +1 -1
- package/dist/src/validation/AclValidator.js +11 -11
- package/dist/src/validation/AclValidator.js.map +1 -1
- package/dist/src/validation/ModelValidator.d.ts.map +1 -1
- package/dist/src/validation/ModelValidator.js +45 -37
- package/dist/src/validation/ModelValidator.js.map +1 -1
- package/dist/src/validation/SchemaValidator.d.ts +6 -2
- package/dist/src/validation/SchemaValidator.d.ts.map +1 -1
- package/dist/src/validation/SchemaValidator.js +7 -2
- package/dist/src/validation/SchemaValidator.js.map +1 -1
- package/dist/src/validation/ValidationValidator.js +4 -4
- package/dist/src/validation/ValidationValidator.js.map +1 -1
- package/dist/src/validation/errors.d.ts +3 -1
- package/dist/src/validation/errors.d.ts.map +1 -1
- package/dist/src/validation/errors.js +2 -2
- package/dist/src/validation/errors.js.map +1 -1
- package/dist/tests/cases/unit/modelValidator.test.js +118 -47
- package/dist/tests/cases/unit/modelValidator.test.js.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/index.ts +3 -1
- package/src/model/NamingHelper.ts +1 -1
- package/src/type-schema/index.ts +1 -0
- package/src/type-schema/settings.ts +8 -0
- package/src/validation/AclValidator.ts +10 -11
- package/src/validation/ModelValidator.ts +46 -37
- package/src/validation/SchemaValidator.ts +17 -4
- package/src/validation/ValidationValidator.ts +4 -4
- package/src/validation/errors.ts +25 -2
- package/tests/cases/unit/modelValidator.test.ts +121 -47
|
@@ -30,7 +30,7 @@ export class ModelValidator {
|
|
|
30
30
|
for (const [entityName, entity] of Object.entries(entities)) {
|
|
31
31
|
const entityErrors = errors.for(entityName)
|
|
32
32
|
if (entity.name !== entityName) {
|
|
33
|
-
entityErrors.add(`Entity name "${entity.name}" does not match the name in a map "${entityName}"`)
|
|
33
|
+
entityErrors.add('MODEL_NAME_MISMATCH', `Entity name "${entity.name}" does not match the name in a map "${entityName}"`)
|
|
34
34
|
}
|
|
35
35
|
this.validateEntity(entity, entityErrors)
|
|
36
36
|
}
|
|
@@ -43,7 +43,7 @@ export class ModelValidator {
|
|
|
43
43
|
const fieldErrors = errors.for(fieldName)
|
|
44
44
|
this.validateField(entity, field, fieldErrors)
|
|
45
45
|
if (field.name !== fieldName) {
|
|
46
|
-
fieldErrors.add(`Field name "${field.name}" does not match the name in a map "${fieldName}"`)
|
|
46
|
+
fieldErrors.add('MODEL_NAME_MISMATCH', `Field name "${field.name}" does not match the name in a map "${fieldName}"`)
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
this.validateUniqueConstraints(
|
|
@@ -57,12 +57,12 @@ export class ModelValidator {
|
|
|
57
57
|
for (const [constraintName, constraint] of Object.entries(uniqueConstraints)) {
|
|
58
58
|
const uniqueErrors = errors.for(constraintName)
|
|
59
59
|
if (constraint.name !== constraintName) {
|
|
60
|
-
uniqueErrors.add(`Constraint name ${constraint.name} does not match the name in a map "${constraintName}"`)
|
|
60
|
+
uniqueErrors.add('MODEL_NAME_MISMATCH', `Constraint name ${constraint.name} does not match the name in a map "${constraintName}"`)
|
|
61
61
|
continue
|
|
62
62
|
}
|
|
63
63
|
for (const field of constraint.fields) {
|
|
64
64
|
if (!fields.has(field)) {
|
|
65
|
-
uniqueErrors.add(`Referenced field ${field} in a constraint does not exists`)
|
|
65
|
+
uniqueErrors.add('MODEL_UNDEFINED_FIELD', `Referenced field ${field} in a constraint does not exists`)
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -74,7 +74,7 @@ export class ModelValidator {
|
|
|
74
74
|
this.validateRelation(partialEntity, field, errors)
|
|
75
75
|
} else {
|
|
76
76
|
if (field.sequence && field.nullable) {
|
|
77
|
-
errors.add('Column with sequence cannot be nullable.')
|
|
77
|
+
errors.add('MODEL_INVALID_COLUMN_DEFINITION', 'Column with sequence cannot be nullable.')
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -84,7 +84,7 @@ export class ModelValidator {
|
|
|
84
84
|
const targetEntityName = field.target
|
|
85
85
|
const targetEntity = this.model.entities[targetEntityName] || undefined
|
|
86
86
|
if (!targetEntity) {
|
|
87
|
-
return errors.add(`Target entity ${targetEntityName} not found`)
|
|
87
|
+
return errors.add('MODEL_UNDEFINED_ENTITY', `Target entity ${targetEntityName} not found`)
|
|
88
88
|
}
|
|
89
89
|
if (((it: Model.AnyRelation): it is Model.AnyRelation & Model.OrderableRelation => 'orderBy' in it)(field)) {
|
|
90
90
|
(field as Model.OrderableRelation).orderBy?.forEach(it => {
|
|
@@ -94,13 +94,13 @@ export class ModelValidator {
|
|
|
94
94
|
const orderByField = entity.fields[it.path[i]]
|
|
95
95
|
const pathStr = it.path.slice(0, i + 1).join('.')
|
|
96
96
|
if (!orderByField) {
|
|
97
|
-
errors.add(`Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not defined`)
|
|
97
|
+
errors.add('ACL_UNDEFINED_FIELD', `Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not defined`)
|
|
98
98
|
return
|
|
99
99
|
}
|
|
100
100
|
if (i + 1 < it.path.length) {
|
|
101
101
|
const targetEntity = getTargetEntity(this.model, entity, orderByField.name)
|
|
102
102
|
if (!targetEntity) {
|
|
103
|
-
errors.add(`Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not a relation`)
|
|
103
|
+
errors.add('MODEL_RELATION_REQUIRED', `Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not a relation`)
|
|
104
104
|
return
|
|
105
105
|
}
|
|
106
106
|
entity = targetEntity
|
|
@@ -110,20 +110,20 @@ export class ModelValidator {
|
|
|
110
110
|
}
|
|
111
111
|
if (partialEntity.view) {
|
|
112
112
|
if (field.type === Model.RelationType.ManyHasMany) {
|
|
113
|
-
return errors.add('Many-has-many relation is not allowed on a view entity.')
|
|
113
|
+
return errors.add('MODEL_INVALID_VIEW_USAGE', 'Many-has-many relation is not allowed on a view entity.')
|
|
114
114
|
}
|
|
115
115
|
if (
|
|
116
116
|
!targetEntity.view &&
|
|
117
117
|
field.type === Model.RelationType.OneHasMany
|
|
118
118
|
) {
|
|
119
|
-
return errors.add('One-has-many relation fields on views must point to a view entity.')
|
|
119
|
+
return errors.add('MODEL_INVALID_VIEW_USAGE', 'One-has-many relation fields on views must point to a view entity.')
|
|
120
120
|
}
|
|
121
121
|
if (
|
|
122
122
|
!targetEntity.view &&
|
|
123
123
|
field.type === Model.RelationType.OneHasOne &&
|
|
124
124
|
!('joiningColumn' in field)
|
|
125
125
|
) {
|
|
126
|
-
return errors.add('One-has-one relation fields on views must be owning or point to a view entity.')
|
|
126
|
+
return errors.add('MODEL_INVALID_VIEW_USAGE', 'One-has-one relation fields on views must be owning or point to a view entity.')
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
if (isInverseRelation(field)) {
|
|
@@ -131,70 +131,70 @@ export class ModelValidator {
|
|
|
131
131
|
const targetField = targetEntity.fields[ownedBy]
|
|
132
132
|
const relationDescription = `Target relation ${targetEntityName}::${ownedBy}:`
|
|
133
133
|
if (!targetField) {
|
|
134
|
-
return errors.add(`${relationDescription} not exists`)
|
|
134
|
+
return errors.add('MODEL_UNDEFINED_FIELD', `${relationDescription} not exists`)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
if (!isRelation(targetField)) {
|
|
138
|
-
return errors.add(`${relationDescription} not a relation`)
|
|
138
|
+
return errors.add('MODEL_RELATION_REQUIRED', `${relationDescription} not a relation`)
|
|
139
139
|
}
|
|
140
140
|
if (targetField.target !== entityName) {
|
|
141
|
-
return errors.add(`${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`)
|
|
141
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`)
|
|
142
142
|
}
|
|
143
143
|
if (!isOwningRelation(targetField)) {
|
|
144
|
-
errors.add(`${relationDescription} not an owning relation`)
|
|
144
|
+
errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} not an owning relation`)
|
|
145
145
|
return
|
|
146
146
|
}
|
|
147
147
|
if (!targetField.inversedBy) {
|
|
148
|
-
return errors.add(`${relationDescription} inverse relation is not set`)
|
|
148
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} inverse relation is not set`)
|
|
149
149
|
}
|
|
150
150
|
if (targetField.inversedBy !== field.name) {
|
|
151
|
-
return errors.add(`${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.inversedBy} given`)
|
|
151
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.inversedBy} given`)
|
|
152
152
|
}
|
|
153
153
|
if (field.type === Model.RelationType.OneHasOne && targetField.type !== Model.RelationType.OneHasOne) {
|
|
154
|
-
return errors.add(`${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
|
|
154
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
|
|
155
155
|
}
|
|
156
156
|
if (field.type === Model.RelationType.OneHasMany && targetField.type !== Model.RelationType.ManyHasOne) {
|
|
157
|
-
return errors.add(`${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`)
|
|
157
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`)
|
|
158
158
|
}
|
|
159
159
|
if (field.type === Model.RelationType.ManyHasMany && targetField.type !== Model.RelationType.ManyHasMany) {
|
|
160
|
-
return errors.add(`${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`)
|
|
160
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`)
|
|
161
161
|
}
|
|
162
162
|
} else {
|
|
163
163
|
const inversedBy = field.inversedBy
|
|
164
164
|
if (targetEntity.view) {
|
|
165
165
|
if ('joiningColumn' in field) {
|
|
166
|
-
return errors.add(`View entity ${targetEntity.name} cannot be referenced from an owning relation. Try switching the owning side.`)
|
|
166
|
+
return errors.add('MODEL_INVALID_VIEW_USAGE', `View entity ${targetEntity.name} cannot be referenced from an owning relation. Try switching the owning side.`)
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
if (inversedBy) {
|
|
170
170
|
const targetField = targetEntity.fields[inversedBy]
|
|
171
171
|
const relationDescription = `Target relation ${targetEntityName}::${inversedBy}:`
|
|
172
172
|
if (!targetField) {
|
|
173
|
-
return errors.add(`${relationDescription} not exists`)
|
|
173
|
+
return errors.add('MODEL_UNDEFINED_FIELD', `${relationDescription} not exists`)
|
|
174
174
|
}
|
|
175
175
|
if (!isRelation(targetField)) {
|
|
176
|
-
return errors.add(`${relationDescription} not a relation`)
|
|
176
|
+
return errors.add('MODEL_RELATION_REQUIRED', `${relationDescription} not a relation`)
|
|
177
177
|
}
|
|
178
178
|
if (targetField.target !== entityName) {
|
|
179
|
-
return errors.add(`${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`)
|
|
179
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`)
|
|
180
180
|
}
|
|
181
181
|
if (!isInverseRelation(targetField)) {
|
|
182
|
-
return errors.add(`${relationDescription} not an inverse relation`)
|
|
182
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} not an inverse relation`)
|
|
183
183
|
}
|
|
184
184
|
if (!targetField.ownedBy) {
|
|
185
|
-
return errors.add(`${relationDescription} owning relation is not set`)
|
|
185
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} owning relation is not set`)
|
|
186
186
|
}
|
|
187
187
|
if (targetField.ownedBy !== field.name) {
|
|
188
|
-
return errors.add(`${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.ownedBy} given`)
|
|
188
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.ownedBy} given`)
|
|
189
189
|
}
|
|
190
190
|
if (field.type === Model.RelationType.OneHasOne && targetField.type !== Model.RelationType.OneHasOne) {
|
|
191
|
-
return errors.add(`${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
|
|
191
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
|
|
192
192
|
}
|
|
193
193
|
if (field.type === Model.RelationType.ManyHasOne && targetField.type !== Model.RelationType.OneHasMany) {
|
|
194
|
-
return errors.add(`${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`)
|
|
194
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`)
|
|
195
195
|
}
|
|
196
196
|
if (field.type === Model.RelationType.ManyHasMany && targetField.type !== Model.RelationType.ManyHasMany) {
|
|
197
|
-
return errors.add(`${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`)
|
|
197
|
+
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`)
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
}
|
|
@@ -202,25 +202,34 @@ export class ModelValidator {
|
|
|
202
202
|
|
|
203
203
|
private validateIdentifier(value: string, errorBuilder: ErrorBuilder) {
|
|
204
204
|
if (!value.match(IDENTIFIER_PATTERN)) {
|
|
205
|
-
return errorBuilder.add(`${value} must match pattern ${IDENTIFIER_PATTERN.source}`)
|
|
205
|
+
return errorBuilder.add('MODEL_INVALID_IDENTIFIER', `${value} must match pattern ${IDENTIFIER_PATTERN.source}`)
|
|
206
206
|
}
|
|
207
207
|
if (RESERVED_WORDS.includes(value)) {
|
|
208
|
-
errorBuilder.add(`${value} is reserved word`)
|
|
208
|
+
errorBuilder.add('MODEL_INVALID_IDENTIFIER', `${value} is reserved word`)
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
private validateCollisions(entities: Model.Entity[], errorBuilder: ErrorBuilder) {
|
|
213
213
|
const relationNames: Record<string, string> = {}
|
|
214
214
|
const aliasedTypes = new Map<string, Model.ColumnType>()
|
|
215
|
+
const entityNames = new Set(entities.map(it => it.name))
|
|
215
216
|
for (const entity of entities) {
|
|
216
217
|
const description = `table name ${entity.tableName} of entity ${entity.name}`
|
|
217
218
|
if (relationNames[entity.tableName]) {
|
|
218
219
|
errorBuilder
|
|
219
220
|
.for(entity.name)
|
|
220
|
-
.add(`${description} collides with a ${relationNames[entity.tableName]}`)
|
|
221
|
+
.add('MODEL_NAME_COLLISION', `${description} collides with a ${relationNames[entity.tableName]}`)
|
|
221
222
|
} else {
|
|
222
223
|
relationNames[entity.tableName] = description
|
|
223
224
|
}
|
|
225
|
+
|
|
226
|
+
if (entity.name.endsWith('Meta')) {
|
|
227
|
+
const baseName = entity.name.substring(0, entity.name.length - 4)
|
|
228
|
+
if (entityNames.has(baseName)) {
|
|
229
|
+
errorBuilder.for(entity.name)
|
|
230
|
+
.add('MODEL_NAME_COLLISION', `entity ${entity.name} collides with entity ${baseName}, because a GraphQL type with "Meta" suffix is created for every entity`)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
224
233
|
}
|
|
225
234
|
for (const entity of entities) {
|
|
226
235
|
const entityErrorBuilder = errorBuilder.for(entity.name)
|
|
@@ -231,7 +240,7 @@ export class ModelValidator {
|
|
|
231
240
|
if (relationNames[joiningTable.tableName]) {
|
|
232
241
|
entityErrorBuilder
|
|
233
242
|
.for(relation.name)
|
|
234
|
-
.add(
|
|
243
|
+
.add('MODEL_NAME_COLLISION',
|
|
235
244
|
`${description} collides with a ${relationNames[joiningTable.tableName]}.` +
|
|
236
245
|
'Consider using plural for a relation name or change the joining table name using .joiningTable(...) in schema definition.',
|
|
237
246
|
)
|
|
@@ -246,7 +255,7 @@ export class ModelValidator {
|
|
|
246
255
|
if (aliasedTypes.has(column.typeAlias) && aliasedTypes.get(column.typeAlias) !== column.type) {
|
|
247
256
|
errorBuilder
|
|
248
257
|
.for(column.name)
|
|
249
|
-
.add(`Type alias ${column.typeAlias} already exists for base type ${column.type}`)
|
|
258
|
+
.add('MODEL_NAME_COLLISION', `Type alias ${column.typeAlias} already exists for base type ${column.type}`)
|
|
250
259
|
}
|
|
251
260
|
aliasedTypes.set(column.typeAlias, column.type)
|
|
252
261
|
},
|
|
@@ -262,7 +271,7 @@ export class ModelValidator {
|
|
|
262
271
|
for (const index of Object.values(entity.indexes)) {
|
|
263
272
|
const description = `index name ${index.name} of entity ${entity.name}`
|
|
264
273
|
if (relationNames[index.name]) {
|
|
265
|
-
entityErrorBuilder.add(`${description} collides with ${relationNames[index.name]}`)
|
|
274
|
+
entityErrorBuilder.add('MODEL_NAME_COLLISION', `${description} collides with ${relationNames[index.name]}`)
|
|
266
275
|
} else {
|
|
267
276
|
relationNames[index.name] = description
|
|
268
277
|
}
|
|
@@ -270,7 +279,7 @@ export class ModelValidator {
|
|
|
270
279
|
for (const unique of Object.values(entity.unique)) {
|
|
271
280
|
const description = `unique index name ${unique.name} of entity ${entity.name}`
|
|
272
281
|
if (relationNames[unique.name]) {
|
|
273
|
-
entityErrorBuilder.add(`${description} collides with ${relationNames[unique.name]}`)
|
|
282
|
+
entityErrorBuilder.add('MODEL_NAME_COLLISION', `${description} collides with ${relationNames[unique.name]}`)
|
|
274
283
|
} else {
|
|
275
284
|
relationNames[unique.name] = description
|
|
276
285
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { Schema } from '@contember/schema'
|
|
2
|
-
import { ValidationError } from './errors'
|
|
2
|
+
import { ValidationError, ValidationErrorCode } from './errors'
|
|
3
3
|
import { AclValidator } from './AclValidator'
|
|
4
4
|
import { ModelValidator } from './ModelValidator'
|
|
5
5
|
import { ValidationValidator } from './ValidationValidator'
|
|
6
6
|
|
|
7
|
+
export interface SchemaValidatorSkippedErrors {
|
|
8
|
+
code: ValidationErrorCode
|
|
9
|
+
path?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
export class SchemaValidator {
|
|
8
|
-
public static validate(schema: Schema): ValidationError[] {
|
|
13
|
+
public static validate(schema: Schema, skippedErrors: SchemaValidatorSkippedErrors[] = []): ValidationError[] {
|
|
9
14
|
const modelValidator = new ModelValidator(schema.model)
|
|
10
15
|
const modelErrors = modelValidator.validate()
|
|
11
16
|
|
|
@@ -15,7 +20,15 @@ export class SchemaValidator {
|
|
|
15
20
|
|
|
16
21
|
const validationValidator = new ValidationValidator(schema.model)
|
|
17
22
|
const validationErrors = validationValidator.validate(schema.validation)
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
const allErrors = [...aclErrors, ...modelErrors, ...validationErrors]
|
|
24
|
+
if (skippedErrors.length === 0) {
|
|
25
|
+
return allErrors
|
|
26
|
+
}
|
|
27
|
+
return allErrors.filter(
|
|
28
|
+
err => !skippedErrors.some(
|
|
29
|
+
rule => err.code === rule.code
|
|
30
|
+
&& (!rule.path || err.path.join('.') === rule.path),
|
|
31
|
+
),
|
|
32
|
+
)
|
|
20
33
|
}
|
|
21
34
|
}
|
|
@@ -11,7 +11,7 @@ export class ValidationValidator {
|
|
|
11
11
|
const entity = this.model.entities[entityName]
|
|
12
12
|
const entityErrorBuilder = errorBuilder.for(entityName)
|
|
13
13
|
if (!entity) {
|
|
14
|
-
entityErrorBuilder.add('Entity not found')
|
|
14
|
+
entityErrorBuilder.add('VALIDATION_UNDEFINED_ENTITY', 'Entity not found')
|
|
15
15
|
} else {
|
|
16
16
|
this.validateEntityRules(entityErrorBuilder, schema[entityName], entity)
|
|
17
17
|
}
|
|
@@ -28,7 +28,7 @@ export class ValidationValidator {
|
|
|
28
28
|
const field = entity.fields[fieldName]
|
|
29
29
|
const fieldErrorBuilder = errorBuilder.for(fieldName)
|
|
30
30
|
if (!field) {
|
|
31
|
-
fieldErrorBuilder.add('Field not found')
|
|
31
|
+
fieldErrorBuilder.add('VALIDATION_UNDEFINED_FIELD', 'Field not found')
|
|
32
32
|
} else {
|
|
33
33
|
this.validateFieldRules(fieldErrorBuilder, entitySchema[fieldName], entity, field)
|
|
34
34
|
}
|
|
@@ -60,7 +60,7 @@ export class ValidationValidator {
|
|
|
60
60
|
},
|
|
61
61
|
})
|
|
62
62
|
if (errorMessage) {
|
|
63
|
-
return errorBuilder.add(errorMessage)
|
|
63
|
+
return errorBuilder.add('VALIDATION_NOT_IMPLEMENTED', errorMessage)
|
|
64
64
|
}
|
|
65
65
|
this.validateValidator(errorBuilder.for('validator'), rule.validator, entity, field)
|
|
66
66
|
}
|
|
@@ -146,7 +146,7 @@ export class ValidationValidator {
|
|
|
146
146
|
visitRelation: () => true,
|
|
147
147
|
})
|
|
148
148
|
if (isRelation) {
|
|
149
|
-
errorBuilder.add('Rules depending on relations are currently not supported.')
|
|
149
|
+
errorBuilder.add('VALIDATION_NOT_IMPLEMENTED', 'Rules depending on relations are currently not supported.')
|
|
150
150
|
return undefined
|
|
151
151
|
}
|
|
152
152
|
return entity.fields[argument.path[0]]
|
package/src/validation/errors.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
|
+
export type ValidationErrorCode =
|
|
2
|
+
| 'MODEL_NAME_MISMATCH'
|
|
3
|
+
| 'MODEL_UNDEFINED_FIELD'
|
|
4
|
+
| 'MODEL_UNDEFINED_ENTITY'
|
|
5
|
+
| 'MODEL_RELATION_REQUIRED'
|
|
6
|
+
| 'MODEL_INVALID_RELATION_DEFINITION'
|
|
7
|
+
| 'MODEL_INVALID_COLUMN_DEFINITION'
|
|
8
|
+
| 'MODEL_INVALID_VIEW_USAGE'
|
|
9
|
+
| 'MODEL_INVALID_IDENTIFIER'
|
|
10
|
+
| 'MODEL_NAME_COLLISION'
|
|
11
|
+
|
|
12
|
+
| 'ACL_INVALID_CONDITION'
|
|
13
|
+
| 'ACL_UNDEFINED_VARIABLE'
|
|
14
|
+
| 'ACL_UNDEFINED_FIELD'
|
|
15
|
+
| 'ACL_UNDEFINED_PREDICATE'
|
|
16
|
+
| 'ACL_UNDEFINED_ROLE'
|
|
17
|
+
| 'ACL_UNDEFINED_ENTITY'
|
|
18
|
+
|
|
19
|
+
| 'VALIDATION_UNDEFINED_ENTITY'
|
|
20
|
+
| 'VALIDATION_UNDEFINED_FIELD'
|
|
21
|
+
| 'VALIDATION_NOT_IMPLEMENTED'
|
|
22
|
+
|
|
1
23
|
export interface ValidationError {
|
|
2
24
|
path: (string | number)[]
|
|
3
25
|
message: string
|
|
26
|
+
code: ValidationErrorCode
|
|
4
27
|
}
|
|
5
28
|
|
|
6
29
|
export class ErrorBuilder {
|
|
@@ -10,7 +33,7 @@ export class ErrorBuilder {
|
|
|
10
33
|
return new ErrorBuilder(this.errors, [...this.path, ...path])
|
|
11
34
|
}
|
|
12
35
|
|
|
13
|
-
add(message: string): void {
|
|
14
|
-
this.errors.push({ path: this.path, message })
|
|
36
|
+
add(code: ValidationErrorCode, message: string): void {
|
|
37
|
+
this.errors.push({ path: this.path, message, code })
|
|
15
38
|
}
|
|
16
39
|
}
|
|
@@ -3,67 +3,141 @@ import { Model } from '@contember/schema'
|
|
|
3
3
|
import { ModelValidator } from '../../../src'
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
|
|
7
|
+
test('index name collision', () => {
|
|
8
|
+
const model: Model.Schema = {
|
|
9
|
+
enums: {},
|
|
10
|
+
entities: {
|
|
11
|
+
Foo: {
|
|
12
|
+
fields: {
|
|
13
|
+
id: {
|
|
14
|
+
columnName: 'id',
|
|
15
|
+
name: 'id',
|
|
16
|
+
nullable: false,
|
|
17
|
+
type: Model.ColumnType.Uuid,
|
|
18
|
+
columnType: 'uuid',
|
|
19
|
+
},
|
|
17
20
|
},
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
test: { fields: ['id'], name: 'test' },
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
Bar: {
|
|
32
|
-
fields: {
|
|
33
|
-
id: {
|
|
34
|
-
columnName: 'id',
|
|
35
|
-
name: 'id',
|
|
36
|
-
nullable: false,
|
|
37
|
-
type: Model.ColumnType.Uuid,
|
|
38
|
-
columnType: 'uuid',
|
|
21
|
+
name: 'Foo',
|
|
22
|
+
primary: 'id',
|
|
23
|
+
primaryColumn: 'id',
|
|
24
|
+
tableName: 'foo',
|
|
25
|
+
unique: {},
|
|
26
|
+
eventLog: {
|
|
27
|
+
enabled: true,
|
|
28
|
+
},
|
|
29
|
+
indexes: {
|
|
30
|
+
test: { fields: ['id'], name: 'test' },
|
|
39
31
|
},
|
|
40
32
|
},
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
33
|
+
Bar: {
|
|
34
|
+
fields: {
|
|
35
|
+
id: {
|
|
36
|
+
columnName: 'id',
|
|
37
|
+
name: 'id',
|
|
38
|
+
nullable: false,
|
|
39
|
+
type: Model.ColumnType.Uuid,
|
|
40
|
+
columnType: 'uuid',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
name: 'Bar',
|
|
44
|
+
primary: 'id',
|
|
45
|
+
primaryColumn: 'id',
|
|
46
|
+
tableName: 'bar',
|
|
47
|
+
unique: {
|
|
48
|
+
test: { fields: ['id'], name: 'test' },
|
|
49
|
+
},
|
|
50
|
+
eventLog: {
|
|
51
|
+
enabled: true,
|
|
52
|
+
},
|
|
53
|
+
indexes: {
|
|
54
|
+
foo: { fields: ['id'], name: 'foo' },
|
|
55
|
+
},
|
|
53
56
|
},
|
|
54
57
|
},
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
test('index name collision', () => {
|
|
58
|
+
}
|
|
58
59
|
const validator = new ModelValidator(model)
|
|
59
60
|
assert.deepStrictEqual(validator.validate(), [
|
|
60
61
|
{
|
|
62
|
+
code: 'MODEL_NAME_COLLISION',
|
|
61
63
|
message: 'index name foo of entity Bar collides with table name foo of entity Foo',
|
|
62
64
|
path: ['entities', 'Bar'],
|
|
63
65
|
},
|
|
64
66
|
{
|
|
67
|
+
code: 'MODEL_NAME_COLLISION',
|
|
65
68
|
message: 'unique index name test of entity Bar collides with index name test of entity Foo',
|
|
66
69
|
path: ['entities', 'Bar'],
|
|
67
70
|
},
|
|
68
71
|
])
|
|
69
72
|
})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
test('"meta" collision', () => {
|
|
76
|
+
const model: Model.Schema = {
|
|
77
|
+
enums: {},
|
|
78
|
+
entities: {
|
|
79
|
+
Foo: {
|
|
80
|
+
fields: {
|
|
81
|
+
id: {
|
|
82
|
+
columnName: 'id',
|
|
83
|
+
name: 'id',
|
|
84
|
+
nullable: false,
|
|
85
|
+
type: Model.ColumnType.Uuid,
|
|
86
|
+
columnType: 'uuid',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
name: 'Foo',
|
|
90
|
+
primary: 'id',
|
|
91
|
+
primaryColumn: 'id',
|
|
92
|
+
tableName: 'foo',
|
|
93
|
+
unique: {},
|
|
94
|
+
eventLog: { enabled: true },
|
|
95
|
+
indexes: {},
|
|
96
|
+
},
|
|
97
|
+
FooMeta: {
|
|
98
|
+
fields: {
|
|
99
|
+
id: {
|
|
100
|
+
columnName: 'id',
|
|
101
|
+
name: 'id',
|
|
102
|
+
nullable: false,
|
|
103
|
+
type: Model.ColumnType.Uuid,
|
|
104
|
+
columnType: 'uuid',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
name: 'FooMeta',
|
|
108
|
+
primary: 'id',
|
|
109
|
+
primaryColumn: 'id',
|
|
110
|
+
tableName: 'foo_meta',
|
|
111
|
+
unique: {},
|
|
112
|
+
eventLog: { enabled: true },
|
|
113
|
+
indexes: {},
|
|
114
|
+
},
|
|
115
|
+
BarMeta: {
|
|
116
|
+
fields: {
|
|
117
|
+
id: {
|
|
118
|
+
columnName: 'id',
|
|
119
|
+
name: 'id',
|
|
120
|
+
nullable: false,
|
|
121
|
+
type: Model.ColumnType.Uuid,
|
|
122
|
+
columnType: 'uuid',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
name: 'BarMeta',
|
|
126
|
+
primary: 'id',
|
|
127
|
+
primaryColumn: 'id',
|
|
128
|
+
tableName: 'bar',
|
|
129
|
+
unique: {},
|
|
130
|
+
eventLog: { enabled: true },
|
|
131
|
+
indexes: {},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
const validator = new ModelValidator(model)
|
|
136
|
+
assert.deepStrictEqual(validator.validate(), [
|
|
137
|
+
{
|
|
138
|
+
code: 'MODEL_NAME_COLLISION',
|
|
139
|
+
message: 'entity FooMeta collides with entity Foo, because a GraphQL type with "Meta" suffix is created for every entity',
|
|
140
|
+
path: ['entities', 'FooMeta'],
|
|
141
|
+
},
|
|
142
|
+
])
|
|
143
|
+
})
|