Skip to content

Commit a4fe457

Browse files
Create Entities in-memory from Autoentities (#3129)
## Why make this change? - #3052 We need to generate all the entities from the autoentities properties. In order to do this we need to use the query that was previously created and, add the newly generated entities into the runtime so the user can use them. ## What is this change? - `MsSqlMetadataProvider.cs`: Finish creating the `GenerateAutoentitiesIntoEntities` function so that it uses the query to receive all of the tables and turn them into entities inside the runtimeConfig. - `RuntimeConfig.cs`: Create new function that adds the new entities to the runtimeConfig. And also change the runtimeConfig to allow for the `entities` property to be missing if the user decides to use the `autoentities` property. ## How was this tested? - [ ] Integration Tests - [X] Unit Tests
1 parent 105fe18 commit a4fe457

16 files changed

Lines changed: 469 additions & 40 deletions

src/Cli.Tests/ConfigGeneratorTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ public void TestSpecialCharactersInConnectionString()
178178
""mode"": ""production""
179179
}
180180
},
181+
""autoentities"": {},
181182
""entities"": {}
182183
}");
183184

src/Cli.Tests/ModuleInitializer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ public static void Init()
107107
VerifierSettings.IgnoreMember<GraphQLRuntimeOptions>(options => options.FeatureFlags);
108108
// Ignore the JSON schema path as that's unimportant from a test standpoint.
109109
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.Schema);
110+
// Ignore the JSON schema path as that's unimportant from a test standpoint.
111+
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.Autoentities);
110112
// Ignore the message as that's not serialized in our config file anyway.
111113
VerifierSettings.IgnoreMember<DataSource>(dataSource => dataSource.DatabaseTypeNotSupportedMessage);
112114
// Ignore DefaultDataSourceName as that's not serialized in our config file.

src/Cli.Tests/TestHelper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,7 @@ public static string GenerateConfigWithGivenDepthLimit(string? depthLimitJson =
13021302
}}
13031303
}}
13041304
}},
1305+
""autoentities"": {{}},
13051306
""entities"": {{}}";
13061307

13071308
return $"{{{SAMPLE_SCHEMA_DATA_SOURCE},{runtimeSection}}}";

