Azure / data-api-builder

Data API builder provides modern REST and GraphQL endpoints to your Azure Databases and on-prem stores.
https://aka.ms/dab/docs
MIT License
786 stars 142 forks source link

[Bug]: Validating entity relationships throws unhandled exception #2184

Closed Aniruddh25 closed 1 month ago

Aniruddh25 commented 2 months ago

What happened?

Cannot figure out what's wrong with this sample config. Doing dab validate causes an Unhandled Exception with the stack trace as provided in the log output.

{
  "$schema": "https://dataapibuilder.azureedge.net/schemas/v0.5.34/dab.draft.schema.json",
  "data-source": {
    "database-type": "mssql",
    "options": {
      "set-session-context": false
    },
    "connection-string": "Server=XXXXX;Persist Security Info=False;User ID=<USERHERE>;Password=<PWD HERE> ;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
  },
  "runtime": {
    "rest": {
      "enabled": true,
      "path": "/api"
    },
    "graphql": {
      "allow-introspection": true,
      "enabled": true,
      "path": "/graphql"
    },
    "host": {
      "mode": "development",
      "cors": {
        "origins": [],
        "allow-credentials": false
      },
      "authentication": {
        "provider": "StaticWebApps"
      }
    }
  },
  "entities": {
    "Publisher": {
      "source": {
        "object": "publishers",
        "type": "table",
        "key-fields": ["id"]
      },
      "graphql": {
        "enabled": true,
        "type": {
          "singular": "Publisher",
          "plural": "Publishers"
        }
      },
      "rest": {
        "enabled": true
      },
      "permissions": [
        {
          "role": "anonymous",
          "actions": [
            {
              "action": "read"
            }
          ]
        },
        {
          "role": "authenticated",
          "actions": [
            {
              "action": "create"
            },
            {
              "action": "read"
            },
            {
              "action": "update"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "policy_tester_01",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.id eq 1940"
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "create"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "policy_tester_02",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.id ne 1940"
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "create"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "policy_tester_03",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.id ne 1940"
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "create"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "policy_tester_04",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.id eq 1940"
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "create"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "policy_tester_06",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.id eq 1940"
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "create"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "database_policy_tester",
          "actions": [
            {
              "action": "create",
              "policy": {
                "database": "@item.name ne 'New publisher'"
              }
            },
            {
              "action": "update",
              "policy": {
                "database": "@item.id ne 1234"
              }
            },
            {
              "action": "read",
              "policy": {
                "database": "@item.id ne 1234 or @item.id gt 1940"
              }
            }
          ]
        }
      ],
      "relationships": {
        "books": {
          "cardinality": "many",
          "target.entity": "Book",
          "source.fields": ["id"],
          "target.fields": ["publisher_id"],
          "linking.source.fields": [],
          "linking.target.fields": []
        }
      }
    },
    "Book": {
      "source": {
        "object": "books",
        "type": "table",
        "key-fields": ["id"]
      },
      "graphql": {
        "enabled": true,
        "type": {
          "singular": "book",
          "plural": "books"
        }
      },
      "rest": {
        "enabled": true
      },
      "permissions": [
        {
          "role": "anonymous",
          "actions": [
            {
              "action": "create"
            },
            {
              "action": "read"
            },
            {
              "action": "update"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "authenticated",
          "actions": [
            {
              "action": "create"
            },
            {
              "action": "read"
            },
            {
              "action": "update"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "policy_tester_01",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.title eq 'Policy-Test-01'"
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "create"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "policy_tester_02",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.title ne 'Policy-Test-01'"
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "create"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "policy_tester_03",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.title eq 'Policy-Test-01'"
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "create"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "policy_tester_04",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.title ne 'Policy-Test-01'"
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "create"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "policy_tester_05",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.id ne 9"
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "create"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "policy_tester_06",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.id ne 10"
              }
            },
            {
              "action": "create"
            },
            {
              "action": "delete"
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            }
          ]
        },
        {
          "role": "policy_tester_07",
          "actions": [
            {
              "action": "delete",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.id ne 9"
              }
            },
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.id ne 9"
              }
            },
            {
              "action": "create"
            }
          ]
        },
        {
          "role": "policy_tester_08",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              }
            },
            {
              "action": "delete",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.id eq 9"
              }
            },
            {
              "action": "update",
              "fields": {
                "exclude": [],
                "include": [
                  "*"
                ]
              },
              "policy": {
                "database": "@item.id eq 9"
              }
            },
            {
              "action": "create"
            }
          ]
        },
        {
          "role": "test_role_with_noread",
          "actions": [
            {
              "action": "create"
            },
            {
              "action": "update"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "test_role_with_excluded_fields",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [
                  "publisher_id"
                ]
              }
            },
            {
              "action": "create"
            },
            {
              "action": "update"
            },
            {
              "action": "delete"
            }
          ]
        },
        {
          "role": "test_role_with_policy_excluded_fields",
          "actions": [
            {
              "action": "read",
              "fields": {
                "exclude": [
                  "publisher_id"
                ]
              },
              "policy": {
                "database": "@item.title ne 'Test'"
              }
            },
            {
              "action": "create"
            },
            {
              "action": "update"
            },
            {
              "action": "delete"
            }
          ]
        }
      ],
      "mappings": {
        "id": "id",
        "title": "title"
      },
      "relationships": {
        "publishers": {
          "cardinality": "one",
          "target.entity": "Publisher",
          "source.fields": ["publisher_id"],
          "target.fields": ["id"],
          "linking.source.fields": [],
          "linking.target.fields": []
        }
      }
    }

  }
}

