-
Notifications
You must be signed in to change notification settings - Fork 308
Create Entities in-memory from Autoentities #3129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a9a4ea0
4acee5c
67e10d7
5453cbc
8fbea75
cf4debf
88b8c66
612d4bd
a396fab
c8ed0d5
947fdec
e5ae16a
2f256e4
6fca50d
8b88815
ba070db
5bd10a9
33f33c3
83d706f
9ab35a6
4a743c7
7570931
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,7 @@ public record RuntimeConfig | |
| [JsonPropertyName("azure-key-vault")] | ||
| public AzureKeyVaultOptions? AzureKeyVault { get; init; } | ||
|
|
||
| public RuntimeAutoentities? Autoentities { get; init; } | ||
| public RuntimeAutoentities Autoentities { get; init; } | ||
|
|
||
| public virtual RuntimeEntities Entities { get; init; } | ||
|
|
||
|
|
@@ -216,6 +216,8 @@ Runtime.GraphQL.FeatureFlags is not null && | |
|
|
||
| private Dictionary<string, string> _entityNameToDataSourceName = new(); | ||
|
|
||
| private Dictionary<string, string> _autoentityNameToDataSourceName = new(); | ||
|
|
||
| private Dictionary<string, string> _entityPathNameToEntityName = new(); | ||
|
|
||
| /// <summary> | ||
|
|
@@ -245,6 +247,21 @@ public bool TryGetEntityNameFromPath(string entityPathName, [NotNullWhen(true)] | |
| return _entityPathNameToEntityName.TryGetValue(entityPathName, out entityName); | ||
| } | ||
|
|
||
| public bool TryAddEntityNameToDataSourceName(string entityName) | ||
| { | ||
| return _entityNameToDataSourceName.TryAdd(entityName, this.DefaultDataSourceName); | ||
| } | ||
|
|
||
| public bool TryAddEntityNameToDataSourceName(string entityName, string autoEntityDefinition) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: rename the function to inform we are adding entity name from AutoentityDefinition |
||
| { | ||
| if (_autoentityNameToDataSourceName.TryGetValue(autoEntityDefinition, out string? dataSourceName)) | ||
| { | ||
| return _entityNameToDataSourceName.TryAdd(entityName, dataSourceName); | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Constructor for runtimeConfig. | ||
| /// To be used when setting up from cli json scenario. | ||
|
|
@@ -268,8 +285,8 @@ public RuntimeConfig( | |
| this.DataSource = DataSource; | ||
| this.Runtime = Runtime; | ||
| this.AzureKeyVault = AzureKeyVault; | ||
| this.Entities = Entities; | ||
| this.Autoentities = Autoentities; | ||
| this.Entities = Entities ?? new RuntimeEntities(new Dictionary<string, Entity>()); | ||
| this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary<string, Autoentity>()); | ||
| this.DefaultDataSourceName = Guid.NewGuid().ToString(); | ||
|
|
||
| if (this.DataSource is null) | ||
|
|
@@ -287,25 +304,38 @@ public RuntimeConfig( | |
| }; | ||
|
|
||
| _entityNameToDataSourceName = new Dictionary<string, string>(); | ||
| if (Entities is null) | ||
| if (Entities is null && this.Entities.Entities.Count == 0 && | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This condition will throw NullException
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not following why this would throw a NullException
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please rename |
||
| Autoentities is null && this.Autoentities.Autoentities.Count == 0) | ||
| { | ||
| throw new DataApiBuilderException( | ||
| message: "entities is a mandatory property in DAB Config", | ||
| message: "Configuration file should contain either at least the entities or autoentities property", | ||
| statusCode: HttpStatusCode.UnprocessableEntity, | ||
| subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); | ||
| } | ||
|
|
||
| foreach (KeyValuePair<string, Entity> entity in Entities) | ||
| if (Entities is not null) | ||
| { | ||
| foreach (KeyValuePair<string, Entity> entity in Entities) | ||
| { | ||
| _entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName); | ||
RubenCerna2079 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| if (Autoentities is not null) | ||
| { | ||
| _entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName); | ||
| foreach (KeyValuePair<string, Autoentity> autoentity in Autoentities) | ||
| { | ||
| _autoentityNameToDataSourceName.TryAdd(autoentity.Key, this.DefaultDataSourceName); | ||
| } | ||
| } | ||
|
|
||
| // Process data source and entities information for each database in multiple database scenario. | ||
| this.DataSourceFiles = DataSourceFiles; | ||
|
|
||
| if (DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null) | ||
| { | ||
| IEnumerable<KeyValuePair<string, Entity>> allEntities = Entities.AsEnumerable(); | ||
| IEnumerable<KeyValuePair<string, Entity>>? allEntities = Entities?.AsEnumerable(); | ||
| IEnumerable<KeyValuePair<string, Autoentity>>? allAutoentities = Autoentities?.AsEnumerable(); | ||
| // Iterate through all the datasource files and load the config. | ||
| IFileSystem fileSystem = new FileSystem(); | ||
| // This loader is not used as a part of hot reload and therefore does not need a handler. | ||
|
|
@@ -322,7 +352,9 @@ public RuntimeConfig( | |
| { | ||
| _dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); | ||
| _entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); | ||
| allEntities = allEntities.Concat(config.Entities.AsEnumerable()); | ||
| _autoentityNameToDataSourceName = _autoentityNameToDataSourceName.Concat(config._autoentityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); | ||
| allEntities = allEntities?.Concat(config.Entities.AsEnumerable()); | ||
| allAutoentities = allAutoentities?.Concat(config.Autoentities.AsEnumerable()); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
|
|
@@ -336,7 +368,8 @@ public RuntimeConfig( | |
| } | ||
| } | ||
|
|
||
| this.Entities = new RuntimeEntities(allEntities.ToDictionary(x => x.Key, x => x.Value)); | ||
| this.Entities = new RuntimeEntities(allEntities != null ? allEntities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary<string, Entity>()); | ||
| this.Autoentities = new RuntimeAutoentities(allAutoentities != null ? allAutoentities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary<string, Autoentity>()); | ||
| } | ||
|
|
||
| SetupDataSourcesUsed(); | ||
|
|
@@ -351,17 +384,19 @@ public RuntimeConfig( | |
| /// <param name="DataSource">Default datasource.</param> | ||
| /// <param name="Runtime">Runtime settings.</param> | ||
| /// <param name="Entities">Entities</param> | ||
| /// <param name="Autoentities">Autoentities</param> | ||
| /// <param name="DataSourceFiles">List of datasource files for multiple db scenario.Null for single db scenario. | ||
| /// <param name="DefaultDataSourceName">DefaultDataSourceName to maintain backward compatibility.</param> | ||
| /// <param name="DataSourceNameToDataSource">Dictionary mapping datasourceName to datasource object.</param> | ||
| /// <param name="EntityNameToDataSourceName">Dictionary mapping entityName to datasourceName.</param> | ||
| /// <param name="DataSourceFiles">Datasource files which represent list of child runtimeconfigs for multi-db scenario.</param> | ||
| public RuntimeConfig(string Schema, DataSource DataSource, RuntimeOptions Runtime, RuntimeEntities Entities, string DefaultDataSourceName, Dictionary<string, DataSource> DataSourceNameToDataSource, Dictionary<string, string> EntityNameToDataSourceName, DataSourceFiles? DataSourceFiles = null, AzureKeyVaultOptions? AzureKeyVault = null) | ||
| public RuntimeConfig(string Schema, DataSource DataSource, RuntimeOptions Runtime, RuntimeEntities Entities, string DefaultDataSourceName, Dictionary<string, DataSource> DataSourceNameToDataSource, Dictionary<string, string> EntityNameToDataSourceName, DataSourceFiles? DataSourceFiles = null, AzureKeyVaultOptions? AzureKeyVault = null, RuntimeAutoentities? Autoentities = null) | ||
| { | ||
| this.Schema = Schema; | ||
| this.DataSource = DataSource; | ||
| this.Runtime = Runtime; | ||
| this.Entities = Entities; | ||
| this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary<string, Autoentity>()); | ||
| this.DefaultDataSourceName = DefaultDataSourceName; | ||
| _dataSourceNameToDataSource = DataSourceNameToDataSource; | ||
| _entityNameToDataSourceName = EntityNameToDataSourceName; | ||
|
|
@@ -451,6 +486,17 @@ public DataSource GetDataSourceFromEntityName(string entityName) | |
| return _dataSourceNameToDataSource[_entityNameToDataSourceName[entityName]]; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets datasourceName from AutoentityNameToDatasourceName dictionary. | ||
| /// </summary> | ||
| /// <param name="autoentityName">autoentityName</param> | ||
| /// <returns>DataSourceName</returns> | ||
| public string GetDataSourceNameFromAutoentityName(string autoentityName) | ||
| { | ||
| CheckAutoentityNamePresent(autoentityName); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of 2 functions, why cant we use |
||
| return _autoentityNameToDataSourceName[autoentityName]; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Validates if datasource is present in runtimeConfig. | ||
| /// </summary> | ||
|
|
@@ -588,6 +634,17 @@ private void CheckEntityNamePresent(string entityName) | |
| } | ||
| } | ||
|
|
||
| private void CheckAutoentityNamePresent(string autoentityName) | ||
| { | ||
| if (!_autoentityNameToDataSourceName.ContainsKey(autoentityName)) | ||
| { | ||
| throw new DataApiBuilderException( | ||
| message: $"{autoentityName} is not a valid autoentity.", | ||
| statusCode: HttpStatusCode.NotFound, | ||
| subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound); | ||
| } | ||
| } | ||
|
|
||
| private void SetupDataSourcesUsed() | ||
| { | ||
| SqlDataSourceUsed = _dataSourceNameToDataSource.Values.Any | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -292,9 +292,93 @@ private bool TryResolveDbType(string sqlDbTypeName, out DbType dbType) | |
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| protected override async Task GenerateAutoentitiesIntoEntities() | ||
| protected override async Task GenerateAutoentitiesIntoEntities(Dictionary<string, Autoentity>? autoentities) | ||
| { | ||
| await Task.CompletedTask; | ||
| RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); | ||
| Dictionary<string, Entity> entities = new(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. define entities where its needed. Since autoentities could still be null, defining here is pre-mature if we are just going to return. |
||
| if (autoentities is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| foreach ((string autoentityName, Autoentity autoentity) in autoentities) | ||
| { | ||
| int addedEntities = 0; | ||
| JsonArray? resultArray = await QueryAutoentitiesAsync(autoentity); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when querying, are we asking the right datasource to query from? How is that information being passed to QueryAutoentitiesAsync? |
||
| if (resultArray is null) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| foreach (JsonObject? resultObject in resultArray) | ||
| { | ||
| if (resultObject is null) | ||
| { | ||
| throw new DataApiBuilderException( | ||
| message: $"Cannot create new entity from autoentity pattern due to an internal error.", | ||
| statusCode: HttpStatusCode.InternalServerError, | ||
| subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); | ||
| } | ||
|
|
||
| // Extract the entity name, schema, and database object name from the query result. | ||
| // The SQL query returns these values with placeholders already replaced. | ||
| string? entityName = resultObject["entity_name"]?.ToString(); | ||
| string? objectName = resultObject["object"]?.ToString(); | ||
| string? schemaName = resultObject["schema"]?.ToString(); | ||
|
|
||
| if (string.IsNullOrWhiteSpace(entityName) || string.IsNullOrWhiteSpace(objectName) || string.IsNullOrWhiteSpace(schemaName)) | ||
| { | ||
| _logger.LogError("Skipping autoentity generation: entity_name or object is null or empty for autoentity pattern '{AutoentityName}'.", autoentityName); | ||
| continue; | ||
| } | ||
|
|
||
| // Create the entity using the template settings and permissions from the autoentity configuration. | ||
| // Currently the source type is always Table for auto-generated entities from database objects. | ||
| Entity generatedEntity = new( | ||
| Source: new EntitySource( | ||
| Object: objectName, | ||
| Type: EntitySourceType.Table, | ||
| Parameters: null, | ||
| KeyFields: null), | ||
| GraphQL: autoentity.Template.GraphQL, | ||
| Rest: autoentity.Template.Rest, | ||
| Mcp: autoentity.Template.Mcp, | ||
| Permissions: autoentity.Permissions, | ||
| Cache: autoentity.Template.Cache, | ||
| Health: autoentity.Template.Health, | ||
| Fields: null, | ||
| Relationships: null, | ||
| Mappings: new()); | ||
|
|
||
| // Add the generated entity to the linking entities dictionary. | ||
| // This allows the entity to be processed later during metadata population. | ||
| if (!entities.TryAdd(entityName, generatedEntity) || !runtimeConfig.TryAddEntityNameToDataSourceName(entityName, autoentityName)) | ||
| { | ||
| throw new DataApiBuilderException( | ||
| message: $"Entity with name '{entityName}' already exists. Cannot create new entity from autoentity pattern with definition-name '{autoentityName}'.", | ||
| statusCode: HttpStatusCode.BadRequest, | ||
| subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); | ||
| } | ||
|
|
||
| if (runtimeConfig.IsRestEnabled) | ||
| { | ||
| _logger.LogInformation("[{entity}] REST path: {globalRestPath}/{entityRestPath}", entityName, runtimeConfig.RestPath, entityName); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is only Rest information being logged? |
||
| } | ||
| else | ||
| { | ||
| _logger.LogInformation(message: "REST calls are disabled for the entity: {entity}", entityName); | ||
| } | ||
|
|
||
| addedEntities++; | ||
| } | ||
|
|
||
| if (addedEntities == 0) | ||
| { | ||
| _logger.LogWarning($"No new entities were generated from the autoentity {autoentityName} defined in the configuration."); | ||
| } | ||
| } | ||
|
|
||
| _runtimeConfigProvider.AddMergedEntitiesToConfig(entities); | ||
| } | ||
|
|
||
| public async Task<JsonArray?> QueryAutoentitiesAsync(Autoentity autoentity) | ||
RubenCerna2079 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.