diff --git a/docs/design/HC16-upgrade.md b/docs/design/HC16-upgrade.md
index a77d8149bb..68c274c5ce 100644
--- a/docs/design/HC16-upgrade.md
+++ b/docs/design/HC16-upgrade.md
@@ -3,7 +3,8 @@
This is a high-level companion to PR #3480. The goal here is to give a reader
the *why* behind each non-obvious code change without forcing them to diff Hot
Chocolate v13/v14 against v16 themselves. For the line-level changes, read the
-PR.
+PR. Sections 1-13 describe the original migration to 16.0.0; section 14 covers
+the follow-up to 16.6.4.
---
@@ -230,3 +231,29 @@ notable test changes are:
`BYTE_TYPE` (`UnsignedByte` → `byte_types`) and `BYTEARRAY_TYPE`
(`Base64String` → `bytearray_types`) because the GraphQL scalar name no
longer matches the test database column-name root.
+
+---
+
+## 14. Follow-up to 16.6.4
+
+The follow-up updates all six centrally managed Hot Chocolate packages from
+16.0.0 to 16.6.4. This crosses minor versions, not just patch versions, and
+requires two compatibility changes.
+
+Hot Chocolate validates GraphQL input defaults against their field types.
+SQL default constraints contain database expressions such as `((1))`,
+`(getdate())`, or `('Placeholder')`, not GraphQL literals. Copying those
+expressions into create-input defaults can prevent the entire schema from
+building.
+
+SQL create fields with database defaults therefore remain nullable but no
+longer have a GraphQL input default. Omitting a field lets the database
+evaluate its default; supplying an explicit `null` remains distinct and is
+still subject to database constraints. This applies to nested and linking
+create inputs as well. The source `@defaultValue` metadata is preserved,
+and Cosmos GraphQL literal defaults and stored-procedure parameter defaults
+are unchanged.
+
+The `FragmentSpreadNode` constructor used in one unit test also needs the
+new explicit arguments collection. An empty collection preserves the
+argument-free fragment spread used by that test.
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 56523d0a94..afa9d3e276 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -13,12 +13,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs
index c2a9b0a9ac..f63ed20a03 100644
--- a/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs
+++ b/src/Service.GraphQLBuilder/Mutations/CreateMutationBuilder.cs
@@ -60,7 +60,7 @@ public static class CreateMutationBuilder
// 1. Scalar input fields.
IEnumerable scalarInputFields = objectTypeDefinitionNode.Fields
.Where(field => IsBuiltInType(field.Type) && !IsAutoGeneratedField(field))
- .Select(field => GenerateScalarInputType(name, field, IsMultipleCreateOperationEnabled));
+ .Select(field => GenerateScalarInputType(name, field, databaseType, IsMultipleCreateOperationEnabled));
// Add scalar input fields to list of input fields for current input type.
// Generate the create input type only if there are any scalar fields that are not auto-generated fields.
@@ -191,7 +191,7 @@ private static InputObjectTypeDefinitionNode GenerateCreateInputTypeForNonRelati
{
if (IsBuiltInType(field.Type))
{
- return GenerateScalarInputType(name, field);
+ return GenerateScalarInputType(name, field, databaseType);
}
string typeName = RelationshipDirectiveType.Target(field);
@@ -269,17 +269,26 @@ private static bool DoesFieldHaveReferencingFieldDirective(FieldDefinitionNode f
/// Name of the field.
/// Field definition.
/// Database type
- /// Indicates whether multiple create operation is enabled
- private static InputValueDefinitionNode GenerateScalarInputType(NameNode name, FieldDefinitionNode fieldDefinition, bool isMultipleCreateOperationEnabled = false)
+ /// Indicates whether multiple create operation is enabled
+ private static InputValueDefinitionNode GenerateScalarInputType(
+ NameNode name,
+ FieldDefinitionNode fieldDefinition,
+ DatabaseType databaseType,
+ bool isMultipleCreateOperationEnabled = false)
{
IValueNode? defaultValue = null;
+ bool isFieldNullable = false;
if (DefaultValueDirectiveType.TryGetDefaultValue(fieldDefinition, out ObjectValueNode? value))
{
- defaultValue = value.Fields[0].Value;
- }
+ isFieldNullable = true;
- bool isFieldNullable = defaultValue is not null;
+ // SQL defaults are expressions evaluated by the database, not GraphQL input defaults.
+ if (!IsRelationalDb(databaseType))
+ {
+ defaultValue = value.Fields[0].Value;
+ }
+ }
if (isMultipleCreateOperationEnabled &&
DoesFieldHaveReferencingFieldDirective(fieldDefinition))
diff --git a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs
index a1478093dd..6dd75f7bb8 100644
--- a/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs
+++ b/src/Service.Tests/GraphQLBuilder/MutationBuilderTests.cs
@@ -5,13 +5,20 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
+using System.Text.Json;
+using System.Threading.Tasks;
using Azure.DataApiBuilder.Auth;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder;
+using Azure.DataApiBuilder.Service.GraphQLBuilder.Directives;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations;
+using Azure.DataApiBuilder.Service.GraphQLBuilder.Sql;
+using Azure.DataApiBuilder.Service.Services;
using Azure.DataApiBuilder.Service.Tests.GraphQLBuilder.Helpers;
+using HotChocolate;
+using HotChocolate.Execution;
using HotChocolate.Language;
using HotChocolate.Types;
using Humanizer;
@@ -989,12 +996,13 @@ type Baz @model(name:""Baz""){
[DataRow(true, "boolean", "Boolean")]
[DataRow(1.2f, "float", "Float")]
[TestCategory("Mutation Builder - Create")]
- public void CreateMutationWillHonorDefaultValue(object defaultValue, string fieldName, string fieldType)
+ public async Task CreateMutationWillHonorDefaultValue(object defaultValue, string fieldName, string fieldType)
{
+ string defaultLiteral = JsonSerializer.Serialize(defaultValue);
string gql =
@$"
type Foo @model(name:""Foo"") {{
- id: {fieldType}! @defaultValue(value: {{ {fieldName}: {(defaultValue is string ? $"\"{defaultValue}\"" : defaultValue)} }})
+ id: {fieldType}! @defaultValue(value: {{ {fieldName}: {defaultLiteral} }})
}}
";
@@ -1012,8 +1020,328 @@ type Foo @model(name:""Foo"") {{
InputObjectTypeDefinitionNode createFooInput = (InputObjectTypeDefinitionNode)mutationRoot.Definitions.First(d => d is InputObjectTypeDefinitionNode node && node.Name.Value == "CreateFooInput");
- // Serialization has them as strings, so we'll just do string compares
- Assert.AreEqual(defaultValue.ToString(), createFooInput.Fields[0].DefaultValue.Value);
+ Assert.AreEqual(defaultLiteral, createFooInput.Fields[0].DefaultValue.ToString());
+
+ Dictionary capturedInput = null;
+ IRequestExecutor executor = CreateSchemaBuilderForCreateInputs(mutationRoot)
+ .AddResolver("Query", "inspect", context =>
+ {
+ capturedInput = context.ArgumentValue>("item");
+ return true;
+ })
+ .Create()
+ .MakeExecutable();
+
+ OperationResult result = (await executor.ExecuteAsync("{ inspect(item: {}) }")).ExpectOperationResult();
+ Assert.AreEqual(0, result.Errors.Count, string.Join(" | ", result.Errors.Select(error => error.ToString())));
+ Assert.IsNotNull(capturedInput);
+ Assert.AreEqual(defaultLiteral, JsonSerializer.Serialize(capturedInput["id"]),
+ "A typed Cosmos default must still be supplied when the input field is omitted.");
+ }
+
+ [DataTestMethod]
+ [DataRow(DatabaseType.MSSQL, typeof(int), "((1))", "7", false)]
+ [DataRow(DatabaseType.MSSQL, typeof(int), "((42))", "7", false)]
+ [DataRow(DatabaseType.MSSQL, typeof(int), "(rand())", "7", false)]
+ [DataRow(DatabaseType.MSSQL, typeof(double), "((0))", "1.25", false)]
+ [DataRow(DatabaseType.MSSQL, typeof(decimal), "((1.5))", "1.25", false)]
+ [DataRow(DatabaseType.MSSQL, typeof(string), "('Placeholder')", "\"provided\"", false)]
+ [DataRow(DatabaseType.MSSQL, typeof(string), "(suser_sname())", "\"provided\"", true)]
+ [DataRow(DatabaseType.MSSQL, typeof(bool), "((1))", "false", false)]
+ [DataRow(DatabaseType.MSSQL, typeof(DateTime), "(getdate())", "\"2026-01-01T00:00:00Z\"", false)]
+ [DataRow(DatabaseType.MSSQL, typeof(DateTime), "(dateadd(day,(1),getdate()))", "\"2026-01-01T00:00:00Z\"", false)]
+ [DataRow(DatabaseType.MSSQL, typeof(DateTimeOffset), "(sysdatetimeoffset())", "\"2026-01-01T00:00:00Z\"", false)]
+ [DataRow(DatabaseType.MSSQL, typeof(Guid), "(newid())", "\"de305d54-75b4-431b-adb2-eb6b9e546014\"", false)]
+ [DataRow(DatabaseType.MySQL, typeof(int), "42", "7", false)]
+ [DataRow(DatabaseType.PostgreSQL, typeof(int), "nextval('counter_seq'::regclass)", "7", false)]
+ [DataRow(DatabaseType.DWSQL, typeof(int), "((42))", "7", false)]
+ [TestCategory("Mutation Builder - Create")]
+ [TestCategory("GraphQL Schema Builder")]
+ public async Task SqlDefaultExpressionsCreateValidInputSchema(
+ DatabaseType databaseType,
+ Type systemType,
+ string defaultExpression,
+ string suppliedLiteral,
+ bool isNullable)
+ {
+ ObjectTypeDefinitionNode objectType = GenerateSqlObjectTypeWithDefault(
+ systemType, defaultExpression, isNullable, databaseType);
+ DocumentNode mutationRoot = MutationBuilder.Build(
+ new DocumentNode(new[] { objectType }),
+ new() { { "Foo", databaseType } },
+ new(new Dictionary { { "Foo", GenerateEmptyEntity() } }),
+ entityPermissionsMap: _entityPermissions);
+
+ Dictionary capturedInput = null;
+ IRequestExecutor executor = CreateSchemaBuilderForCreateInputs(mutationRoot)
+ .AddResolver("Query", "inspect", context =>
+ {
+ // Use DAB's extraction path; HC's dictionary coercion fills omitted nullable fields with null.
+ IDictionary parameters = ExecutionHelper.GetParametersFromSchemaAndQueryFields(
+ context.Selection.Field, context.Selection.RequireFieldNode(), context.Variables);
+ Assert.IsInstanceOfType>(parameters["item"]);
+ IEnumerable fields = (IEnumerable)parameters["item"];
+ InputObjectType inputType = ExecutionHelper.InputObjectTypeFromIInputField(context.Selection.Field.Arguments["item"]);
+ capturedInput = ExtractInputFields(fields, inputType, context.Variables);
+ return true;
+ })
+ .Create()
+ .MakeExecutable();
+
+ foreach (bool useVariables in new[] { false, true })
+ {
+ capturedInput = null;
+ OperationResult omitted = await ExecuteCreateInputAsync(
+ executor, "item", "{ id: 1 }", """{ "id": 1 }""", useVariables);
+ Assert.AreEqual(0, omitted.Errors.Count, string.Join(" | ", omitted.Errors.Select(error => error.ToString())));
+ Assert.IsNotNull(capturedInput);
+ Assert.IsFalse(capturedInput.ContainsKey("defaulted"),
+ "An omitted SQL column must remain absent so the database, not GraphQL, evaluates its default.");
+
+ capturedInput = null;
+ OperationResult explicitNull = await ExecuteCreateInputAsync(
+ executor, "item", "{ id: 1, defaulted: null }", """{ "id": 1, "defaulted": null }""", useVariables);
+ Assert.AreEqual(0, explicitNull.Errors.Count, string.Join(" | ", explicitNull.Errors.Select(error => error.ToString())));
+ Assert.IsNotNull(capturedInput);
+ Assert.IsTrue(capturedInput.ContainsKey("defaulted"), "Explicit null must remain distinct from omission.");
+ Assert.IsNull(capturedInput["defaulted"], "A SQL default must not replace an explicitly supplied null.");
+
+ capturedInput = null;
+ OperationResult supplied = await ExecuteCreateInputAsync(
+ executor, "item", $"{{ id: 1, defaulted: {suppliedLiteral} }}",
+ $$"""{ "id": 1, "defaulted": {{suppliedLiteral}} }""", useVariables);
+ Assert.AreEqual(0, supplied.Errors.Count, string.Join(" | ", supplied.Errors.Select(error => error.ToString())));
+ Assert.IsNotNull(capturedInput);
+ Assert.IsTrue(capturedInput.ContainsKey("defaulted"));
+ Assert.AreEqual(suppliedLiteral, JsonSerializer.Serialize(capturedInput["defaulted"]),
+ "An explicitly supplied value must survive input coercion unchanged.");
+
+ capturedInput = null;
+ OperationResult missingRequired = await ExecuteCreateInputAsync(executor, "item", "{}", "{}", useVariables);
+ Assert.IsTrue(missingRequired.Errors.Count > 0, "A non-null column without a default must still be required.");
+ Assert.IsNull(capturedInput, "Invalid input must be rejected before invoking a resolver.");
+ }
+
+ InputObjectTypeDefinitionNode input = mutationRoot.Definitions.OfType()
+ .Single(node => node.Name.Value == "CreateFooInput");
+ Assert.IsNull(input.Fields.Single(field => field.Name.Value == "defaulted").DefaultValue);
+ Assert.IsFalse(input.Fields.Single(field => field.Name.Value == "defaulted").Type.IsNonNullType());
+ Assert.IsTrue(input.Fields.Single(field => field.Name.Value == "id").Type.IsNonNullType());
+ }
+
+ [DataTestMethod]
+ [DataRow(false, false)]
+ [DataRow(false, true)]
+ [DataRow(true, false)]
+ [DataRow(true, true)]
+ [TestCategory("Mutation Builder - Create")]
+ [TestCategory("GraphQL Schema Builder")]
+ public async Task SqlDefaultsRemainOptionalForMultipleAndLinkingCreate(bool createMultiple, bool useVariables)
+ {
+ DocumentNode relationshipTypes = Utf8GraphQLParser.Parse("""
+ type Foo @model(name: "Foo") {
+ bars: [Bar!]! @relationship(target: "Bar", cardinality: "many")
+ }
+ type Bar @model(name: "Bar") {
+ id: Int! @primaryKey
+ }
+ """);
+ ObjectTypeDefinitionNode source = GenerateSqlObjectTypeWithDefault(typeof(int), "((42))", false, DatabaseType.MSSQL);
+ source = source.WithFields(source.Fields.Concat(relationshipTypes.Definitions.OfType().First().Fields).ToArray());
+ ObjectTypeDefinitionNode target = relationshipTypes.Definitions.OfType().Last();
+ string linkingTypeName = GraphQLNaming.GenerateLinkingNodeName("Foo", "Bar");
+ ObjectTypeDefinitionNode linking = GenerateSqlObjectTypeWithDefault(
+ typeof(double), "((0.5))", false, DatabaseType.MSSQL, linkingTypeName, "royalty_percentage")
+ .WithDirectives(Array.Empty());
+
+ Entity sourceEntity = GenerateEmptyEntity() with
+ {
+ Relationships = new()
+ {
+ { "bars", new EntityRelationship(Cardinality.Many, "Bar", new[] { "id" }, new[] { "id" },
+ "dbo.foo_bar", new[] { "foo_id" }, new[] { "bar_id" }) }
+ }
+ };
+ DocumentNode mutationRoot = MutationBuilder.Build(
+ new DocumentNode(new[] { source, target, linking }),
+ new() { { "Foo", DatabaseType.MSSQL }, { "Bar", DatabaseType.MSSQL } },
+ new(new Dictionary
+ {
+ { "Foo", sourceEntity },
+ { "Bar", GraphQLTestHelpers.GenerateEntityWithSingularPlural("Bar", "Bars") }
+ }),
+ entityPermissionsMap: _entityPermissions,
+ IsMultipleCreateOperationEnabled: true);
+
+ string mutationName = createMultiple ? "createFoos" : "createFoo";
+ string argumentName = createMultiple ? MutationBuilder.ARRAY_INPUT_ARGUMENT_NAME : MutationBuilder.ITEM_INPUT_ARGUMENT_NAME;
+ Dictionary capturedInput = null;
+ Dictionary capturedLinkingInput = null;
+ IRequestExecutor executor = CreateSchemaBuilderForCreateInputs(mutationRoot, mutationName)
+ .AddResolver("Query", "inspect", context =>
+ {
+ IDictionary parameters = ExecutionHelper.GetParametersFromSchemaAndQueryFields(
+ context.Selection.Field, context.Selection.RequireFieldNode(), context.Variables);
+ IEnumerable fields;
+ if (createMultiple)
+ {
+ Assert.IsInstanceOfType>(parameters[argumentName]);
+ IValueNode inputItem = ((IEnumerable)parameters[argumentName]).Single();
+ Assert.IsInstanceOfType(inputItem);
+ fields = ((ObjectValueNode)inputItem).Fields;
+ }
+ else
+ {
+ Assert.IsInstanceOfType>(parameters[argumentName]);
+ fields = (IEnumerable)parameters[argumentName];
+ }
+
+ InputObjectType inputType = ExecutionHelper.InputObjectTypeFromIInputField(context.Selection.Field.Arguments[argumentName]);
+ capturedInput = ExtractInputFields(fields, inputType, context.Variables);
+
+ Assert.IsInstanceOfType>(capturedInput["bars"]);
+ IValueNode linkingItem = ((IEnumerable)capturedInput["bars"]).Single();
+ Assert.IsInstanceOfType(linkingItem);
+ InputObjectType linkingInputType = ExecutionHelper.InputObjectTypeFromIInputField(inputType.Fields["bars"]);
+ capturedLinkingInput = ExtractInputFields(((ObjectValueNode)linkingItem).Fields, linkingInputType, context.Variables);
+ return true;
+ })
+ .Create()
+ .MakeExecutable();
+
+ string item = "{ id: 1, bars: [{ id: 2 }] }";
+ string variableItem = """{ "id": 1, "bars": [{ "id": 2 }] }""";
+ OperationResult omitted = await ExecuteCreateInputAsync(
+ executor, argumentName, createMultiple ? $"[{item}]" : item,
+ createMultiple ? $"[{variableItem}]" : variableItem, useVariables);
+ Assert.AreEqual(0, omitted.Errors.Count, string.Join(" | ", omitted.Errors.Select(error => error.ToString())));
+ Assert.IsNotNull(capturedInput);
+ Assert.IsNotNull(capturedLinkingInput);
+ Assert.IsFalse(capturedInput.ContainsKey("defaulted"), "An omitted SQL column must remain absent.");
+ Assert.IsFalse(capturedLinkingInput.ContainsKey("royalty_percentage"), "An omitted linking column must remain absent.");
+
+ capturedInput = null;
+ capturedLinkingInput = null;
+ item = "{ id: 1, defaulted: null, bars: [{ id: 2, royalty_percentage: null }] }";
+ variableItem = """{ "id": 1, "defaulted": null, "bars": [{ "id": 2, "royalty_percentage": null }] }""";
+ OperationResult explicitNull = await ExecuteCreateInputAsync(
+ executor, argumentName, createMultiple ? $"[{item}]" : item,
+ createMultiple ? $"[{variableItem}]" : variableItem, useVariables);
+ Assert.AreEqual(0, explicitNull.Errors.Count, string.Join(" | ", explicitNull.Errors.Select(error => error.ToString())));
+ Assert.IsNotNull(capturedInput);
+ Assert.IsNotNull(capturedLinkingInput);
+ Assert.IsTrue(capturedInput.ContainsKey("defaulted"), "Explicit null must remain distinct from omission.");
+ Assert.IsNull(capturedInput["defaulted"]);
+ Assert.IsTrue(capturedLinkingInput.ContainsKey("royalty_percentage"), "Explicit null must remain present on a linking input.");
+ Assert.IsNull(capturedLinkingInput["royalty_percentage"]);
+
+ capturedInput = null;
+ capturedLinkingInput = null;
+ item = "{ id: 1, defaulted: 7, bars: [{ id: 2, royalty_percentage: 0.75 }] }";
+ variableItem = """{ "id": 1, "defaulted": 7, "bars": [{ "id": 2, "royalty_percentage": 0.75 }] }""";
+ OperationResult supplied = await ExecuteCreateInputAsync(
+ executor, argumentName, createMultiple ? $"[{item}]" : item,
+ createMultiple ? $"[{variableItem}]" : variableItem, useVariables);
+ Assert.AreEqual(0, supplied.Errors.Count, string.Join(" | ", supplied.Errors.Select(error => error.ToString())));
+ Assert.IsNotNull(capturedInput);
+ Assert.IsNotNull(capturedLinkingInput);
+ Assert.AreEqual(7, capturedInput["defaulted"], "An explicit value must not be replaced by a SQL default.");
+ Assert.AreEqual(0.75, capturedLinkingInput["royalty_percentage"], "An explicit linking value must remain unchanged.");
+
+ capturedInput = null;
+ capturedLinkingInput = null;
+ item = "{ id: 1, bars: [{}] }";
+ variableItem = """{ "id": 1, "bars": [{}] }""";
+ OperationResult missingRequired = await ExecuteCreateInputAsync(
+ executor, argumentName, createMultiple ? $"[{item}]" : item,
+ createMultiple ? $"[{variableItem}]" : variableItem, useVariables);
+ Assert.IsTrue(missingRequired.Errors.Count > 0, "Non-defaulted fields on the linking input must remain required.");
+ Assert.IsNull(capturedInput, "Invalid input must be rejected before invoking a resolver.");
+ Assert.IsNull(capturedLinkingInput);
+
+ InputObjectTypeDefinitionNode linkingInput = mutationRoot.Definitions.OfType()
+ .Single(node => node.Name.Value == CreateMutationBuilder.GenerateInputTypeName(linkingTypeName).Value);
+ InputValueDefinitionNode defaultedField = linkingInput.Fields.Single(field => field.Name.Value == "royalty_percentage");
+ Assert.IsNull(defaultedField.DefaultValue);
+ Assert.IsFalse(defaultedField.Type.IsNonNullType());
+ Assert.AreEqual("Float", defaultedField.Type.NamedType().Name.Value);
+ }
+
+ private static async Task ExecuteCreateInputAsync(
+ IRequestExecutor executor,
+ string argumentName,
+ string literalInput,
+ string variableInput,
+ bool useVariables)
+ {
+ OperationRequestBuilder request = OperationRequestBuilder.New();
+ if (useVariables)
+ {
+ IInputType inputType = executor.Schema.QueryType.Fields["inspect"].Arguments[argumentName].Type;
+ request.SetDocument($"query (${argumentName}: {inputType}) {{ inspect({argumentName}: ${argumentName}) }}")
+ .SetVariableValues($"{{ \"{argumentName}\": {variableInput} }}");
+ }
+ else
+ {
+ request.SetDocument($"{{ inspect({argumentName}: {literalInput}) }}");
+ }
+
+ return (await executor.ExecuteAsync(request.Build())).ExpectOperationResult();
+ }
+
+ private static Dictionary ExtractInputFields(
+ IEnumerable fields,
+ InputObjectType inputType,
+ IVariableValueCollection variables)
+ {
+ return fields.ToDictionary(
+ field => field.Name.Value,
+ field => ExecutionHelper.ExtractValueFromIValueNode(field.Value, inputType.Fields[field.Name.Value], variables));
+ }
+
+ private static ObjectTypeDefinitionNode GenerateSqlObjectTypeWithDefault(
+ Type systemType,
+ string defaultExpression,
+ bool isNullable,
+ DatabaseType databaseType,
+ string entityName = "Foo",
+ string defaultFieldName = "defaulted")
+ {
+ SourceDefinition table = new();
+ table.Columns.Add("id", new ColumnDefinition { SystemType = typeof(int), IsNullable = false });
+ table.Columns.Add(defaultFieldName, new ColumnDefinition
+ {
+ SystemType = systemType,
+ IsNullable = isNullable,
+ DefaultValue = defaultExpression
+ });
+ table.PrimaryKey.Add("id");
+
+ Entity entity = GraphQLTestHelpers.GenerateEntityWithSingularPlural(entityName, entityName.Pluralize());
+ IEnumerable roles = new[] { "anonymous" };
+ return SchemaConverter.GenerateObjectTypeDefinitionForDatabaseObject(
+ entityName,
+ new DatabaseTable { TableDefinition = table },
+ entity,
+ new(new Dictionary { { entityName, entity } }),
+ rolesAllowedForEntity: roles,
+ rolesAllowedForFields: table.Columns.Keys.ToDictionary(column => column, _ => roles),
+ databaseType: databaseType);
+ }
+
+ private static ISchemaBuilder CreateSchemaBuilderForCreateInputs(DocumentNode mutationRoot, string mutationName = "createFoo")
+ {
+ FieldDefinitionNode mutation = GetMutationNode(mutationRoot).Fields.Single(field => field.Name.Value == mutationName);
+ DocumentNode inputs = new(mutationRoot.Definitions.OfType().ToArray());
+
+ // Use the generated input types and argument shape while keeping database resolvers out of this schema.
+ return SchemaBuilder.New()
+ .AddDocumentFromString(inputs.ToString())
+ .AddDocumentFromString($"type Query {{ inspect({string.Join(", ", mutation.Arguments.Select(argument => $"{argument.Name}: {argument.Type}"))}): Boolean }}")
+ .AddDirectiveType()
+ .AddType()
+ .AddType()
+ .AddType();
}
public static ObjectTypeDefinitionNode GetMutationNode(DocumentNode mutationRoot)
diff --git a/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs b/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs
index f5f8641cc2..ee35167962 100644
--- a/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs
+++ b/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs
@@ -99,7 +99,11 @@ public void AddGraphQLFields_FragmentSpreadWithoutContextThrows()
SqlQueryStructure structure = CreateStructure();
ISelectionNode[] selections =
{
- new FragmentSpreadNode(null, new NameNode("BookFields"), System.Array.Empty())
+ new FragmentSpreadNode(
+ location: null,
+ name: new NameNode("BookFields"),
+ arguments: System.Array.Empty(),
+ directives: System.Array.Empty())
};
TargetInvocationException exception = Assert.ThrowsException(() =>