-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathGraphQLLiteOptions.cs
More file actions
156 lines (135 loc) · 10.5 KB
/
Copy pathGraphQLLiteOptions.cs
File metadata and controls
156 lines (135 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
namespace CoreEx.Data.GraphQL;
/// <summary>
/// Provides the explicit, DI-driven registration of GraphQL-lite query roots.
/// </summary>
/// <remarks>Registered via <see cref="GraphQLExtensions.AddCoreExGraphQLLite(IServiceCollection, Action{GraphQLLiteOptions, IServiceProvider})"/>. Each root binds a GraphQL root field name to an
/// existing <see cref="QueryArgsConfig"/> (list roots) or a single-item resolver (get roots) — no attribute-based auto-discovery.</remarks>
public sealed class GraphQLLiteOptions
{
private readonly Dictionary<string, GraphQLQueryRoot> _queryRoots = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, GraphQLItemRoot> _itemRoots = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets the registered list query roots.
/// </summary>
public IReadOnlyDictionary<string, GraphQLQueryRoot> QueryRoots => _queryRoots;
/// <summary>
/// Gets the registered single-item roots.
/// </summary>
public IReadOnlyDictionary<string, GraphQLItemRoot> ItemRoots => _itemRoots;
/// <summary>
/// Gets or sets the maximum number of root fields (including repeated/aliased occurrences) permitted in a single document's selection set.
/// </summary>
/// <remarks>Defaults to <see langword="null"/> (unlimited), matching the engine's existing behavior. Each root field can independently drive a backend query, so a document with many
/// aliased occurrences of the same (or different) root fields can fan out to many backend calls from one inbound request; setting a cap here bounds that fan-out. See
/// <see cref="GraphQLEngine.ExecuteAsync(string, string?, IReadOnlyDictionary{string, object?}?, CancellationToken)"/>, which rejects a document exceeding this limit with a
/// <c>TOO_MANY_ROOT_FIELDS</c> error before any backend work is performed.</remarks>
public int? MaxRootFields { get; set; }
/// <summary>
/// Gets or sets whether the <c>__schema</c>/<c>__type</c> introspection meta-fields are answerable over the query endpoint.
/// </summary>
/// <remarks>Defaults to <see langword="false"/> (secure-by-default): a request for either meta-field produces an <c>INTROSPECTION_DISABLED</c> error rather than being executed. This only
/// gates the client-facing <c>__schema</c>/<c>__type</c> query fields - <see cref="IGraphQLEngine.GetSchemaAsync(CancellationToken)"/> (the direct API, e.g. for internal tooling or
/// documentation generation) is unaffected and always available. Enable this where client tooling (e.g. GraphiQL, Postman, Apollo/Relay codegen) needs to introspect the schema.</remarks>
public bool EnableIntrospection { get; set; }
/// <summary>
/// Gets or sets whether <see cref="GraphQLQueryRoot"/>'s <c>Debug</c>-level invocation logging includes the literal <see cref="QueryArgs.Filter"/>/<see cref="QueryArgs.OrderBy"/>
/// text, rather than just whether each was specified.
/// </summary>
/// <remarks>Defaults to <see langword="false"/>: the literal filter/order-by text embeds client-supplied filter values verbatim (e.g. <c>name eq 'Jane Doe'</c>), so it is not
/// logged by default even at <c>Debug</c> level. Mirrors EF Core's <c>EnableSensitiveDataLogging</c> - an explicit, opt-in acknowledgement that the exact query text is useful
/// enough while debugging to accept the values ending up in logs; do not enable this in a shared/production log sink.</remarks>
public bool EnableSensitiveDataLogging { get; set; }
/// <summary>
/// Registers a list query root field (e.g. <c>products</c>) bound to an existing <see cref="QueryArgsConfig"/> and <c>QueryAsync</c>-shaped delegate.
/// </summary>
/// <typeparam name="TItem">The projected item <see cref="Type"/>.</typeparam>
/// <param name="name">The GraphQL root field name.</param>
/// <param name="queryArgsConfig">The existing <see cref="QueryArgsConfig"/> used to validate/parse the <c>filter</c>/<c>orderby</c> arguments (e.g. <c>ProductQueryArgsConfig.Default</c>).</param>
/// <param name="resolver">The existing query delegate (e.g. <c>(qa, pa, ct) => service.QueryAsync(qa, pa, ct)</c>).</param>
/// <returns>The <see cref="GraphQLLiteOptions"/> to support fluent-style method-chaining.</returns>
/// <exception cref="ArgumentException">Thrown where <paramref name="name"/> is <c>__</c>-prefixed (reserved for GraphQL introspection) or already registered as a query or item root.</exception>
public GraphQLLiteOptions AddQuery<TItem>(string name, QueryArgsConfig queryArgsConfig, Func<QueryArgs?, PagingArgs?, CancellationToken, Task<IItemsResult<TItem>>> resolver)
{
name.ThrowIfNullOrEmpty();
queryArgsConfig.ThrowIfNull();
resolver.ThrowIfNull();
ThrowIfReservedOrDuplicate(name);
_queryRoots[name] = new GraphQLQueryRoot(name, typeof(TItem), queryArgsConfig, async (qa, pa, ct) => await resolver(qa, pa, ct).ConfigureAwait(false), this);
return this;
}
/// <summary>
/// Registers a list query root field (e.g. <c>products</c>) bound to an existing <see cref="QueryArgsConfig"/> and <c>QueryAsync</c>-shaped delegate.
/// </summary>
/// <param name="type">The item <see cref="Type"/>.</param>
/// <param name="name">The GraphQL root field name.</param>
/// <param name="queryArgsConfig">The existing <see cref="QueryArgsConfig"/> used to validate/parse the <c>filter</c>/<c>orderby</c> arguments (e.g. <c>ProductQueryArgsConfig.Default</c>).</param>
/// <param name="resolver">The existing query delegate (e.g. <c>(qa, pa, ct) => service.QueryAsync(qa, pa, ct)</c>).</param>
/// <returns>The <see cref="GraphQLLiteOptions"/> to support fluent-style method-chaining.</returns>
/// <exception cref="ArgumentException">Thrown where <paramref name="name"/> is <c>__</c>-prefixed (reserved for GraphQL introspection) or already registered as a query or item root.</exception>
public GraphQLLiteOptions AddQuery(Type type, string name, QueryArgsConfig queryArgsConfig, Func<QueryArgs?, PagingArgs?, CancellationToken, Task<IItemsResult>> resolver)
{
type.ThrowIfNull();
name.ThrowIfNullOrEmpty();
queryArgsConfig.ThrowIfNull();
resolver.ThrowIfNull();
ThrowIfReservedOrDuplicate(name);
_queryRoots[name] = new GraphQLQueryRoot(name, type, queryArgsConfig, async (qa, pa, ct) => await resolver(qa, pa, ct).ConfigureAwait(false), this);
return this;
}
/// <summary>
/// Registers a single-item root field (e.g. <c>product</c>) bound to an existing single-item <c>GetAsync</c>-shaped delegate.
/// </summary>
/// <typeparam name="TItem">The item <see cref="Type"/>.</typeparam>
/// <param name="name">The GraphQL root field name.</param>
/// <param name="resolver">The resolver delegate, receiving the resolved GraphQL field arguments (e.g. <c>id</c>).</param>
/// <returns>The <see cref="GraphQLLiteOptions"/> to support fluent-style method-chaining.</returns>
/// <exception cref="ArgumentException">Thrown where <paramref name="name"/> is <c>__</c>-prefixed (reserved for GraphQL introspection) or already registered as a query or item root.</exception>
public GraphQLLiteOptions AddGet<TItem>(string name, Func<GraphQLLiteArgs, CancellationToken, Task<TItem?>> resolver) where TItem : CoreEx.Entities.Abstractions.IReadOnlyIdentifier
{
name.ThrowIfNullOrEmpty();
resolver.ThrowIfNull();
ThrowIfReservedOrDuplicate(name);
_itemRoots[name] = new GraphQLItemRoot(name, typeof(TItem), async (args, ct) => await resolver(args, ct).ConfigureAwait(false));
return this;
}
/// <summary>
/// Registers a list query root field for every reference data type known to the <see cref="ReferenceDataOrchestrator"/>, keyed by its alternate/GraphQL-friendly name.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> used to resolve the <see cref="ReferenceDataOrchestrator"/>.</param>
/// <param name="queryArgsConfig">The <see cref="QueryArgsConfig"/> used to validate/parse the <c>filter</c>/<c>orderby</c> arguments.</param>
/// <param name="prefix">The optional prefix to apply to the root field names (defaults to <c>ref_</c>) to help distinguish them.</param>
/// <param name="excludeTypes">The reference data types to exclude.</param>
/// <returns>The <see cref="GraphQLLiteOptions"/> to support fluent-style method-chaining.</returns>
public GraphQLLiteOptions AddReferenceDataQueries(IServiceProvider serviceProvider, QueryArgsConfig queryArgsConfig, string? prefix = "ref_", params Type[] excludeTypes)
{
queryArgsConfig.ThrowIfNull();
var orchestrator = serviceProvider.GetRequiredService<ReferenceDataOrchestrator>();
foreach (var kvp in orchestrator.GetAlternateNameMappings())
{
var name = $"{prefix}{kvp.Key.Replace('-', '_')}"; // GraphQL doesn't allow hyphens in field names, so replace with underscore.
var type = kvp.Value;
if (excludeTypes.Contains(type))
continue;
ThrowIfReservedOrDuplicate(name);
var capturedType = type;
Task<IItemsResult> resolver(QueryArgs? qa, PagingArgs? pa, CancellationToken ct) => orchestrator.QueryAsync(capturedType, qa, pa, ct);
_queryRoots[name] = new GraphQLQueryRoot(name, type, queryArgsConfig, resolver, this);
}
return this;
}
/// <summary>
/// Validates that a root field name conforms to the GraphQL <c>Name</c> grammar, is not <c>__</c>-prefixed (reserved for GraphQL introspection), and has not already been
/// registered as a query or item root.
/// </summary>
/// <param name="name">The GraphQL root field name.</param>
/// <exception cref="ArgumentException">Thrown where <paramref name="name"/> is not a valid GraphQL name, is reserved, or already registered.</exception>
private void ThrowIfReservedOrDuplicate(string name)
{
if (name.StartsWith("__", StringComparison.Ordinal))
throw new ArgumentException($"Root field name '{name}' is reserved for GraphQL introspection; names starting with '__' are not permitted.", nameof(name));
if (!GraphQLNameValidator.IsValidName(name))
throw new ArgumentException($"Root field name '{name}' is not a valid GraphQL name.", nameof(name));
if (_queryRoots.ContainsKey(name) || _itemRoots.ContainsKey(name))
throw new ArgumentException($"A root field named '{name}' is already registered.", nameof(name));
}
}