src/Config/Converters/RuntimeAutoentitiesConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class RuntimeAutoentitiesConverter : JsonConverter<RuntimeAutoentities>
2929
public override void Write(Utf8JsonWriter writer, RuntimeAutoentities value, JsonSerializerOptions options)
3030
{
3131
writer.WriteStartObject();
32-
foreach ((string key, Autoentity autoEntity) in value.AutoEntities)
32+
foreach ((string key, Autoentity autoEntity) in value.Autoentities)
3333
{
3434
writer.WritePropertyName(key);
3535
JsonSerializer.Serialize(writer, autoEntity, options);
Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Collections;
45
using System.Text.Json.Serialization;
56
using Azure.DataApiBuilder.Config.Converters;
67

@@ -10,19 +11,29 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
1011
/// Represents a collection of <see cref="Autoentity"/> available from the RuntimeConfig.
1112
/// </summary>
1213
[JsonConverter(typeof(RuntimeAutoentitiesConverter))]
13-
public record RuntimeAutoentities
14+
public record RuntimeAutoentities : IEnumerable<KeyValuePair<string, Autoentity>>
1415
{
1516
/// <summary>
1617
/// The collection of <see cref="Autoentity"/> available from the RuntimeConfig.
1718
/// </summary>
18-
public IReadOnlyDictionary<string, Autoentity> AutoEntities { get; init; }
19+
public IReadOnlyDictionary<string, Autoentity> Autoentities { get; init; }
1920

2021
/// <summary>
2122
/// Creates a new instance of the <see cref="RuntimeAutoentities"/> class using a collection of entities.
2223
/// </summary>
23-
/// <param name="autoEntities">The collection of auto-entities to map to RuntimeAutoentities.</param>
24-
public RuntimeAutoentities(IReadOnlyDictionary<string, Autoentity> autoEntities)
24+
/// <param name="autoentities">The collection of auto-entities to map to RuntimeAutoentities.</param>
25+
public RuntimeAutoentities(IReadOnlyDictionary<string, Autoentity> autoentities)
2526
{
26-
AutoEntities = autoEntities;
27+
Autoentities = autoentities;
28+
}
29+
30+
public IEnumerator<KeyValuePair<string, Autoentity>> GetEnumerator()
31+
{
32+
return Autoentities.GetEnumerator();
33+
}
34+
35+
IEnumerator IEnumerable.GetEnumerator()
36+
{
37+
return GetEnumerator();
2738
}
2839
}

src/Config/ObjectModel/RuntimeConfig.cs

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public record RuntimeConfig
2525
[JsonPropertyName("azure-key-vault")]
2626
public AzureKeyVaultOptions? AzureKeyVault { get; init; }
2727

28-
public RuntimeAutoentities? Autoentities { get; init; }
28+
public RuntimeAutoentities Autoentities { get; init; }
2929

3030
public virtual RuntimeEntities Entities { get; init; }
3131

@@ -216,6 +216,8 @@ Runtime.GraphQL.FeatureFlags is not null &&
216216

217217
private Dictionary<string, string> _entityNameToDataSourceName = new();
218218

219+
private Dictionary<string, string> _autoentityNameToDataSourceName = new();
220+
219221
private Dictionary<string, string> _entityPathNameToEntityName = new();
220222

221223
/// <summary>
@@ -245,6 +247,21 @@ public bool TryGetEntityNameFromPath(string entityPathName, [NotNullWhen(true)]
245247
return _entityPathNameToEntityName.TryGetValue(entityPathName, out entityName);
246248
}
247249

250+
public bool TryAddEntityNameToDataSourceName(string entityName)
251+
{
252+
return _entityNameToDataSourceName.TryAdd(entityName, this.DefaultDataSourceName);
253+
}
254+
255+
public bool TryAddGeneratedAutoentityNameToDataSourceName(string entityName, string autoEntityDefinition)
256+
{
257+
if (_autoentityNameToDataSourceName.TryGetValue(autoEntityDefinition, out string? dataSourceName))
258+
{
259+
return _entityNameToDataSourceName.TryAdd(entityName, dataSourceName);
260+
}
261+
262+
return false;
263+
}
264+
248265
/// <summary>
249266
/// Constructor for runtimeConfig.
250267
/// To be used when setting up from cli json scenario.
@@ -268,8 +285,8 @@ public RuntimeConfig(
268285
this.DataSource = DataSource;
269286
this.Runtime = Runtime;
270287
this.AzureKeyVault = AzureKeyVault;
271-
this.Entities = Entities;
272-
this.Autoentities = Autoentities;
288+
this.Entities = Entities ?? new RuntimeEntities(new Dictionary<string, Entity>());
289+
this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary<string, Autoentity>());
273290
this.DefaultDataSourceName = Guid.NewGuid().ToString();
274291

275292
if (this.DataSource is null)
@@ -287,25 +304,38 @@ public RuntimeConfig(
287304
};
288305

289306
_entityNameToDataSourceName = new Dictionary<string, string>();
290-
if (Entities is null)
307+
if (Entities is null && this.Entities.Entities.Count == 0 &&
308+
Autoentities is null && this.Autoentities.Autoentities.Count == 0)
291309
{
292310
throw new DataApiBuilderException(
293-
message: "entities is a mandatory property in DAB Config",
311+
message: "Configuration file should contain either at least the entities or autoentities property",
294312
statusCode: HttpStatusCode.UnprocessableEntity,
295313
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError);
296314
}
297315

298-
foreach (KeyValuePair<string, Entity> entity in Entities)
316+
if (Entities is not null)
317+
{
318+
foreach (KeyValuePair<string, Entity> entity in Entities)
319+
{
320+
_entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName);
321+
}
322+
}
323+
324+
if (Autoentities is not null)
299325
{
300-
_entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName);
326+
foreach (KeyValuePair<string, Autoentity> autoentity in Autoentities)
327+
{
328+
_autoentityNameToDataSourceName.TryAdd(autoentity.Key, this.DefaultDataSourceName);
329+
}
301330
}
302331

303332
// Process data source and entities information for each database in multiple database scenario.
304333
this.DataSourceFiles = DataSourceFiles;
305334