Version

0.12.0-rc

What database are you using?

Azure SQL

What hosting model are you using?

Local (including CLI)

Which API approach are you accessing DAB through?

No response

Relevant log output

Information: Validating entity relationships.
Unhandled exception. System.AggregateException: One or more errors occurred. (Value cannot be null. (Parameter 'key'))
 ---> System.ArgumentNullException: Value cannot be null. (Parameter 'key')
   at System.Collections.Generic.Dictionary`2.FindValue(TKey key)
   at System.Collections.Generic.Dictionary`2.TryGetValue(TKey key, TValue& value)
   at Azure.DataApiBuilder.Core.Services.SqlMetadataProvider`3.TryGetBackingColumn(String entityName, String field, String& name) in /_/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs:line 220
   at Azure.DataApiBuilder.Core.Configurations.RuntimeConfigValidator.GetFieldsNotBackedByColumnsInDB(List`1 invalidColumns, String[] fields, String entityName, ISqlMetadataProvider sqlMetadataProvider) in /_/src/Core/Configurations/RuntimeConfigValidator.cs:line 1093
   at Azure.DataApiBuilder.Core.Configurations.RuntimeConfigValidator.ValidateRelationshipsInConfig(RuntimeConfig runtimeConfig, IMetadataProviderFactory sqlMetadataProviderFactory) in /_/src/Core/Configurations/RuntimeConfigValidator.cs:line 875
   at Azure.DataApiBuilder.Core.Configurations.RuntimeConfigValidator.ValidateEntitiesMetadata(RuntimeConfig runtimeConfig, ILoggerFactory loggerFactory) in /_/src/Core/Configurations/RuntimeConfigValidator.cs:line 232      
   at Azure.DataApiBuilder.Core.Configurations.RuntimeConfigValidator.TryValidateConfig(String configFilePath, ILoggerFactory loggerFactory) in /_/src/Core/Configurations/RuntimeConfigValidator.cs:line 169
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at System.Threading.Tasks.Task`1.get_Result()
   at Cli.ConfigGenerator.IsConfigValid(ValidateOptions options, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem) in /_/src/Cli/ConfigGenerator.cs:line 1132
   at Cli.Commands.ValidateOptions.Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem) in /_/src/Cli/Commands/ValidateOptions.cs:line 31
   at Cli.Program.<>c__DisplayClass2_0.<Execute>b__5(ValidateOptions options) in /_/src/Cli/Program.cs:line 67
   at CommandLine.ParserResultExtensions.MapResult[T1,T2,T3,T4,T5,T6,T7,TResult](ParserResult`1 result, Func`2 parsedFunc1, Func`2 parsedFunc2, Func`2 parsedFunc3, Func`2 parsedFunc4, Func`2 parsedFunc5, Func`2 parsedFunc6, Func`2 parsedFunc7, Func`2 notParsedFunc)
   at Cli.Program.Execute(String[] args, ILogger cliLogger, IFileSystem fileSystem, FileSystemRuntimeConfigLoader loader) in /_/src/Cli/Program.cs:line 61
   at Cli.Program.Main(String[] args) in /_/src/Cli/Program.cs:line 41

