validate method
List<ValidationIssue>
validate(
- SchemaDocument schema, {
- Map<
String, Map< nativeTypeRulesByProvider = const <String, Map<String, NativeTypeRule>>{},String, NativeTypeRule> >
Returns validation issues for schema.
Implementation
List<ValidationIssue> validate(
SchemaDocument schema, {
Map<String, Map<String, NativeTypeRule>> nativeTypeRulesByProvider =
const <String, Map<String, NativeTypeRule>>{},
}) {
final effectiveSchema = schema.withoutIgnored();
final issues = <ValidationIssue>[];
final seenModels = <String>{};
final seenEnums = <String>{};
final seenDatasources = <String>{};
final seenGenerators = <String>{};
// Tracks effective database table names to catch @@map conflicts.
final seenTableNames = <String>{};
for (final datasource in schema.datasources) {
if (!seenDatasources.add(datasource.name)) {
issues.add(
ValidationIssue(
modelName: datasource.name,
message: 'Duplicate datasource name.',
),
);
}
if (!_hasNonEmptyProperty(datasource.properties, 'provider')) {
issues.add(
ValidationIssue(
modelName: datasource.name,
message: 'Datasource must declare a provider.',
),
);
}
if (!_hasNonEmptyProperty(datasource.properties, 'url')) {
issues.add(
ValidationIssue(
modelName: datasource.name,
message: 'Datasource must declare a url.',
),
);
}
}
for (final generator in schema.generators) {
if (!seenGenerators.add(generator.name)) {
issues.add(
ValidationIssue(
modelName: generator.name,
message: 'Duplicate generator name.',
),
);
}
if (!_hasNonEmptyProperty(generator.properties, 'provider')) {
issues.add(
ValidationIssue(
modelName: generator.name,
message: 'Generator must declare a provider.',
),
);
}
}
for (final definition in schema.enums) {
if (!seenEnums.add(definition.name)) {
issues.add(
ValidationIssue(
modelName: definition.name,
message: 'Duplicate enum name.',
),
);
}
if (seenModels.contains(definition.name)) {
issues.add(
ValidationIssue(
modelName: definition.name,
message: 'Schema type name is already used by a model.',
),
);
}
final seenValues = <String>{};
for (final value in definition.values) {
if (!seenValues.add(value)) {
issues.add(
ValidationIssue(
modelName: definition.name,
fieldName: value,
message: 'Duplicate enum value.',
),
);
}
}
for (final attribute in definition.attributes) {
switch (attribute.name) {
case 'map':
final value = attribute.arguments['value'];
if (value == null || value.isEmpty) {
issues.add(
ValidationIssue(
modelName: definition.name,
message: '@@map requires a mapped type name.',
),
);
}
default:
issues.add(
ValidationIssue(
modelName: definition.name,
message: 'Unsupported enum attribute @@${attribute.name}.',
),
);
}
}
}
for (final model in effectiveSchema.models) {
if (!seenModels.add(model.name)) {
issues.add(
ValidationIssue(
modelName: model.name,
message: 'Duplicate model name.',
),
);
}
if (!seenTableNames.add(model.databaseName)) {
issues.add(
ValidationIssue(
modelName: model.name,
message:
'Database table name "${model.databaseName}" conflicts with another model.',
),
);
}
if (seenEnums.contains(model.name)) {
issues.add(
ValidationIssue(
modelName: model.name,
message: 'Schema type name is already used by an enum.',
),
);
}
final seenFields = <String>{};
// Tracks effective database column names to catch @map conflicts.
final seenDbFieldNames = <String>{};
var idCount = 0;
final modelId = model.attribute('id');
if (modelId != null) {
final idFields = _parseListArgument(
modelId.arguments['fields'] ?? modelId.arguments['value'] ?? '',
);
if (idFields.isEmpty) {
issues.add(
ValidationIssue(
modelName: model.name,
message: '@@id requires at least one field.',
),
);
}
_validateModelAttributeFields(
model: model,
attribute: modelId,
issues: issues,
);
}
for (final attribute in model.attributes) {
switch (attribute.name) {
case 'unique':
case 'index':
_validateModelAttributeFields(
model: model,
attribute: attribute,
issues: issues,
);
case 'map':
final value = attribute.arguments['value'];
if (value == null || value.isEmpty) {
issues.add(
ValidationIssue(
modelName: model.name,
message: '@@map requires a mapped table name.',
),
);
}
case 'ignore':
case 'id':
break;
default:
issues.add(
ValidationIssue(
modelName: model.name,
message: 'Unsupported model attribute @@${attribute.name}.',
),
);
}
}
for (final field in model.fields) {
if (!seenFields.add(field.name)) {
issues.add(
ValidationIssue(
modelName: model.name,
fieldName: field.name,
message: 'Duplicate field name.',
),
);
}
// Relation fields have no database column; skip the db-name check.
final isColumnField =
field.isScalar || effectiveSchema.findEnum(field.type) != null;
if (isColumnField && !seenDbFieldNames.add(field.databaseName)) {
issues.add(
ValidationIssue(
modelName: model.name,
fieldName: field.name,
message:
'Field database name "${field.databaseName}" conflicts with another field in model "${model.name}".',
),
);
}
if (field.isId) {
idCount++;
if (field.isNullable || field.isList) {
issues.add(
ValidationIssue(
modelName: model.name,
fieldName: field.name,
message: 'ID field must be singular and non-nullable.',
),
);
}
}
if (!field.isScalar &&
effectiveSchema.findEnum(field.type) == null &&
effectiveSchema.findModel(field.type) == null) {
issues.add(
ValidationIssue(
modelName: model.name,
fieldName: field.name,
message: 'Unknown relation target model "${field.type}".',
),
);
}
final relation = field.attribute('relation');
if (relation != null) {
_validateRelationArguments(
effectiveSchema,
model,
field,
relation,
issues,
);
}
final updatedAt = field.attribute('updatedAt');
if (updatedAt != null) {
if (updatedAt.arguments.isNotEmpty) {
issues.add(
ValidationIssue(
modelName: model.name,
fieldName: field.name,
message: '@updatedAt does not accept arguments.',
),
);
}
if (field.type != 'DateTime' || field.isList || field.isNullable) {
issues.add(
ValidationIssue(
modelName: model.name,
fieldName: field.name,
message:
'@updatedAt field must be a singular non-nullable DateTime.',
),
);
}
}
final nativeType = field.nativeTypeAttribute;
if (nativeType != null) {
final provider = _resolveNativeTypeProvider(schema);
final issue = _validateNativeType(
provider: provider,
nativeTypeRulesByProvider: nativeTypeRulesByProvider,
schema: schema,
model: model,
field: field,
nativeType: nativeType,
);
if (issue != null) {
issues.add(issue);
}
}
}
if (idCount == 0 && modelId == null) {
issues.add(
ValidationIssue(
modelName: model.name,
message: 'Model must have an @id field.',
),
);
}
if (idCount > 1) {
issues.add(
ValidationIssue(
modelName: model.name,
message:
'Use model-level @@id([fieldA, fieldB]) instead of multiple @id fields.',
),
);
}
if (idCount > 0 && modelId != null) {
issues.add(
ValidationIssue(
modelName: model.name,
message:
'Use either field-level @id or model-level @@id, not both.',
),
);
}
}
_validateRelationTopology(effectiveSchema, issues);
return issues
.map((issue) => _enrichIssue(schema, issue))
.toList(growable: false);
}