306335
if (DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null)
307336
{
308-
IEnumerable<KeyValuePair<string, Entity>> allEntities = Entities.AsEnumerable();
337+
IEnumerable<KeyValuePair<string, Entity>>? allEntities = Entities?.AsEnumerable();
338+
IEnumerable<KeyValuePair<string, Autoentity>>? allAutoentities = Autoentities?.AsEnumerable();
309339
// Iterate through all the datasource files and load the config.
310340
IFileSystem fileSystem = new FileSystem();
311341
// This loader is not used as a part of hot reload and therefore does not need a handler.
@@ -322,7 +352,9 @@ public RuntimeConfig(
322352
{
323353
_dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
324354
_entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
325-
allEntities = allEntities.Concat(config.Entities.AsEnumerable());
355+
_autoentityNameToDataSourceName = _autoentityNameToDataSourceName.Concat(config._autoentityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
356+
allEntities = allEntities?.Concat(config.Entities.AsEnumerable());
357+
allAutoentities = allAutoentities?.Concat(config.Autoentities.AsEnumerable());
326358
}
327359
catch (Exception e)
328360
{
@@ -336,7 +368,8 @@ public RuntimeConfig(
336368
}
337369
}
338370

339-
this.Entities = new RuntimeEntities(allEntities.ToDictionary(x => x.Key, x => x.Value));
371+
this.Entities = new RuntimeEntities(allEntities != null ? allEntities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary<string, Entity>());
372+
this.Autoentities = new RuntimeAutoentities(allAutoentities != null ? allAutoentities.ToDictionary(x => x.Key, x => x.Value) : new Dictionary<string, Autoentity>());
340373
}
341374

342375
SetupDataSourcesUsed();
@@ -351,17 +384,19 @@ public RuntimeConfig(
351384
/// <param name="DataSource">Default datasource.</param>
352385
/// <param name="Runtime">Runtime settings.</param>
353386
/// <param name="Entities">Entities</param>
387+
/// <param name="Autoentities">Autoentities</param>
354388
/// <param name="DataSourceFiles">List of datasource files for multiple db scenario.Null for single db scenario.
355389
/// <param name="DefaultDataSourceName">DefaultDataSourceName to maintain backward compatibility.</param>
356390
/// <param name="DataSourceNameToDataSource">Dictionary mapping datasourceName to datasource object.</param>
357391
/// <param name="EntityNameToDataSourceName">Dictionary mapping entityName to datasourceName.</param>
358392
/// <param name="DataSourceFiles">Datasource files which represent list of child runtimeconfigs for multi-db scenario.</param>
359-
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)
393+
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)
360394
{
361395
this.Schema = Schema;
362396
this.DataSource = DataSource;
363397
this.Runtime = Runtime;
364398
this.Entities = Entities;
399+
this.Autoentities = Autoentities ?? new RuntimeAutoentities(new Dictionary<string, Autoentity>());
365400
this.DefaultDataSourceName = DefaultDataSourceName;
366401
_dataSourceNameToDataSource = DataSourceNameToDataSource;
367402
_entityNameToDataSourceName = EntityNameToDataSourceName;
@@ -451,6 +486,24 @@ public DataSource GetDataSourceFromEntityName(string entityName)
451486
return _dataSourceNameToDataSource[_entityNameToDataSourceName[entityName]];
452487
}
453488

489+
/// <summary>
490+
/// Gets datasourceName from AutoentityNameToDatasourceName dictionary.
491+
/// </summary>
492+
/// <param name="autoentityName">autoentityName</param>
493+
/// <returns>DataSourceName</returns>
494+
public string GetDataSourceNameFromAutoentityName(string autoentityName)
495+
{
496+
if (!_autoentityNameToDataSourceName.TryGetValue(autoentityName, out string? autoentityDataSource))
497+
{
498+
throw new DataApiBuilderException(
499+
message: $"{autoentityName} is not a valid autoentity.",
500+
statusCode: HttpStatusCode.NotFound,
501+
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
502+
}
503+
504+
return autoentityDataSource;
505+
}
506+
454507
/// <summary>
455508
/// Validates if datasource is present in runtimeConfig.
456509
/// </summary>

src/Config/RuntimeConfigLoader.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,4 +499,9 @@ public void InsertWantedChangesInProductionMode()
499499
RuntimeConfig = runtimeConfigCopy;
500500
}
501501
}
502+
503+
public void EditRuntimeConfig(RuntimeConfig newRuntimeConfig)
504+
{
505+
RuntimeConfig = newRuntimeConfig;
506+
}
502507
}

src/Core/Configurations/RuntimeConfigProvider.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,19 @@ private static RuntimeConfig HandleCosmosNoSqlConfiguration(string? schema, Runt
411411

412412
return runtimeConfig;
413413
}
414+
415+
public void AddMergedEntitiesToConfig(Dictionary<string, Entity> newEntities)
416+
{
417+
Dictionary<string, Entity> entities = new(_configLoader.RuntimeConfig!.Entities);
418+
foreach ((string name, Entity entity) in newEntities)
419+
{
420+
entities.Add(name, entity);
421+
}
422+
423+
RuntimeConfig newRuntimeConfig = _configLoader.RuntimeConfig! with
424+
{
425+
Entities = new(entities)
426+
};
427+
_configLoader.EditRuntimeConfig(newRuntimeConfig);
428+
}
414429
}