Code of Conduct

seantleonard commented 2 months ago

Does "$schema": "https://dataapibuilder.azureedge.net/schemas/v0.5.34/dab.draft.schema.json", not cause issues with JSON validation given that the version used is 0.5.34 instead of "$schema": "https://github.com/Azure/data-api-builder/releases/download/v0.12.0-rc/dab.draft.schema.json",

seantleonard commented 2 months ago

I get a different stack trace:

Information: Validating entity relationships. Unhandled exception. System.AggregateException: One or more errors occurred. (The given key 'Publisher' was not present in the dictionary.) ---> System.Collections.Generic.KeyNotFoundException: The given key 'Publisher' was not present in the dictionary. at System.Collections.Generic.Dictionary2.get_Item(TKey key) at Azure.DataApiBuilder.Core.Services.SqlMetadataProvider3.TryGetBackingColumn(String entityName, String field, String& name) in //src/Core/Services/MetadataProviders/SqlMetadataProvider.cs:line 220 at Azure.DataApiBuilder.Core.Configurations.RuntimeConfigValidator.GetFieldsNotBackedByColumnsInDB(List`1 invalidColumns, String[] fields, String entityName, ISqlMetadataProvider sqlMetadataProvider) in //src/Core/Configurations/RuntimeConfigValidator.cs:line 1093 at Azure.DataApiBuilder.Core.Configurations.RuntimeConfigValidator.ValidateRelationshipsInConfig(RuntimeConfig runtimeConfig, IMetadataProviderFactory sqlMetadataProviderFactory) in //src/Core/Configurations/RuntimeConfigValidator.cs:line 875 at Azure.DataApiBuilder.Core.Configurations.RuntimeConfigValidator.ValidateEntitiesMetadata(RuntimeConfig runtimeConfig, ILoggerFactory loggerFactory) in //src/Core/Configurations/RuntimeConfigValidator.cs:line 232 at Azure.DataApiBuilder.Core.Configurations.RuntimeConfigValidator.TryValidateConfig(String configFilePath, ILoggerFactory loggerFactory) in /_/src/Core/Configurations/RuntimeConfigValidator.cs:line 169

seantleonard commented 2 months ago

This issue looks like it stems from collections in the metadataprovider not being fully populated at time of validation: image

seantleonard commented 2 months ago

this was not caught presumably because it doesn't occur on dab start and only appears during dab validate

seantleonard commented 1 month ago

This seems to be caused by SqlMetadataProvider::GenerateExposedToBackingColumnMapsForEntities() being outside the scope of _isValidateOnly code.

Image

seantleonard commented 1 month ago

The above comment holds true when the dab-config.json you provide to dab validate --config "path" has an invalid connection string. Because dab validate accumulates all errors instead of halting at first error, it attempts to find out everything wrong with the config. That won't work when dab can't connect to the database and proceed to validate your config against db metadata.

@Aniruddh25 , can you confirm whether your config had a valid connection string or if you were using an environment variable placeholder for your connection string?

Agreed that the error message isn't helpful so one path forward here is:

aaronburtle commented 1 month ago

The above comment holds true when the dab-config.json you provide to dab validate --config "path" has an invalid connection string. Because dab validate accumulates all errors instead of halting at first error, it attempts to find out everything wrong with the config. That won't work when dab can't connect to the database and proceed to validate your config against db metadata.

@Aniruddh25 , can you confirm whether your config had a valid connection string or if you were using an environment variable placeholder for your connection string?

Agreed that the error message isn't helpful so one path forward here is:

  • halt validation when the connection string is invalid/can't connect to DB. When DAB can't connect to DB, DAB can't confirm whether config is correct. (@abhishekkumams , thoughts?

I found the same in my investiation. This only arises when the connection string is invalid. My thought was to add a try catch to the validation steps that rely on the connection string and then some messaging to help explain the problem.