diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 13d005c3ac..80cfd953ad 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -797,6 +797,21 @@ } ] }, + "fields": { + "type": "array", + "description": "Defines the fields (columns) exposed for this entity, with metadata.", + "items": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "Database column name." }, + "alias": { "type": "string", "description": "Exposed name for the field." }, + "description": { "type": "string", "description": "Field description." }, + "primary-key": { "type": "boolean", "description": "Indicates whether this field is a primary key." } + }, + "required": ["name"] + }, + "uniqueItems": true + }, "rest": { "oneOf": [ { @@ -1116,7 +1131,22 @@ } } }, - "required": ["source", "permissions"] + "required": ["source", "permissions"], + "allOf": [ + { + "if": { + "required": ["fields"] + }, + "then": { + "not": { + "anyOf": [ + { "required": ["mappings"] }, + { "properties": { "source": { "properties": { "key-fields": { } }, "required": ["key-fields"] } } } + ] + } + } + } + ] } } } diff --git a/src/Cli.Tests/AddEntityTests.cs b/src/Cli.Tests/AddEntityTests.cs index c12b0a2d4a..9386916f7f 100644 --- a/src/Cli.Tests/AddEntityTests.cs +++ b/src/Cli.Tests/AddEntityTests.cs @@ -49,7 +49,11 @@ public Task AddNewEntityWhenEntitiesEmpty() parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); return ExecuteVerifyTest(options); } @@ -82,7 +86,11 @@ public Task AddNewEntityWhenEntitiesNotEmpty() parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); string initialConfiguration = AddPropertiesToJson(INITIAL_CONFIG, GetFirstEntityConfiguration()); @@ -118,7 +126,11 @@ public void AddDuplicateEntity() parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); string initialConfiguration = AddPropertiesToJson(INITIAL_CONFIG, GetFirstEntityConfiguration()); @@ -158,7 +170,11 @@ public Task AddEntityWithAnExistingNameButWithDifferentCase() parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); string initialConfiguration = AddPropertiesToJson(INITIAL_CONFIG, GetFirstEntityConfiguration()); @@ -193,7 +209,11 @@ public Task AddEntityWithCachingEnabled() parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); return ExecuteVerifyTest(options); @@ -234,7 +254,11 @@ public Task AddEntityWithPolicyAndFieldProperties( parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); // Create VerifySettings and add all arguments to the method as parameters @@ -271,7 +295,11 @@ public Task AddNewEntityWhenEntitiesWithSourceAsStoredProcedure() parametersNameCollection: null, parametersDescriptionCollection: ["This is a test parameter description."], parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); return ExecuteVerifyTest(options); @@ -307,7 +335,11 @@ public Task TestAddStoredProcedureWithRestMethodsAndGraphQLOperations() parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); return ExecuteVerifyTest(options); @@ -339,7 +371,11 @@ public void AddEntityWithDescriptionAndVerifyInConfig() parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); string config = INITIAL_CONFIG; @@ -398,7 +434,11 @@ public void TestAddNewEntityWithSourceObjectHavingValidFields( parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? runtimeConfig); @@ -462,7 +502,11 @@ public Task TestAddNewSpWithDifferentRestAndGraphQLOptions( parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); VerifySettings settings = new(); @@ -502,7 +546,11 @@ public void TestAddStoredProcedureWithConflictingRestGraphQLOptions( parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? runtimeConfig); @@ -545,7 +593,11 @@ public void TestAddEntityPermissionWithInvalidOperation(IEnumerable perm parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [] ); RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? runtimeConfig); diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index 28fecfb2d5..7fe017501f 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -771,9 +771,11 @@ public void TestUpdateEntity() CollectionAssert.AreEqual(new string[] { "todo_id" }, relationship.LinkingSourceFields); CollectionAssert.AreEqual(new string[] { "id" }, relationship.LinkingTargetFields); - Assert.IsNotNull(entity.Mappings); - Assert.AreEqual("identity", entity.Mappings["id"]); - Assert.AreEqual("Company Name", entity.Mappings["name"]); + Assert.IsNotNull(entity.Fields); + Assert.AreEqual(2, entity.Fields.Count); + Assert.AreEqual(entity.Fields[0].Alias, "identity"); + Assert.AreEqual(entity.Fields[1].Alias, "Company Name"); + Assert.IsNull(entity.Mappings); } /// diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_036a859f50ce167c.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_036a859f50ce167c.verified.txt index a78465898d..260eecd0c9 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_036a859f50ce167c.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_036a859f50ce167c.verified.txt @@ -27,12 +27,18 @@ MyEntity: { Source: { Object: s001.book, - Type: View, - KeyFields: [ - col1, - col2 - ] + Type: View }, + Fields: [ + { + Name: col1, + PrimaryKey: true + }, + { + Name: col2, + PrimaryKey: true + } + ], GraphQL: { Singular: MyEntity, Plural: MyEntities, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_103655d39b48d89f.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_103655d39b48d89f.verified.txt index d3ed32cf42..80f61e17ac 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_103655d39b48d89f.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_103655d39b48d89f.verified.txt @@ -27,12 +27,18 @@ MyEntity: { Source: { Object: s001.book, - Type: Table, - KeyFields: [ - id, - name - ] + Type: Table }, + Fields: [ + { + Name: id, + PrimaryKey: true + }, + { + Name: name, + PrimaryKey: true + } + ], GraphQL: { Singular: MyEntity, Plural: MyEntities, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_442649c7ef2176bd.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_442649c7ef2176bd.verified.txt index a78465898d..260eecd0c9 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_442649c7ef2176bd.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_442649c7ef2176bd.verified.txt @@ -27,12 +27,18 @@ MyEntity: { Source: { Object: s001.book, - Type: View, - KeyFields: [ - col1, - col2 - ] + Type: View }, + Fields: [ + { + Name: col1, + PrimaryKey: true + }, + { + Name: col2, + PrimaryKey: true + } + ], GraphQL: { Singular: MyEntity, Plural: MyEntities, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt index 2b4a7b8518..2d00804545 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt @@ -29,6 +29,16 @@ Object: s001.book, Type: stored-procedure }, + Fields: [ + { + Name: id, + PrimaryKey: true + }, + { + Name: name, + PrimaryKey: true + } + ], GraphQL: { Singular: MyEntity, Plural: MyEntities, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithMappings.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithMappings.verified.txt index 63ba7e2898..54d9077f1c 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithMappings.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithMappings.verified.txt @@ -33,6 +33,18 @@ Object: MyTable, Type: Table }, + Fields: [ + { + Name: id, + Alias: Identity, + PrimaryKey: false + }, + { + Name: name, + Alias: Company Name, + PrimaryKey: false + } + ], GraphQL: { Singular: MyEntity, Plural: MyEntities, @@ -53,11 +65,7 @@ } ] } - ], - Mappings: { - id: Identity, - name: Company Name - } + ] } } ] diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithSpecialCharacterInMappings.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithSpecialCharacterInMappings.verified.txt index 8dcadec7b1..1906f87425 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithSpecialCharacterInMappings.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithSpecialCharacterInMappings.verified.txt @@ -33,6 +33,28 @@ Object: MyTable, Type: Table }, + Fields: [ + { + Name: Macaroni, + Alias: Mac & Cheese, + PrimaryKey: false + }, + { + Name: region, + Alias: United State's Region, + PrimaryKey: false + }, + { + Name: russian, + Alias: русский, + PrimaryKey: false + }, + { + Name: chinese, + Alias: 中文, + PrimaryKey: false + } + ], GraphQL: { Singular: MyEntity, Plural: MyEntities, @@ -53,13 +75,7 @@ } ] } - ], - Mappings: { - chinese: 中文, - Macaroni: Mac & Cheese, - region: United State's Region, - russian: русский - } + ] } } ] diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateExistingMappings.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateExistingMappings.verified.txt index 13e994a5cc..56ce5b55c3 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateExistingMappings.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateExistingMappings.verified.txt @@ -33,6 +33,23 @@ Object: MyTable, Type: Table }, + Fields: [ + { + Name: name, + Alias: Company Name, + PrimaryKey: false + }, + { + Name: addr, + Alias: Company Address, + PrimaryKey: false + }, + { + Name: number, + Alias: Contact Details, + PrimaryKey: false + } + ], GraphQL: { Singular: MyEntity, Plural: MyEntities, @@ -53,12 +70,7 @@ } ] } - ], - Mappings: { - addr: Company Address, - name: Company Name, - number: Contact Details - } + ] } } ] diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_574e1995f787740f.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_574e1995f787740f.verified.txt index a78465898d..260eecd0c9 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_574e1995f787740f.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_574e1995f787740f.verified.txt @@ -27,12 +27,18 @@ MyEntity: { Source: { Object: s001.book, - Type: View, - KeyFields: [ - col1, - col2 - ] + Type: View }, + Fields: [ + { + Name: col1, + PrimaryKey: true + }, + { + Name: col2, + PrimaryKey: true + } + ], GraphQL: { Singular: MyEntity, Plural: MyEntities, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a13a9ca73b21f261.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a13a9ca73b21f261.verified.txt index d3ed32cf42..80f61e17ac 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a13a9ca73b21f261.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a13a9ca73b21f261.verified.txt @@ -27,12 +27,18 @@ MyEntity: { Source: { Object: s001.book, - Type: Table, - KeyFields: [ - id, - name - ] + Type: Table }, + Fields: [ + { + Name: id, + PrimaryKey: true + }, + { + Name: name, + PrimaryKey: true + } + ], GraphQL: { Singular: MyEntity, Plural: MyEntities, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a5ce76c8bea25cc8.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a5ce76c8bea25cc8.verified.txt index d3ed32cf42..80f61e17ac 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a5ce76c8bea25cc8.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a5ce76c8bea25cc8.verified.txt @@ -27,12 +27,18 @@ MyEntity: { Source: { Object: s001.book, - Type: Table, - KeyFields: [ - id, - name - ] + Type: Table }, + Fields: [ + { + Name: id, + PrimaryKey: true + }, + { + Name: name, + PrimaryKey: true + } + ], GraphQL: { Singular: MyEntity, Plural: MyEntities, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceKeyFields.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceKeyFields.verified.txt index 697074cedf..544a3484f9 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceKeyFields.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceKeyFields.verified.txt @@ -27,12 +27,18 @@ MyEntity: { Source: { Object: s001.book, - Type: Table, - KeyFields: [ - col1, - col2 - ] + Type: Table }, + Fields: [ + { + Name: col1, + PrimaryKey: true + }, + { + Name: col2, + PrimaryKey: true + } + ], GraphQL: { Singular: MyEntity, Plural: MyEntities, diff --git a/src/Cli.Tests/UpdateEntityTests.cs b/src/Cli.Tests/UpdateEntityTests.cs index 9acb6c6f81..3a106c0adc 100644 --- a/src/Cli.Tests/UpdateEntityTests.cs +++ b/src/Cli.Tests/UpdateEntityTests.cs @@ -1030,6 +1030,7 @@ public void EnsureFailure_AddRelationshipToEntityWithDisabledGraphQL() Entity sampleEntity1 = new( Source: new("SOURCE1", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new("SOURCE1", "SOURCE1s"), Permissions: new[] { permissionForEntity }, @@ -1040,6 +1041,7 @@ public void EnsureFailure_AddRelationshipToEntityWithDisabledGraphQL() // entity with graphQL disabled Entity sampleEntity2 = new( Source: new("SOURCE2", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new("SOURCE2", "SOURCE2s", false), Permissions: new[] { permissionForEntity }, @@ -1191,7 +1193,11 @@ private static UpdateOptions GenerateBaseUpdateOptions( parametersNameCollection: null, parametersDescriptionCollection: null, parametersRequiredCollection: null, - parametersDefaultCollection: null + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null ); } diff --git a/src/Cli/Commands/AddOptions.cs b/src/Cli/Commands/AddOptions.cs index 635a438082..b7d9fbeb08 100644 --- a/src/Cli/Commands/AddOptions.cs +++ b/src/Cli/Commands/AddOptions.cs @@ -39,6 +39,10 @@ public AddOptions( IEnumerable? parametersDescriptionCollection, IEnumerable? parametersRequiredCollection, IEnumerable? parametersDefaultCollection, + IEnumerable? fieldsNameCollection, + IEnumerable? fieldsAliasCollection, + IEnumerable? fieldsDescriptionCollection, + IEnumerable? fieldsPrimaryKeyCollection, string? config ) : base( @@ -61,6 +65,10 @@ public AddOptions( parametersDescriptionCollection, parametersRequiredCollection, parametersDefaultCollection, + fieldsNameCollection, + fieldsAliasCollection, + fieldsDescriptionCollection, + fieldsPrimaryKeyCollection, config ) { diff --git a/src/Cli/Commands/EntityOptions.cs b/src/Cli/Commands/EntityOptions.cs index a11d6fe450..7f26816800 100644 --- a/src/Cli/Commands/EntityOptions.cs +++ b/src/Cli/Commands/EntityOptions.cs @@ -30,6 +30,10 @@ public EntityOptions( IEnumerable? parametersDescriptionCollection, IEnumerable? parametersRequiredCollection, IEnumerable? parametersDefaultCollection, + IEnumerable? fieldsNameCollection, + IEnumerable? fieldsAliasCollection, + IEnumerable? fieldsDescriptionCollection, + IEnumerable? fieldsPrimaryKeyCollection, string? config ) : base(config) @@ -53,6 +57,10 @@ public EntityOptions( ParametersDescriptionCollection = parametersDescriptionCollection; ParametersRequiredCollection = parametersRequiredCollection; ParametersDefaultCollection = parametersDefaultCollection; + FieldsNameCollection = fieldsNameCollection; + FieldsAliasCollection = fieldsAliasCollection; + FieldsDescriptionCollection = fieldsDescriptionCollection; + FieldsPrimaryKeyCollection = fieldsPrimaryKeyCollection; } // Entity is required but we have made required as false to have custom error message (more user friendly), if not provided. @@ -112,5 +120,17 @@ public EntityOptions( [Option("parameters.default", Required = false, Separator = ',', HelpText = "Comma-separated list of parameter default values for stored procedure.")] public IEnumerable? ParametersDefaultCollection { get; } + + [Option("fields.name", Required = false, Separator = ',', HelpText = "Name of the database column to expose as a field.")] + public IEnumerable? FieldsNameCollection { get; } + + [Option("fields.alias", Required = false, Separator = ',', HelpText = "Alias for the field.")] + public IEnumerable? FieldsAliasCollection { get; } + + [Option("fields.description", Required = false, Separator = ',', HelpText = "Description for the field.")] + public IEnumerable? FieldsDescriptionCollection { get; } + + [Option("fields.primary-key", Required = false, Separator = ',', HelpText = "Set this field as a primary key.")] + public IEnumerable? FieldsPrimaryKeyCollection { get; } } } diff --git a/src/Cli/Commands/UpdateOptions.cs b/src/Cli/Commands/UpdateOptions.cs index 2c1c0e74c7..fe1664c5bb 100644 --- a/src/Cli/Commands/UpdateOptions.cs +++ b/src/Cli/Commands/UpdateOptions.cs @@ -47,6 +47,10 @@ public UpdateOptions( IEnumerable? parametersDescriptionCollection, IEnumerable? parametersRequiredCollection, IEnumerable? parametersDefaultCollection, + IEnumerable? fieldsNameCollection, + IEnumerable? fieldsAliasCollection, + IEnumerable? fieldsDescriptionCollection, + IEnumerable? fieldsPrimaryKeyCollection, string? config) : base(entity, sourceType, @@ -67,6 +71,10 @@ public UpdateOptions( parametersDescriptionCollection, parametersRequiredCollection, parametersDefaultCollection, + fieldsNameCollection, + fieldsAliasCollection, + fieldsDescriptionCollection, + fieldsPrimaryKeyCollection, config) { Source = source; diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index b54f87b2fd..9a56f83c4a 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -453,6 +453,7 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt // Create new entity. Entity entity = new( Source: source, + Fields: null, Rest: restOptions, GraphQL: graphqlOptions, Permissions: permissionSettings, @@ -1682,24 +1683,182 @@ public static bool TryUpdateExistingEntity(UpdateOptions options, RuntimeConfig updatedRelationships[options.Relationship] = new_relationship; } - if (options.Map is not null && options.Map.Any()) + bool hasFields = options.FieldsNameCollection != null && options.FieldsNameCollection.Count() > 0; + bool hasMappings = options.Map != null && options.Map.Any(); + bool hasKeyFields = options.SourceKeyFields != null && options.SourceKeyFields.Any(); + + List? fields; + if (hasFields) { - // Parsing mappings dictionary from Collection - if (!TryParseMappingDictionary(options.Map, out updatedMappings)) + if (hasMappings && hasKeyFields) + { + _logger.LogError("Entity cannot define 'fields', 'mappings', and 'key-fields' together. Please use only one."); + return false; + } + + if (hasMappings) + { + _logger.LogError("Entity cannot define both 'fields' and 'mappings'. Please use only one."); + return false; + } + + if (hasKeyFields) { + _logger.LogError("Entity cannot define both 'fields' and 'key-fields'. Please use only one."); return false; } + + // Merge updated fields with existing fields + List existingFields = entity.Fields?.ToList() ?? []; + List updatedFieldsList = ComposeFieldsFromOptions(options); + Dictionary updatedFieldsDict = updatedFieldsList.ToDictionary(f => f.Name, f => f); + List mergedFields = []; + + foreach (FieldMetadata field in existingFields) + { + if (updatedFieldsDict.TryGetValue(field.Name, out FieldMetadata? updatedField)) + { + mergedFields.Add(new FieldMetadata + { + Name = updatedField.Name, + Alias = updatedField.Alias ?? field.Alias, + Description = updatedField.Description ?? field.Description, + PrimaryKey = updatedField.PrimaryKey + }); + updatedFieldsDict.Remove(field.Name); // Remove so only new fields remain + } + else + { + mergedFields.Add(field); // Keep existing field + } + } + + // Add any new fields that didn't exist before + mergedFields.AddRange(updatedFieldsDict.Values); + + fields = mergedFields; + + // If user didn't mark any PK in fields, carry over existing source key-fields + if (!fields.Any(f => f.PrimaryKey) && updatedSource.KeyFields is { Length: > 0 }) + { + foreach (string k in updatedSource.KeyFields) + { + FieldMetadata? f = fields.FirstOrDefault(f => string.Equals(f.Name, k, StringComparison.OrdinalIgnoreCase)); + if (f is not null) + { + f.PrimaryKey = true; + } + else + { + fields.Add(new FieldMetadata { Name = k, PrimaryKey = true }); + } + } + } + + // Remove legacy props if fields present + updatedSource = updatedSource with { KeyFields = null }; + updatedMappings = null; + } + else if (hasMappings || hasKeyFields) + { + // If mappings or key-fields are provided, convert them to fields and remove legacy props + // Start with existing fields + List existingFields = entity.Fields?.ToList() ?? new List(); + + // Build a dictionary for quick lookup and merging + Dictionary fieldDict = existingFields + .ToDictionary(f => f.Name, StringComparer.OrdinalIgnoreCase); + + // Parse mappings from options + if (hasMappings) + { + if (options.Map is null || !TryParseMappingDictionary(options.Map, out updatedMappings)) + { + _logger.LogError("Failed to parse mappings from --map option."); + return false; + } + + foreach (KeyValuePair mapping in updatedMappings) + { + if (fieldDict.TryGetValue(mapping.Key, out FieldMetadata? existing) && existing != null) + { + // Update alias, preserve PK and description + existing.Alias = mapping.Value ?? existing.Alias; + } + else + { + // New field from mapping + fieldDict[mapping.Key] = new FieldMetadata + { + Name = mapping.Key, + Alias = mapping.Value + }; + } + } + } + + // Always carry over existing PKs on the entity/update, not only when the user re-supplies --source.key-fields. + string[]? existingKeys = updatedSource.KeyFields; + if (existingKeys is not null && existingKeys.Length > 0) + { + foreach (string key in existingKeys) + { + if (fieldDict.TryGetValue(key, out FieldMetadata? pkField) && pkField != null) + { + pkField.PrimaryKey = true; + } + else + { + fieldDict[key] = new FieldMetadata { Name = key, PrimaryKey = true }; + } + } + } + + // Final merged list, no duplicates + fields = fieldDict.Values.ToList(); + + // Remove legacy props only after we have safely embedded PKs into fields. + updatedSource = updatedSource with { KeyFields = null }; + updatedMappings = null; + } + else if (!hasFields && !hasMappings && !hasKeyFields && entity.Source.KeyFields?.Length > 0) + { + // If no fields, mappings, or key-fields are provided with update command, use the entity's key-fields added using add command. + fields = entity.Source.KeyFields.Select(k => new FieldMetadata + { + Name = k, + PrimaryKey = true + }).ToList(); + + updatedSource = updatedSource with { KeyFields = null }; + updatedMappings = null; + } + else + { + fields = entity.Fields?.ToList() ?? new List(); + if (entity.Mappings is not null || entity.Source?.KeyFields is not null) + { + _logger.LogWarning("Using legacy 'mappings' and 'key-fields' properties. Consider using 'fields' for new entities."); + } + } + + if (!ValidateFields(fields, out string errorMessage)) + { + _logger.LogError(errorMessage); + return false; } Entity updatedEntity = new( Source: updatedSource, + Fields: fields, Rest: updatedRestDetails, GraphQL: updatedGraphQLDetails, Permissions: updatedPermissions, Relationships: updatedRelationships, Mappings: updatedMappings, Cache: updatedCacheOptions, - Description: string.IsNullOrWhiteSpace(options.Description) ? entity.Description : options.Description); + Description: string.IsNullOrWhiteSpace(options.Description) ? entity.Description : options.Description + ); IDictionary entities = new Dictionary(initialConfig.Entities.Entities) { [options.Entity] = updatedEntity @@ -2220,7 +2379,29 @@ public static bool IsConfigValid(ValidateOptions options, FileSystemRuntimeConfi ILogger runtimeConfigValidatorLogger = LoggerFactoryForCli.CreateLogger(); RuntimeConfigValidator runtimeConfigValidator = new(runtimeConfigProvider, fileSystem, runtimeConfigValidatorLogger, true); - return runtimeConfigValidator.TryValidateConfig(runtimeConfigFile, LoggerFactoryForCli).Result; + bool isValid = runtimeConfigValidator.TryValidateConfig(runtimeConfigFile, LoggerFactoryForCli).Result; + + // Additional validation: warn if fields are missing and MCP is enabled + if (isValid) + { + if (runtimeConfigProvider.TryGetConfig(out RuntimeConfig? config) && config is not null) + { + bool mcpEnabled = config.Runtime?.Mcp?.Enabled == true; + if (mcpEnabled) + { + foreach (KeyValuePair entity in config.Entities) + { + if (entity.Value.Fields == null || !entity.Value.Fields.Any()) + { + _logger.LogWarning($"Entity '{entity.Key}' is missing 'fields' definition while MCP is enabled. " + + "It's recommended to define fields explicitly to ensure optimal performance with MCP."); + } + } + } + } + } + + return isValid; } /// @@ -2616,5 +2797,68 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( return false; } } + + /// + /// Helper to build a list of FieldMetadata from UpdateOptions. + /// + private static List ComposeFieldsFromOptions(UpdateOptions options) + { + List fields = []; + if (options.FieldsNameCollection != null) + { + List names = options.FieldsNameCollection.ToList(); + List aliases = options.FieldsAliasCollection?.ToList() ?? []; + List descriptions = options.FieldsDescriptionCollection?.ToList() ?? []; + List keys = options.FieldsPrimaryKeyCollection?.ToList() ?? []; + + for (int i = 0; i < names.Count; i++) + { + fields.Add(new FieldMetadata + { + Name = names[i], + Alias = aliases.Count > i ? aliases[i] : null, + Description = descriptions.Count > i ? descriptions[i] : null, + PrimaryKey = keys.Count > i && keys[i], + }); + } + } + + return fields; + } + + /// + /// Validates that the provided fields are valid against the database columns and constraints. + /// + private static bool ValidateFields( + List fields, + out string errorMessage) + { + errorMessage = string.Empty; + HashSet aliases = []; + HashSet keys = []; + + foreach (FieldMetadata field in fields) + { + if (!string.IsNullOrEmpty(field.Alias)) + { + if (!aliases.Add(field.Alias)) + { + errorMessage = $"Alias '{field.Alias}' is not unique within the entity."; + return false; + } + } + + if (field.PrimaryKey) + { + if (!keys.Add(field.Name)) + { + errorMessage = $"Duplicate key field '{field.Name}' detected."; + return false; + } + } + } + + return true; + } } } diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index 4b56e0478c..c9f247e0f6 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -30,13 +30,13 @@ public record Entity public const string PROPERTY_METHODS = "methods"; public string? Description { get; init; } public EntitySource Source { get; init; } + public List? Fields { get; init; } public EntityGraphQLOptions GraphQL { get; init; } public EntityRestOptions Rest { get; init; } public EntityPermission[] Permissions { get; init; } public Dictionary? Mappings { get; init; } public Dictionary? Relationships { get; init; } public EntityCacheOptions? Cache { get; init; } - public EntityHealthCheckConfig? Health { get; init; } [JsonIgnore] @@ -46,6 +46,7 @@ public record Entity public Entity( EntitySource Source, EntityGraphQLOptions GraphQL, + List? Fields, EntityRestOptions Rest, EntityPermission[] Permissions, Dictionary? Mappings, @@ -57,6 +58,7 @@ public Entity( { this.Health = Health; this.Source = Source; + this.Fields = Fields; this.GraphQL = GraphQL; this.Rest = Rest; this.Permissions = Permissions; diff --git a/src/Config/ObjectModel/FieldMetadata.cs b/src/Config/ObjectModel/FieldMetadata.cs new file mode 100644 index 0000000000..118f38c0c2 --- /dev/null +++ b/src/Config/ObjectModel/FieldMetadata.cs @@ -0,0 +1,28 @@ +namespace Azure.DataApiBuilder.Config.ObjectModel +{ + /// + /// Represents metadata for a field in an entity. + /// + public class FieldMetadata + { + /// + /// The name of the field (must match a database column). + /// + public string Name { get; set; } = string.Empty; + + /// + /// The alias for the field (must be unique per entity). + /// + public string? Alias { get; set; } + + /// + /// The description for the field. + /// + public string? Description { get; set; } + + /// + /// Whether this field is a key (must be unique). + /// + public bool PrimaryKey { get; set; } + } +} diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 4acb52f21b..7d02798427 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -253,6 +253,7 @@ protected override void PopulateMetadataForLinkingObject( // GraphQL is enabled/disabled. The linking object definitions are not exposed in the schema to the user. Entity linkingEntity = new( Source: new EntitySource(Type: EntitySourceType.Table, Object: linkingObject, Parameters: null, KeyFields: null), + Fields: null, Rest: new(Array.Empty(), Enabled: false), GraphQL: new(Singular: linkingEntityName, Plural: linkingEntityName, Enabled: false), Permissions: Array.Empty(), diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 75fff1b932..8553e08136 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -217,13 +217,33 @@ public StoredProcedureDefinition GetStoredProcedureDefinition(string entityName) /// public bool TryGetExposedColumnName(string entityName, string backingFieldName, [NotNullWhen(true)] out string? name) { - Dictionary? backingColumnsToExposedNamesMap; - if (!EntityBackingColumnsToExposedNames.TryGetValue(entityName, out backingColumnsToExposedNamesMap)) + if (!EntityBackingColumnsToExposedNames.TryGetValue(entityName, out Dictionary? backingToExposed)) { throw new KeyNotFoundException($"Initialization of metadata incomplete for entity: {entityName}"); } - return backingColumnsToExposedNamesMap.TryGetValue(backingFieldName, out name); + if (backingToExposed.TryGetValue(backingFieldName, out name)) + { + return true; + } + + if (_entities.TryGetValue(entityName, out Entity? entityDefinition) && entityDefinition.Fields is not null) + { + // Find the field by backing name and use its Alias if present. + FieldMetadata? matched = entityDefinition + .Fields + .FirstOrDefault(f => f.Name.Equals(backingFieldName, StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrEmpty(f.Alias)); + + if (matched is not null) + { + name = matched.Alias!; + return true; + } + } + + name = null; + return false; } /// @@ -235,6 +255,23 @@ public bool TryGetBackingColumn(string entityName, string field, [NotNullWhen(tr throw new KeyNotFoundException($"Initialization of metadata incomplete for entity: {entityName}"); } + if (exposedNamesToBackingColumnsMap.TryGetValue(field, out name)) + { + return true; + } + + if (_entities.TryGetValue(entityName, out Entity? entityDefinition) && entityDefinition.Fields is not null) + { + FieldMetadata? matchedField = entityDefinition.Fields.FirstOrDefault(f => + f.Alias != null && f.Alias.Equals(field, StringComparison.OrdinalIgnoreCase)); + + if (matchedField is not null) + { + name = matchedField.Name; + return true; + } + } + return exposedNamesToBackingColumnsMap.TryGetValue(field, out name); } @@ -1099,22 +1136,81 @@ await PopulateResultSetDefinitionsForStoredProcedureAsync( } else if (entitySourceType is EntitySourceType.Table) { + List pkFields = new(); + + // Resolve PKs from fields first + if (entity.Fields is not null && entity.Fields.Any()) + { + pkFields = entity.Fields + .Where(f => f.PrimaryKey) + .Select(f => f.Name) + .ToList(); + } + + // Fallback to key-fields from config + if (pkFields.Count == 0 && entity.Source.KeyFields is not null) + { + pkFields = entity.Source.KeyFields.ToList(); + } + + // If still empty, fallback to DB schema PKs + if (pkFields.Count == 0) + { + DataTable dataTable = await GetTableWithSchemaFromDataSetAsync( + entityName, + GetSchemaName(entityName), + GetDatabaseObjectName(entityName)); + + pkFields = dataTable.PrimaryKey.Select(pk => pk.ColumnName).ToList(); + } + + // Final safeguard + pkFields ??= new List(); + await PopulateSourceDefinitionAsync( entityName, GetSchemaName(entityName), GetDatabaseObjectName(entityName), GetSourceDefinition(entityName), - entity.Source.KeyFields); + pkFields); } else { + List pkFields = new(); + + // Resolve PKs from fields first + if (entity.Fields is not null && entity.Fields.Any()) + { + pkFields = entity.Fields + .Where(f => f.PrimaryKey) + .Select(f => f.Name) + .ToList(); + } + + // Fallback to key-fields from config + if (pkFields.Count == 0 && entity.Source.KeyFields is not null) + { + pkFields = entity.Source.KeyFields.ToList(); + } + + // If still empty, fallback to DB schema PKs + if (pkFields.Count == 0) + { + DataTable dataTable = await GetTableWithSchemaFromDataSetAsync( + entityName, + GetSchemaName(entityName), + GetDatabaseObjectName(entityName)); + + pkFields = dataTable.PrimaryKey.Select(pk => pk.ColumnName).ToList(); + } + ViewDefinition viewDefinition = (ViewDefinition)GetSourceDefinition(entityName); await PopulateSourceDefinitionAsync( entityName, GetSchemaName(entityName), GetDatabaseObjectName(entityName), viewDefinition, - entity.Source.KeyFields); + pkFields); } } catch (Exception e) @@ -1215,19 +1311,66 @@ private void GenerateExposedToBackingColumnMapUtil(string entityName) { try { - // For StoredProcedures, result set definitions become the column definition. - Dictionary? mapping = GetMappingForEntity(entityName); - EntityBackingColumnsToExposedNames[entityName] = mapping is not null ? mapping : new(); - EntityExposedNamesToBackingColumnNames[entityName] = EntityBackingColumnsToExposedNames[entityName].ToDictionary(x => x.Value, x => x.Key); + // Build case-insensitive maps per entity. + Dictionary backToExposed = new(StringComparer.OrdinalIgnoreCase); + Dictionary exposedToBack = new(StringComparer.OrdinalIgnoreCase); + + // Pull definitions. + _entities.TryGetValue(entityName, out Entity? entity); SourceDefinition sourceDefinition = GetSourceDefinition(entityName); - foreach (string columnName in sourceDefinition.Columns.Keys) + + // 1) Prefer new-style fields (backing = f.Name, exposed = f.Alias ?? f.Name) + if (entity?.Fields is not null) { - if (!EntityExposedNamesToBackingColumnNames[entityName].ContainsKey(columnName) && !EntityBackingColumnsToExposedNames[entityName].ContainsKey(columnName)) + foreach (FieldMetadata f in entity.Fields) { - EntityBackingColumnsToExposedNames[entityName].Add(columnName, columnName); - EntityExposedNamesToBackingColumnNames[entityName].Add(columnName, columnName); + string backing = f.Name; + string exposed = string.IsNullOrWhiteSpace(f.Alias) ? backing : f.Alias!; + backToExposed[backing] = exposed; + exposedToBack[exposed] = backing; + } + } + + // 2) Overlay legacy mappings (backing -> alias) only where we don't already have an alias from fields. + if (entity?.Mappings is not null) + { + foreach (KeyValuePair kvp in entity.Mappings) + { + string backing = kvp.Key; + string exposed = kvp.Value; + + // If fields already provided an alias for this backing column, keep fields precedence. + if (!backToExposed.ContainsKey(backing)) + { + backToExposed[backing] = exposed; + } + + // Always ensure reverse map is coherent (fields still take precedence if the same exposed already exists). + if (!exposedToBack.ContainsKey(exposed)) + { + exposedToBack[exposed] = backing; + } } } + + // 3) Ensure all physical columns are mapped (identity default). + foreach (string backing in sourceDefinition.Columns.Keys) + { + if (!backToExposed.ContainsKey(backing)) + { + backToExposed[backing] = backing; + } + + string exposed = backToExposed[backing]; + if (!exposedToBack.ContainsKey(exposed)) + { + exposedToBack[exposed] = backing; + } + } + + // 4) Store maps for runtime + EntityBackingColumnsToExposedNames[entityName] = backToExposed; + EntityExposedNamesToBackingColumnNames[entityName] = exposedToBack; } catch (Exception e) { @@ -1235,18 +1378,6 @@ private void GenerateExposedToBackingColumnMapUtil(string entityName) } } - /// - /// Obtains the underlying mapping that belongs - /// to a given entity. - /// - /// entity whose map we get. - /// mapping belonging to entity. - private Dictionary? GetMappingForEntity(string entityName) - { - _entities.TryGetValue(entityName, out Entity? entity); - return entity?.Mappings; - } - /// /// Initialize OData parser by building OData model. /// The parser will be used for parsing filter clause and order by clause. @@ -1269,19 +1400,9 @@ private async Task PopulateSourceDefinitionAsync( string schemaName, string tableName, SourceDefinition sourceDefinition, - string[]? runtimeConfigKeyFields) + List pkFields) { - DataTable dataTable = await GetTableWithSchemaFromDataSetAsync(entityName, schemaName, tableName); - - List primaryKeys = new(dataTable.PrimaryKey); - if (runtimeConfigKeyFields is null || runtimeConfigKeyFields.Length == 0) - { - sourceDefinition.PrimaryKey = new(primaryKeys.Select(primaryKey => primaryKey.ColumnName)); - } - else - { - sourceDefinition.PrimaryKey = new(runtimeConfigKeyFields); - } + sourceDefinition.PrimaryKey = [.. pkFields]; if (sourceDefinition.PrimaryKey.Count == 0) { @@ -1297,6 +1418,7 @@ private async Task PopulateSourceDefinitionAsync( await PopulateTriggerMetadataForTable(entityName, schemaName, tableName, sourceDefinition); } + DataTable dataTable = await GetTableWithSchemaFromDataSetAsync(entityName, schemaName, tableName); using DataTableReader reader = new(dataTable); DataTable schemaTable = reader.GetSchemaTable(); RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); @@ -1404,12 +1526,21 @@ public static bool IsGraphQLReservedName(Entity entity, string databaseColumnNam if (entity.GraphQL is null || (entity.GraphQL.Enabled)) { if (entity.Mappings is not null - && entity.Mappings.TryGetValue(databaseColumnName, out string? fieldAlias) - && !string.IsNullOrWhiteSpace(fieldAlias)) + && entity.Mappings.TryGetValue(databaseColumnName, out string? fieldAlias) + && !string.IsNullOrWhiteSpace(fieldAlias)) { databaseColumnName = fieldAlias; } + if (entity.Fields is not null) + { + FieldMetadata? fieldMeta = entity.Fields.FirstOrDefault(f => f.Name == databaseColumnName); + if (fieldMeta != null && !string.IsNullOrWhiteSpace(fieldMeta.Alias)) + { + databaseColumnName = fieldMeta.Alias; + } + } + return IsIntrospectionField(databaseColumnName); } } diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 33105db289..87fb96bc32 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -1011,13 +1011,13 @@ private Dictionary CreateComponentSchemas(RuntimeEntities // Response body schema whose properties map to the stored procedure's first result set columns // as described by sys.dm_exec_describe_first_result_set. - schemas.Add(entityName + SP_RESPONSE_SUFFIX, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider)); + schemas.Add(entityName + SP_RESPONSE_SUFFIX, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities)); } else { // Create component schema for FULL entity with all primary key columns (included auto-generated) // which will typically represent the response body of a request or a stored procedure's request body. - schemas.Add(entityName, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider)); + schemas.Add(entityName, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities)); // Create an entity's request body component schema excluding autogenerated primary keys. // A POST request requires any non-autogenerated primary key references to be in the request body. @@ -1037,7 +1037,7 @@ private Dictionary CreateComponentSchemas(RuntimeEntities } } - schemas.Add($"{entityName}_NoAutoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider)); + schemas.Add($"{entityName}_NoAutoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities)); // Create an entity's request body component schema excluding all primary keys // by removing the tracked non-autogenerated primary key column names and removing them from @@ -1053,7 +1053,7 @@ private Dictionary CreateComponentSchemas(RuntimeEntities } } - schemas.Add($"{entityName}_NoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider)); + schemas.Add($"{entityName}_NoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities)); } } @@ -1113,10 +1113,12 @@ private static OpenApiSchema CreateSpRequestComponentSchema(Dictionary /// Name of the entity. /// List of mapped (alias) field names. + /// Metadata provider for database objects. + /// Runtime entities from configuration. /// Raised when an entity's database metadata can't be found, /// indicating a failure due to the provided entityName. /// Entity's OpenApiSchema representation. - private static OpenApiSchema CreateComponentSchema(string entityName, HashSet fields, ISqlMetadataProvider metadataProvider) + private static OpenApiSchema CreateComponentSchema(string entityName, HashSet fields, ISqlMetadataProvider metadataProvider, RuntimeEntities entities) { if (!metadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null) { @@ -1128,6 +1130,8 @@ private static OpenApiSchema CreateComponentSchema(string entityName, HashSet properties = new(); + Entity? entityConfig = entities.TryGetValue(entityName, out Entity? ent) ? ent : null; + // Get backing column metadata to resolve the correct system type which is then // used to resolve the correct Json data type. foreach (string field in fields) @@ -1136,15 +1140,24 @@ private static OpenApiSchema CreateComponentSchema(string entityName, HashSet f.Alias == field || f.Name == field); + fieldDescription = fieldMetadata?.Description; + } + properties.Add(field, new OpenApiSchema() { Type = typeMetadata, - Format = formatMetadata + Format = formatMetadata, + Description = fieldDescription }); } } @@ -1152,7 +1165,8 @@ private static OpenApiSchema CreateComponentSchema(string entityName, HashSet f.Name == columnName); + if (fieldMetadata != null && !string.IsNullOrEmpty(fieldMetadata.Alias)) + { + exposedColumnName = fieldMetadata.Alias; + } + } + NamedTypeNode fieldType = new(GetGraphQLTypeFromSystemType(column.SystemType)); FieldDefinitionNode field = new( location: null, new(exposedColumnName), - description: null, + description: fieldMetadata?.Description is null ? null : new StringValueNode(fieldMetadata.Description), new List(), column.IsNullable ? fieldType : new NonNullTypeNode(fieldType), directives); diff --git a/src/Service.Tests/Authorization/AuthorizationHelpers.cs b/src/Service.Tests/Authorization/AuthorizationHelpers.cs index 85f05a1c3b..bdd5630a50 100644 --- a/src/Service.Tests/Authorization/AuthorizationHelpers.cs +++ b/src/Service.Tests/Authorization/AuthorizationHelpers.cs @@ -112,6 +112,7 @@ public static RuntimeConfig InitRuntimeConfig( Entity sampleEntity = new( Source: entitySource, + Fields: null, Rest: new(Array.Empty()), GraphQL: new(entityName.Singularize(), entityName.Pluralize()), Permissions: new EntityPermission[] { permissionForEntity }, diff --git a/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs b/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs index 39a77bffff..0dff3ac016 100644 --- a/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs +++ b/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs @@ -1424,6 +1424,7 @@ private static RuntimeConfig BuildTestRuntimeConfig(EntityPermission[] permissio { Entity sampleEntity = new( Source: new(entityName, EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new("", ""), Permissions: permissions, diff --git a/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs b/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs index 706a0c42ad..02b7ca6492 100644 --- a/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs +++ b/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs @@ -740,6 +740,7 @@ private static Mock CreateMockRuntimeConfigProvider(strin { Entity entity = new( Source: new EntitySource(string.Empty, null, null, null), + Fields: null, GraphQL: new EntityGraphQLOptions(string.Empty, string.Empty), Rest: new EntityRestOptions(), Permissions: Array.Empty(), diff --git a/src/Service.Tests/Caching/HealthEndpointCachingTests.cs b/src/Service.Tests/Caching/HealthEndpointCachingTests.cs index 94216a4409..fcc3e097e5 100644 --- a/src/Service.Tests/Caching/HealthEndpointCachingTests.cs +++ b/src/Service.Tests/Caching/HealthEndpointCachingTests.cs @@ -119,6 +119,7 @@ private static void SetupCachingTest(int? cacheTtlSeconds) { Entity requiredEntity = new( Health: new(enabled: true), + Fields: null, Source: new("books", EntitySourceType.Table, null, null), Rest: new(Enabled: true), GraphQL: new("book", "books", true), diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 0be24fa886..65f6e6643b 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -1613,6 +1613,7 @@ public async Task TestSqlMetadataForInvalidConfigEntities() // creating an entity with invalid table name Entity entityWithInvalidSourceName = new( Source: new("bokos", EntitySourceType.Table, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "book", Plural: "books"), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -1622,6 +1623,7 @@ public async Task TestSqlMetadataForInvalidConfigEntities() Entity entityWithInvalidSourceType = new( Source: new("publishers", EntitySourceType.StoredProcedure, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "publisher", Plural: "publishers"), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_AUTHENTICATED) }, @@ -1684,6 +1686,7 @@ public async Task TestSqlMetadataValidationForEntitiesWithInvalidSource() // creating an entity with invalid table name Entity entityWithInvalidSource = new( Source: new(null, EntitySourceType.Table, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "book", Plural: "books"), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -1694,6 +1697,7 @@ public async Task TestSqlMetadataValidationForEntitiesWithInvalidSource() // creating an entity with invalid source object and adding relationship with an entity with invalid source Entity entityWithInvalidSourceAndRelationship = new( Source: new(null, EntitySourceType.Table, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "publisher", Plural: "publishers"), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -2642,6 +2646,7 @@ public async Task ValidateErrorMessageForMutationWithoutReadPermission() new EntityPermission( Role: AuthorizationResolver.ROLE_AUTHENTICATED , Actions: new[] { readAction, createAction, deleteAction })}; Entity entity = new(Source: new("stocks", EntitySourceType.Table, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "Stock", Plural: "Stocks"), Permissions: permissions, @@ -2944,6 +2949,7 @@ public async Task ValidateInheritanceOfReadPermissionFromAnonymous() new EntityPermission( Role: AuthorizationResolver.ROLE_AUTHENTICATED , Actions: new[] { createAction })}; Entity entity = new(Source: new("stocks", EntitySourceType.Table, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "Stock", Plural: "Stocks"), Permissions: permissions, @@ -3072,6 +3078,7 @@ public async Task ValidateLocationHeaderFieldForPostRequests(EntitySourceType en if (entityType is EntitySourceType.StoredProcedure) { Entity entity = new(Source: new("get_books", EntitySourceType.StoredProcedure, null, null), + Fields: null, Rest: new(new SupportedHttpVerb[] { SupportedHttpVerb.Get, SupportedHttpVerb.Post }), GraphQL: null, Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -3171,6 +3178,7 @@ public async Task ValidateLocationHeaderWhenBaseRouteIsConfigured( if (entityType is EntitySourceType.StoredProcedure) { Entity entity = new(Source: new("get_books", EntitySourceType.StoredProcedure, null, null), + Fields: null, Rest: new(new SupportedHttpVerb[] { SupportedHttpVerb.Get, SupportedHttpVerb.Post }), GraphQL: null, Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -3344,6 +3352,7 @@ public async Task TestEngineSupportViewsWithoutKeyFieldsInConfigForMsSQL() GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); Entity viewEntity = new( Source: new("books_view_all", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new("", ""), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -3683,6 +3692,7 @@ public void TestInvalidDatabaseColumnNameHandling( Entity entity = new( Source: new("graphql_incompatible", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: false), GraphQL: new("graphql_incompatible", "graphql_incompatibles", entityGraphQLEnabled), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -4308,6 +4318,7 @@ public async Task OpenApi_GlobalEntityRestPath(bool globalRestEnabled, bool expe // file creation function. Entity requiredEntity = new( Source: new("books", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: false), GraphQL: new("book", "books"), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -4369,6 +4380,7 @@ public async Task HealthEndpoint_ValidateContents() // config file creation. Entity requiredEntity = new( Source: new("books", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: false), GraphQL: new("book", "books"), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -4418,6 +4430,7 @@ public async Task OpenApi_EntityLevelRestEndpoint() // Create the entities under test. Entity restEnabledEntity = new( Source: new("books", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new("", "", false), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -4426,6 +4439,7 @@ public async Task OpenApi_EntityLevelRestEndpoint() Entity restDisabledEntity = new( Source: new("publishers", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: false), GraphQL: new("publisher", "publishers", true), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -4508,6 +4522,7 @@ public async Task ValidateNextLinkUsage(bool isNextLinkRelative) // file creation function. Entity requiredEntity = new( Source: new("bookmarks", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new(Singular: "", Plural: "", Enabled: false), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -4633,6 +4648,7 @@ public async Task ValidateNextLinkRespectsXForwardedHostAndProto(string forwarde Entity requiredEntity = new( Source: new("bookmarks", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new(Singular: "", Plural: "", Enabled: false), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -5349,6 +5365,7 @@ public static RuntimeConfig InitialzieRuntimeConfigForMultipleCreateTests(bool i LinkingTargetFields: null); Entity bookEntity = new(Source: new("books", EntitySourceType.Table, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "book", Plural: "books"), Permissions: permissions, @@ -5372,6 +5389,7 @@ public static RuntimeConfig InitialzieRuntimeConfigForMultipleCreateTests(bool i Entity publisherEntity = new( Source: new("publishers", EntitySourceType.Table, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "publisher", Plural: "publishers"), Permissions: permissions, @@ -5405,6 +5423,7 @@ public static RuntimeConfig InitMinimalRuntimeConfig( { entity ??= new( Source: new("books", EntitySourceType.Table, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "book", Plural: "books"), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -5422,6 +5441,7 @@ public static RuntimeConfig InitMinimalRuntimeConfig( // Adding an entity with only Authorized Access Entity anotherEntity = new( Source: new("publishers", EntitySourceType.Table, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "publisher", Plural: "publishers"), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_AUTHENTICATED) }, @@ -5529,6 +5549,7 @@ private static RuntimeConfig CreateBasicRuntimeConfigWithSingleEntityAndAuthOpti { Entity entity = new( Source: new("books", EntitySourceType.Table, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "book", Plural: "books"), Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, diff --git a/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs b/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs index 2a83697a3a..9ad36bfa15 100644 --- a/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs +++ b/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs @@ -49,6 +49,7 @@ public async Task ComprehensiveHealthEndpoint_RolesTests(string role, HostMode h // config file creation. Entity requiredEntity = new( Health: new(enabled: true), + Fields: null, Source: new("books", EntitySourceType.Table, null, null), Rest: new(Enabled: true), GraphQL: new("book", "books", true), diff --git a/src/Service.Tests/Configuration/HealthEndpointTests.cs b/src/Service.Tests/Configuration/HealthEndpointTests.cs index 70e14e0108..1eac7416e3 100644 --- a/src/Service.Tests/Configuration/HealthEndpointTests.cs +++ b/src/Service.Tests/Configuration/HealthEndpointTests.cs @@ -519,6 +519,7 @@ private static RuntimeConfig SetupCustomConfigFile(bool enableGlobalHealth, bool Entity requiredEntity = new( Health: new(enabled: enableEntityHealth), Source: new("books", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: enableEntityRest), GraphQL: new("book", "bookLists", enableEntityGraphQL), Permissions: new[] { ConfigurationTests.GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, diff --git a/src/Service.Tests/Configuration/HotReload/AuthorizationResolverHotReloadTests.cs b/src/Service.Tests/Configuration/HotReload/AuthorizationResolverHotReloadTests.cs index b5fcb6162b..2175c8ac83 100644 --- a/src/Service.Tests/Configuration/HotReload/AuthorizationResolverHotReloadTests.cs +++ b/src/Service.Tests/Configuration/HotReload/AuthorizationResolverHotReloadTests.cs @@ -65,6 +65,7 @@ public async Task ValidateAuthorizationResolver_HotReload() Entity requiredEntityHR = new( Source: new("publishers", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new(Singular: "", Plural: "", Enabled: false), Permissions: new[] { permissionsHR }, @@ -178,6 +179,7 @@ public static async Task ClassInitializeAsync(TestContext context) // file creation function. Entity requiredEntity = new( Source: new("books", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new(Singular: "", Plural: "", Enabled: false), Permissions: new[] { permissions }, diff --git a/src/Service.Tests/CosmosTests/MutationTests.cs b/src/Service.Tests/CosmosTests/MutationTests.cs index 2f82541806..de931dcf22 100644 --- a/src/Service.Tests/CosmosTests/MutationTests.cs +++ b/src/Service.Tests/CosmosTests/MutationTests.cs @@ -542,6 +542,7 @@ type Planet @model(name:""Planet"") { new EntityPermission( Role: AuthorizationResolver.ROLE_AUTHENTICATED , Actions: new[] { readAction, createAction, deleteAction })}; Entity entity = new(Source: new($"graphqldb.{_containerName}", null, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "Planet", Plural: "Planets"), Permissions: permissions, @@ -672,6 +673,7 @@ type Planet @model(name:""Planet"") { new EntityPermission( Role: AuthorizationResolver.ROLE_AUTHENTICATED , Actions: new[] { createAction })}; Entity entity = new(Source: new($"graphqldb.{_containerName}", null, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "Planet", Plural: "Planets"), Permissions: permissions, diff --git a/src/Service.Tests/CosmosTests/QueryTests.cs b/src/Service.Tests/CosmosTests/QueryTests.cs index c40c95c75b..52afa8e788 100644 --- a/src/Service.Tests/CosmosTests/QueryTests.cs +++ b/src/Service.Tests/CosmosTests/QueryTests.cs @@ -710,6 +710,7 @@ type Planet @model(name:""Planet"") { EntityPermission[] permissions = new[] { new EntityPermission(Role: AuthorizationResolver.ROLE_ANONYMOUS, Actions: new[] { createAction, readAction, deleteAction }) }; Entity entity = new(Source: new($"graphqldb.{_containerName}", null, null, null), + Fields: null, Rest: null, GraphQL: new(Singular: "Planet", Plural: "Planets"), Permissions: permissions, diff --git a/src/Service.Tests/CosmosTests/SchemaGeneratorFactoryTests.cs b/src/Service.Tests/CosmosTests/SchemaGeneratorFactoryTests.cs index 562a5174d2..f10ac17354 100644 --- a/src/Service.Tests/CosmosTests/SchemaGeneratorFactoryTests.cs +++ b/src/Service.Tests/CosmosTests/SchemaGeneratorFactoryTests.cs @@ -83,6 +83,7 @@ public async Task ExportGraphQLFromCosmosDB_GeneratesSchemaSuccessfully(string g { {"Container1", new Entity( Source: new(entitySource, EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: false), GraphQL: new("Container1", "Container1s"), Permissions: new EntityPermission[] {}, @@ -90,6 +91,7 @@ public async Task ExportGraphQLFromCosmosDB_GeneratesSchemaSuccessfully(string g Mappings: null) }, {"Container2", new Entity( Source: new("mydb2.container2", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: false), GraphQL: new("Container2", "Container2s"), Permissions: new EntityPermission[] {}, @@ -97,6 +99,7 @@ public async Task ExportGraphQLFromCosmosDB_GeneratesSchemaSuccessfully(string g Mappings: null) }, {"Container0", new Entity( Source: new(null, EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: false), GraphQL: new("Container0", "Container0s"), Permissions: new EntityPermission[] {}, diff --git a/src/Service.Tests/GraphQLBuilder/Helpers/GraphQLTestHelpers.cs b/src/Service.Tests/GraphQLBuilder/Helpers/GraphQLTestHelpers.cs index 73f71db446..737e29f48f 100644 --- a/src/Service.Tests/GraphQLBuilder/Helpers/GraphQLTestHelpers.cs +++ b/src/Service.Tests/GraphQLBuilder/Helpers/GraphQLTestHelpers.cs @@ -80,6 +80,7 @@ public static Dictionary CreateStubEntityPermissionsMap( public static Entity GenerateEmptyEntity(EntitySourceType sourceType = EntitySourceType.Table) { return new Entity(Source: new EntitySource(Type: sourceType, Object: "foo", Parameters: null, KeyFields: null), + Fields: null, Rest: new(Array.Empty()), GraphQL: new("", ""), Permissions: Array.Empty(), @@ -106,6 +107,7 @@ public static Entity GenerateStoredProcedureEntity( { IEnumerable actions = (permissionOperations ?? new string[] { }).Select(a => new EntityAction(EnumExtensions.Deserialize(a), null, new(null, null))); Entity entity = new(Source: new EntitySource(Type: EntitySourceType.StoredProcedure, Object: "foo", Parameters: parameters, KeyFields: null), + Fields: null, Rest: new(Array.Empty()), GraphQL: new(Singular: graphQLTypeName, Plural: "", Enabled: true, Operation: graphQLOperation), Permissions: new[] { new EntityPermission(Role: "anonymous", Actions: actions.ToArray()) }, @@ -123,6 +125,7 @@ public static Entity GenerateStoredProcedureEntity( public static Entity GenerateEntityWithSingularPlural(string singularNameForEntity, string pluralNameForEntity, EntitySourceType sourceType = EntitySourceType.Table) { return new Entity(Source: new EntitySource(Type: sourceType, Object: "foo", Parameters: null, KeyFields: null), + Fields: null, Rest: new(Array.Empty()), GraphQL: new(singularNameForEntity, pluralNameForEntity), Permissions: Array.Empty(), @@ -139,6 +142,7 @@ public static Entity GenerateEntityWithSingularPlural(string singularNameForEnti public static Entity GenerateEntityWithStringType(string singularGraphQLName, EntitySourceType sourceType = EntitySourceType.Table) { return new Entity(Source: new EntitySource(Type: sourceType, Object: "foo", Parameters: null, KeyFields: null), + Fields: null, Rest: new(Array.Empty()), GraphQL: new(singularGraphQLName, ""), Permissions: Array.Empty(), diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs index 4ebe842c36..a1478093dd 100644 --- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs @@ -45,6 +45,7 @@ private static Entity GenerateEmptyEntity() { return new Entity( Source: new("dbo.entity", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: false), GraphQL: new("Foo", "Foos", Enabled: true), Permissions: Array.Empty(), diff --git a/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs b/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs index e472097fad..84806adc78 100644 --- a/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs +++ b/src/Service.Tests/GraphQLBuilder/Sql/SchemaConverterTests.cs @@ -743,6 +743,7 @@ public static Entity GenerateEmptyEntity(string entityName) { return new Entity( Source: new($"{SCHEMA_NAME}.{TABLE_NAME}", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new(entityName, ""), Permissions: Array.Empty(), diff --git a/src/Service.Tests/OpenApiDocumentor/DocumentVerbosityTests.cs b/src/Service.Tests/OpenApiDocumentor/DocumentVerbosityTests.cs index de8a35212b..fa43617f4f 100644 --- a/src/Service.Tests/OpenApiDocumentor/DocumentVerbosityTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/DocumentVerbosityTests.cs @@ -42,6 +42,7 @@ public async Task ResponseObjectSchemaIncludesTypeProperty() // Arrange Entity entity = new( Source: new(Object: "books", EntitySourceType.Table, null, null), + Fields: null, GraphQL: new(Singular: null, Plural: null, Enabled: false), Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), diff --git a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs index afed2b1f3e..7c0e0225ae 100644 --- a/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/ParameterValidationTests.cs @@ -234,6 +234,7 @@ private async static Task GenerateOpenApiDocumentForGivenEntity { Entity entity = new( Source: entitySource, + Fields: null, GraphQL: new(Singular: null, Plural: null, Enabled: false), Rest: new(Methods: supportedHttpMethods ?? EntityRestOptions.DEFAULT_SUPPORTED_VERBS), Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), diff --git a/src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs b/src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs index bff1333497..5f478b3b80 100644 --- a/src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/PathValidationTests.cs @@ -45,6 +45,7 @@ public async Task ValidateEntityRestPath(string entityName, string configuredRes { Entity entity = new( Source: new(Object: "books", EntitySourceType.Table, null, null), + Fields: null, GraphQL: new(Singular: null, Plural: null, Enabled: false), Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS, Path: configuredRestPath), Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), diff --git a/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs b/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs index b7105dfa45..ffd5aaadde 100644 --- a/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs +++ b/src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs @@ -55,6 +55,7 @@ public static void CreateEntities() { Entity entity1 = new( Source: new(Object: "insert_and_display_all_books_for_given_publisher", EntitySourceType.StoredProcedure, null, null), + Fields: null, GraphQL: new(Singular: null, Plural: null, Enabled: false), Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS), Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 05fec88fff..b622552ef5 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -509,6 +509,18 @@ Object: books, Type: Table }, + Fields: [ + { + Name: id, + Alias: id, + PrimaryKey: false + }, + { + Name: title, + Alias: title, + PrimaryKey: false + } + ], GraphQL: { Singular: book, Plural: books, @@ -893,10 +905,6 @@ ] } ], - Mappings: { - id: id, - title: title - }, Relationships: { authors: { Cardinality: Many, @@ -1624,6 +1632,13 @@ Object: type_table, Type: Table }, + Fields: [ + { + Name: id, + Alias: typeid, + PrimaryKey: false + } + ], GraphQL: { Singular: SupportedType, Plural: SupportedTypes, @@ -1667,10 +1682,7 @@ } ] } - ], - Mappings: { - id: typeid - } + ] } }, { @@ -1765,6 +1777,18 @@ Object: trees, Type: Table }, + Fields: [ + { + Name: species, + Alias: Scientific Name, + PrimaryKey: false + }, + { + Name: region, + Alias: United State's Region, + PrimaryKey: false + } + ], GraphQL: { Singular: Tree, Plural: Trees, @@ -1808,11 +1832,7 @@ } ] } - ], - Mappings: { - region: United State's Region, - species: Scientific Name - } + ] } }, { @@ -1821,6 +1841,13 @@ Object: trees, Type: Table }, + Fields: [ + { + Name: species, + Alias: fancyName, + PrimaryKey: false + } + ], GraphQL: { Singular: Shrub, Plural: Shrubs, @@ -1866,9 +1893,6 @@ ] } ], - Mappings: { - species: fancyName - }, Relationships: { fungus: { TargetEntity: Fungus, @@ -1888,6 +1912,13 @@ Object: fungi, Type: Table }, + Fields: [ + { + Name: spores, + Alias: hazards, + PrimaryKey: false + } + ], GraphQL: { Singular: fungus, Plural: fungi, @@ -1948,9 +1979,6 @@ ] } ], - Mappings: { - spores: hazards - }, Relationships: { Shrub: { TargetEntity: Shrub, @@ -1968,11 +1996,14 @@ books_view_all: { Source: { Object: books_view_all, - Type: View, - KeyFields: [ - id - ] + Type: View }, + Fields: [ + { + Name: id, + PrimaryKey: true + } + ], GraphQL: { Singular: books_view_all, Plural: books_view_alls, @@ -2014,11 +2045,15 @@ books_view_with_mapping: { Source: { Object: books_view_with_mapping, - Type: View, - KeyFields: [ - id - ] + Type: View }, + Fields: [ + { + Name: id, + Alias: book_id, + PrimaryKey: true + } + ], GraphQL: { Singular: books_view_with_mapping, Plural: books_view_with_mappings, @@ -2036,22 +2071,25 @@ } ] } - ], - Mappings: { - id: book_id - } + ] } }, { stocks_view_selected: { Source: { Object: stocks_view_selected, - Type: View, - KeyFields: [ - categoryid, - pieceid - ] + Type: View }, + Fields: [ + { + Name: categoryid, + PrimaryKey: true + }, + { + Name: pieceid, + PrimaryKey: true + } + ], GraphQL: { Singular: stocks_view_selected, Plural: stocks_view_selecteds, @@ -2093,12 +2131,18 @@ books_publishers_view_composite: { Source: { Object: books_publishers_view_composite, - Type: View, - KeyFields: [ - id, - pub_id - ] + Type: View }, + Fields: [ + { + Name: id, + PrimaryKey: true + }, + { + Name: pub_id, + PrimaryKey: true + } + ], GraphQL: { Singular: books_publishers_view_composite, Plural: books_publishers_view_composites, @@ -2352,6 +2396,28 @@ Object: aow, Type: Table }, + Fields: [ + { + Name: DetailAssessmentAndPlanning, + Alias: 始計, + PrimaryKey: false + }, + { + Name: WagingWar, + Alias: 作戰, + PrimaryKey: false + }, + { + Name: StrategicAttack, + Alias: 謀攻, + PrimaryKey: false + }, + { + Name: NoteNum, + Alias: ┬─┬ノ( º _ ºノ), + PrimaryKey: false + } + ], GraphQL: { Singular: ArtOfWar, Plural: ArtOfWars, @@ -2377,13 +2443,7 @@ } ] } - ], - Mappings: { - DetailAssessmentAndPlanning: 始計, - NoteNum: ┬─┬ノ( º _ ºノ), - StrategicAttack: 謀攻, - WagingWar: 作戰 - } + ] } }, { @@ -3110,6 +3170,18 @@ Object: GQLmappings, Type: Table }, + Fields: [ + { + Name: __column1, + Alias: column1, + PrimaryKey: false + }, + { + Name: __column2, + Alias: column2, + PrimaryKey: false + } + ], GraphQL: { Singular: GQLmappings, Plural: GQLmappings, @@ -3135,11 +3207,7 @@ } ] } - ], - Mappings: { - __column1: column1, - __column2: column2 - } + ] } }, { @@ -3182,6 +3250,18 @@ Object: mappedbookmarks, Type: Table }, + Fields: [ + { + Name: id, + Alias: bkid, + PrimaryKey: false + }, + { + Name: bkname, + Alias: name, + PrimaryKey: false + } + ], GraphQL: { Singular: MappedBookmarks, Plural: MappedBookmarks, @@ -3207,11 +3287,7 @@ } ] } - ], - Mappings: { - bkname: name, - id: bkid - } + ] } }, { @@ -3417,6 +3493,18 @@ Object: books, Type: Table }, + Fields: [ + { + Name: id, + Alias: id, + PrimaryKey: false + }, + { + Name: title, + Alias: title, + PrimaryKey: false + } + ], GraphQL: { Singular: bookNF, Plural: booksNF, @@ -3489,10 +3577,6 @@ ] } ], - Mappings: { - id: id, - title: title - }, Relationships: { authors: { Cardinality: Many, diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt index f34141c964..6c81c138ce 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt @@ -374,6 +374,18 @@ Object: books, Type: Table }, + Fields: [ + { + Name: id, + Alias: id, + PrimaryKey: false + }, + { + Name: title, + Alias: title, + PrimaryKey: false + } + ], GraphQL: { Singular: book, Plural: books, @@ -735,10 +747,6 @@ ] } ], - Mappings: { - id: id, - title: title - }, Relationships: { authors: { Cardinality: Many, @@ -1143,6 +1151,13 @@ Object: type_table, Type: Table }, + Fields: [ + { + Name: id, + Alias: typeid, + PrimaryKey: false + } + ], GraphQL: { Singular: SupportedType, Plural: SupportedTypes, @@ -1186,10 +1201,7 @@ } ] } - ], - Mappings: { - id: typeid - } + ] } }, { @@ -1241,6 +1253,18 @@ Object: trees, Type: Table }, + Fields: [ + { + Name: species, + Alias: Scientific Name, + PrimaryKey: false + }, + { + Name: region, + Alias: United State's Region, + PrimaryKey: false + } + ], GraphQL: { Singular: Tree, Plural: Trees, @@ -1284,11 +1308,7 @@ } ] } - ], - Mappings: { - region: United State's Region, - species: Scientific Name - } + ] } }, { @@ -1297,6 +1317,13 @@ Object: trees, Type: Table }, + Fields: [ + { + Name: species, + Alias: fancyName, + PrimaryKey: false + } + ], GraphQL: { Singular: Shrub, Plural: Shrubs, @@ -1342,9 +1369,6 @@ ] } ], - Mappings: { - species: fancyName - }, Relationships: { fungus: { TargetEntity: Fungus, @@ -1364,6 +1388,13 @@ Object: fungi, Type: Table }, + Fields: [ + { + Name: spores, + Alias: hazards, + PrimaryKey: false + } + ], GraphQL: { Singular: fungus, Plural: fungi, @@ -1424,9 +1455,6 @@ ] } ], - Mappings: { - spores: hazards - }, Relationships: { Shrub: { TargetEntity: Shrub, @@ -1444,11 +1472,14 @@ books_view_all: { Source: { Object: books_view_all, - Type: View, - KeyFields: [ - id - ] + Type: View }, + Fields: [ + { + Name: id, + PrimaryKey: true + } + ], GraphQL: { Singular: books_view_all, Plural: books_view_alls, @@ -1490,11 +1521,15 @@ books_view_with_mapping: { Source: { Object: books_view_with_mapping, - Type: View, - KeyFields: [ - id - ] + Type: View }, + Fields: [ + { + Name: id, + Alias: book_id, + PrimaryKey: true + } + ], GraphQL: { Singular: books_view_with_mapping, Plural: books_view_with_mappings, @@ -1512,22 +1547,25 @@ } ] } - ], - Mappings: { - id: book_id - } + ] } }, { stocks_view_selected: { Source: { Object: stocks_view_selected, - Type: View, - KeyFields: [ - categoryid, - pieceid - ] + Type: View }, + Fields: [ + { + Name: categoryid, + PrimaryKey: true + }, + { + Name: pieceid, + PrimaryKey: true + } + ], GraphQL: { Singular: stocks_view_selected, Plural: stocks_view_selecteds, @@ -1569,12 +1607,18 @@ books_publishers_view_composite: { Source: { Object: books_publishers_view_composite, - Type: View, - KeyFields: [ - id, - pub_id - ] + Type: View }, + Fields: [ + { + Name: id, + PrimaryKey: true + }, + { + Name: pub_id, + PrimaryKey: true + } + ], GraphQL: { Singular: books_publishers_view_composite, Plural: books_publishers_view_composites, @@ -1828,6 +1872,28 @@ Object: aow, Type: Table }, + Fields: [ + { + Name: DetailAssessmentAndPlanning, + Alias: 始計, + PrimaryKey: false + }, + { + Name: WagingWar, + Alias: 作戰, + PrimaryKey: false + }, + { + Name: StrategicAttack, + Alias: 謀攻, + PrimaryKey: false + }, + { + Name: NoteNum, + Alias: ┬─┬ノ( º _ ºノ), + PrimaryKey: false + } + ], GraphQL: { Singular: ArtOfWar, Plural: ArtOfWars, @@ -1853,13 +1919,7 @@ } ] } - ], - Mappings: { - DetailAssessmentAndPlanning: 始計, - NoteNum: ┬─┬ノ( º _ ºノ), - StrategicAttack: 謀攻, - WagingWar: 作戰 - } + ] } }, { @@ -2073,6 +2133,18 @@ Object: GQLmappings, Type: Table }, + Fields: [ + { + Name: __column1, + Alias: column1, + PrimaryKey: false + }, + { + Name: __column2, + Alias: column2, + PrimaryKey: false + } + ], GraphQL: { Singular: GQLmappings, Plural: GQLmappings, @@ -2098,11 +2170,7 @@ } ] } - ], - Mappings: { - __column1: column1, - __column2: column2 - } + ] } }, { @@ -2145,6 +2213,18 @@ Object: mappedbookmarks, Type: Table }, + Fields: [ + { + Name: id, + Alias: bkid, + PrimaryKey: false + }, + { + Name: bkname, + Alias: name, + PrimaryKey: false + } + ], GraphQL: { Singular: MappedBookmarks, Plural: MappedBookmarks, @@ -2170,11 +2250,7 @@ } ] } - ], - Mappings: { - bkname: name, - id: bkid - } + ] } }, { diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt index 75490a804b..5e8631d46f 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt @@ -407,6 +407,18 @@ Object: books, Type: Table }, + Fields: [ + { + Name: id, + Alias: id, + PrimaryKey: false + }, + { + Name: title, + Alias: title, + PrimaryKey: false + } + ], GraphQL: { Singular: book, Plural: books, @@ -768,10 +780,6 @@ ] } ], - Mappings: { - id: id, - title: title - }, Relationships: { authors: { Cardinality: Many, @@ -1193,6 +1201,13 @@ Object: type_table, Type: Table }, + Fields: [ + { + Name: id, + Alias: typeid, + PrimaryKey: false + } + ], GraphQL: { Singular: SupportedType, Plural: SupportedTypes, @@ -1236,10 +1251,7 @@ } ] } - ], - Mappings: { - id: typeid - } + ] } }, { @@ -1312,6 +1324,18 @@ Object: trees, Type: Table }, + Fields: [ + { + Name: species, + Alias: Scientific Name, + PrimaryKey: false + }, + { + Name: region, + Alias: United State's Region, + PrimaryKey: false + } + ], GraphQL: { Singular: Tree, Plural: Trees, @@ -1355,11 +1379,7 @@ } ] } - ], - Mappings: { - region: United State's Region, - species: Scientific Name - } + ] } }, { @@ -1368,6 +1388,13 @@ Object: trees, Type: Table }, + Fields: [ + { + Name: species, + Alias: fancyName, + PrimaryKey: false + } + ], GraphQL: { Singular: Shrub, Plural: Shrubs, @@ -1413,9 +1440,6 @@ ] } ], - Mappings: { - species: fancyName - }, Relationships: { fungus: { TargetEntity: Fungus, @@ -1435,6 +1459,13 @@ Object: fungi, Type: Table }, + Fields: [ + { + Name: spores, + Alias: hazards, + PrimaryKey: false + } + ], GraphQL: { Singular: fungus, Plural: fungi, @@ -1495,9 +1526,6 @@ ] } ], - Mappings: { - spores: hazards - }, Relationships: { Shrub: { TargetEntity: Shrub, @@ -1683,11 +1711,15 @@ books_view_with_mapping: { Source: { Object: books_view_with_mapping, - Type: View, - KeyFields: [ - id - ] + Type: View }, + Fields: [ + { + Name: id, + Alias: book_id, + PrimaryKey: true + } + ], GraphQL: { Singular: books_view_with_mapping, Plural: books_view_with_mappings, @@ -1705,10 +1737,7 @@ } ] } - ], - Mappings: { - id: book_id - } + ] } }, { @@ -1986,6 +2015,28 @@ Object: aow, Type: Table }, + Fields: [ + { + Name: DetailAssessmentAndPlanning, + Alias: 始計, + PrimaryKey: false + }, + { + Name: WagingWar, + Alias: 作戰, + PrimaryKey: false + }, + { + Name: StrategicAttack, + Alias: 謀攻, + PrimaryKey: false + }, + { + Name: NoteNum, + Alias: ┬─┬ノ( º _ ºノ), + PrimaryKey: false + } + ], GraphQL: { Singular: ArtOfWar, Plural: ArtOfWars, @@ -2011,13 +2062,7 @@ } ] } - ], - Mappings: { - DetailAssessmentAndPlanning: 始計, - NoteNum: ┬─┬ノ( º _ ºノ), - StrategicAttack: 謀攻, - WagingWar: 作戰 - } + ] } }, { @@ -2135,6 +2180,18 @@ Object: gqlmappings, Type: Table }, + Fields: [ + { + Name: __column1, + Alias: column1, + PrimaryKey: false + }, + { + Name: __column2, + Alias: column2, + PrimaryKey: false + } + ], GraphQL: { Singular: GQLmappings, Plural: GQLmappings, @@ -2160,11 +2217,7 @@ } ] } - ], - Mappings: { - __column1: column1, - __column2: column2 - } + ] } }, { @@ -2207,6 +2260,18 @@ Object: mappedbookmarks, Type: Table }, + Fields: [ + { + Name: id, + Alias: bkid, + PrimaryKey: false + }, + { + Name: bkname, + Alias: name, + PrimaryKey: false + } + ], GraphQL: { Singular: MappedBookmarks, Plural: MappedBookmarks, @@ -2232,11 +2297,7 @@ } ] } - ], - Mappings: { - bkname: name, - id: bkid - } + ] } }, { @@ -2390,6 +2451,18 @@ Object: books, Type: Table }, + Fields: [ + { + Name: id, + Alias: id, + PrimaryKey: false + }, + { + Name: title, + Alias: title, + PrimaryKey: false + } + ], GraphQL: { Singular: bookNF, Plural: booksNF, @@ -2449,10 +2522,6 @@ ] } ], - Mappings: { - id: id, - title: title - }, Relationships: { authors: { Cardinality: Many, @@ -2574,6 +2643,18 @@ Object: dimaccount, Type: Table }, + Fields: [ + { + Name: parentaccountkey, + Alias: ParentAccountKey, + PrimaryKey: false + }, + { + Name: accountkey, + Alias: AccountKey, + PrimaryKey: false + } + ], GraphQL: { Singular: dbo_DimAccount, Plural: dbo_DimAccounts, @@ -2592,10 +2673,6 @@ ] } ], - Mappings: { - accountkey: AccountKey, - parentaccountkey: ParentAccountKey - }, Relationships: { child_accounts: { Cardinality: Many, diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index 55a1becb4a..16c1a878fa 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -2295,6 +2295,7 @@ public virtual async Task TestConfigTakesPrecedenceForRelationshipFieldsOverDB( Entity clubEntity = new( Source: new("clubs", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new("club", "clubs"), Permissions: new[] { ConfigurationTests.GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -2304,6 +2305,7 @@ public virtual async Task TestConfigTakesPrecedenceForRelationshipFieldsOverDB( Entity playerEntity = new( Source: new("players", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new("player", "players"), Permissions: new[] { ConfigurationTests.GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs index f65e7a5088..00930103c7 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs @@ -855,6 +855,7 @@ private static Entity CreateEntityWithDescription(string description) return new( source, gqlOptions, + null, restOptions, [], null, diff --git a/src/Service.Tests/TestHelper.cs b/src/Service.Tests/TestHelper.cs index f1ccd7f13e..b94470b96b 100644 --- a/src/Service.Tests/TestHelper.cs +++ b/src/Service.Tests/TestHelper.cs @@ -78,8 +78,21 @@ public static RuntimeConfigProvider GetRuntimeConfigProvider(FileSystemRuntimeCo /// The source name of the entity. public static RuntimeConfig AddMissingEntitiesToConfig(RuntimeConfig config, string entityKey, string entityName, string[] keyfields = null) { + List fields = []; + if (keyfields != null) + { + foreach (string key in keyfields) + { + if (!string.IsNullOrWhiteSpace(key)) + { + fields.Add(new FieldMetadata { Name = key, PrimaryKey = true }); + } + } + } + Entity entity = new( Source: new(entityName, EntitySourceType.Table, null, keyfields), + Fields: fields, GraphQL: new(entityKey, entityKey.Pluralize()), Rest: new(Enabled: true), Permissions: new[] diff --git a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs index 16caf29b49..119e6637c6 100644 --- a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs @@ -116,6 +116,7 @@ public void InvalidCRUDForStoredProcedure( Entity testEntity = new( Source: entitySource, + Fields: null, Rest: new(EntityRestOptions.DEFAULT_HTTP_VERBS_ENABLED_FOR_SP), GraphQL: new(AuthorizationHelpers.TEST_ENTITY, AuthorizationHelpers.TEST_ENTITY + "s"), Permissions: permissionSettings.ToArray(), @@ -1000,6 +1001,7 @@ public void TestOperationValidityAndCasing(string operationName, bool exceptionE Entity sampleEntity = new( Source: new(AuthorizationHelpers.TEST_ENTITY, EntitySourceType.Table, null, null), + Fields: null, Rest: null, GraphQL: null, Permissions: new[] { permissionForEntity }, @@ -1537,6 +1539,7 @@ private static Entity GetSampleEntityUsingSourceAndRelationshipMap( Entity sampleEntity = new( Source: new(source, EntitySourceType.Table, null, null), + Fields: null, Rest: restDetails ?? new(Enabled: false), GraphQL: graphQLDetails, Permissions: new[] { permissionForEntity }, @@ -2012,6 +2015,7 @@ public void ValidateRestMethodsForEntityInConfig( string entityName = "EntityA"; // Sets REST method for the entity Entity entity = new(Source: new("TEST_SOURCE", sourceType, null, null), + Fields: null, Rest: new(Methods: methods), GraphQL: new(entityName, ""), Permissions: Array.Empty(), @@ -2337,6 +2341,7 @@ public void TestRuntimeConfigSetupWithNonJsonConstructor() Entity sampleEntity1 = new( Source: entitySource, + Fields: null, GraphQL: null, Rest: null, Permissions: null, diff --git a/src/Service.Tests/UnitTests/RequestValidatorUnitTests.cs b/src/Service.Tests/UnitTests/RequestValidatorUnitTests.cs index 186f254c51..5be1375c0f 100644 --- a/src/Service.Tests/UnitTests/RequestValidatorUnitTests.cs +++ b/src/Service.Tests/UnitTests/RequestValidatorUnitTests.cs @@ -361,7 +361,7 @@ public static void PerformTest( ), Entities: new(new Dictionary() { - { DEFAULT_NAME, new Entity(entitySource, new EntityGraphQLOptions(findRequestContext.EntityName, findRequestContext.EntityName), new EntityRestOptions(new SupportedHttpVerb[0]), null, null, null) } + { DEFAULT_NAME, new Entity(entitySource, new EntityGraphQLOptions(findRequestContext.EntityName, findRequestContext.EntityName), null, new EntityRestOptions(new SupportedHttpVerb[0]), null, null, null) } }) ); MockFileSystem fileSystem = new(); diff --git a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs index f1f9c4255d..8b4ed68f60 100644 --- a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs +++ b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs @@ -347,6 +347,7 @@ public void ValidateGraphQLReservedNaming_DatabaseColumns(string dbColumnName, s Entity sampleEntity = new( Source: new("sampleElement", EntitySourceType.Table, null, null), + Fields: null, Rest: new(Enabled: false), GraphQL: new("", ""), Permissions: new EntityPermission[] { ConfigurationTests.GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, @@ -423,6 +424,7 @@ public async Task ValidateExceptionForInvalidResultFieldNames(string invalidFiel { "get_book_by_id", new Entity( Source: new("dbo.get_book_by_id", EntitySourceType.StoredProcedure, null, null), + Fields: null, Rest: new(Enabled: true), GraphQL: new("get_book_by_id", "get_book_by_ids", Enabled: true), Permissions: new EntityPermission[] {