src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,93 @@ private bool TryResolveDbType(string sqlDbTypeName, out DbType dbType)
292292
}
293293

294294
/// <inheritdoc/>
295-
protected override async Task GenerateAutoentitiesIntoEntities()
295+
protected override async Task GenerateAutoentitiesIntoEntities(IReadOnlyDictionary<string, Autoentity>? autoentities)
296296
{
297-
await Task.CompletedTask;
297+
if (autoentities is null)
298+
{
299+
return;
300+
}
301+
302+
RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();
303+
Dictionary<string, Entity> entities = new();
304+
foreach ((string autoentityName, Autoentity autoentity) in autoentities)
305+
{
306+
int addedEntities = 0;
307+
JsonArray? resultArray = await QueryAutoentitiesAsync(autoentity);
308+
if (resultArray is null)
309+
{
310+
continue;
311+
}
312+
313+
foreach (JsonObject? resultObject in resultArray)
314+
{
315+
if (resultObject is null)
316+
{
317+
throw new DataApiBuilderException(
318+
message: $"Cannot create new entity from autoentity pattern due to an internal error.",
319+
statusCode: HttpStatusCode.InternalServerError,
320+
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
321+
}
322+
323+
// Extract the entity name, schema, and database object name from the query result.
324+
// The SQL query returns these values with placeholders already replaced.
325+
string? entityName = resultObject["entity_name"]?.ToString();
326+
string? objectName = resultObject["object"]?.ToString();
327+
string? schemaName = resultObject["schema"]?.ToString();
328+
329+
if (string.IsNullOrWhiteSpace(entityName) || string.IsNullOrWhiteSpace(objectName) || string.IsNullOrWhiteSpace(schemaName))
330+
{
331+
_logger.LogError("Skipping autoentity generation: entity_name or object is null or empty for autoentity pattern '{AutoentityName}'.", autoentityName);
332+
continue;
333+
}
334+
335+
// Create the entity using the template settings and permissions from the autoentity configuration.
336+
// Currently the source type is always Table for auto-generated entities from database objects.
337+
Entity generatedEntity = new(
338+
Source: new EntitySource(
339+
Object: objectName,
340+
Type: EntitySourceType.Table,
341+
Parameters: null,
342+
KeyFields: null),
343+
GraphQL: autoentity.Template.GraphQL,
344+
Rest: autoentity.Template.Rest,
345+
Mcp: autoentity.Template.Mcp,
346+
Permissions: autoentity.Permissions,
347+
Cache: autoentity.Template.Cache,
348+
Health: autoentity.Template.Health,
349+
Fields: null,
350+
Relationships: null,
351+
Mappings: new());
352+
353+
// Add the generated entity to the linking entities dictionary.
354+
// This allows the entity to be processed later during metadata population.
355+
if (!entities.TryAdd(entityName, generatedEntity) || !runtimeConfig.TryAddGeneratedAutoentityNameToDataSourceName(entityName, autoentityName))
356+
{
357+
throw new DataApiBuilderException(
358+
message: $"Entity with name '{entityName}' already exists. Cannot create new entity from autoentity pattern with definition-name '{autoentityName}'.",
359+
statusCode: HttpStatusCode.BadRequest,
360+
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
361+
}
362+
363+
if (runtimeConfig.IsRestEnabled)
364+
{
365+
_logger.LogInformation("[{entity}] REST path: {globalRestPath}/{entityRestPath}", entityName, runtimeConfig.RestPath, entityName);
366+
}
367+
else
368+
{
369+
_logger.LogInformation(message: "REST calls are disabled for the entity: {entity}", entityName);
370+
}
371+
372+
addedEntities++;
373+
}
374+
375+
if (addedEntities == 0)
376+
{
377+
_logger.LogWarning($"No new entities were generated from the autoentity {autoentityName} defined in the configuration.");
378+
}
379+
}
380+
381+
_runtimeConfigProvider.AddMergedEntitiesToConfig(entities);
298382
}
299383

300384
public async Task<JsonArray?> QueryAutoentitiesAsync(Autoentity autoentity)

0 commit comments

Comments
 (0)