diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index f890d9cf..119a201b 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -11,9 +11,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup .NET 9.0.x - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: | 6.0.x diff --git a/.vscode/launch.json b/.vscode/launch.json index 5751364b..49c7c3c0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/src/examples/demo/bin/Debug/net9.0/demo.dll", + "program": "${workspaceFolder}/src/examples/demo/bin/Debug/net10.0/demo.dll", "args": [], "cwd": "${workspaceFolder}/src/examples/demo", "stopAtEntry": false, diff --git a/.vscode/settings.json b/.vscode/settings.json index 5d12d5d4..21025396 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,4 +4,7 @@ "[csharp]": { "editor.defaultFormatter": "csharpier.csharpier-vscode" }, + "cSpell.words": [ + "Parlot" + ], } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e40e501..dc861eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,94 @@ -# 5.8.0 +# 6.0.0-beta3 + +## Breaking Changes + +- Fields in `ExecutableDirectiveLocation` enum have been renamed in C# style +- #476 - Introduce generic custom type converters (from-to, to-only, from-only) for flexible runtime value conversion. + - Expand `isAny` to support additional types through the method provider + - Removes `ICustomTypeConverter`. See updated docs and the more flexible converting methods available +- Renamed `SchemaBuilderSchemaOptions` to `SchemaProviderOptions` for better clarity between schema builder reflection options and schema provider configuration options +- `AddGraphQLOptions` refactored from inheritance to composition: + - Now exposes `Builder` property for `SchemaBuilderOptions` (controls building/reflection behavior) + - Now exposes `Schema` property for `SchemaProviderOptions` (controls schema provider configuration like authorization, introspection, error handling, field naming) + - Removed `FieldNamer` property - now accessed via `options.Schema.FieldNamer` + - **`PreBuildSchemaFromContext` moved from `SchemaProviderOptions` to `SchemaBuilderOptions`** - now accessed via `options.Builder.PreBuildSchemaFromContext` (it's called during the schema building phase, not runtime) +- Removed `introspectionEnabled` parameter from `AddGraphQLSchema` extension method - configure via `options.Schema.IntrospectionEnabled` instead +- **Authorization refactoring**: `RequiredAuthorization` now uses a keyed data structure for extensibility + - `RequiredAuthorization` no longer has separate `Roles` backing field - roles are stored via `"egql:core:roles"` key in `KeyedData` dictionary + - The `Roles` property is now nullable and computed from keyed data + - Policy support moved entirely to `EntityGraphQL.AspNet` package using `"egql:aspnet:policies"` key + - `RequiredAuthorization` constructor that took roles parameter has been removed - use `RequiresAnyRole()` or `RequiresAllRoles()` methods instead + - Custom authorization implementations should use namespaced keys (e.g., `"myapp:custom-auth"`) to avoid conflicts ## Changes +- Added support for [document descriptions](https://spec.graphql.org/September2025/#sec-Descriptions) as outlined in the latest 2025 spec. Basically string comments in the query document +- #474 - Add support for `TimeSpan` as a scalar in the default schema +- Replaced the previous 3rd party GraphQL document parser. +- Add `net10.0` as a target +- When using Offset Paging the total items expression is now only executed if the query requests `hasNextPage` or `totalItems` fields +- When using Connection Paging the total count expression is now only executed if the query requests `totalCount` or `pageInfo` fields or the 'last' argument is used +- Added `IField.AsService()` to signal that a field should (or can) be treated as a service field. This means null-checks are differently and they are executed after EF related calls (if `ExecutionOptions.ExecuteServiceFieldsSeparately = true`). Useful for fields that operate with data fully in-memory and do not need a registered service to fetch data or work +- `AddGraphQLSchema` in ASP.NET now automatically detects and applies environment-based defaults: + - Uses `PolicyOrRoleBasedAuthorization` by default (instead of requiring explicit configuration) + - Automatically sets `IsDevelopment = false` in non-Development environments + +## Fixes + +- #481 - handle `ResolveAsync` on a top level field +- #488 - Support async fields with `[GraphQLField]` +- #487 - Fix `ResolveAsync` with arguments +- #484 - Better support for nullable numeric types in filter expressions. Nullable int/short/long fields can now be compared to numeric literals without type mismatch errors. The fix also prioritizes converting literals over field expressions to avoid database column casts that could prevent index usage. +- As per spec - `_Type.fields` should be `null` for `INPUT_OBJECT`. Previously the `inputFields` were repeated. + +# 6.0.0-beta2 + +## Changes + +- Added `selectMany` method support to filter expressions, enabling flattening using nested collections within queries. + +## Fixes + +- Fixed (maybe a regression introduced in 6.0.0-beta1) `UseFilter()` incorrectly adding a default value to the filter argument when applied to fields that already have other arguments defined. + +# 6.0.0-beta1 + +## Breaking Changes + +- Support for partial results as per spec. **This actually _fixes_ EntityGraphQL to follow the GraphQL spec regarding partial results.** However it does change behavior. + + - EntityGraphQL basically executes each "top level" field in the operation separately, now if any fail, you'll get the partial results of those that succeeded and error information about the failed ones. + - `AddGraphQLValidator` now registers `IGraphQLValidator` as `Transient`. This _was_ the original intent as the docs have examples of bailing early by checking if the validator has any errors. The intent was errors for that field. This change helps enable partial results. If you want the old behavior, remove the use of `AddGraphQLValidator` and just add `IGraphQLValidator` yourself as `Scoped` + - As per spec [If an error was raised during the execution that prevented a valid response, the "data" entry in the response should be null](https://spec.graphql.org/draft/#sec-Data). This was not always the case + +- Removed methods/properties marked as obsolete + + - `IField.UseArgumentsFromField` use `GetExpressionAndArguments` + - `IField.UseArgumentsFrom` use `GetExpressionAndArguments` + - `IField.ResolveWithService` use `Resolve` + - `IFieldExtension.GetExpression` use the new `GetExpressionAndArguments` + +- `MapGraphQL` `followSpec = true` is now the default behavior, it follows https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md + +- You can no longer add filter support by using `ArgumentHelper.EntityQuery` or `EntityQueryType` in the field args, e.g. `schemaProvider.Query().ReplaceField("users", new { filter = ArgumentHelper.EntityQuery() }, "Users optionally filtered")`. Please use the `UseFilter` extension which supports filters referencing service fields. + +- `IFieldExtension.GetExpressionAndArguments` now takes the current GraphQL node `BaseGraphQLField fieldNode` as an argument. `parentNode` has been removed. Access it via `fieldNode.ParentNode`. + +- `EntityGraphQL.AspNet` package drops `net6.0` and `net7.0` as targets. `EntityGraphQL` package drops `net6.0` as a target however as it still targets `netstandard2.1` you can still use it with those previous dotnet versions. + +## Changes + +- Partial results support - #467 - New implementation for handling `async` fields. See updated docs and use the `.ResolveAsync<>()` methods when adding fields. - New support for `CancellationToken`. A `CancellationToken` can be passed into the `ExecuteRequestAsync` method. The token will be checked throughout execution and passed to other async operations. You can use it in `.ResolveAsync((context, service, ct) => service.DoSomethingAsync(context.Field, ct))` to pass it to your `async` fields. If you use `MapGraphQL()` for ASP.NET it will use the `context.RequestAborted` as the cancellation token. -- #469 - Make filter grammar immutable as it should be +- #469 - Make filter grammar immutable as it should be for performance +- #303 - You can now reference service fields in the `UseFilter/[UseFilter]` expression. Like normal the filter will first be applied with non service fields, then applied again with service fields is `ExecutionOptions.ExecuteServiceFieldsSeparately == true` (default). +- #396 - Filter expressions now support GraphQL variables using `$variableName` syntax. This allows parameterized and dynamic filters (e.g., `filter: "name == $searchTerm && age > $minAge"`). +- Related to #331 and the `followSpec = true` above, errors and exceptions were refactored internally. Following the GraphQL spec if the core library gets an error it will still return a `GraphQLResult` with the errors (and potentially partial results). + +# Fixes + +- #429 Validation attributes now work with `[GraphQLInputType]` # 5.7.1 diff --git a/docs/docs/authorization.md b/docs/docs/authorization.md index 1e0c9aa5..53fb0762 100644 --- a/docs/docs/authorization.md +++ b/docs/docs/authorization.md @@ -6,6 +6,37 @@ sidebar_position: 5 You should secure the route where you app/client posts request to in any ASP.NET supports. Given GraphQL works with a schema you likely want to provide authorization within the schema. EntityGraphQL provides support for checking claims on a `ClaimsPrincipal` object. +## Authorization Services + +EntityGraphQL supports different authorization service implementations: + +- **`RoleBasedAuthorization`** - The default. Checks roles on the `ClaimsPrincipal`. Use when you only need role-based authorization. +- **`PolicyOrRoleBasedAuthorization`** - Supports both ASP.NET Core policies and roles. This is the default when calling `AddGraphQLSchema()` in `EntityGraphQL.AspNet` if `IAuthorizationService` is available. + +### Configuring Authorization Service + +When using `AddGraphQLSchema()` in ASP.NET, `PolicyOrRoleBasedAuthorization` is used by default. To use a different authorization service: + +```cs +builder.Services.AddSingleton(); + +// Or use role-based authorization only +builder.Services.AddSingleton(); + +builder.Services.AddGraphQLSchema(options => +{ + // Configure things +}); +``` + +When creating a schema manually outside of ASP.NET: + +```cs +var schema = new SchemaProvider( + authorizationService: new MyCustomAuthService() +); +``` + ## Passing in the User First pass in the `ClaimsPrincipal` to the query call @@ -103,3 +134,167 @@ Note when using `AddField()` and `AddType()` these functions will automatically ## Authorization without ASP.Net You can use the `GraphQLAuthorizeAttribute` with role claims to provide authorization without the ASP.Net dependency. + +## Custom Authorization + +EntityGraphQL's authorization system is built on a flexible keyed data structure that allows you to extend it with custom authorization requirements. + +### How Authorization Works + +Authorization requirements are stored in a `RequiredAuthorization` object which uses a keyed data dictionary. This allows different authorization implementations to store their own custom data: + +- **Core library** uses `"egql:core:roles"` key for role-based authorization +- **EntityGraphQL.AspNet** uses `"egql:aspnet:policies"` key for policy-based authorization +- **Your custom implementation** can use any namespaced key (e.g., `"myapp:custom-auth"`) + +### Creating a Custom Authorization Service + +To implement custom authorization, create a class that extends `RoleBasedAuthorization` or implements `IGqlAuthorizationService`: + +```cs +public class CustomAuthorizationService : RoleBasedAuthorization +{ + private const string CustomDataKey = "myapp:custom-permissions"; + + public override bool IsAuthorized(ClaimsPrincipal? user, RequiredAuthorization? requiredAuthorization) + { + if (requiredAuthorization != null && requiredAuthorization.Any()) + { + // Check your custom authorization data + // The data is List> to support AND/OR combinations + // Each inner list is OR'd together, outer lists are AND'd + // Example: [["perm1", "perm2"], ["perm3"]] means (perm1 OR perm2) AND perm3 + if (requiredAuthorization.TryGetData(CustomDataKey, out var permissionGroups)) + { + foreach (var permissionGroup in permissionGroups) + { + // User must have at least one permission from this group (OR) + var hasAnyPermission = permissionGroup.Any(permission => + UserHasPermission(user, permission)); + + if (!hasAnyPermission) + return false; // User doesn't have any permission from this group (AND failed) + } + } + + // Also check roles + return base.IsAuthorized(user, requiredAuthorization); + } + return true; + } + + private bool UserHasPermission(ClaimsPrincipal? user, string permission) + { + // Your custom permission logic + return user?.HasClaim("permission", permission) ?? false; + } +} +``` + +### Adding Custom Authorization Data + +You can add custom authorization requirements using extension methods: + +````cs +public static class CustomAuthorizationExtensions +{ + private const string CustomDataKey = "myapp:custom-permissions"; + + public static IField RequiresAnyPermission(this IField field, params string[] permissions) + { + field.RequiredAuthorization ??= new RequiredAuthorization(); + + // Get existing permission groups or create new list + if (!field.RequiredAuthorization.TryGetData(CustomDataKey, out var permissionGroups)) + { + permissionGroups = new List>(); + field.RequiredAuthorization.SetData(CustomDataKey, permissionGroups); + } + + // Add as a new group where any permission satisfies (OR within group) + permissionGroups.Add(permissions.ToList()); + return field; + } + + public static IField RequiresAllPermissions(this IField field, params string[] permissions) + { + field.RequiredAuthorization ??= new RequiredAuthorization(); + + if (!field.RequiredAuthorization.TryGetData(CustomDataKey, out var permissionGroups)) + { + permissionGroups = new List>(); + field.RequiredAuthorization.SetData(CustomDataKey, permissionGroups); + } + + // Add each permission as a separate group (AND across groups) + foreach (var permission in permissions) + { + permissionGroups.Add(new List { permission }); + } + return field; + } + + public static SchemaType RequiresAnyPermission( + this SchemaType schemaType, params string[] permissions) + { + schemaType.RequiredAuthorization ??= new RequiredAuthorization(); + + if (!schemaType.RequiredAuthorization.TryGetData(CustomDataKey, out var permissionGroups)) +// Use in your schema +schemaProvider.AddField("sensitiveData", (db) => db.SensitiveEntities, "Sensitive data") + .RequiresAnyPermission("read:sensitive-data"); + +schemaProvider.Type() + .ReplaceField("salary", u => u.Salary, "User's salary") + .RequiresAllPermissions("read:salaries", "read:user-data"); +``` } + + public static SchemaType RequiresAllPermissions( + this SchemaType schemaType, params string[] permissions) + { + schemaType.RequiredAuthorization ??= new RequiredAuthorization(); + + if (!schemaType.RequiredAuthorization.TryGetData(CustomDataKey, out var permissionGroups)) + { + permissionGroups = new List>(); + schemaType.RequiredAuthorization.SetData(CustomDataKey, permissionGroups); + } + + foreach (var permission in permissions) + { + permissionGroups.Add(new List { permission }); + } + return schemaType; + } +} +```` + +### Using Custom Authorization + +```cs +// Configure your custom authorization service +services.AddGraphQLSchema(options => { + options.Schema.AuthorizationService = new CustomAuthorizationService(); +}); + +// Use in your schema +schemaProvider.AddField("sensitiveData", (db) => db.SensitiveEntities, "Sensitive data") + .RequiresPermission("read:sensitive-data"); + +schemaProvider.Type() + .ReplaceField("salary", u => u.Salary, "User's salary") + .RequiresPermission("read:salaries"); +``` + +### Combining Multiple Authorization Types + +The keyed data structure allows multiple authorization requirements to coexist: + +```cs +schemaProvider.AddField("adminData", (db) => db.AdminData, "Admin only data") + .RequiresAllRoles("admin") // Role-based auth + .RequiresAnyPermission("read:admin") // Custom permission auth + .RequiresAllPolicies("AdminPolicy"); // ASP.NET policy auth (requires EntityGraphQL.AspNet) +``` + +All authorization requirements must be satisfied for access to be granted. diff --git a/docs/docs/directives/schema-directives.md b/docs/docs/directives/schema-directives.md index 1c731f7b..46ef27b7 100644 --- a/docs/docs/directives/schema-directives.md +++ b/docs/docs/directives/schema-directives.md @@ -152,7 +152,7 @@ Handlers need to be added to the schema before any reflection happens to build t ```cs // If building yourself -var schema = SchemaBuilder.FromObject(new SchemaBuilderSchemaOptions +var schema = SchemaBuilder.FromObject(new SchemaProviderOptions { PreBuildSchemaFromContext = (context) => { @@ -163,7 +163,7 @@ var schema = SchemaBuilder.FromObject(new SchemaBuilderSchemaOption // with the ASP.NET extensions builder.Services.AddGraphQLSchema(options => { - PreBuildSchemaFromContext = (context) => + options.Builder.PreBuildSchemaFromContext = (context) => { context.AddAttributeHandler(new AuthorizeAttributeHandler()); } diff --git a/docs/docs/error-handling.md b/docs/docs/error-handling.md new file mode 100644 index 00000000..71cdb8ae --- /dev/null +++ b/docs/docs/error-handling.md @@ -0,0 +1,295 @@ +--- +sidebar_position: 7 +--- + +# Error Handling + +EntityGraphQL provides comprehensive error handling that follows the GraphQL specification. This page covers all aspects of how errors work in EntityGraphQL, from validation errors to execution errors and partial results. + +## Types of Errors + +EntityGraphQL handles several types of errors differently: + +### 1. Validation Errors + +These occur during query parsing and validation, before execution begins: + +- **Query syntax errors**: Invalid GraphQL syntax +- **Schema validation**: Fields that don't exist, wrong argument types, etc. +- **Argument validation**: Using `[Required]`, `[Range]`, `[StringLength]` attributes + +```csharp +public class PersonArgs +{ + [Required(ErrorMessage = "Name is required")] + public string Name { get; set; } + + [Range(0, 150, ErrorMessage = "Age must be between 0 and 150")] + public int Age { get; set; } +} +``` + +Validation errors: + +- Prevent query execution entirely +- Are returned in the `errors` array with `data: null` + +### 2. Execution Errors + +These occur during field execution: + +```csharp +[GraphQLMutation] +public Person AddPerson(string name) +{ + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Name cannot be empty"); + + // ... rest of logic +} +``` + +Execution errors: + +- Allow partial results (other fields can still succeed) +- Are returned alongside successful data + +## Error Response Format + +### Validation Error Response + +```json +{ + "data": null, + "errors": [ + { + "message": "Field 'nonExistentField' does not exist on type 'Query'" + } + ] +} +``` + +### Execution Error Response + +```json +{ + "data": { + "successfulField": "some data", + "failedField": null + }, + "errors": [ + { + "message": "Name cannot be empty", + "path": ["failedField"] + } + ] +} +``` + +## Partial Results + +EntityGraphQL supports **partial results** according to the GraphQL specification. When multiple fields are requested and some fail, you get results from successful fields plus error information. + +### How It Works + +EntityGraphQL executes each top-level field separately: + +```graphql +query MultipleFields { + users { + id + name + } # Succeeds + posts { + id + title + } # Fails + comments { + id + text + } # Succeeds +} +``` + +**Response:** + +```json +{ + "data": { + "users": [{ "id": "1", "name": "Alice" }], + "posts": null, + "comments": [{ "id": "1", "text": "Great post!" }] + }, + "errors": [ + { + "message": "Access denied to posts", + "path": ["posts"] + } + ] +} +``` + +### Nullable vs Non-Nullable Fields + +Field nullability affects error behavior: + +#### Nullable Fields + +```csharp +// This field is nullable +schema.Query().AddField("optionalData", ctx => MightFail()); +``` + +- Failed nullable fields return `null` +- Error included in `errors` array +- Other fields continue executing + +#### Non-Nullable Fields + +```csharp +// This field is non-nullable +schema.Query().AddField("requiredData", ctx => MightFail()).IsNullable(false); +``` + +- Failed non-nullable fields bubble up to the next nullable parent +- May cause entire `data` to become `null` if error reaches the root +- Follows GraphQL spec error propagation rules + +### Aliases + +When using field aliases, the path uses the alias name: + +```graphql +query { + primaryUser: user(id: 1) { + name + } + backupUser: user(id: 2) { + name + } +} +``` + +Error path: `["primaryUser"]` or `["backupUser"]` + +## Using IGraphQLValidator + +For collecting multiple validation errors in mutations: + +```csharp +[GraphQLMutation] +public Person AddPersonWithValidation(PersonInput input, IGraphQLValidator validator) +{ + if (string.IsNullOrEmpty(input.Name)) + validator.AddError("Name is required"); + + if (input.Age < 0) + validator.AddError("Age must be positive"); + + if (input.Age > 150) + validator.AddError("Age seems unrealistic"); + + // Check for errors before proceeding + if (validator.HasErrors) + return null; // Errors automatically included with field path + + return CreatePerson(input); +} +``` + +**Register the validator:** + +```csharp +services.AddGraphQLValidator(); // In ASP.NET Core +``` + +This returns multiple errors for a single field: + +```json +{ + "data": { "addPersonWithValidation": null }, + "errors": [ + { + "message": "Name is required", + "path": ["addPersonWithValidation"] + }, + { + "message": "Age must be positive", + "path": ["addPersonWithValidation"] + } + ] +} +``` + +## Exception Handling + +### Development vs Production + +In development mode, exception details are exposed: + +```json +{ + "message": "Object reference not set to an instance of an object" +} +``` + +In production, generic messages are shown unless exceptions are marked with `AllowedExceptionAttribute` or add to `Schema.AllowedExceptions`. + +### Allowed Exceptions + +```csharp +// Use AllowedExceptionAttribute +[AllowedException] +public class ValidationException : Exception +{ + public ValidationException(string message) : base(message) { } +} + +// Configure at schema creation +services.AddGraphQLSchema(options => +{ + options.Schema.AllowedExceptions.Add(new AllowedException(typeof(ArgumentException))); + options.Schema.AllowedExceptions.Add(new AllowedException(typeof(InvalidOperationException))); +}); +``` + +## Custom Error Extensions + +Add custom data to errors: + +```csharp +[GraphQLMutation] +public Person AddPerson(PersonInput input, IGraphQLValidator validator) +{ + if (input.Age < 18) + { + validator.AddError("Age restriction", new Dictionary + { + ["code"] = "AGE_RESTRICTION", + ["minimumAge"] = 18, + ["providedAge"] = input.Age + }); + return null; + } + + return CreatePerson(input); +} +``` + +**Response:** + +```json +{ + "errors": [ + { + "message": "Age restriction", + "path": ["addPerson"], + "extensions": { + "code": "AGE_RESTRICTION", + "minimumAge": 18, + "providedAge": 15 + } + } + ] +} +``` diff --git a/docs/docs/field-extensions/filtering.md b/docs/docs/field-extensions/filtering.md index 4b330be3..c177e8db 100644 --- a/docs/docs/field-extensions/filtering.md +++ b/docs/docs/field-extensions/filtering.md @@ -176,7 +176,232 @@ The expression language supports the following methods, these are called against } ``` +## Custom Type Converters for Filters + +EntityGraphQL provides a flexible type converter system that enables runtime value conversion between types. Type converters work throughout EntityGraphQL (for mutation arguments, query variables, etc.), but are particularly useful in filter expressions when working with custom types like `Version`, `Uri`, or custom structs. + +### Using Custom Types in Filters + +To use a custom type in filter expressions, you need to register a type converter: + +**Type Converter** (`schema.AddCustomTypeConverter`) - Enables conversion of string values to custom types. This handles: +- Runtime conversion (query variables, mutation arguments) +- Compile-time conversion (string literals in binary comparisons like `version >= "1.2.3"`) +- Array conversions (`isAny` arrays) + +Type converters work throughout EntityGraphQL, not just in filters. + +### Example: Filtering by Version + +```cs +public class Product +{ + public string Name { get; set; } + public Version Version { get; set; } +} + +var schema = SchemaBuilder.FromObject(); + +// Add type converter - handles both runtime and compile-time conversion +schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + +// Mark the products field as filterable +schema.ReplaceField("products", ctx => ctx.Products, "List of products") + .UseFilter(); +``` + +Now you can use both binary comparisons and `isAny` with Version in filters: + +```graphql +{ + # Binary comparison with string literal + products(filter: "version >= \"1.2.0\"") { + name + version + } + + # Using isAny with Version + products(filter: "version.isAny([\"1.2.3\", \"2.0.0\"])") { + name + version + } + + # Combining both + products(filter: "version >= \"1.2.0\" && version.isAny([\"1.2.3\", \"2.0.0\"])") { + name + version + } +} +``` + +### Converter Patterns + +EntityGraphQL supports three registration approaches: + +**From-To Converter** - Maps a specific source type to a target type: + +```cs +schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); +``` + +**To-Only Converter** - Converts any source to a particular target type: + +```cs +schema.AddCustomTypeConverter( + (obj, _) => obj switch + { + string s => new Uri(s, UriKind.RelativeOrAbsolute), + Uri u => u, + _ => new Uri(obj!.ToString()!, UriKind.RelativeOrAbsolute), + } +); +``` + +**From-Only Converter** - Converts a source type to multiple possible targets: + +```cs +schema.AddCustomTypeConverter( + (s, toType, _) => + { + if (toType == typeof(Uri)) + return new Uri(s, UriKind.RelativeOrAbsolute); + if (toType == typeof(Version)) + return Version.Parse(s); + return s; + } +); +``` + +### Enum Converters + +Custom converters work great with enum types in filters: + +```cs +public enum Status { Active, Inactive, Pending } + +schema.AddCustomTypeConverter( + (s, _) => + { + if (Enum.TryParse(s, ignoreCase: true, out var val)) + return val; + throw new ArgumentException($"Invalid enum value '{s}'"); + } +); + +// Use in filter +{ + people(filter: "status.isAny([\"Active\", \"Pending\"])") { ... } +} +``` + +### Using with GraphQL Variables + +Type converters automatically work with GraphQL variables: + +```graphql +query GetProductsByVersions($versions: [String!]!) { + products(filter: "version.isAny($versions)") { + name + version + } +} +``` + +```json +{ + "versions": ["1.2.3", "2.0.0"] +} +``` + The expression language supports ternary and conditional: - `__ ? __ : __` - `if __ then __ else __` + +## GraphQL Variables in Filters + +The filter extension supports GraphQL variables using the `$variableName` syntax. This allows you to parameterize your filter expressions, making them more dynamic and reusable. + +### Example Variable Usage + +You can use multiple variables in a single filter expression: + +```graphql +query GetPeopleByRange($minAge: Int!, $status: String!) { + people(filter: "age >= $minAge && status == $status") { + firstName + lastName + age + } +} +``` + +With variables: + +```json +{ + "minAge": 18, + "status": "active" +} +``` + +## Service Fields in Filters + +When using the filter extension with fields that resolve data from services (using `Resolve()`) and have two-pass execution enabled (`ExecuteServiceFieldsSeparately = true`, which is the default), EntityGraphQL automatically handles filter splitting to optimize query performance. + +### How Filter Splitting Works + +The filter extension uses a `FilterSplitter` that automatically separates filter expressions into two parts: + +1. **Database-safe filters**: Expressions that only reference database fields, executed directly against the database (e.g., Entity Framework) +2. **Service-dependent filters**: Expressions that reference service fields, executed in-memory after the service data is resolved + +This ensures that: + +- Entity Framework can optimize database queries with only the database-safe portion of the filter +- Service fields work correctly in filters without causing EF translation errors +- Performance is optimized by filtering as much as possible at the database level + +### Example with Service Fields + +Given a `Person` type with a service field: + +```cs +schema.UpdateType(type => { + type.AddField("age", "Person's calculated age") + .Resolve((person, ager) => ager.GetAge(person.Birthday)); +}); +``` + +You can use filters that mix database and service fields: + +```graphql +{ + # Filter combining database field (name) and service field (age) + people(filter: "name.startsWith('John') && age > 21") { + name + age + } +} +``` + +EntityGraphQL will automatically split this into: + +1. **Database filter**: `name.startsWith('John')` - executed against the database / main context +2. **Service filter**: `age > 21` - executed in-memory after age calculation + +### Filter Splitting Rules + +The filter splitter follows these rules for optimal performance: + +- **AND expressions**: Split into separate database and service parts when possible +- **OR expressions**: Moved entirely to service execution if they contain any service fields (cannot be safely split) +- **NOT expressions**: Handled appropriately based on whether they contain service fields + +:::info Performance Note + +Filter splitting is automatically enabled when `ExecuteServiceFieldsSeparately = true` (the default). This provides optimal performance by leveraging database query optimization while supporting service fields in filters. + +If you disable two-pass execution (`ExecuteServiceFieldsSeparately = false`), all filters will execute in-memory, which may impact performance for large datasets if your query context is a database context. + +::: diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 6699d81d..e6704c1a 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -123,7 +123,7 @@ To add one or more security policies when using `MapGraphQL()` you can pass a co services.AddAuthentication() services.AddAuthorization(options => { - options.AddPolicy("authorized", policy => policy.RequireAuthenticatedUser(); + options.AddPolicy("authorized", policy => policy.RequireAuthenticatedUser()); }); diff --git a/docs/docs/other-extensibility/event-system.md b/docs/docs/other-extensibility/event-system.md index 97b7c71b..7ab4bec5 100644 --- a/docs/docs/other-extensibility/event-system.md +++ b/docs/docs/other-extensibility/event-system.md @@ -31,7 +31,7 @@ public static class OneOfDirectiveExtensions var singleField = value.GetType().GetProperties().Count(x => x.GetValue(value) != null); if (singleField != 1) // we got multiple set - throw new EntityGraphQLValidationException($"Exactly one field must be specified for argument of type {type.Name}."); + throw new EntityGraphQLException($"Exactly one field must be specified for argument of type {type.Name}."); } }; } diff --git a/docs/docs/schema-creation/mutations.md b/docs/docs/schema-creation/mutations.md index de0cb62e..e4b0e522 100644 --- a/docs/docs/schema-creation/mutations.md +++ b/docs/docs/schema-creation/mutations.md @@ -78,8 +78,6 @@ When `AutoCreateInputTypes` is true, to inject a service, pass it to the constru **SchemaBuilderOptions.AddNonAttributedMethodsInControllers** If true (default = false), EntityGraphQL will add any method in the mutation class as a mutation without needing the `[GraphQLMutation]` attribute. Methods must be **Public** and **not inherited** but can be either **static** or **instance**. -`SchemaBuilderOptions` inherits from `SchemaBuilderOptions` and those options are passed to the `SchemaBuilder` methods. An important one for mutations is - **SchemaBuilderOptions.AutoCreateNewComplexTypes** If true (default = true) any complex class types that a mutation returns is added to the schema as a query type if it is not already there. @@ -541,3 +539,63 @@ public class PeopleMutations - **Mutations**: Use `[GraphQLMutation]` on methods in mutation controller classes. Return `Task` for async operations. No concurrency control as each mutation field executes in order sequently as per the spec. - **Async Fields**: Use `.ResolveAsync()` when adding fields to types. Supports concurrency control and hierarchical limiting. + +## Partial Results and Error Handling + +EntityGraphQL supports **partial results** according to the GraphQL specification. This means that when executing operations with multiple fields, if some fields succeed and others fail, you'll receive the successful results along with error information about the failed ones. + +### How Partial Results Work + +EntityGraphQL executes each top-level field in an operation separately. If any field fails during execution, the operation doesn't completely fail - instead, you get: + +1. **Partial data**: Results from fields that executed successfully +2. **Error information**: Detailed errors with paths pointing to the failed fields +3. **Proper null handling**: Failed nullable fields become `null`, while non-nullable field failures may bubble up + +### Example with Multiple Mutations + +```graphql +mutation MultipleOperations { + # This mutation succeeds + addPerson(name: "Alice", age: 30) { + id + name + } + + # This mutation fails + addPersonError(name: "Bob") { + id + name + } + + # This mutation also succeeds + addPerson2: addPerson(name: "Charlie", age: 25) { + id + name + } +} +``` + +**Response with partial results:** + +```json +{ + "data": { + "addPerson": { + "id": "1", + "name": "Alice" + }, + "addPersonError": null, + "addPerson2": { + "id": "2", + "name": "Charlie" + } + }, + "errors": [ + { + "message": "Name can not be null (Parameter 'name')", + "path": ["addPersonError"] + } + ] +} +``` diff --git a/docs/docs/schema-creation/scalar-types.md b/docs/docs/schema-creation/scalar-types.md index b615b9fd..a8ffb0f5 100644 --- a/docs/docs/schema-creation/scalar-types.md +++ b/docs/docs/schema-creation/scalar-types.md @@ -30,7 +30,7 @@ AddScalarType("String", "String scalar"); AddScalarType("ID", "ID scalar"); AddScalarType("Char", "Char scalar"); -AddScalarType("Date", "Date with time scalar"); +AddScalarType("DateTime", "Date with time scalar"); AddScalarType("DateTimeOffset", "DateTimeOffset scalar"); AddScalarType("DateOnly", "Date value only scalar"); AddScalarType("TimeOnly", "Time value only scalar"); @@ -40,7 +40,7 @@ It is best to have scalar types added to the schema before adding other fields t ```cs services.AddGraphQLSchema(options => { - options.PreBuildSchemaFromContext = schema => + options.Builder.PreBuildSchemaFromContext = schema => { // remove and/or add scalar types or mappings here. e.g. schema.RemoveType(); @@ -69,3 +69,16 @@ float -> Float decimal -> Float byte[] -> String ``` + +## Custom Type Converters + +EntityGraphQL provides a type converter system that enables runtime conversion between types. This is useful for: + +- Converting query variables and mutation arguments +- Using custom types in filter expressions (with `isAny`, binary comparisons, etc.) +- Handling JSON library types (see [Newtonsoft JSON](../serialization-naming/newtonsoft-json)) + +For examples and detailed usage, see: + +- [Custom Type Converters for Filters](../field-extensions/filtering#custom-type-converters-for-filters) - Using converters with custom types like `Version`, `Uri`, etc. in filters +- [Newtonsoft JSON](../serialization-naming/newtonsoft-json) - Converting JObject/JToken types from JSON deserialization diff --git a/docs/docs/schema-creation/schema-creation.md b/docs/docs/schema-creation/schema-creation.md index 490243af..8e735197 100644 --- a/docs/docs/schema-creation/schema-creation.md +++ b/docs/docs/schema-creation/schema-creation.md @@ -17,6 +17,14 @@ To create a new schema we need to supply a base context type. This base type is ```cs // Using EntityGraphQL.AspNet extension method to add the schema auto-populated from the base query type. Schema has types and fields built from DemoContext. See optional arguments for customizing the behavior. services.AddGraphQLSchema(options => { + // Configure how the schema builder reflects the object graph + options.Builder.AutoCreateFieldWithIdArguments = true; + options.Builder.AutoCreateEnumTypes = true; + + // Configure the schema provider settings + options.Schema.FieldNamer = name => name; // Override default camelCase naming + options.Schema.IntrospectionEnabled = true; + options.ConfigureSchema = (schema) => { // configure schema here }; @@ -119,25 +127,21 @@ var schema = SchemaBuilder.FromObject(); Optional arguments for the schema builder: -1. `SchemaBuilderSchemaOptions` - options that get passed to the created schema +1. `SchemaProviderOptions` - options that get passed to the created schema - `.FieldNamer` - A `Func` lambda used to generate field names. The default `fieldNamer` adopts the GraphQL standard of naming fields `lowerCamelCase` - `.IntrospectionEnabled` - Weather or not GraphQL query introspection is enabled or not for the schema. Default is `true` - - `.AuthorizationService` - An `IGqlAuthorizationService` to control how auth is handled. Default is `RoleBasedAuthorization` - - `.PreBuildSchemaFromContext` - Called after the schema object is created but before the context is reflected into it. Use for set up of type mappings or anything that may be needed for the schema to be built correctly. - - `.IsDevelopment` - If `true` (default), all exceptions will have their messages rendered in the 'errors' object. If `false`, exceptions not included in `AllowedExceptions` will have their message replaced with 'Error occurred' + - `.AuthorizationService` - An `IGqlAuthorizationService` to control how auth is handled. Default is `RoleBasedAuthorization` (or `PolicyOrRoleBasedAuthorization` when using `AddGraphQLSchema` in ASP.NET) + - `.IsDevelopment` - If `true` (default), all exceptions will have their messages rendered in the 'errors' object. If `false`, exceptions not included in `AllowedExceptions` will have their message replaced with 'Error occurred'. When using `AddGraphQLSchema` in ASP.NET this is automatically set to `false` in non-Development environments. - `.AllowedExceptions` - List of allowed exceptions that will be rendered in the 'errors' object when `IsDevelopment` is `false`. You can also mark your exceptions with `AllowedExceptionAttribute`. These exceptions are included by default. ```cs -public List AllowedExceptions { get; set; } = new List { - new AllowedException(typeof(EntityGraphQLArgumentException)), - new AllowedException(typeof(EntityGraphQLException)), - new AllowedException(typeof(EntityGraphQLFieldException)), - new AllowedException(typeof(EntityGraphQLAccessException)), - new AllowedException(typeof(EntityGraphQLValidationException)), -}; +public List AllowedExceptions { get; set; } = [ + new(typeof(EntityGraphQLException)), + new(typeof(EntityGraphQLFieldException)), + new(typeof(EntityGraphQLSchemaException))]; ``` -2. `SchemaBuilderOptions` - options used to control how the schema builder builds the schema +2. `SchemaBuilderOptions` - options used to control how the schema builder reflects the object graph to auto-create schema types and fields - `.AutoCreateFieldWithIdArguments` - for any fields that return a list of an Object Type that has a field called `Id`, it will create a singular field in the schema with an `id` argument. For example the `DemoContext` used in Getting Started the `DemoContext.People` will create the following GraphQL schema. Default is `true` @@ -184,6 +188,7 @@ public List AllowedExceptions { get; set; } = new List`. This can be used as an example to build other converters._ -You can tell EntityGraphQL how to convert types when it is mapping incoming data classes/arguments using the `AddCustomTypeConverter(new MyICustomTypeConverter())` on the schema provider. +You can tell EntityGraphQL how to convert types when it is mapping incoming data classes/arguments using the `AddCustomTypeConverter` method on the schema provider. Here is an example to use this to handle Newtonsoft.Json types: ```cs -internal class JObjectTypeConverter : ICustomTypeConverter -{ - public Type Type => typeof(JObject); - - public object ChangeType(object value, Type toType, ISchemaProvider schema) - { - return ((JObject)value).ToObject(toType); - } -} - -internal class JTokenTypeConverter : ICustomTypeConverter -{ - public Type Type => typeof(JToken); - - public object ChangeType(object value, Type toType, ISchemaProvider schema) - { - return ((JToken)value).ToObject(toType); - } -} - -internal class JValueTypeConverter : ICustomTypeConverter -{ - public Type Type => typeof(JValue); - - public object ChangeType(object value, Type toType, ISchemaProvider schema) - { - return ((JValue)value).ToString(); - } -} - // Where you build schema -schema.AddCustomTypeConverter(new JObjectTypeConverter()); -schema.AddCustomTypeConverter(new JTokenTypeConverter()); -schema.AddCustomTypeConverter(new JValueTypeConverter()); +// Convert JObject to any target type +schema.AddCustomTypeConverter( + (jObj, toType, schema) => jObj.ToObject(toType)! +); + +// Convert JToken to any target type +schema.AddCustomTypeConverter( + (jToken, toType, schema) => jToken.ToObject(toType)! +); + +// Convert JValue to any target type (returns string) +schema.AddCustomTypeConverter( + (jValue, toType, schema) => jValue.ToString() +); ``` -Now EntityGraphQL can convert `JObject`, `JToken` & `JValue` types to classes/types using your version of Newtonsoft.Json. You can use `ICustomTypeConverter` to handle any customer conversion. +Now EntityGraphQL can convert `JObject`, `JToken` & `JValue` types to classes/types using your version of Newtonsoft.Json. diff --git a/docs/docs/serialization-naming/serialization-naming.md b/docs/docs/serialization-naming/serialization-naming.md index 02791ee7..6219e58d 100644 --- a/docs/docs/serialization-naming/serialization-naming.md +++ b/docs/docs/serialization-naming/serialization-naming.md @@ -29,7 +29,7 @@ To override the default behavior you can pass in your own `fieldNamer` function ```cs services.AddGraphQLSchema(options => { - options.FieldNamer = name => name; // use the dotnet name as is + options.Schema.FieldNamer = name => name; // use the dotnet name as is }); ``` @@ -37,7 +37,7 @@ Then make sure you follow your naming policy when adding fields to the schema. ```cs services.AddGraphQLSchema(options => { - options.FieldNamer = name => name; // use the dotnet name as is + options.Schema.FieldNamer = name => name; // use the dotnet name as is options.ConfigureSchema = schema => { schema.Query().AddField("SomeField", ...) }; @@ -156,7 +156,7 @@ services.AddSingleton(new DefaultGraphQLResponseSeri services.AddGraphQLSchema(options => { - options.FieldNamer = name => name; + options.Schema.FieldNamer = name => name; }); ``` diff --git a/docs/docs/upgrade-6-0.md b/docs/docs/upgrade-6-0.md new file mode 100644 index 00000000..ce6a82df --- /dev/null +++ b/docs/docs/upgrade-6-0.md @@ -0,0 +1,254 @@ +--- +sidebar_position: 12 +--- + +# Upgrading from 5.x to 6.x + +EntityGraphQL respects [Semantic Versioning](https://semver.org/), meaning version 6.0.0 contains breaking changes. Below highlights those changes and the impact to those coming from version 5.x. + +:::tip +You can see the full changelog which includes other changes and bug fixes as well as links back to GitHub issues/MRs with more information [here on GitHub](https://github.com/EntityGraphQL/EntityGraphQL/blob/master/CHANGELOG.md). +::: + +## Partial Results Support + +EntityGraphQL now properly follows the GraphQL spec regarding partial results. Previously if any field failed, the entire operation would fail. Now: + +- Each top-level field in the operation is executed separately +- If any fields fail, you'll get partial results from those that succeeded plus error information about the failed ones +- `AddGraphQLValidator` now registers `IGraphQLValidator` as `Transient` (this was the original intent). If you want the old behavior, remove the use of `AddGraphQLValidator` and register `IGraphQLValidator` yourself as `Scoped` +- As per spec, if an error prevented a valid response, the "data" entry will be `null` + +## Schema Configuration Options Refactored + +The options for configuring schemas have been reorganized for better clarity between schema builder reflection behavior and schema provider configuration. + +### `SchemaBuilderSchemaOptions` renamed to `SchemaProviderOptions` + +The class has been renamed to better reflect that it configures the schema provider, not the builder. + +**Before (5.x):** + +```cs +var schema = SchemaBuilder.FromObject( + schemaOptions: new SchemaBuilderSchemaOptions { ... } +); +``` + +**After (6.x):** + +```cs +var schema = SchemaBuilder.FromObject( + schemaOptions: new SchemaProviderOptions { ... } +); +``` + +### `AddGraphQLOptions` now uses composition + +`AddGraphQLOptions` has been refactored from inheritance to composition, making it clearer which options control what. + +**Before (5.x):** + +```cs +services.AddGraphQLSchema(options => { + // All options were at the top level + options.FieldNamer = name => name; + options.AutoCreateFieldWithIdArguments = true; + options.IntrospectionEnabled = true; + options.AuthorizationService = new CustomAuthService(); +}); +``` + +**After (6.x):** + +```cs +services.AddGraphQLSchema(options => { + // Builder options control reflection/auto-creation behavior + options.Builder.AutoCreateFieldWithIdArguments = true; + options.Builder.AutoCreateEnumTypes = true; + options.Builder.IgnoreProps.Add("MyProp"); + options.Builder.PreBuildSchemaFromContext = schema => { + // Set up type mappings before reflection + }; + + // Schema options control schema provider configuration + options.Schema.FieldNamer = name => name; + options.Schema.IntrospectionEnabled = true; + options.Schema.AuthorizationService = new CustomAuthService(); + options.Schema.IsDevelopment = false; +}); +``` + +### `introspectionEnabled` parameter removed from `AddGraphQLSchema` + +The `introspectionEnabled` parameter has been removed from the `AddGraphQLSchema` extension method. + +**Before (5.x):** + +```cs +services.AddGraphQLSchema(introspectionEnabled: false); +``` + +**After (6.x):** + +```cs +services.AddGraphQLSchema(options => { + options.Schema.IntrospectionEnabled = false; +}); +``` + +### ASP.NET Auto-Configuration + +When using `AddGraphQLSchema` in ASP.NET, the following defaults are now automatically applied: + +- `AuthorizationService` defaults to `PolicyOrRoleBasedAuthorization` (previously required explicit configuration) +- `IsDevelopment` is automatically set to `false` in non-Development environments + +## Custom Type Converters + +The type converter system has been redesigned for more flexibility. + +### `ICustomTypeConverter` removed + +The `ICustomTypeConverter` interface has been removed. Use the new generic custom type converter methods on `SchemaProvider` instead: + +**Before (5.x):** + +```cs +public class MyConverter : ICustomTypeConverter +{ + // implementation +} + +schema.AddCustomTypeConverter(new MyConverter()); +``` + +**After (6.x):** + +```cs +// From-to converter (bidirectional) +schema.AddTypeConverter( + source => ConvertTo(source), + target => ConvertFrom(target) +); + +// To-only converter (one direction) +schema.AddTypeConverter( + source => ConvertTo(source) +); + +// From-only converter (for input types) +schema.AddInputTypeConverter( + target => ConvertFrom(target) +); +``` + +See the updated documentation for more flexible converting methods available. + +## `ExecutableDirectiveLocation` Enum Renamed + +Fields in the `ExecutableDirectiveLocation` enum have been renamed to follow C# naming conventions (PascalCase instead of SCREAMING_SNAKE_CASE). + +**Before (5.x):** + +```cs +ExecutableDirectiveLocation.QUERY +ExecutableDirectiveLocation.FIELD +``` + +**After (6.x):** + +```cs +ExecutableDirectiveLocation.Query +ExecutableDirectiveLocation.Field +``` + +## Removed Obsolete Methods + +The following methods and properties marked as obsolete in previous versions have been removed: + +- `IField.UseArgumentsFromField` - use `GetExpressionAndArguments` instead +- `IField.UseArgumentsFrom` - use `GetExpressionAndArguments` instead +- `IField.ResolveWithService` - use `Resolve` instead +- `IFieldExtension.GetExpression` - use `GetExpressionAndArguments` instead + +## `MapGraphQL` Default Behavior + +`MapGraphQL` now defaults to the previous `followSpec = true`, which follows https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md. + +## Filter Support via `UseFilter` + +You can no longer add filter support by using `ArgumentHelper.EntityQuery` or `EntityQueryType` in field args. + +**Before (5.x):** + +```cs +schemaProvider.Query().ReplaceField("users", + new { filter = ArgumentHelper.EntityQuery() }, + "Users optionally filtered" +); +``` + +**After (6.x):** + +```cs +schemaProvider.Query() + .GetField("users") + .UseFilter(); +``` + +The `UseFilter` extension now supports filters referencing service fields. + +## Date Scalar Type Renamed to DateTime + +The built-in scalar type for `System.DateTime` has been renamed from `"Date"` to `"DateTime"` to better reflect that it includes time information and to avoid confusion with the `DateOnly` scalar type. + +**Before (5.x):** + +```graphql +type Person { + name: String! + birthDate: Date +} +``` + +**After (6.x):** + +```graphql +type Person { + name: String! + birthDate: DateTime +} +``` + +**Migration Steps:** + +1. Update your GraphQL queries to use `DateTime` instead of `Date` +2. If you have custom schema introspection or code generation tools, update them to recognize `DateTime` +3. The CLR type remains `System.DateTime` - only the GraphQL scalar name has changed + +## Authorization Refactoring + +The authorization system has been refactored to use a keyed data structure for better extensibility. This allows any package to add custom authorization requirements without modifying core classes. **Role-based authorization methods are now extension methods**, following the same pattern as policy-based authorization. + +### `RequiredAuthorization` Changes + +`RequiredAuthorization` is now a pure data container that uses a keyed data dictionary (`AuthData`). All authorization logic is provided via extension methods. + +**Key Changes:** + +- `RequiresAllPolicies`, `RequiresAnyPolicy`, etc. have been moved to the `EntityGraphQL.AspNet` package as extension methods +- `RequiresAllRoles()` and `RequiresAnyRole()` are now extension methods on `IField` and `SchemaType` (from `RoleAuthorizationExtensions`) +- Roles are stored under the `"egql:core:roles"` key - use `GetRoles()` extension method to retrieve them +- Policies (in EntityGraphQL.AspNet) are stored under the `"egql:aspnet:policies"` key - use `GetPolicies()` extension method to retrieve them + +## `IFieldExtension.GetExpressionAndArguments` Signature Change + +The method signature has changed to take the current GraphQL node instead of the parent node. + +## Target Framework Changes + +The following target frameworks have been dropped: + +- `EntityGraphQL.AspNet` package: Dropped `net6.0` and `net7.0` +- `EntityGraphQL` package: Dropped `net6.0` (but still targets `netstandard2.1`, so it can still be used with those versions) diff --git a/docs/package-lock.json b/docs/package-lock.json index bf99617f..06f5ad89 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8,75 +8,132 @@ "name": "entity-graphql-docs", "version": "0.0.0", "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/preset-classic": "3.8.1", - "@mdx-js/react": "3.1.0", + "@docusaurus/core": "3.9.2", + "@docusaurus/preset-classic": "3.9.2", + "@mdx-js/react": "3.1.1", "prism-react-renderer": "2.4.1", - "react": "18.3.1", - "react-dom": "18.3.1" + "react": "19.2.0", + "react-dom": "19.2.0" }, "devDependencies": { - "@docusaurus/module-type-aliases": "3.8.1", + "@docusaurus/module-type-aliases": "3.9.2", "gh-pages": "6.3.0" }, "engines": { "node": ">=18.0" } }, + "node_modules/@ai-sdk/gateway": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.17.tgz", + "integrity": "sha512-oVAG6q72KsjKlrYdLhWjRO7rcqAR8CjokAbYuyVZoCO4Uh2PH/VzZoxZav71w2ipwlXhHCNaInGYWNs889MMDA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.18", + "@vercel/oidc": "3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz", + "integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "2.0.105", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.105.tgz", + "integrity": "sha512-d/nr3fuAsgLli7g9CcShqME+QdTN3S6vbtyL9ZT8iAWfR0xBKYuNrzX3a89vY49lnbdgAqB65l67hsVNCsmVIg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "3.0.18", + "ai": "5.0.105", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.25.76 || ^4.1.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@algolia/abtesting": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.1.0.tgz", - "integrity": "sha512-sEyWjw28a/9iluA37KLGu8vjxEIlb60uxznfTUmXImy7H5NvbpSO6yYgmgH5KiD7j+zTUUihiST0jEP12IoXow==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.11.0.tgz", + "integrity": "sha512-a7oQ8dwiyoyVmzLY0FcuBqyqcNSq78qlcOtHmNBumRlHCSnXDcuoYGBGPN1F6n8JoGhviDDsIaF/oQrzTzs6Lg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/autocomplete-core": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.9.tgz", - "integrity": "sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==", + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", + "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", "license": "MIT", "dependencies": { - "@algolia/autocomplete-plugin-algolia-insights": "1.17.9", - "@algolia/autocomplete-shared": "1.17.9" + "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", + "@algolia/autocomplete-shared": "1.19.2" } }, "node_modules/@algolia/autocomplete-plugin-algolia-insights": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.9.tgz", - "integrity": "sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ==", + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", + "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", "license": "MIT", "dependencies": { - "@algolia/autocomplete-shared": "1.17.9" + "@algolia/autocomplete-shared": "1.19.2" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, - "node_modules/@algolia/autocomplete-preset-algolia": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.9.tgz", - "integrity": "sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ==", - "license": "MIT", - "dependencies": { - "@algolia/autocomplete-shared": "1.17.9" - }, - "peerDependencies": { - "@algolia/client-search": ">= 4.9.1 < 6", - "algoliasearch": ">= 4.9.1 < 6" - } - }, "node_modules/@algolia/autocomplete-shared": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz", - "integrity": "sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ==", + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", + "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", "license": "MIT", "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", @@ -84,99 +141,99 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.35.0.tgz", - "integrity": "sha512-uUdHxbfHdoppDVflCHMxRlj49/IllPwwQ2cQ8DLC4LXr3kY96AHBpW0dMyi6ygkn2MtFCc6BxXCzr668ZRhLBQ==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.45.0.tgz", + "integrity": "sha512-WTW0VZA8xHMbzuQD5b3f41ovKZ0MNTIXkWfm0F2PU+XGcLxmxX15UqODzF2sWab0vSbi3URM1xLhJx+bXbd1eQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.35.0.tgz", - "integrity": "sha512-SunAgwa9CamLcRCPnPHx1V2uxdQwJGqb1crYrRWktWUdld0+B2KyakNEeVn5lln4VyeNtW17Ia7V7qBWyM/Skw==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.45.0.tgz", + "integrity": "sha512-I3g7VtvG/QJOH3tQO7E7zWTwBfK/nIQXShFLR8RvPgWburZ626JNj332M3wHCYcaAMivN9WJG66S2JNXhm6+Xg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.35.0.tgz", - "integrity": "sha512-ipE0IuvHu/bg7TjT2s+187kz/E3h5ssfTtjpg1LbWMgxlgiaZIgTTbyynM7NfpSJSKsgQvCQxWjGUO51WSCu7w==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.45.0.tgz", + "integrity": "sha512-/nTqm1tLiPtbUr+8kHKyFiCOfhRfgC+JxLvOCq471gFZZOlsh6VtFRiKI60/zGmHTojFC6B0mD80PB7KeK94og==", "license": "MIT", "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-insights": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.35.0.tgz", - "integrity": "sha512-UNbCXcBpqtzUucxExwTSfAe8gknAJ485NfPN6o1ziHm6nnxx97piIbcBQ3edw823Tej2Wxu1C0xBY06KgeZ7gA==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.45.0.tgz", + "integrity": "sha512-suQTx/1bRL1g/K2hRtbK3ANmbzaZCi13487sxxmqok+alBDKKw0/TI73ZiHjjFXM2NV52inwwcmW4fUR45206Q==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.35.0.tgz", - "integrity": "sha512-/KWjttZ6UCStt4QnWoDAJ12cKlQ+fkpMtyPmBgSS2WThJQdSV/4UWcqCUqGH7YLbwlj3JjNirCu3Y7uRTClxvA==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.45.0.tgz", + "integrity": "sha512-CId/dbjpzI3eoUhPU6rt/z4GrRsDesqFISEMOwrqWNSrf4FJhiUIzN42Ac+Gzg69uC0RnzRYy60K1y4Na5VSMw==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.35.0.tgz", - "integrity": "sha512-8oCuJCFf/71IYyvQQC+iu4kgViTODbXDk3m7yMctEncRSRV+u2RtDVlpGGfPlJQOrAY7OONwJlSHkmbbm2Kp/w==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.45.0.tgz", + "integrity": "sha512-tjbBKfA8fjAiFtvl9g/MpIPiD6pf3fj7rirVfh1eMIUi8ybHP4ovDzIaE216vHuRXoePQVCkMd2CokKvYq1CLw==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.35.0.tgz", - "integrity": "sha512-FfmdHTrXhIduWyyuko1YTcGLuicVbhUyRjO3HbXE4aP655yKZgdTIfMhZ/V5VY9bHuxv/fGEh3Od1Lvv2ODNTg==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.45.0.tgz", + "integrity": "sha512-nxuCid+Nszs4xqwIMDw11pRJPes2c+Th1yup/+LtpjFH8QWXkr3SirNYSD3OXAeM060HgWWPLA8/Fxk+vwxQOA==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" }, "engines": { "node": ">= 14.0.0" @@ -189,99 +246,86 @@ "license": "MIT" }, "node_modules/@algolia/ingestion": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.35.0.tgz", - "integrity": "sha512-gPzACem9IL1Co8mM1LKMhzn1aSJmp+Vp434An4C0OBY4uEJRcqsLN3uLBlY+bYvFg8C8ImwM9YRiKczJXRk0XA==", + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.45.0.tgz", + "integrity": "sha512-t+1doBzhkQTeOOjLHMlm4slmXBhvgtEGQhOmNpMPTnIgWOyZyESWdm+XD984qM4Ej1i9FRh8VttOGrdGnAjAng==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.35.0.tgz", - "integrity": "sha512-w9MGFLB6ashI8BGcQoVt7iLgDIJNCn4OIu0Q0giE3M2ItNrssvb8C0xuwJQyTy1OFZnemG0EB1OvXhIHOvQwWw==", + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.45.0.tgz", + "integrity": "sha512-IaX3ZX1A/0wlgWZue+1BNWlq5xtJgsRo7uUk/aSiYD7lPbJ7dFuZ+yTLFLKgbl4O0QcyHTj1/mSBj9ryF1Lizg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.35.0.tgz", - "integrity": "sha512-AhrVgaaXAb8Ue0u2nuRWwugt0dL5UmRgS9LXe0Hhz493a8KFeZVUE56RGIV3hAa6tHzmAV7eIoqcWTQvxzlJeQ==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.45.0.tgz", + "integrity": "sha512-1jeMLoOhkgezCCPsOqkScwYzAAc1Jr5T2hisZl0s32D94ZV7d1OHozBukgOjf8Dw+6Hgi6j52jlAdUWTtkX9Mg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.35.0.tgz", - "integrity": "sha512-diY415KLJZ6x1Kbwl9u96Jsz0OstE3asjXtJ9pmk1d+5gPuQ5jQyEsgC+WmEXzlec3iuVszm8AzNYYaqw6B+Zw==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.45.0.tgz", + "integrity": "sha512-46FIoUkQ9N7wq4/YkHS5/W9Yjm4Ab+q5kfbahdyMpkBPJ7IBlwuNEGnWUZIQ6JfUZuJVojRujPRHMihX4awUMg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0" + "@algolia/client-common": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.35.0.tgz", - "integrity": "sha512-uydqnSmpAjrgo8bqhE9N1wgcB98psTRRQXcjc4izwMB7yRl9C8uuAQ/5YqRj04U0mMQ+fdu2fcNF6m9+Z1BzDQ==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.45.0.tgz", + "integrity": "sha512-XFTSAtCwy4HdBhSReN2rhSyH/nZOM3q3qe5ERG2FLbYId62heIlJBGVyAPRbltRwNlotlydbvSJ+SQ0ruWC2cw==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0" + "@algolia/client-common": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.35.0.tgz", - "integrity": "sha512-RgLX78ojYOrThJHrIiPzT4HW3yfQa0D7K+MQ81rhxqaNyNBu4F1r+72LNHYH/Z+y9I1Mrjrd/c/Ue5zfDgAEjQ==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.45.0.tgz", + "integrity": "sha512-8mTg6lHx5i44raCU52APsu0EqMsdm4+7Hch/e4ZsYZw0hzwkuaMFh826ngnkYf9XOl58nHoou63aZ874m8AbpQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0" + "@algolia/client-common": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -297,30 +341,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -345,13 +389,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -398,17 +442,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.27.1", + "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "engines": { @@ -428,13 +472,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -479,13 +523,13 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -505,14 +549,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -599,9 +643,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -617,39 +661,39 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", - "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -659,13 +703,13 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -722,13 +766,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", - "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -902,9 +946,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", - "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", + "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -933,12 +977,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", - "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -949,9 +993,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", - "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -959,7 +1003,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -985,13 +1029,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", - "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1079,9 +1123,9 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1172,9 +1216,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1234,15 +1278,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1329,16 +1373,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", - "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1379,9 +1423,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1538,9 +1582,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz", - "integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1584,9 +1628,9 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.0.tgz", - "integrity": "sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", + "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -1689,13 +1733,13 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", - "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", + "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" @@ -1771,20 +1815,20 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz", - "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", + "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.0", + "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", @@ -1793,42 +1837,42 @@ "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.28.0", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.0", + "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1878,14 +1922,14 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", - "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" @@ -1898,16 +1942,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", - "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.27.1" + "@babel/plugin-transform-typescript": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1929,9 +1973,9 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.2.tgz", - "integrity": "sha512-FVFaVs2/dZgD3Y9ZD+AKNKjyGKzwu0C54laAXWUXgLcVXcCX6YZ6GhK2cp7FogSN2OA0Fu+QT8dP3FUdo9ShSQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", + "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", "license": "MIT", "dependencies": { "core-js-pure": "^3.43.0" @@ -1955,17 +1999,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -1973,13 +2017,13 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1989,6 +2033,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", "optional": true, "engines": { "node": ">=0.1.90" @@ -2018,9 +2063,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", - "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", "funding": [ { "type": "github", @@ -2060,9 +2105,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", - "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "funding": [ { "type": "github", @@ -2075,7 +2120,7 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.0.2", + "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "engines": { @@ -2150,6 +2195,35 @@ "@csstools/css-tokenizer": "^3.0.4" } }, + "node_modules/@csstools/postcss-alpha-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz", + "integrity": "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/@csstools/postcss-cascade-layers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", @@ -2199,9 +2273,9 @@ } }, "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -2212,9 +2286,38 @@ } }, "node_modules/@csstools/postcss-color-function": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.10.tgz", - "integrity": "sha512-4dY0NBu7NVIpzxZRgh/Q/0GPSz/jLSw0i/u3LTUor0BkQcz/fNhN10mSWBDsL0p9nDb0Ky1PD6/dcGbhACuFTQ==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz", + "integrity": "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-function-display-p3-linear": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz", + "integrity": "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==", "funding": [ { "type": "github", @@ -2227,10 +2330,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2241,9 +2344,9 @@ } }, "node_modules/@csstools/postcss-color-mix-function": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.10.tgz", - "integrity": "sha512-P0lIbQW9I4ShE7uBgZRib/lMTf9XMjJkFl/d6w4EMNHu2qvQ6zljJGEcBkw/NsBtq/6q3WrmgxSS8kHtPMkK4Q==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz", + "integrity": "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==", "funding": [ { "type": "github", @@ -2256,10 +2359,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2270,9 +2373,9 @@ } }, "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.0.tgz", - "integrity": "sha512-Z5WhouTyD74dPFPrVE7KydgNS9VvnjB8qcdes9ARpCOItb4jTnm7cHp4FhxCRUoyhabD0WVv43wbkJ4p8hLAlQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz", + "integrity": "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==", "funding": [ { "type": "github", @@ -2285,10 +2388,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2299,9 +2402,37 @@ } }, "node_modules/@csstools/postcss-content-alt-text": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.6.tgz", - "integrity": "sha512-eRjLbOjblXq+byyaedQRSrAejKGNAFued+LcbzT+LCL78fabxHkxYjBbxkroONxHHYu2qxhFK2dBStTLPG3jpQ==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz", + "integrity": "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-contrast-color-function": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz", + "integrity": "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==", "funding": [ { "type": "github", @@ -2314,9 +2445,10 @@ ], "license": "MIT-0", "dependencies": { + "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2380,9 +2512,9 @@ } }, "node_modules/@csstools/postcss-gamut-mapping": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.10.tgz", - "integrity": "sha512-QDGqhJlvFnDlaPAfCYPsnwVA6ze+8hhrwevYWlnUeSjkkZfBpcCO42SaUD8jiLlq7niouyLgvup5lh+f1qessg==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz", + "integrity": "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==", "funding": [ { "type": "github", @@ -2395,7 +2527,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, @@ -2407,9 +2539,9 @@ } }, "node_modules/@csstools/postcss-gradients-interpolation-method": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.10.tgz", - "integrity": "sha512-HHPauB2k7Oits02tKFUeVFEU2ox/H3OQVrP3fSOKDxvloOikSal+3dzlyTZmYsb9FlY9p5EUpBtz0//XBmy+aw==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz", + "integrity": "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==", "funding": [ { "type": "github", @@ -2422,10 +2554,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2436,9 +2568,9 @@ } }, "node_modules/@csstools/postcss-hwb-function": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.10.tgz", - "integrity": "sha512-nOKKfp14SWcdEQ++S9/4TgRKchooLZL0TUFdun3nI4KPwCjETmhjta1QT4ICQcGVWQTvrsgMM/aLB5We+kMHhQ==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz", + "integrity": "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==", "funding": [ { "type": "github", @@ -2451,10 +2583,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2465,9 +2597,9 @@ } }, "node_modules/@csstools/postcss-ic-unit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.2.tgz", - "integrity": "sha512-lrK2jjyZwh7DbxaNnIUjkeDmU8Y6KyzRBk91ZkI5h8nb1ykEfZrtIVArdIjX4DHMIBGpdHrgP0n4qXDr7OHaKA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz", + "integrity": "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==", "funding": [ { "type": "github", @@ -2480,7 +2612,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, @@ -2562,9 +2694,9 @@ } }, "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -2575,9 +2707,9 @@ } }, "node_modules/@csstools/postcss-light-dark-function": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.9.tgz", - "integrity": "sha512-1tCZH5bla0EAkFAI2r0H33CDnIBeLUaJh1p+hvvsylJ4svsv2wOmJjJn+OXwUZLXef37GYbRIVKX+X+g6m+3CQ==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", + "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", "funding": [ { "type": "github", @@ -2592,7 +2724,7 @@ "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2826,9 +2958,9 @@ } }, "node_modules/@csstools/postcss-oklab-function": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.10.tgz", - "integrity": "sha512-ZzZUTDd0fgNdhv8UUjGCtObPD8LYxMH+MJsW9xlZaWTV8Ppr4PtxlHYNMmF4vVWGl0T6f8tyWAKjoI6vePSgAg==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz", + "integrity": "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==", "funding": [ { "type": "github", @@ -2841,10 +2973,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2855,9 +2987,9 @@ } }, "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.1.0.tgz", - "integrity": "sha512-YrkI9dx8U4R8Sz2EJaoeD9fI7s7kmeEBfmO+UURNeL6lQI7VxF6sBE+rSqdCBn4onwqmxFdBU3lTwyYb/lCmxA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz", + "integrity": "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==", "funding": [ { "type": "github", @@ -2907,9 +3039,9 @@ } }, "node_modules/@csstools/postcss-relative-color-syntax": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.10.tgz", - "integrity": "sha512-8+0kQbQGg9yYG8hv0dtEpOMLwB9M+P7PhacgIzVzJpixxV4Eq9AUQtQw8adMmAJU1RBBmIlpmtmm3XTRd/T00g==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz", + "integrity": "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==", "funding": [ { "type": "github", @@ -2922,10 +3054,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2961,9 +3093,9 @@ } }, "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -3028,9 +3160,9 @@ } }, "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.2.tgz", - "integrity": "sha512-8XvCRrFNseBSAGxeaVTaNijAu+FzUvjwFXtcrynmazGb/9WUdsPCpBX+mHEHShVRq47Gy4peYAoxYs8ltUnmzA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz", + "integrity": "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==", "funding": [ { "type": "github", @@ -3043,7 +3175,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/color-helpers": "^5.0.2", + "@csstools/color-helpers": "^5.1.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -3133,22 +3265,48 @@ "node": ">=10.0.0" } }, + "node_modules/@docsearch/core": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.3.1.tgz", + "integrity": "sha512-ktVbkePE+2h9RwqCUMbWXOoebFyDOxHqImAqfs+lC8yOU+XwEW4jgvHGJK079deTeHtdhUNj0PXHSnhJINvHzQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/@docsearch/css": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.9.0.tgz", - "integrity": "sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.3.2.tgz", + "integrity": "sha512-K3Yhay9MgkBjJJ0WEL5MxnACModX9xuNt3UlQQkDEDZJZ0+aeWKtOkxHNndMRkMBnHdYvQjxkm6mdlneOtU1IQ==", "license": "MIT" }, "node_modules/@docsearch/react": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.9.0.tgz", - "integrity": "sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.3.2.tgz", + "integrity": "sha512-74SFD6WluwvgsOPqifYOviEEVwDxslxfhakTlra+JviaNcs7KK/rjsPj89kVEoQc9FUxRkAofaJnHIR7pb4TSQ==", "license": "MIT", "dependencies": { - "@algolia/autocomplete-core": "1.17.9", - "@algolia/autocomplete-preset-algolia": "1.17.9", - "@docsearch/css": "3.9.0", - "algoliasearch": "^5.14.2" + "@ai-sdk/react": "^2.0.30", + "@algolia/autocomplete-core": "1.19.2", + "@docsearch/core": "4.3.1", + "@docsearch/css": "4.3.2", + "ai": "^5.0.30", + "algoliasearch": "^5.28.0", + "marked": "^16.3.0", + "zod": "^4.1.8" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 20.0.0", @@ -3172,9 +3330,9 @@ } }, "node_modules/@docusaurus/babel": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.8.1.tgz", - "integrity": "sha512-3brkJrml8vUbn9aeoZUlJfsI/GqyFcDgQJwQkmBtclJgWDEQBKKeagZfOgx0WfUQhagL1sQLNW0iBdxnI863Uw==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.9.2.tgz", + "integrity": "sha512-GEANdi/SgER+L7Japs25YiGil/AUDnFFHaCGPBbundxoWtCkA2lmy7/tFmgED4y1htAy6Oi4wkJEQdGssnw9MA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", @@ -3187,28 +3345,28 @@ "@babel/runtime": "^7.25.9", "@babel/runtime-corejs3": "^7.25.9", "@babel/traverse": "^7.25.9", - "@docusaurus/logger": "3.8.1", - "@docusaurus/utils": "3.8.1", + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", "babel-plugin-dynamic-import-node": "^2.3.3", "fs-extra": "^11.1.1", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/bundler": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.8.1.tgz", - "integrity": "sha512-/z4V0FRoQ0GuSLToNjOSGsk6m2lQUG4FRn8goOVoZSRsTrU8YR2aJacX5K3RG18EaX9b+52pN4m1sL3MQZVsQA==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.9.2.tgz", + "integrity": "sha512-ZOVi6GYgTcsZcUzjblpzk3wH1Fya2VNpd5jtHoCCFcJlMQ1EYXZetfAnRHLcyiFeBABaI1ltTYbOBtH/gahGVA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", - "@docusaurus/babel": "3.8.1", - "@docusaurus/cssnano-preset": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", + "@docusaurus/babel": "3.9.2", + "@docusaurus/cssnano-preset": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", "babel-loader": "^9.2.1", "clean-css": "^5.3.3", "copy-webpack-plugin": "^11.0.0", @@ -3229,7 +3387,7 @@ "webpackbar": "^6.0.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/faster": "*" @@ -3241,18 +3399,18 @@ } }, "node_modules/@docusaurus/core": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.8.1.tgz", - "integrity": "sha512-ENB01IyQSqI2FLtOzqSI3qxG2B/jP4gQPahl2C3XReiLebcVh5B5cB9KYFvdoOqOWPyr5gXK4sjgTKv7peXCrA==", - "license": "MIT", - "dependencies": { - "@docusaurus/babel": "3.8.1", - "@docusaurus/bundler": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.2.tgz", + "integrity": "sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==", + "license": "MIT", + "dependencies": { + "@docusaurus/babel": "3.9.2", + "@docusaurus/bundler": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "boxen": "^6.2.1", "chalk": "^4.1.2", "chokidar": "^3.5.3", @@ -3286,14 +3444,14 @@ "update-notifier": "^6.0.2", "webpack": "^5.95.0", "webpack-bundle-analyzer": "^4.10.2", - "webpack-dev-server": "^4.15.2", + "webpack-dev-server": "^5.2.2", "webpack-merge": "^6.0.1" }, "bin": { "docusaurus": "bin/docusaurus.mjs" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@mdx-js/react": "^3.0.0", @@ -3302,9 +3460,9 @@ } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.8.1.tgz", - "integrity": "sha512-G7WyR2N6SpyUotqhGznERBK+x84uyhfMQM2MmDLs88bw4Flom6TY46HzkRkSEzaP9j80MbTN8naiL1fR17WQug==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.9.2.tgz", + "integrity": "sha512-8gBKup94aGttRduABsj7bpPFTX7kbwu+xh3K9NMCF5K4bWBqTFYW+REKHF6iBVDHRJ4grZdIPbvkiHd/XNKRMQ==", "license": "MIT", "dependencies": { "cssnano-preset-advanced": "^6.1.2", @@ -3313,31 +3471,31 @@ "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/logger": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.8.1.tgz", - "integrity": "sha512-2wjeGDhKcExEmjX8k1N/MRDiPKXGF2Pg+df/bDDPnnJWHXnVEZxXj80d6jcxp1Gpnksl0hF8t/ZQw9elqj2+ww==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.9.2.tgz", + "integrity": "sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/mdx-loader": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.8.1.tgz", - "integrity": "sha512-DZRhagSFRcEq1cUtBMo4TKxSNo/W6/s44yhr8X+eoXqCLycFQUylebOMPseHi5tc4fkGJqwqpWJLz6JStU9L4w==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.9.2.tgz", + "integrity": "sha512-wiYoGwF9gdd6rev62xDU8AAM8JuLI/hlwOtCzMmYcspEkzecKrP8J8X+KpYnTlACBUUtXNJpSoCwFWJhLRevzQ==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "@mdx-js/mdx": "^3.0.0", "@slorber/remark-comment": "^1.0.0", "escape-html": "^1.0.3", @@ -3361,7 +3519,7 @@ "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3369,12 +3527,12 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.8.1.tgz", - "integrity": "sha512-6xhvAJiXzsaq3JdosS7wbRt/PwEPWHr9eM4YNYqVlbgG1hSK3uQDXTVvQktasp3VO6BmfYWPozueLWuj4gB+vg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.2.tgz", + "integrity": "sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.8.1", + "@docusaurus/types": "3.9.2", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -3388,19 +3546,19 @@ } }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.8.1.tgz", - "integrity": "sha512-vNTpMmlvNP9n3hGEcgPaXyvTljanAKIUkuG9URQ1DeuDup0OR7Ltvoc8yrmH+iMZJbcQGhUJF+WjHLwuk8HSdw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/theme-common": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.9.2.tgz", + "integrity": "sha512-3I2HXy3L1QcjLJLGAoTvoBnpOwa6DPUa3Q0dMK19UTY9mhPkKQg/DYhAGTiBUKcTR0f08iw7kLPqOhIgdV3eVQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "cheerio": "1.0.0-rc.12", "feed": "^4.2.2", "fs-extra": "^11.1.1", @@ -3413,7 +3571,7 @@ "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", @@ -3422,20 +3580,20 @@ } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.1.tgz", - "integrity": "sha512-oByRkSZzeGNQByCMaX+kif5Nl2vmtj2IHQI2fWjCfCootsdKZDPFLonhIp5s3IGJO7PLUfe0POyw0Xh/RrGXJA==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/module-type-aliases": "3.8.1", - "@docusaurus/theme-common": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", + "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.1.1", @@ -3447,7 +3605,7 @@ "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3455,22 +3613,22 @@ } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.8.1.tgz", - "integrity": "sha512-a+V6MS2cIu37E/m7nDJn3dcxpvXb6TvgdNI22vJX8iUTp8eoMoPa0VArEbWvCxMY/xdC26WzNv4wZ6y0iIni/w==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.9.2.tgz", + "integrity": "sha512-s4849w/p4noXUrGpPUF0BPqIAfdAe76BLaRGAGKZ1gTDNiGxGcpsLcwJ9OTi1/V8A+AzvsmI9pkjie2zjIQZKA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "@docusaurus/core": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "fs-extra": "^11.1.1", "tslib": "^2.6.0", "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3478,36 +3636,36 @@ } }, "node_modules/@docusaurus/plugin-css-cascade-layers": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.8.1.tgz", - "integrity": "sha512-VQ47xRxfNKjHS5ItzaVXpxeTm7/wJLFMOPo1BkmoMG4Cuz4nuI+Hs62+RMk1OqVog68Swz66xVPK8g9XTrBKRw==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.9.2.tgz", + "integrity": "sha512-w1s3+Ss+eOQbscGM4cfIFBlVg/QKxyYgj26k5AnakuHkKxH6004ZtuLe5awMBotIYF2bbGDoDhpgQ4r/kcj4rQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/plugin-debug": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.8.1.tgz", - "integrity": "sha512-nT3lN7TV5bi5hKMB7FK8gCffFTBSsBsAfV84/v293qAmnHOyg1nr9okEw8AiwcO3bl9vije5nsUvP0aRl2lpaw==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.9.2.tgz", + "integrity": "sha512-j7a5hWuAFxyQAkilZwhsQ/b3T7FfHZ+0dub6j/GxKNFJp2h9qk/P1Bp7vrGASnvA9KNQBBL1ZXTe7jlh4VdPdA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", "fs-extra": "^11.1.1", "react-json-view-lite": "^2.3.0", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3515,18 +3673,18 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.8.1.tgz", - "integrity": "sha512-Hrb/PurOJsmwHAsfMDH6oVpahkEGsx7F8CWMjyP/dw1qjqmdS9rcV1nYCGlM8nOtD3Wk/eaThzUB5TSZsGz+7Q==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.9.2.tgz", + "integrity": "sha512-mAwwQJ1Us9jL/lVjXtErXto4p4/iaLlweC54yDUK1a97WfkC6Z2k5/769JsFgwOwOP+n5mUQGACXOEQ0XDuVUw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3534,19 +3692,19 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.8.1.tgz", - "integrity": "sha512-tKE8j1cEZCh8KZa4aa80zpSTxsC2/ZYqjx6AAfd8uA8VHZVw79+7OTEP2PoWi0uL5/1Is0LF5Vwxd+1fz5HlKg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.9.2.tgz", + "integrity": "sha512-YJ4lDCphabBtw19ooSlc1MnxtYGpjFV9rEdzjLsUnBCeis2djUyCozZaFhCg6NGEwOn7HDDyMh0yzcdRpnuIvA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "@types/gtag.js": "^0.0.12", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3554,18 +3712,18 @@ } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.8.1.tgz", - "integrity": "sha512-iqe3XKITBquZq+6UAXdb1vI0fPY5iIOitVjPQ581R1ZKpHr0qe+V6gVOrrcOHixPDD/BUKdYwkxFjpNiEN+vBw==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.9.2.tgz", + "integrity": "sha512-LJtIrkZN/tuHD8NqDAW1Tnw0ekOwRTfobWPsdO15YxcicBo2ykKF0/D6n0vVBfd3srwr9Z6rzrIWYrMzBGrvNw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3573,23 +3731,23 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.8.1.tgz", - "integrity": "sha512-+9YV/7VLbGTq8qNkjiugIelmfUEVkTyLe6X8bWq7K5qPvGXAjno27QAfFq63mYfFFbJc7z+pudL63acprbqGzw==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.9.2.tgz", + "integrity": "sha512-WLh7ymgDXjG8oPoM/T4/zUP7KcSuFYRZAUTl8vR6VzYkfc18GBM4xLhcT+AKOwun6kBivYKUJf+vlqYJkm+RHw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "fs-extra": "^11.1.1", "sitemap": "^7.1.1", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3597,22 +3755,22 @@ } }, "node_modules/@docusaurus/plugin-svgr": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.8.1.tgz", - "integrity": "sha512-rW0LWMDsdlsgowVwqiMb/7tANDodpy1wWPwCcamvhY7OECReN3feoFwLjd/U4tKjNY3encj0AJSTxJA+Fpe+Gw==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.9.2.tgz", + "integrity": "sha512-n+1DE+5b3Lnf27TgVU5jM1d4x5tUh2oW5LTsBxJX4PsAPV0JGcmI6p3yLYtEY0LRVEIJh+8RsdQmRE66wSV8mw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "@svgr/core": "8.1.0", "@svgr/webpack": "^8.1.0", "tslib": "^2.6.0", "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3620,29 +3778,29 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.8.1.tgz", - "integrity": "sha512-yJSjYNHXD8POMGc2mKQuj3ApPrN+eG0rO1UPgSx7jySpYU+n4WjBikbrA2ue5ad9A7aouEtMWUoiSRXTH/g7KQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/plugin-content-blog": "3.8.1", - "@docusaurus/plugin-content-docs": "3.8.1", - "@docusaurus/plugin-content-pages": "3.8.1", - "@docusaurus/plugin-css-cascade-layers": "3.8.1", - "@docusaurus/plugin-debug": "3.8.1", - "@docusaurus/plugin-google-analytics": "3.8.1", - "@docusaurus/plugin-google-gtag": "3.8.1", - "@docusaurus/plugin-google-tag-manager": "3.8.1", - "@docusaurus/plugin-sitemap": "3.8.1", - "@docusaurus/plugin-svgr": "3.8.1", - "@docusaurus/theme-classic": "3.8.1", - "@docusaurus/theme-common": "3.8.1", - "@docusaurus/theme-search-algolia": "3.8.1", - "@docusaurus/types": "3.8.1" + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.9.2.tgz", + "integrity": "sha512-IgyYO2Gvaigi21LuDIe+nvmN/dfGXAiMcV/murFqcpjnZc7jxFAxW+9LEjdPt61uZLxG4ByW/oUmX/DDK9t/8w==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/plugin-content-blog": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/plugin-content-pages": "3.9.2", + "@docusaurus/plugin-css-cascade-layers": "3.9.2", + "@docusaurus/plugin-debug": "3.9.2", + "@docusaurus/plugin-google-analytics": "3.9.2", + "@docusaurus/plugin-google-gtag": "3.9.2", + "@docusaurus/plugin-google-tag-manager": "3.9.2", + "@docusaurus/plugin-sitemap": "3.9.2", + "@docusaurus/plugin-svgr": "3.9.2", + "@docusaurus/theme-classic": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-search-algolia": "3.9.2", + "@docusaurus/types": "3.9.2" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3650,27 +3808,26 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.8.1.tgz", - "integrity": "sha512-bqDUCNqXeYypMCsE1VcTXSI1QuO4KXfx8Cvl6rYfY0bhhqN6d2WZlRkyLg/p6pm+DzvanqHOyYlqdPyP0iz+iw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/module-type-aliases": "3.8.1", - "@docusaurus/plugin-content-blog": "3.8.1", - "@docusaurus/plugin-content-docs": "3.8.1", - "@docusaurus/plugin-content-pages": "3.8.1", - "@docusaurus/theme-common": "3.8.1", - "@docusaurus/theme-translations": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.9.2.tgz", + "integrity": "sha512-IGUsArG5hhekXd7RDb11v94ycpJpFdJPkLnt10fFQWOVxAtq5/D7hT6lzc2fhyQKaaCE62qVajOMKL7OiAFAIA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/plugin-content-blog": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/plugin-content-pages": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-translations": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", - "copy-text-to-clipboard": "^3.2.0", "infima": "0.2.0-alpha.45", "lodash": "^4.17.21", "nprogress": "^0.2.0", @@ -3683,7 +3840,7 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3691,15 +3848,15 @@ } }, "node_modules/@docusaurus/theme-common": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.8.1.tgz", - "integrity": "sha512-UswMOyTnPEVRvN5Qzbo+l8k4xrd5fTFu2VPPfD6FcW/6qUtVLmJTQCktbAL3KJ0BVXGm5aJXz/ZrzqFuZERGPw==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", + "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", "license": "MIT", "dependencies": { - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/module-type-aliases": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -3710,7 +3867,7 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", @@ -3719,21 +3876,21 @@ } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.8.1.tgz", - "integrity": "sha512-NBFH5rZVQRAQM087aYSRKQ9yGEK9eHd+xOxQjqNpxMiV85OhJDD4ZGz6YJIod26Fbooy54UWVdzNU0TFeUUUzQ==", - "license": "MIT", - "dependencies": { - "@docsearch/react": "^3.9.0", - "@docusaurus/core": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/plugin-content-docs": "3.8.1", - "@docusaurus/theme-common": "3.8.1", - "@docusaurus/theme-translations": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "algoliasearch": "^5.17.1", - "algoliasearch-helper": "^3.22.6", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", + "integrity": "sha512-GBDSFNwjnh5/LdkxCKQHkgO2pIMX1447BxYUBG2wBiajS21uj64a+gH/qlbQjDLxmGrbrllBrtJkUHxIsiwRnw==", + "license": "MIT", + "dependencies": { + "@docsearch/react": "^3.9.0 || ^4.1.0", + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-translations": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "algoliasearch": "^5.37.0", + "algoliasearch-helper": "^3.26.0", "clsx": "^2.0.0", "eta": "^2.2.0", "fs-extra": "^11.1.1", @@ -3742,7 +3899,7 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3750,26 +3907,27 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.8.1.tgz", - "integrity": "sha512-OTp6eebuMcf2rJt4bqnvuwmm3NVXfzfYejL+u/Y1qwKhZPrjPoKWfk1CbOP5xH5ZOPkiAsx4dHdQBRJszK3z2g==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.9.2.tgz", + "integrity": "sha512-vIryvpP18ON9T9rjgMRFLr2xJVDpw1rtagEGf8Ccce4CkTrvM/fRB8N2nyWYOW5u3DdjkwKw5fBa+3tbn9P4PA==", "license": "MIT", "dependencies": { "fs-extra": "^11.1.1", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/types": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.8.1.tgz", - "integrity": "sha512-ZPdW5AB+pBjiVrcLuw3dOS6BFlrG0XkS2lDGsj8TizcnREQg3J8cjsgfDviszOk4CweNfwo1AEELJkYaMUuOPg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", + "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", "license": "MIT", "dependencies": { "@mdx-js/mdx": "^3.0.0", "@types/history": "^4.7.11", + "@types/mdast": "^4.0.2", "@types/react": "*", "commander": "^5.1.0", "joi": "^17.9.2", @@ -3798,14 +3956,14 @@ } }, "node_modules/@docusaurus/utils": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.8.1.tgz", - "integrity": "sha512-P1ml0nvOmEFdmu0smSXOqTS1sxU5tqvnc0dA4MTKV39kye+bhQnjkIKEE18fNOvxjyB86k8esoCIFM3x4RykOQ==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.2.tgz", + "integrity": "sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils-common": "3.8.1", + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-common": "3.9.2", "escape-string-regexp": "^4.0.0", "execa": "5.1.1", "file-loader": "^6.2.0", @@ -3826,31 +3984,31 @@ "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/utils-common": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.8.1.tgz", - "integrity": "sha512-zTZiDlvpvoJIrQEEd71c154DkcriBecm4z94OzEE9kz7ikS3J+iSlABhFXM45mZ0eN5pVqqr7cs60+ZlYLewtg==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.9.2.tgz", + "integrity": "sha512-I53UC1QctruA6SWLvbjbhCpAw7+X7PePoe5pYcwTOEXD/PxeP8LnECAhTHHwWCblyUX5bMi4QLRkxvyZ+IT8Aw==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.8.1", + "@docusaurus/types": "3.9.2", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/utils-validation": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.8.1.tgz", - "integrity": "sha512-gs5bXIccxzEbyVecvxg6upTwaUbfa0KMmTj7HhHzc016AGyxH2o73k1/aOD0IFrdCsfJNt37MqNI47s2MgRZMA==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.9.2.tgz", + "integrity": "sha512-l7yk3X5VnNmATbwijJkexdhulNsQaNDwoagiwujXoxFbWLcxHQqNQ+c/IAlzrfMMOfa/8xSBZ7KEKDesE/2J7A==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", "fs-extra": "^11.2.0", "joi": "^17.9.2", "js-yaml": "^4.1.0", @@ -3858,7 +4016,7 @@ "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@hapi/hoek": { @@ -3906,48 +4064,173 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz", + "integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.14.0.tgz", + "integrity": "sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.1", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -3955,15 +4238,16 @@ "license": "MIT" }, "node_modules/@mdx-js/mdx": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz", - "integrity": "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", @@ -3991,9 +4275,9 @@ } }, "node_modules/@mdx-js/react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", - "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", "dependencies": { "@types/mdx": "^2.0.0" @@ -4039,10 +4323,20 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", "engines": { "node": ">=12.22.0" } @@ -4051,6 +4345,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "license": "MIT", "dependencies": { "graceful-fs": "4.2.10" }, @@ -4061,12 +4356,14 @@ "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "license": "ISC" }, "node_modules/@pnpm/npm-conf": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz", - "integrity": "sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "license": "MIT", "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", @@ -4077,9 +4374,9 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "license": "MIT" }, "node_modules/@sideway/address": { @@ -4132,6 +4429,12 @@ "micromark-util-symbol": "^1.0.1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -4393,6 +4696,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.1" }, @@ -4400,19 +4704,10 @@ "node": ">=14.16" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "license": "MIT", "dependencies": { "@types/connect": "*", @@ -4477,9 +4772,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -4492,9 +4787,9 @@ } }, "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -4504,18 +4799,6 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.4.tgz", - "integrity": "sha512-5kz9ScmzBdzTgB/3susoCgfqNDzBjvLL4taparufgSvlwjdLy6UyUy9T/tCpYd2GIdIilCatC4iSQS0QSYHt0w==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", @@ -4550,23 +4833,25 @@ "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "license": "MIT" }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT" }, "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.15", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", - "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -4599,7 +4884,8 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" }, "node_modules/@types/mdast": { "version": "4.0.4", @@ -4628,17 +4914,18 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.10.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", - "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.12.0" } }, "node_modules/@types/node-forge": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", - "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -4656,9 +4943,9 @@ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/qs": { - "version": "6.9.17", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "license": "MIT" }, "node_modules/@types/range-parser": { @@ -4707,9 +4994,9 @@ } }, "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "license": "MIT" }, "node_modules/@types/sax": { @@ -4727,9 +5014,9 @@ "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -4746,9 +5033,9 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -4772,18 +5059,18 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "license": "MIT", "dependencies": { "@types/yargs-parser": "*" @@ -4801,6 +5088,15 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@vercel/oidc": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", + "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -5003,9 +5299,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5014,6 +5310,18 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -5039,6 +5347,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -5056,10 +5365,28 @@ "node": ">=8" } }, + "node_modules/ai": { + "version": "5.0.105", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.105.tgz", + "integrity": "sha512-waQZAvv44KYzys6S3l25ti2jcSuJnkyWFTliSKy3swASL6w6ttPxJTm80d+v9sLWoIxrqE3OwhTJbweNp065fg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "2.0.17", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.18", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -5102,34 +5429,34 @@ } }, "node_modules/algoliasearch": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.35.0.tgz", - "integrity": "sha512-Y+moNhsqgLmvJdgTsO4GZNgsaDWv8AOGAaPeIeHKlDn/XunoAqYbA+XNpBd1dW8GOXAUDyxC9Rxc7AV4kpFcIg==", - "license": "MIT", - "dependencies": { - "@algolia/abtesting": "1.1.0", - "@algolia/client-abtesting": "5.35.0", - "@algolia/client-analytics": "5.35.0", - "@algolia/client-common": "5.35.0", - "@algolia/client-insights": "5.35.0", - "@algolia/client-personalization": "5.35.0", - "@algolia/client-query-suggestions": "5.35.0", - "@algolia/client-search": "5.35.0", - "@algolia/ingestion": "1.35.0", - "@algolia/monitoring": "1.35.0", - "@algolia/recommend": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.45.0.tgz", + "integrity": "sha512-wrj4FGr14heLOYkBKV3Fbq5ZBGuIFeDJkTilYq/G+hH1CSlQBtYvG2X1j67flwv0fUeQJwnWxxRIunSemAZirA==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.11.0", + "@algolia/client-abtesting": "5.45.0", + "@algolia/client-analytics": "5.45.0", + "@algolia/client-common": "5.45.0", + "@algolia/client-insights": "5.45.0", + "@algolia/client-personalization": "5.45.0", + "@algolia/client-query-suggestions": "5.45.0", + "@algolia/client-search": "5.45.0", + "@algolia/ingestion": "1.45.0", + "@algolia/monitoring": "1.45.0", + "@algolia/recommend": "5.45.0", + "@algolia/requester-browser-xhr": "5.45.0", + "@algolia/requester-fetch": "5.45.0", + "@algolia/requester-node-http": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/algoliasearch-helper": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.0.tgz", - "integrity": "sha512-Rv2x3GXleQ3ygwhkhJubhhYGsICmShLAiqtUuJTUkr9uOCOXyF2E71LVT4XDnVffbknv8XgScP4U0Oxtgm+hIw==", + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.1.tgz", + "integrity": "sha512-CAlCxm4fYBXtvc5MamDzP6Svu8rW4z9me4DCBY1rQ2UDJ0u0flWmusQ8M3nOExZsLLRcUwUPoRAPMrhzOG3erw==", "license": "MIT", "dependencies": { "@algolia/events": "^4.0.1" @@ -5142,6 +5469,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", "dependencies": { "string-width": "^4.1.0" } @@ -5149,12 +5477,14 @@ "node_modules/ansi-align/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/ansi-align/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -5207,6 +5537,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -5215,6 +5546,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -5229,6 +5561,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -5279,9 +5612,9 @@ "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", "funding": [ { "type": "opencollective", @@ -5298,9 +5631,9 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -5402,7 +5735,17 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } }, "node_modules/batch": { "version": "0.6.1", @@ -5420,11 +5763,15 @@ } }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/body-parser": { @@ -5488,12 +5835,14 @@ "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" }, "node_modules/boxen": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", + "license": "MIT", "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^6.2.0", @@ -5533,9 +5882,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -5552,10 +5901,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -5567,7 +5917,23 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/bytes": { "version": "3.0.0", @@ -5582,6 +5948,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", "engines": { "node": ">=14.16" } @@ -5590,6 +5957,7 @@ "version": "10.2.14", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "license": "MIT", "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", @@ -5603,17 +5971,6 @@ "node": ">=14.16" } }, - "node_modules/cacheable-request/node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -5633,9 +5990,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5646,13 +6003,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -5674,6 +6031,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -5683,6 +6041,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -5703,9 +6062,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "funding": [ { "type": "opencollective", @@ -5736,6 +6095,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5835,15 +6195,10 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -5856,14 +6211,18 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", "engines": { "node": ">=6.0" } @@ -5878,6 +6237,7 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } @@ -5886,6 +6246,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", "dependencies": { "source-map": "~0.6.0" }, @@ -5897,6 +6258,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -5914,6 +6276,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -5922,9 +6285,10 @@ } }, "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", "dependencies": { "string-width": "^4.2.0" }, @@ -5938,12 +6302,14 @@ "node_modules/cli-table3/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/cli-table3/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -5990,6 +6356,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -6000,7 +6367,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/colord": { "version": "2.9.3", @@ -6018,6 +6386,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/combine-promises/-/combine-promises-1.2.0.tgz", "integrity": "sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==", + "license": "MIT", "engines": { "node": ">=10" } @@ -6036,6 +6405,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", "engines": { "node": ">= 6" } @@ -6065,9 +6435,9 @@ } }, "node_modules/compressible/node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -6118,21 +6488,30 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/configstore": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", + "license": "BSD-2-Clause", "dependencies": { "dot-prop": "^6.0.1", "graceful-fs": "^4.2.6", @@ -6204,18 +6583,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/copy-text-to-clipboard": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz", - "integrity": "sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/copy-webpack-plugin": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", @@ -6284,22 +6651,23 @@ } }, "node_modules/core-js": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz", - "integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==", + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", + "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", "hasInstallScript": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, "node_modules/core-js-compat": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz", - "integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==", + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", "license": "MIT", "dependencies": { - "browserslist": "^4.25.1" + "browserslist": "^4.28.0" }, "funding": { "type": "opencollective", @@ -6307,9 +6675,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.0.tgz", - "integrity": "sha512-OtwjqcDpY2X/eIIg1ol/n0y/X8A9foliaNt1dSK0gV3J2/zw+89FcNG3mPK+N8YWts4ZFUPxnrAzsxs/lf8yDA==", + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz", + "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -6353,6 +6721,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -6366,6 +6735,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "license": "MIT", "dependencies": { "type-fest": "^1.0.1" }, @@ -6380,6 +6750,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -6413,9 +6784,9 @@ } }, "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6426,9 +6797,9 @@ } }, "node_modules/css-declaration-sorter": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", - "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", + "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", "license": "ISC", "engines": { "node": "^14 || ^16 || >=18" @@ -6438,9 +6809,9 @@ } }, "node_modules/css-has-pseudo": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz", - "integrity": "sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz", + "integrity": "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==", "funding": [ { "type": "github", @@ -6487,9 +6858,9 @@ } }, "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6630,9 +7001,10 @@ } }, "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -6641,9 +7013,9 @@ } }, "node_modules/cssdb": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.3.1.tgz", - "integrity": "sha512-XnDRQMXucLueX92yDe0LPKupXetWoFOgawr4O4X41l5TltgK2NVbJJVDnnOywDYfW1sTJ28AcXGKOqdRKwCcmQ==", + "version": "8.4.3", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.4.3.tgz", + "integrity": "sha512-8aaDS5nVqMXmYjlmmJpqlDJosiqbl2NJkYuSFOXR6RTY14qNosMrqT4t7O+EUm+OdduQg3GNI2ZwC03No1Y58Q==", "funding": [ { "type": "opencollective", @@ -6810,9 +7182,9 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -6843,6 +7215,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, @@ -6857,6 +7230,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -6868,6 +7242,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -6881,22 +7256,39 @@ "node": ">=0.10.0" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "license": "BSD-2-Clause", + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", "dependencies": { - "execa": "^5.0.0" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" }, "engines": { - "node": ">= 10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", "engines": { "node": ">=10" } @@ -6922,6 +7314,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", "engines": { "node": ">=8" } @@ -6978,9 +7371,10 @@ "license": "MIT" }, "node_modules/detect-port": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.5.1.tgz", - "integrity": "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", + "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", + "license": "MIT", "dependencies": { "address": "^1.0.1", "debug": "4" @@ -6988,6 +7382,9 @@ "bin": { "detect": "bin/detect-port.js", "detect-port": "bin/detect-port.js" + }, + "engines": { + "node": ">= 4.0.0" } }, "node_modules/devlop": { @@ -7030,6 +7427,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "license": "MIT", "dependencies": { "utila": "~0.4" } @@ -7057,7 +7455,8 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "5.0.3", @@ -7092,6 +7491,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -7101,6 +7501,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", "dependencies": { "is-obj": "^2.0.0" }, @@ -7115,6 +7516,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", "engines": { "node": ">=8" } @@ -7136,12 +7538,14 @@ "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", @@ -7150,9 +7554,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.197", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.197.tgz", - "integrity": "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "license": "ISC" }, "node_modules/email-addresses": { @@ -7164,7 +7568,8 @@ "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" }, "node_modules/emojilib": { "version": "2.4.0", @@ -7201,12 +7606,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -7225,9 +7631,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -7252,14 +7658,15 @@ } }, "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7313,6 +7720,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -7323,12 +7731,14 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -7340,6 +7750,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -7365,6 +7776,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -7376,6 +7788,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -7384,6 +7797,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -7457,9 +7871,9 @@ } }, "node_modules/estree-util-value-to-estree": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.4.0.tgz", - "integrity": "sha512-Zlp+gxis+gCfK12d3Srl2PdX2ybsEA8ZYy6vQGVQTNNYLEGRQQ56XB64bjemN8kxIKXP1nC9ip4Z+ILy9LGzvQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", + "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -7504,6 +7918,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", + "license": "MIT", "engines": { "node": ">=6.0.0" }, @@ -7542,10 +7957,20 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", "engines": { "node": ">=0.8.x" } }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7570,39 +7995,39 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -7648,6 +8073,21 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/express/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/express/node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7678,7 +8118,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -7698,12 +8139,13 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", - "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "funding": [ { "type": "github", @@ -7806,9 +8248,9 @@ } }, "node_modules/file-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -7966,9 +8408,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -7989,6 +8431,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", "engines": { "node": ">= 14.17" } @@ -8011,15 +8454,15 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -8045,21 +8488,12 @@ "node": ">=14.14" } }, - "node_modules/fs-monkey": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", - "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -8072,6 +8506,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8086,17 +8521,17 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -8132,6 +8567,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -8268,25 +8704,6 @@ "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", "license": "ISC" }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -8298,15 +8715,33 @@ "node": ">= 6" } }, + "node_modules/glob-to-regex.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz", + "integrity": "sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" }, "node_modules/global-dirs": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "license": "MIT", "dependencies": { "ini": "2.0.0" }, @@ -8317,14 +8752,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/global-dirs/node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "engines": { - "node": ">=10" - } - }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -8360,6 +8787,7 @@ "version": "12.6.1", "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "license": "MIT", "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", @@ -8384,6 +8812,7 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -8421,9 +8850,9 @@ } }, "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -8437,6 +8866,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", "dependencies": { "duplexer": "^0.1.2" }, @@ -8457,6 +8887,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -8489,6 +8920,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -8684,6 +9116,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", "bin": { "he": "bin/he" } @@ -8692,6 +9125,7 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2", "loose-envify": "^1.2.0", @@ -8705,6 +9139,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } @@ -8757,22 +9192,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -8813,6 +9232,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -8831,9 +9251,10 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", - "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz", + "integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==", + "license": "MIT", "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", @@ -8865,6 +9286,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", "engines": { "node": ">= 12" } @@ -8873,6 +9295,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", "dependencies": { "camel-case": "^4.1.2", "clean-css": "^5.2.2", @@ -8909,9 +9332,10 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" }, "node_modules/http-deceiver": { "version": "1.2.7", @@ -8936,9 +9360,9 @@ } }, "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", "license": "MIT" }, "node_modules/http-proxy": { @@ -8995,6 +9419,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -9012,6 +9437,15 @@ "node": ">=10.17.0" } }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -9076,6 +9510,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "license": "MIT", "engines": { "node": ">=8" } @@ -9084,6 +9519,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -9106,35 +9542,32 @@ "node": ">=12" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "license": "ISC", + "engines": { + "node": ">=10" + } }, "node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" } @@ -9182,6 +9615,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -9193,6 +9627,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "license": "MIT", "dependencies": { "ci-info": "^3.2.0" }, @@ -9229,6 +9664,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -9260,6 +9696,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "engines": { "node": ">=8" } @@ -9285,10 +9722,44 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-installed-globally": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "license": "MIT", "dependencies": { "global-dirs": "^3.0.0", "is-path-inside": "^3.0.2" @@ -9300,10 +9771,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-npm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", - "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -9332,6 +9816,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -9384,12 +9869,14 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", "dependencies": { "is-docker": "^2.0.0" }, @@ -9401,6 +9888,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", + "license": "MIT", "engines": { "node": ">=12" } @@ -9408,12 +9896,14 @@ "node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", @@ -9499,9 +9989,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9525,12 +10015,20 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -9565,6 +10063,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -9573,6 +10072,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -9581,6 +10081,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", "engines": { "node": ">=6" } @@ -9589,6 +10090,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "license": "MIT", "dependencies": { "package-json": "^8.1.0" }, @@ -9600,19 +10102,20 @@ } }, "node_modules/launch-editor": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", - "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", + "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", "license": "MIT", "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" } }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", "engines": { "node": ">=6" } @@ -9636,11 +10139,16 @@ "license": "MIT" }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -9673,9 +10181,10 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -9720,6 +10229,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.3" } @@ -9728,6 +10238,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -9790,6 +10301,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10147,9 +10670,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -10217,14 +10740,21 @@ } }, "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "version": "4.47.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.47.0.tgz", + "integrity": "sha512-Xey8IZA57tfotV/TN4d6BmccQuhFP+CqRiI7TTNdipZdZBzF2WnzUcH//Cudw6X4zJiUbo/LTuU/HPA/iC/pNg==", + "license": "Apache-2.0", "dependencies": { - "fs-monkey": "^1.0.4" + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" }, - "engines": { - "node": ">= 4.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" } }, "node_modules/merge-descriptors": { @@ -10239,7 +10769,8 @@ "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", @@ -12100,6 +12631,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -12108,9 +12640,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.3.tgz", - "integrity": "sha512-tRA0+PsS4kLVijnN1w9jUu5lkxBwUk9E8SbgEB5dBJqchE6pVYdawROG6uQtpmAri7tdCK9i7b1bULeVWqS6Ag==", + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", + "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", "license": "MIT", "dependencies": { "schema-utils": "^4.0.0", @@ -12134,9 +12666,10 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12148,14 +12681,15 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "license": "MIT", "engines": { "node": ">=10" @@ -12210,12 +12744,14 @@ "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -12237,24 +12773,25 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -12268,9 +12805,21 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "node_modules/normalize-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "license": "MIT", "dependencies": { @@ -12290,6 +12839,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" }, @@ -12318,9 +12868,9 @@ } }, "node_modules/null-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -12375,9 +12925,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -12442,14 +12992,6 @@ "node": ">= 0.8" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -12469,6 +13011,7 @@ "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -12494,6 +13037,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "license": "MIT", "engines": { "node": ">=12.20" } @@ -12569,16 +13113,20 @@ } }, "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", "license": "MIT", "dependencies": { - "@types/retry": "0.12.0", + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", "retry": "^0.13.1" }, "engines": { - "node": ">=8" + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-timeout": { @@ -12606,6 +13154,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "license": "MIT", "dependencies": { "got": "^12.1.0", "registry-auth-token": "^5.0.1", @@ -12623,6 +13172,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -12739,6 +13289,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -12753,14 +13304,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-is-inside": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", @@ -12771,6 +13314,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", "engines": { "node": ">=8" } @@ -12884,9 +13428,9 @@ } }, "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -12928,9 +13472,9 @@ } }, "node_modules/postcss-color-functional-notation": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.10.tgz", - "integrity": "sha512-k9qX+aXHBiLTRrWoCJuUFI6F1iF6QJQUXNVWJVSbqZgj57jDhBlOvD8gNUGl35tgqDivbGLhZeW3Ongz4feuKA==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", + "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", "funding": [ { "type": "github", @@ -12943,10 +13487,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -13128,9 +13672,9 @@ } }, "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -13166,9 +13710,9 @@ } }, "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -13242,9 +13786,9 @@ } }, "node_modules/postcss-double-position-gradients": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.2.tgz", - "integrity": "sha512-7qTqnL7nfLRyJK/AHSVrrXOuvDDzettC+wGoienURV8v2svNbu6zJC52ruZtHaO6mfcagFmuTGFdzRsJKB3k5Q==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", + "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", "funding": [ { "type": "github", @@ -13257,7 +13801,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, @@ -13294,9 +13838,9 @@ } }, "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -13332,9 +13876,9 @@ } }, "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -13402,9 +13946,9 @@ } }, "node_modules/postcss-lab-function": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.10.tgz", - "integrity": "sha512-tqs6TCEv9tC1Riq6fOzHuHcZyhg4k3gIAMB8GGY/zA1ssGdm6puHMVE7t75aOSoFg7UD2wyrFFhbldiCMyyFTQ==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", + "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", "funding": [ { "type": "github", @@ -13417,10 +13961,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -13621,9 +14165,9 @@ } }, "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -13649,9 +14193,9 @@ } }, "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -13748,9 +14292,9 @@ } }, "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -13991,9 +14535,9 @@ } }, "node_modules/postcss-preset-env": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.2.4.tgz", - "integrity": "sha512-q+lXgqmTMdB0Ty+EQ31SuodhdfZetUlwCA/F0zRcd/XdxjzI+Rl2JhZNz5US2n/7t9ePsvuhCnEN4Bmu86zXlA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.4.0.tgz", + "integrity": "sha512-2kqpOthQ6JhxqQq1FSAAZGe9COQv75Aw8WbsOvQVNJ2nSevc9Yx/IKZGuZ7XJ+iOTtVon7LfO7ELRzg8AZ+sdw==", "funding": [ { "type": "github", @@ -14006,20 +14550,23 @@ ], "license": "MIT-0", "dependencies": { + "@csstools/postcss-alpha-function": "^1.0.1", "@csstools/postcss-cascade-layers": "^5.0.2", - "@csstools/postcss-color-function": "^4.0.10", - "@csstools/postcss-color-mix-function": "^3.0.10", - "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.0", - "@csstools/postcss-content-alt-text": "^2.0.6", + "@csstools/postcss-color-function": "^4.0.12", + "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", + "@csstools/postcss-color-mix-function": "^3.0.12", + "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", + "@csstools/postcss-content-alt-text": "^2.0.8", + "@csstools/postcss-contrast-color-function": "^2.0.12", "@csstools/postcss-exponential-functions": "^2.0.9", "@csstools/postcss-font-format-keywords": "^4.0.0", - "@csstools/postcss-gamut-mapping": "^2.0.10", - "@csstools/postcss-gradients-interpolation-method": "^5.0.10", - "@csstools/postcss-hwb-function": "^4.0.10", - "@csstools/postcss-ic-unit": "^4.0.2", + "@csstools/postcss-gamut-mapping": "^2.0.11", + "@csstools/postcss-gradients-interpolation-method": "^5.0.12", + "@csstools/postcss-hwb-function": "^4.0.12", + "@csstools/postcss-ic-unit": "^4.0.4", "@csstools/postcss-initial": "^2.0.1", "@csstools/postcss-is-pseudo-class": "^5.0.3", - "@csstools/postcss-light-dark-function": "^2.0.9", + "@csstools/postcss-light-dark-function": "^2.0.11", "@csstools/postcss-logical-float-and-clear": "^3.0.0", "@csstools/postcss-logical-overflow": "^2.0.0", "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", @@ -14029,38 +14576,38 @@ "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", "@csstools/postcss-nested-calc": "^4.0.0", "@csstools/postcss-normalize-display-values": "^4.0.0", - "@csstools/postcss-oklab-function": "^4.0.10", - "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-oklab-function": "^4.0.12", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/postcss-random-function": "^2.0.1", - "@csstools/postcss-relative-color-syntax": "^3.0.10", + "@csstools/postcss-relative-color-syntax": "^3.0.12", "@csstools/postcss-scope-pseudo-class": "^4.0.1", "@csstools/postcss-sign-functions": "^1.1.4", "@csstools/postcss-stepped-value-functions": "^4.0.9", - "@csstools/postcss-text-decoration-shorthand": "^4.0.2", + "@csstools/postcss-text-decoration-shorthand": "^4.0.3", "@csstools/postcss-trigonometric-functions": "^4.0.9", "@csstools/postcss-unset-value": "^4.0.0", "autoprefixer": "^10.4.21", - "browserslist": "^4.25.0", + "browserslist": "^4.26.0", "css-blank-pseudo": "^7.0.1", - "css-has-pseudo": "^7.0.2", + "css-has-pseudo": "^7.0.3", "css-prefers-color-scheme": "^10.0.0", - "cssdb": "^8.3.0", + "cssdb": "^8.4.2", "postcss-attribute-case-insensitive": "^7.0.1", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^7.0.10", + "postcss-color-functional-notation": "^7.0.12", "postcss-color-hex-alpha": "^10.0.0", "postcss-color-rebeccapurple": "^10.0.0", "postcss-custom-media": "^11.0.6", "postcss-custom-properties": "^14.0.6", "postcss-custom-selectors": "^8.0.5", "postcss-dir-pseudo-class": "^9.0.1", - "postcss-double-position-gradients": "^6.0.2", + "postcss-double-position-gradients": "^6.0.4", "postcss-focus-visible": "^10.0.1", "postcss-focus-within": "^9.0.1", "postcss-font-variant": "^5.0.0", "postcss-gap-properties": "^6.0.0", "postcss-image-set-function": "^7.0.0", - "postcss-lab-function": "^7.0.10", + "postcss-lab-function": "^7.0.12", "postcss-logical": "^8.1.0", "postcss-nesting": "^13.0.2", "postcss-opacity-percentage": "^3.0.0", @@ -14104,9 +14651,9 @@ } }, "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -14197,9 +14744,9 @@ } }, "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -14290,6 +14837,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "license": "MIT", "dependencies": { "lodash": "^4.17.20", "renderkid": "^3.0.0" @@ -14336,6 +14884,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -14367,7 +14916,8 @@ "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -14391,10 +14941,20 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pupa": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", - "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "license": "MIT", "dependencies": { "escape-goat": "^4.0.0" }, @@ -14443,6 +15003,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -14454,6 +15015,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -14495,6 +15057,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -14505,46 +15068,54 @@ "rc": "cli.js" } }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.0" } }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" }, "node_modules/react-helmet-async": { + "name": "@slorber/react-helmet-async", "version": "1.3.0", - "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz", - "integrity": "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==", + "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", + "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", + "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.12.5", "invariant": "^2.2.4", @@ -14553,8 +15124,8 @@ "shallowequal": "^1.1.0" }, "peerDependencies": { - "react": "^16.6.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" + "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-is": { @@ -14563,9 +15134,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-json-view-lite": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.4.2.tgz", - "integrity": "sha512-m7uTsXDgPQp8R9bJO4HD/66+i218eyQPAb+7/dGQpwg8i4z2afTFqtHJPQFHvJfgDCjGQ1HSGlL3HtrZDa3Tdg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", + "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", "license": "MIT", "engines": { "node": ">=18" @@ -14590,6 +15161,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.3" }, @@ -14605,6 +15177,7 @@ "version": "5.3.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -14624,6 +15197,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz", "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2" }, @@ -14636,6 +15210,7 @@ "version": "5.3.4", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -14667,6 +15242,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -14748,9 +15324,9 @@ "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -14765,26 +15341,27 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" } }, "node_modules/registry-auth-token": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", - "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", + "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "license": "MIT", "dependencies": { "@pnpm/npm-conf": "^2.1.0" }, @@ -14796,6 +15373,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "license": "MIT", "dependencies": { "rc": "1.2.8" }, @@ -14813,29 +15391,17 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/rehype-raw": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", @@ -14870,6 +15436,7 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -14941,9 +15508,9 @@ } }, "node_modules/remark-mdx": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.0.tgz", - "integrity": "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", "license": "MIT", "dependencies": { "mdast-util-mdx": "^3.0.0", @@ -15006,6 +15573,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "license": "MIT", "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", @@ -15018,6 +15586,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", @@ -15033,6 +15602,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -15046,6 +15616,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" }, @@ -15060,6 +15631,7 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -15073,6 +15645,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } @@ -15088,6 +15661,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", @@ -15128,12 +15702,12 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -15150,7 +15724,8 @@ "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" }, "node_modules/resolve-from": { "version": "4.0.0", @@ -15164,12 +15739,14 @@ "node_modules/resolve-pathname": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", + "license": "MIT" }, "node_modules/responselike": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "license": "MIT", "dependencies": { "lowercase-keys": "^3.0.0" }, @@ -15198,20 +15775,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rtlcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", @@ -15230,6 +15793,18 @@ "node": ">=12.0.0" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -15269,7 +15844,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -15278,18 +15854,19 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/schema-dts": { "version": "1.1.5", @@ -15298,9 +15875,9 @@ "license": "Apache-2.0" }, "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -15356,12 +15933,10 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -15373,6 +15948,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", + "license": "MIT", "dependencies": { "semver": "^7.3.5" }, @@ -15383,22 +15959,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -15466,15 +16026,15 @@ } }, "node_modules/serve-handler": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", - "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", "license": "MIT", "dependencies": { "bytes": "3.0.0", "content-disposition": "0.5.2", "mime-types": "2.1.18", - "minimatch": "3.1.2", + "minimatch": "3.1.5", "path-is-inside": "1.0.2", "path-to-regexp": "3.3.0", "range-parser": "1.2.0" @@ -15617,12 +16177,14 @@ "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -15634,14 +16196,19 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -15721,7 +16288,8 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" }, "node_modules/sirv": { "version": "2.0.4", @@ -15740,7 +16308,8 @@ "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" }, "node_modules/sitemap": { "version": "7.1.2", @@ -15839,6 +16408,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15848,6 +16418,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -15920,9 +16491,9 @@ } }, "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "license": "MIT" }, "node_modules/string_decoder": { @@ -15938,6 +16509,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -15951,9 +16523,10 @@ } }, "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -15962,9 +16535,10 @@ } }, "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -16007,6 +16581,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -16066,21 +16641,21 @@ } }, "node_modules/style-to-js": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", - "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", "license": "MIT", "dependencies": { - "style-to-object": "1.0.9" + "style-to-object": "1.0.14" } }, "node_modules/style-to-object": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", - "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", "license": "MIT", "dependencies": { - "inline-style-parser": "0.2.4" + "inline-style-parser": "0.2.7" } }, "node_modules/stylehacks": { @@ -16103,6 +16678,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -16129,18 +16705,18 @@ "license": "MIT" }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" @@ -16162,21 +16738,40 @@ "node": ">= 10" } }, + "node_modules/swr": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.7.tgz", + "integrity": "sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", - "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -16188,15 +16783,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -16220,33 +16816,11 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/terser-webpack-plugin/node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -16256,32 +16830,11 @@ "node": ">= 10.13.0" } }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser-webpack-plugin/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -16295,7 +16848,36 @@ "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/thingies": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/thunky": { "version": "1.1.0", @@ -16304,14 +16886,16 @@ "license": "MIT" }, "node_modules/tiny-invariant": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", - "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" }, "node_modules/tinypool": { "version": "1.1.1", @@ -16351,6 +16935,22 @@ "node": ">=6" } }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -16393,14 +16993,16 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, @@ -16446,14 +17048,16 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", "dependencies": { "is-typedarray": "^1.0.0" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -16487,18 +17091,18 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "license": "MIT", "engines": { "node": ">=4" @@ -16527,6 +17131,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "license": "MIT", "dependencies": { "crypto-random-string": "^4.0.0" }, @@ -16538,9 +17143,9 @@ } }, "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -16605,9 +17210,9 @@ } }, "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -16636,9 +17241,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -16669,6 +17274,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", + "license": "BSD-2-Clause", "dependencies": { "boxen": "^7.0.0", "chalk": "^5.0.1", @@ -16696,6 +17302,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", + "license": "MIT", "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^7.0.1", @@ -16717,6 +17324,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -16725,9 +17333,10 @@ } }, "node_modules/update-notifier/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -16739,18 +17348,11 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/uri-js/node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, "node_modules/url-loader": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", @@ -16779,9 +17381,9 @@ } }, "node_modules/url-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -16848,6 +17450,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -16857,7 +17468,8 @@ "node_modules/utila": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "license": "MIT" }, "node_modules/utility-types": { "version": "3.11.0", @@ -16889,7 +17501,8 @@ "node_modules/value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", + "license": "MIT" }, "node_modules/vary": { "version": "1.1.2", @@ -16943,9 +17556,10 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -16974,34 +17588,36 @@ } }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -17055,44 +17671,50 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } } }, "node_modules/webpack-dev-middleware/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/webpack-dev-middleware/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -17108,54 +17730,52 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", - "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", - "license": "MIT", - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", + "express": "^4.21.2", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.4", - "ws": "^8.13.0" + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { @@ -17166,10 +17786,40 @@ } } }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -17202,45 +17852,19 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", "engines": { "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -17249,6 +17873,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -17256,23 +17881,6 @@ "node": ">= 0.6" } }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/webpackbar": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz", @@ -17372,6 +17980,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -17386,6 +17995,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "license": "MIT", "dependencies": { "string-width": "^5.0.1" }, @@ -17406,6 +18016,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -17419,9 +18030,10 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -17430,9 +18042,10 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -17441,9 +18054,10 @@ } }, "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -17454,15 +18068,11 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, "node_modules/write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", @@ -17491,10 +18101,41 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xdg-basedir": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -17521,9 +18162,9 @@ "license": "ISC" }, "node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", "license": "MIT", "engines": { "node": ">=12.20" @@ -17532,6 +18173,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/docs/package.json b/docs/package.json index 9a9e55af..7c2e57cf 100644 --- a/docs/package.json +++ b/docs/package.json @@ -15,15 +15,15 @@ "typecheck": "tsc" }, "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/preset-classic": "3.8.1", - "@mdx-js/react": "3.1.0", + "@docusaurus/core": "3.9.2", + "@docusaurus/preset-classic": "3.9.2", + "@mdx-js/react": "3.1.1", "prism-react-renderer": "2.4.1", - "react": "18.3.1", - "react-dom": "18.3.1" + "react": "19.2.0", + "react-dom": "19.2.0" }, "devDependencies": { - "@docusaurus/module-type-aliases": "3.8.1", + "@docusaurus/module-type-aliases": "3.9.2", "gh-pages": "6.3.0" }, "browserslist": { diff --git a/src/Benchmarks/ArgumentsBenchmarks.cs b/src/Benchmarks/ArgumentsBenchmarks.cs index 5ae3ce96..405bc44d 100644 --- a/src/Benchmarks/ArgumentsBenchmarks.cs +++ b/src/Benchmarks/ArgumentsBenchmarks.cs @@ -55,7 +55,7 @@ public static void ObjectWithReflection() for (int i = 0; i < 10000; i++) { - ExpressionUtil.ConvertObjectType(variables["names"], typeof(InputType), null, null); + ExpressionUtil.ConvertObjectType(variables["names"], typeof(InputType), null); } } @@ -75,7 +75,7 @@ public static void ListWithReflection() for (int i = 0; i < 10000; i++) { - ExpressionUtil.ConvertObjectType(variables["names"], typeof(List), null, null); + ExpressionUtil.ConvertObjectType(variables["names"], typeof(List), null); } } } diff --git a/src/Benchmarks/CompileGqlDocumentOnlyBenchmarks.cs b/src/Benchmarks/CompileGqlDocumentOnlyBenchmarks.cs index 08b948b2..025e823f 100644 --- a/src/Benchmarks/CompileGqlDocumentOnlyBenchmarks.cs +++ b/src/Benchmarks/CompileGqlDocumentOnlyBenchmarks.cs @@ -22,6 +22,15 @@ namespace Benchmarks; /// | Query_List | 2.431 us | 0.0055 us | 0.0046 us | 0.9117 | 0.0076 | 5.59 KB | /// | Query_ListWithTakeArg | 8.482 us | 0.1298 us | 0.1151 us | 2.5177 | 0.0305 | 15.49 KB | /// +/// 6.0.0 +/// | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +/// |-------------------------------------------------- |---------:|----------:|----------:|-------:|-------:|----------:| +/// | Query_SingleObjectWithArg | 3.897 us | 0.0454 us | 0.0379 us | 1.0376 | - | 6.5 KB | +/// | Query_SingleObjectWithArg_IncludeSubObject | 4.452 us | 0.0890 us | 0.1060 us | 1.1902 | - | 7.39 KB | +/// | Query_SingleObjectWithArg_IncludeSubObjectAndList | 5.113 us | 0.1020 us | 0.1558 us | 1.3733 | - | 8.5 KB | +/// | Query_List | 1.784 us | 0.0120 us | 0.0113 us | 0.5760 | 0.0019 | 3.54 KB | +/// | Query_ListWithTakeArg | 7.991 us | 0.0361 us | 0.0320 us | 2.1515 | 0.0153 | 13.2 KB | +/// /// [MemoryDiagnoser] public class CompileGqlDocumentOnlyBenchmarks : BaseBenchmark @@ -29,7 +38,7 @@ public class CompileGqlDocumentOnlyBenchmarks : BaseBenchmark [Benchmark] public void Query_SingleObjectWithArg() { - new GraphQLCompiler(Schema).Compile( + GraphQLParser.Parse( new QueryRequest { Query = @@ -38,14 +47,15 @@ public void Query_SingleObjectWithArg() id name released } }", - } + }, + Schema ); } [Benchmark] public void Query_SingleObjectWithArg_IncludeSubObject() { - new GraphQLCompiler(Schema).Compile( + GraphQLParser.Parse( new QueryRequest { Query = @@ -57,14 +67,15 @@ id name dob } } }", - } + }, + Schema ); } [Benchmark] public void Query_SingleObjectWithArg_IncludeSubObjectAndList() { - new GraphQLCompiler(Schema).Compile( + GraphQLParser.Parse( new QueryRequest { Query = @@ -79,14 +90,15 @@ id name dob } } }", - } + }, + Schema ); } [Benchmark] public void Query_List() { - new GraphQLCompiler(Schema).Compile( + GraphQLParser.Parse( new QueryRequest { Query = @@ -95,7 +107,8 @@ public void Query_List() id name released } }", - } + }, + Schema ); } @@ -108,7 +121,7 @@ public void ModifyField() [Benchmark] public void Query_ListWithTakeArg() { - new GraphQLCompiler(Schema).Compile( + GraphQLParser.Parse( new QueryRequest { Query = @@ -117,7 +130,8 @@ public void Query_ListWithTakeArg() id name released } }", - } + }, + Schema ); } } diff --git a/src/Benchmarks/EqlBenchmarks.cs b/src/Benchmarks/EqlBenchmarks.cs index af605696..d2578532 100644 --- a/src/Benchmarks/EqlBenchmarks.cs +++ b/src/Benchmarks/EqlBenchmarks.cs @@ -47,7 +47,13 @@ public class EqlBenchmarks : BaseBenchmark public object SimpleExpression() { var expressionStr = "name == \"foo\""; - var expression = EntityQueryCompiler.CompileWith(expressionStr, _context, Schema, new QueryRequestContext(null, null), new ExecutionOptions()); + var expression = EntityQueryCompiler.CompileWith( + expressionStr, + _context, + Schema, + new QueryRequestContext(null, null), + new EqlCompileContext(new EntityGraphQL.Compiler.CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null)) + ); return expression; } @@ -56,7 +62,13 @@ public object SimpleExpression() public object ComplexExpression() { var expressionStr = "name == \"foo\" && director.name == \"Jimmy\" && director.dob > \"1978-02-04\" && genre.name == \"Action\" && rating > 3"; - var expression = EntityQueryCompiler.CompileWith(expressionStr, _context, Schema, new QueryRequestContext(null, null), new ExecutionOptions()); + var expression = EntityQueryCompiler.CompileWith( + expressionStr, + _context, + Schema, + new QueryRequestContext(null, null), + new EqlCompileContext(new EntityGraphQL.Compiler.CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null)) + ); return expression; } @@ -66,7 +78,13 @@ public object ComplexWithMethodCallExpression() { var expressionStr = "name.contains(\"fo\") && director.name.toLower().startsWith(\"ji\") && director.dob > \"1978-01-01\" && actors.orderBy(name).first().name.startsWith(\"bob\") && rating > 3"; - var expression = EntityQueryCompiler.CompileWith(expressionStr, _context, Schema, new QueryRequestContext(null, null), new ExecutionOptions()); + var expression = EntityQueryCompiler.CompileWith( + expressionStr, + _context, + Schema, + new QueryRequestContext(null, null), + new EqlCompileContext(new EntityGraphQL.Compiler.CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null)) + ); return expression; } diff --git a/src/Benchmarks/Program.cs b/src/Benchmarks/Program.cs index 2d439304..e03984f1 100644 --- a/src/Benchmarks/Program.cs +++ b/src/Benchmarks/Program.cs @@ -1,5 +1,4 @@ -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; namespace Benchmarks; diff --git a/src/EntityGraphQL.AspNet/EntityGraphQL.AspNet.csproj b/src/EntityGraphQL.AspNet/EntityGraphQL.AspNet.csproj index 844dbafb..1e503541 100644 --- a/src/EntityGraphQL.AspNet/EntityGraphQL.AspNet.csproj +++ b/src/EntityGraphQL.AspNet/EntityGraphQL.AspNet.csproj @@ -1,10 +1,10 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 EntityGraphQL.AspNet EntityGraphQL.AspNet 13 - 5.8.0 + 6.0.0-beta3 Contains ASP.NET extensions and middleware for EntityGraphQL Luke Murray https://github.com/lukemurray/EntityGraphQL diff --git a/src/EntityGraphQL.AspNet/Extensions/AddGraphQLOptions.cs b/src/EntityGraphQL.AspNet/Extensions/AddGraphQLOptions.cs index 96e7a369..a2cc93ac 100644 --- a/src/EntityGraphQL.AspNet/Extensions/AddGraphQLOptions.cs +++ b/src/EntityGraphQL.AspNet/Extensions/AddGraphQLOptions.cs @@ -1,27 +1,40 @@ using System; using EntityGraphQL.Schema; +using Microsoft.AspNetCore.Authorization; namespace EntityGraphQL.AspNet; -public class AddGraphQLOptions : SchemaBuilderOptions +/// +/// Options for configuring GraphQL schema setup in ASP.NET applications. +/// Uses composition to provide access to both reflection and provider options. +/// +/// The type of the schema context +public class AddGraphQLOptions { + public AddGraphQLOptions(IAuthorizationService? authService) + { + // Default for asp.net if IAuthorizationService available + if (authService != null) + Schema.AuthorizationService = new PolicyOrRoleBasedAuthorization(authService); + } + /// - /// If true the schema will be built via reflection on the context type. You can customise this with the properties inherited from SchemaBuilderOptions - /// If false the schema will be created with the TSchemaContext as it's context but will be empty of fields/types. - /// You can fully populate it in the ConfigureSchema callback + /// Options that control how SchemaBuilder reflects the object graph to auto-create schema types and fields. /// - public bool AutoBuildSchemaFromContext { get; set; } = true; + public SchemaBuilderOptions Builder { get; } = new(); /// - /// Overwrite the default field naming convention. (Default is lowerCaseFields) + /// Options for configuring the SchemaProvider instance (authorization, introspection, error handling, field naming). /// - public Func FieldNamer { get; set; } = SchemaBuilderSchemaOptions.DefaultFieldNamer; + public SchemaProviderOptions Schema { get; } = new(); /// - /// Called after the schema object is created but before the context is reflected into it. Use for set up of type mappings or - /// anything that may be needed for the schema to be built correctly. + /// If true (default) the schema will be built via SchemaBuilder on the context type. + /// You can customise this with the Builder property options. + /// If false the schema will be created with the TSchemaContext as its context but will be empty of fields/types. + /// You can fully populate it in the ConfigureSchema callback /// - public Action>? PreBuildSchemaFromContext { get; set; } + public bool AutoBuildSchemaFromContext { get; set; } = true; /// /// Called after the context has been reflected into a schema to allow further customisation. diff --git a/src/EntityGraphQL.AspNet/Extensions/DefaultGraphQLRequestDeserializer.cs b/src/EntityGraphQL.AspNet/Extensions/DefaultGraphQLRequestDeserializer.cs index d1bb5686..edace730 100644 --- a/src/EntityGraphQL.AspNet/Extensions/DefaultGraphQLRequestDeserializer.cs +++ b/src/EntityGraphQL.AspNet/Extensions/DefaultGraphQLRequestDeserializer.cs @@ -21,6 +21,7 @@ public DefaultGraphQLRequestDeserializer(JsonSerializerOptions? jsonOptions = nu { this.jsonOptions = new JsonSerializerOptions { IncludeFields = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; this.jsonOptions.Converters.Add(new JsonStringEnumConverter()); + this.jsonOptions.UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow; } } diff --git a/src/EntityGraphQL.AspNet/Extensions/EntityGraphQLAspNetServiceCollectionExtensions.cs b/src/EntityGraphQL.AspNet/Extensions/EntityGraphQLAspNetServiceCollectionExtensions.cs index 2d6566a6..9aaa8bb4 100644 --- a/src/EntityGraphQL.AspNet/Extensions/EntityGraphQLAspNetServiceCollectionExtensions.cs +++ b/src/EntityGraphQL.AspNet/Extensions/EntityGraphQLAspNetServiceCollectionExtensions.cs @@ -11,37 +11,30 @@ namespace EntityGraphQL.AspNet; public static class EntityGraphQLAspNetServiceCollectionExtensions { /// - /// Adds a SchemaProvider as a singleton to the service collection. + /// Adds a SchemaProvider<TSchemaContext> as a singleton to the service collection. /// /// /// Function to further configure your schema /// /// - public static IServiceCollection AddGraphQLSchema( - this IServiceCollection serviceCollection, - Action>? configure = null, - bool introspectionEnabled = true - ) + public static IServiceCollection AddGraphQLSchema(this IServiceCollection serviceCollection, Action>? configure = null) { - serviceCollection.AddGraphQLSchema( - options => - { - options.ConfigureSchema = configure; - }, - introspectionEnabled - ); + serviceCollection.AddGraphQLSchema(options => + { + options.ConfigureSchema = configure; + }); return serviceCollection; } /// - /// Adds a SchemaProvider as a singleton to the service collection. + /// Adds a SchemaProvider<TSchemaContext> as a singleton to the service collection. /// /// Context type to build the schema on /// /// Callback to configure the AddGraphQLOptions /// - public static IServiceCollection AddGraphQLSchema(this IServiceCollection serviceCollection, Action> configure, bool introspectionEnabled = true) + public static IServiceCollection AddGraphQLSchema(this IServiceCollection serviceCollection, Action> configure) { // We don't want the DI of JsonSerializerOptions as they may not be set up correctly for the dynamic types // They used IGraphQLRequestDeserializer/IGraphQLResponseSerializer to override the default JSON serialization @@ -52,18 +45,36 @@ public static IServiceCollection AddGraphQLSchema(this IServiceC var authService = serviceProvider.GetService(); var webHostEnvironment = serviceProvider.GetService(); - var options = new AddGraphQLOptions(); + var options = new AddGraphQLOptions(authService); configure(options); + // Apply environment-based defaults if not explicitly set + var schemaOptions = options.Schema; + + // If user hasn't explicitly set IsDevelopment, detect from environment + // We check if it's still the default value (true) and the environment is not Development + if (webHostEnvironment != null && !webHostEnvironment.IsEnvironment("Development")) + { + schemaOptions.IsDevelopment = false; + } + var schema = new SchemaProvider( - new PolicyOrRoleBasedAuthorization(authService), - options.FieldNamer, - introspectionEnabled: introspectionEnabled, - isDevelopment: webHostEnvironment?.IsEnvironment("Development") ?? true + schemaOptions.AuthorizationService, + schemaOptions.FieldNamer, + introspectionEnabled: schemaOptions.IntrospectionEnabled, + isDevelopment: schemaOptions.IsDevelopment ); - options.PreBuildSchemaFromContext?.Invoke(schema); + + // Apply allowed exceptions + foreach (var allowedException in schemaOptions.AllowedExceptions) + { + if (!schema.AllowedExceptions.Contains(allowedException)) + schema.AllowedExceptions.Add(allowedException); + } + + options.Builder.PreBuildSchemaFromContext?.Invoke(schema); if (options.AutoBuildSchemaFromContext) - schema.PopulateFromContext(options); + schema.PopulateFromContext(options.Builder); options.ConfigureSchema?.Invoke(schema); serviceCollection.AddSingleton(schema); @@ -71,13 +82,13 @@ public static IServiceCollection AddGraphQLSchema(this IServiceC } /// - /// Registers the default IGraphQLValidator implementation to use as a service in your method fields to report a colletion of errors + /// Registers the default IGraphQLValidator implementation to use as a service in your method fields to report a collection of errors /// /// /// public static IServiceCollection AddGraphQLValidator(this IServiceCollection serviceCollection) { - serviceCollection.TryAddScoped(); + serviceCollection.TryAddTransient(); return serviceCollection; } } diff --git a/src/EntityGraphQL.AspNet/Extensions/EntityGraphQLEndpointRouteExtensions.cs b/src/EntityGraphQL.AspNet/Extensions/EntityGraphQLEndpointRouteExtensions.cs index f0f694d5..9c1d749f 100644 --- a/src/EntityGraphQL.AspNet/Extensions/EntityGraphQLEndpointRouteExtensions.cs +++ b/src/EntityGraphQL.AspNet/Extensions/EntityGraphQLEndpointRouteExtensions.cs @@ -21,16 +21,13 @@ public static class EntityGraphQLEndpointRouteExtensions /// The path to create the route at. Defaults to `graphql` /// ExecutionOptions to use when executing queries /// Callback to continue modifying the endpoint via the IEndpointConventionBuilder interface - /// Defaults to false. If true it will return status code 200 for queries with - /// errors as per https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md /// /// public static IEndpointRouteBuilder MapGraphQL( this IEndpointRouteBuilder builder, string path = "graphql", ExecutionOptions? options = null, - Action? configureEndpoint = null, - bool followSpec = false + Action? configureEndpoint = null ) { path = path.TrimEnd('/'); @@ -40,24 +37,22 @@ public static IEndpointRouteBuilder MapGraphQL( { var acceptValues = context.Request.GetTypedHeaders().Accept; var sorted = acceptValues.OrderByDescending(h => h.Quality ?? 1.0).ToList(); - if (followSpec) + + // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md + // "May reply with error if not supplied" choosing not to + if ( + acceptValues.Count != 0 + && !sorted.Any(h => h.MediaType.StartsWith(APP_JSON_TYPE_START, StringComparison.InvariantCulture) == true) + && !sorted.Any(h => h.MediaType.StartsWith(APP_GQL_TYPE_START, StringComparison.InvariantCulture) == true) + && !sorted.Any(h => h.MediaType.StartsWith("*/*", StringComparison.InvariantCulture) == true) + && !sorted.Any(h => h.MediaType.StartsWith("application/*", StringComparison.InvariantCulture) == true) + ) { - // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md - // "May reply with error if not supplied" choosing not to - if ( - acceptValues.Count != 0 - && !sorted.Any(h => h.MediaType.StartsWith(APP_JSON_TYPE_START, StringComparison.InvariantCulture) == true) - && !sorted.Any(h => h.MediaType.StartsWith(APP_GQL_TYPE_START, StringComparison.InvariantCulture) == true) - && !sorted.Any(h => h.MediaType.StartsWith("*/*", StringComparison.InvariantCulture) == true) - && !sorted.Any(h => h.MediaType.StartsWith("application/*", StringComparison.InvariantCulture) == true) - ) - { - context.Response.StatusCode = StatusCodes.Status406NotAcceptable; - return; - } + context.Response.StatusCode = StatusCodes.Status406NotAcceptable; + return; } // checking for ContentType == null is technically a breaking change so only do it for the followSpec case until 6.0 - if ((followSpec && context.Request.ContentType == null) || context.Request.ContentType?.StartsWith(APP_JSON_TYPE_START, StringComparison.InvariantCulture) == false) + if (context.Request.ContentType == null || context.Request.ContentType?.StartsWith(APP_JSON_TYPE_START, StringComparison.InvariantCulture) == false) { context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; return; @@ -71,60 +66,46 @@ public static IEndpointRouteBuilder MapGraphQL( } var deserializer = context.RequestServices.GetRequiredService(); + QueryRequest query; try { - var query = await deserializer.DeserializeAsync(context.Request.Body); + query = await deserializer.DeserializeAsync(context.Request.Body); + } + catch (Exception) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + var schema = + context.RequestServices.GetService>() + ?? throw new InvalidOperationException( + "No SchemaProvider found in the service collection. Make sure you set up your Startup.ConfigureServices() to call AddGraphQLSchema()." + ); + var requestedType = sorted + .Where(t => t != null) + .FirstOrDefault(t => + t.MediaType.StartsWith(APP_JSON_TYPE_START, StringComparison.InvariantCulture) || t.MediaType.StartsWith(APP_GQL_TYPE_START, StringComparison.InvariantCulture) + ) + ?.MediaType.ToString(); - var schema = - context.RequestServices.GetService>() - ?? throw new InvalidOperationException( - "No SchemaProvider found in the service collection. Make sure you set up your Startup.ConfigureServices() to call AddGraphQLSchema()." - ); + try + { var gqlResult = await schema.ExecuteRequestAsync(query, context.RequestServices, context.User, options, context.RequestAborted); - if (followSpec) - { - var requestedType = sorted - .Where(t => t != null) - .FirstOrDefault(t => - t.MediaType.StartsWith(APP_JSON_TYPE_START, StringComparison.InvariantCulture) || t.MediaType.StartsWith(APP_GQL_TYPE_START, StringComparison.InvariantCulture) - ) - ?.MediaType.ToString(); - context.Response.ContentType = requestedType ?? $"{APP_GQL_TYPE_START}; charset=utf-8"; - } - else - { - context.Response.ContentType = $"{APP_JSON_TYPE_START}; charset=utf-8"; - } + context.Response.ContentType = requestedType ?? $"{APP_GQL_TYPE_START}; charset=utf-8"; + + // Per GraphQL over HTTP spec: GraphQL errors should return 200 with error details + context.Response.StatusCode = StatusCodes.Status200OK; - if (gqlResult.Errors?.Count > 0) - { - // TODO: change with 6.0. This is here as changing how errors are thrown would be a breaking change - // But following the spec this is not a valid request and should be a 400 - if (followSpec && gqlResult.Errors.Count == 1 && gqlResult.Errors[0].Message == "Please provide a persisted query hash or a query string") - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - return; - } - context.Response.StatusCode = followSpec ? StatusCodes.Status200OK : StatusCodes.Status400BadRequest; - } var serializer = context.RequestServices.GetRequiredService(); await serializer.SerializeAsync(context.Response.Body, gqlResult); } catch (Exception) { - // only exceptions we should get are ones that mean the request is invalid, e.g. deserialization errors - // all other graphql specific errors should be in the response data - if (followSpec) - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - return; - } - else - { - // keep the old behavior for v 5.x - throw; - } + // something went very wrong + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + return; } } ); diff --git a/src/EntityGraphQL.AspNet/PolicyAuthorizationExtensions.cs b/src/EntityGraphQL.AspNet/PolicyAuthorizationExtensions.cs new file mode 100644 index 00000000..15804ee9 --- /dev/null +++ b/src/EntityGraphQL.AspNet/PolicyAuthorizationExtensions.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using EntityGraphQL.Schema; + +namespace EntityGraphQL.AspNet; + +/// +/// Extension methods to add ASP.NET policy-based authorization to GraphQL fields and types +/// +public static class PolicyAuthorizationExtensions +{ + /// + /// To access this field all policies listed here are required + /// + /// The field to add policy requirements to + /// The policies required + /// The field for method chaining + public static IField RequiresAllPolicies(this IField field, params string[] policies) + { + field.RequiredAuthorization ??= new RequiredAuthorization(); + AddAllPolicies(field.RequiredAuthorization, policies); + return field; + } + + /// + /// To access this field any policy listed is required + /// + /// The field to add policy requirements to + /// The policies required (any one of them) + /// The field for method chaining + public static IField RequiresAnyPolicy(this IField field, params string[] policies) + { + field.RequiredAuthorization ??= new RequiredAuthorization(); + AddAnyPolicy(field.RequiredAuthorization, policies); + return field; + } + + /// + /// To access this type all policies listed here are required + /// + /// The type to add policy requirements to + /// The policies required + /// The type for method chaining + public static SchemaType RequiresAllPolicies(this SchemaType schemaType, params string[] policies) + { + schemaType.RequiredAuthorization ??= new RequiredAuthorization(); + AddAllPolicies(schemaType.RequiredAuthorization, policies); + return schemaType; + } + + /// + /// To access this type any of the policies listed is required + /// + /// The type to add policy requirements to + /// The policies required (any one of them) + /// The type for method chaining + public static SchemaType RequiresAnyPolicy(this SchemaType schemaType, params string[] policies) + { + schemaType.RequiredAuthorization ??= new RequiredAuthorization(); + AddAnyPolicy(schemaType.RequiredAuthorization, policies); + return schemaType; + } + + /// + /// Get the policies from a RequiredAuthorization object + /// + public static IEnumerable>? GetPolicies(this RequiredAuthorization requiredAuthorization) + { + if (requiredAuthorization.TryGetData(PolicyOrRoleBasedAuthorization.PoliciesKey, out var policies)) + { + return policies; + } + return null; + } + + private static void AddAnyPolicy(RequiredAuthorization auth, params string[] policies) + { + var policyList = GetOrCreatePolicyList(auth); + policyList.Add(policies.ToList()); + } + + private static void AddAllPolicies(RequiredAuthorization auth, params string[] policies) + { + var policyList = GetOrCreatePolicyList(auth); + policyList.AddRange(policies.Select(p => new List { p })); + } + + private static List> GetOrCreatePolicyList(RequiredAuthorization auth) + { + if (!auth.TryGetData(PolicyOrRoleBasedAuthorization.PoliciesKey, out var policyList) || policyList == null) + { + policyList = []; + auth.SetData(PolicyOrRoleBasedAuthorization.PoliciesKey, policyList); + } + return policyList; + } +} diff --git a/src/EntityGraphQL.AspNet/PolicyOrRoleBasedAuthorization.cs b/src/EntityGraphQL.AspNet/PolicyOrRoleBasedAuthorization.cs index 03c2e2ce..0d9c639d 100644 --- a/src/EntityGraphQL.AspNet/PolicyOrRoleBasedAuthorization.cs +++ b/src/EntityGraphQL.AspNet/PolicyOrRoleBasedAuthorization.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -13,6 +14,8 @@ namespace EntityGraphQL.AspNet; /// public class PolicyOrRoleBasedAuthorization : RoleBasedAuthorization { + public const string PoliciesKey = "egql:aspnet:policies"; + private readonly IAuthorizationService? authService; public PolicyOrRoleBasedAuthorization(IAuthorizationService? authService) @@ -31,11 +34,12 @@ public override bool IsAuthorized(ClaimsPrincipal? user, RequiredAuthorization? // if the list is empty it means identity.IsAuthenticated needs to be true, if full it requires certain authorization if (requiredAuthorization != null && requiredAuthorization.Any()) { - // check polices if principal with used - if (authService != null && user != null) + // check policies if we have any + var policies = requiredAuthorization.GetPolicies(); + if (policies != null && authService != null && user != null) { var allPoliciesValid = true; - foreach (var policy in requiredAuthorization.Policies) + foreach (var policy in policies) { // each policy now is an OR var hasValidPolicy = policy.Any(p => authService.AuthorizeAsync(user, p).GetAwaiter().GetResult().Succeeded); @@ -53,21 +57,46 @@ public override bool IsAuthorized(ClaimsPrincipal? user, RequiredAuthorization? return true; } - private static RequiredAuthorization GetRequiredAuth(RequiredAuthorization? requiredAuth, ICustomAttributeProvider thing) + private static RequiredAuthorization? GetRequiredAuth(RequiredAuthorization? requiredAuth, ICustomAttributeProvider thing) { var attributes = thing.GetCustomAttributes(typeof(AuthorizeAttribute), true).Cast(); var requiredRoles = attributes.Where(c => !string.IsNullOrEmpty(c.Roles)).Select(c => c.Roles!.Split(",").ToList()).ToList(); var requiredPolicies = attributes.Where(c => !string.IsNullOrEmpty(c.Policy)).Select(c => c.Policy!.Split(",").ToList()).ToList(); - var newAuth = new RequiredAuthorization(requiredRoles, requiredPolicies); - if (requiredAuth != null) - requiredAuth = requiredAuth.Concat(newAuth); - else - requiredAuth = newAuth; + + if (requiredRoles.Count > 0 || requiredPolicies.Count > 0) + { + var newAuth = new RequiredAuthorization(); + foreach (var roles in requiredRoles) + { + newAuth.RequiresAnyRole(roles.ToArray()); + } + if (requiredPolicies.Count > 0) + { + var policyList = new List>(requiredPolicies); + newAuth.SetData(PolicyOrRoleBasedAuthorization.PoliciesKey, policyList); + } + + if (requiredAuth != null) + requiredAuth = requiredAuth.Concat(newAuth); + else + requiredAuth = newAuth; + } var attributes2 = thing.GetCustomAttributes(typeof(GraphQLAuthorizePolicyAttribute), true).Cast(); + var morePolicies = attributes2.Where(c => c.Policies?.Count > 0).Select(c => c.Policies.ToList()).ToList(); + + if (morePolicies.Count > 0) + { + var policyAuth = new RequiredAuthorization(); + var policyList = new List>(morePolicies); + policyAuth.SetData(PolicyOrRoleBasedAuthorization.PoliciesKey, policyList); + + if (requiredAuth != null) + requiredAuth = requiredAuth.Concat(policyAuth); + else + requiredAuth = policyAuth; + } - requiredPolicies = attributes2.Where(c => c.Policies?.Count > 0).Select(c => c.Policies.ToList()).ToList(); - requiredAuth = requiredAuth.Concat(new RequiredAuthorization(null, requiredPolicies)); return requiredAuth; } @@ -82,19 +111,19 @@ private static RequiredAuthorization GetRequiredAuth(RequiredAuthorization? requ return requiredAuth; } - public override RequiredAuthorization GetRequiredAuthFromMember(MemberInfo field) + public override RequiredAuthorization? GetRequiredAuthFromMember(MemberInfo field) { var requiredAuth = base.GetRequiredAuthFromMember(field); - requiredAuth = GetRequiredAuth(requiredAuth, field); + var authFromAttributes = GetRequiredAuth(requiredAuth, field); - return requiredAuth; + return authFromAttributes ?? requiredAuth; } - public override RequiredAuthorization GetRequiredAuthFromType(Type type) + public override RequiredAuthorization? GetRequiredAuthFromType(Type type) { var requiredAuth = base.GetRequiredAuthFromType(type); - requiredAuth = GetRequiredAuth(requiredAuth, type); + var authFromAttributes = GetRequiredAuth(requiredAuth, type); - return requiredAuth; + return authFromAttributes ?? requiredAuth; } } diff --git a/src/EntityGraphQL/Compiler/EntityGraphQLCompilerException.cs b/src/EntityGraphQL/Compiler/EntityGraphQLCompilerException.cs deleted file mode 100644 index f26079da..00000000 --- a/src/EntityGraphQL/Compiler/EntityGraphQLCompilerException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace EntityGraphQL.Compiler; - -public class EntityGraphQLCompilerException : Exception -{ - public EntityGraphQLCompilerException(string message, Exception? innerException = null) - : base(message, innerException) { } -} - -public class EntityGraphQLExecutionException : Exception -{ - public EntityGraphQLExecutionException(string message) - : base(message) { } -} - -public class EntityGraphQLAccessException : Exception -{ - public EntityGraphQLAccessException(string message) - : base(message) { } -} diff --git a/src/EntityGraphQL/Compiler/EntityGraphQLQueryWalker.cs b/src/EntityGraphQL/Compiler/EntityGraphQLQueryWalker.cs deleted file mode 100644 index 23c08494..00000000 --- a/src/EntityGraphQL/Compiler/EntityGraphQLQueryWalker.cs +++ /dev/null @@ -1,600 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using EntityGraphQL.Compiler.Util; -using EntityGraphQL.Directives; -using EntityGraphQL.Extensions; -using EntityGraphQL.Schema; -using HotChocolate.Language; - -namespace EntityGraphQL.Compiler; - -/// -/// Visits nodes of a GraphQL request to build a representation of the query against the context objects via LINQ methods. -/// -internal sealed class EntityGraphQLQueryWalker : QuerySyntaxWalker -{ - private readonly ISchemaProvider schemaProvider; - private readonly QueryVariables variables; - private ExecutableGraphQLStatement? currentOperation; - - /// - /// The root - the query document. This is what we "return" - /// - /// - public GraphQLDocument? Document { get; private set; } - - public EntityGraphQLQueryWalker(ISchemaProvider schemaProvider, QueryVariables? variables) - { - this.schemaProvider = schemaProvider; - variables ??= []; - this.variables = variables; - } - - /// - /// This is out TOP level GQL document - /// - /// - /// - protected override void VisitDocument(DocumentNode node, IGraphQLNode? context) - { - if (context != null) - throw new ArgumentException("context should be null", nameof(context)); - - Document = new GraphQLDocument(schemaProvider); - base.VisitDocument(node, context); - - // Validate fragment spreads must not form cycles - ValidateFragmentCycles(); - } - - protected override void VisitOperationDefinition(OperationDefinitionNode node, IGraphQLNode? context) - { - if (Document == null) - throw new EntityGraphQLCompilerException("Document should not be null visiting operation definition"); - - // these are the variables that can change each request for the same query - var operationVariables = ProcessVariableDefinitions(node); - - if (node.Operation == OperationType.Query) - { - var rootParameterContext = Expression.Parameter(schemaProvider.QueryContextType, $"query_ctx"); - context = new GraphQLQueryStatement(schemaProvider, node.Name?.Value, rootParameterContext, rootParameterContext, operationVariables); - if (node.Directives?.Any() == true) - context.AddDirectives(ProcessFieldDirectives(ExecutableDirectiveLocation.QUERY, node.Directives)); - currentOperation = (GraphQLQueryStatement)context; - } - else if (node.Operation == OperationType.Mutation) - { - // we never build expression from this parameter but the type is used to look up the ISchemaType - var rootParameterContext = Expression.Parameter(schemaProvider.MutationType, $"mut_ctx"); - context = new GraphQLMutationStatement(schemaProvider, node.Name?.Value, rootParameterContext, rootParameterContext, operationVariables); - if (node.Directives?.Any() == true) - context.AddDirectives(ProcessFieldDirectives(ExecutableDirectiveLocation.MUTATION, node.Directives)); - currentOperation = (GraphQLMutationStatement)context; - } - else if (node.Operation == OperationType.Subscription) - { - // we never build expression from this parameter but the type is used to look up the ISchemaType - var rootParameterContext = Expression.Parameter(schemaProvider.SubscriptionType, $"sub_ctx"); - context = new GraphQLSubscriptionStatement(schemaProvider, node.Name?.Value, rootParameterContext, operationVariables); - if (node.Directives?.Any() == true) - context.AddDirectives(ProcessFieldDirectives(ExecutableDirectiveLocation.SUBSCRIPTION, node.Directives)); - currentOperation = (GraphQLSubscriptionStatement)context; - } - - if (context != null) - { - Document.Operations.Add((ExecutableGraphQLStatement)context); - base.VisitOperationDefinition(node, context); - } - } - - private Dictionary ProcessVariableDefinitions(OperationDefinitionNode node) - { - if (Document == null) - throw new EntityGraphQLCompilerException("Document should not be null visiting operation definition"); - - var documentVariables = new Dictionary(); - - foreach (var item in node.VariableDefinitions) - { - var argName = item.Variable.Name.Value; - object? defaultValue = null; - (var gqlTypeName, var isList, var isRequired) = GetGqlType(item.Type); - - var schemaType = schemaProvider.GetSchemaType(gqlTypeName, null); - var varTypeInSchema = schemaType.TypeDotnet ?? throw new EntityGraphQLCompilerException($"Variable {argName} has no type"); - if (!isRequired && (varTypeInSchema.IsValueType || varTypeInSchema.IsEnum)) - varTypeInSchema = typeof(Nullable<>).MakeGenericType(varTypeInSchema); - - if (isList) - varTypeInSchema = typeof(List<>).MakeGenericType(varTypeInSchema); - - if (item.DefaultValue != null) - defaultValue = Expression.Lambda(Expression.Constant(QueryWalkerHelper.ProcessArgumentValue(schemaProvider, item.DefaultValue, argName, varTypeInSchema))).Compile().DynamicInvoke(); - - documentVariables.Add( - argName, - new ArgType( - gqlTypeName, - schemaType.TypeDotnet.Name, - new GqlTypeInfo(() => schemaType, varTypeInSchema) { TypeNotNullable = isRequired, ElementTypeNullable = !isRequired }, - null, - varTypeInSchema - ) - { - DefaultValue = new DefaultArgValue(item.DefaultValue != null, defaultValue), - IsRequired = isRequired, - } - ); - - if (item.Directives?.Any() == true) - { - var directives = ProcessFieldDirectives(ExecutableDirectiveLocation.VARIABLE_DEFINITION, item.Directives); - foreach (var directive in directives) - { - directive.VisitNode(ExecutableDirectiveLocation.VARIABLE_DEFINITION, schemaProvider, null, new Dictionary(), null, null); - } - } - - if (item.Type.Kind == SyntaxKind.NonNullType && !variables.ContainsKey(argName)) - { - throw new EntityGraphQLCompilerException($"Missing required variable '{argName}' on operation '{node.Name?.Value}'"); - } - } - return documentVariables; - } - - private static (string typeName, bool isList, bool isRequired) GetGqlType(ITypeNode item) - { - switch (item.Kind) - { - case SyntaxKind.NamedType: - return (((NamedTypeNode)item).Name.Value, false, false); - case SyntaxKind.NonNullType: - { - var (_, isList, _) = GetGqlType(((NonNullTypeNode)item).Type); - return (((NonNullTypeNode)item).NamedType().Name.Value, isList, true); - } - case SyntaxKind.ListType: - return (((ListTypeNode)item).Type.NamedType().Name.Value, true, false); - default: - throw new EntityGraphQLCompilerException($"Unexpected node kind {item.Kind}"); - } - ; - } - - protected override void VisitField(FieldNode node, IGraphQLNode? context) - { - if (context == null) - throw new EntityGraphQLCompilerException("context should not be null visiting field"); - if (context.NextFieldContext == null) - throw new EntityGraphQLCompilerException("context.NextFieldContext should not be null visiting field"); - - var schemaType = context.Field?.ReturnType.SchemaType ?? schemaProvider.GetSchemaType(context.NextFieldContext.Type, context.Field?.FromType.GqlType == GqlTypes.InputObject, null); - var actualField = schemaType.GetField(node.Name.Value, null); - - var args = node.Arguments != null ? ProcessArguments(actualField, node.Arguments) : null; - var resultName = node.Alias?.Value ?? actualField.Name; - - if (actualField.FieldType == GraphQLQueryFieldType.Mutation) - { - var mutationField = (MutationField)actualField; - - var nextContextParam = Expression.Parameter(mutationField.ReturnType.TypeDotnet, $"mut_{actualField.Name}"); - var graphqlMutationField = new GraphQLMutationField(schemaProvider, resultName, mutationField, args, nextContextParam, nextContextParam, context); - - if (node.SelectionSet != null) - { - var select = ParseFieldSelect(nextContextParam, actualField, resultName, graphqlMutationField, node.SelectionSet, args); - if (mutationField.ReturnType.IsList) - { - // nulls are not known until mutation is executed. Will be handled in GraphQLMutationStatement - var newSelect = new GraphQLListSelectionField( - schemaProvider, - actualField, - resultName, - (ParameterExpression)select.NextFieldContext!, - select.RootParameter, - select.RootParameter!, - context, - args - ); - foreach (var queryField in select.QueryFields) - { - newSelect.AddField(queryField); - } - select = newSelect; - } - graphqlMutationField.ResultSelection = select; - } - if (node.Directives?.Any() == true) - { - graphqlMutationField.AddDirectives(ProcessFieldDirectives(ExecutableDirectiveLocation.FIELD, node.Directives)); - } - - context.AddField(graphqlMutationField); - } - else if (actualField.FieldType == GraphQLQueryFieldType.Subscription) - { - var subscriptionField = (SubscriptionField)actualField; - - var nextContextParam = Expression.Parameter(subscriptionField.ReturnType.TypeDotnet, $"sub_{actualField.Name}"); - var graphqlSubscriptionField = new GraphQLSubscriptionField(schemaProvider, resultName, subscriptionField, args, nextContextParam, nextContextParam, context); - - if (node.SelectionSet != null) - { - var select = ParseFieldSelect(nextContextParam, actualField, resultName, graphqlSubscriptionField, node.SelectionSet, args); - if (subscriptionField.ReturnType.IsList) - { - // nulls are not known until subscription is executed. Will be handled in GraphQLSubscriptionStatement - var newSelect = new GraphQLListSelectionField( - schemaProvider, - actualField, - resultName, - (ParameterExpression)select.NextFieldContext!, - select.RootParameter, - select.RootParameter!, - context, - args - ); - foreach (var queryField in select.QueryFields) - { - newSelect.AddField(queryField); - } - select = newSelect; - } - graphqlSubscriptionField.ResultSelection = select; - } - if (node.Directives?.Any() == true) - { - graphqlSubscriptionField.AddDirectives(ProcessFieldDirectives(ExecutableDirectiveLocation.FIELD, node.Directives)); - } - - context.AddField(graphqlSubscriptionField); - } - else - { - BaseGraphQLField? fieldResult; - - if (node.SelectionSet != null) - { - fieldResult = ParseFieldSelect(actualField.ResolveExpression!, actualField, resultName, context, node.SelectionSet, args); - } - else if (actualField.ReturnType.SchemaType.RequiresSelection) - { - // wild card query - select out all the fields for the object - throw new EntityGraphQLCompilerException($"Field '{actualField.Name}' requires a selection set defining the fields you would like to select."); - } - else - { - var rootParam = context.NextFieldContext?.NodeType == ExpressionType.Parameter ? actualField.FieldParam : context.RootParameter; - fieldResult = new GraphQLScalarField(schemaProvider, actualField, resultName, actualField.ResolveExpression!, rootParam, context, args); - } - - if (node.Directives?.Any() == true) - { - fieldResult.AddDirectives(ProcessFieldDirectives(ExecutableDirectiveLocation.FIELD, node.Directives)); - } - if (fieldResult != null) - { - context.AddField(fieldResult); - } - } - } - - public BaseGraphQLQueryField ParseFieldSelect(Expression fieldExp, IField fieldContext, string name, IGraphQLNode context, SelectionSetNode selection, Dictionary? arguments) - { - if (fieldContext.ReturnType.IsList) - { - return BuildDynamicSelectOnCollection(fieldContext, fieldExp, fieldContext.ReturnType.SchemaType, name, context, selection, arguments); - } - - var graphQLNode = BuildDynamicSelectForObjectGraph(fieldContext, fieldExp, context, name, selection, arguments); - // Could be a list.First().Blah that we need to turn into a select, or - // other levels are object selection. e.g. from the top level people query I am selecting all their children { field1, etc. } - // Can we turn a list.First().Blah into and list.Select(i => new {i.Blah}).First() - var listExp = ExpressionUtil.FindEnumerable(fieldExp); - if (listExp.Item1 != null && listExp.Item2 != null) - { - // yes we can - // rebuild the Expression so we keep any ConstantParameters - var returnType = schemaProvider.GetSchemaType(listExp.Item1.Type.GetEnumerableOrArrayType()!, context.Field?.FromType.GqlType == GqlTypes.InputObject, null); - // TODO this doubles the field visit - var collectionNode = BuildDynamicSelectOnCollection(fieldContext, listExp.Item1, returnType, name, context, selection, arguments); - return new GraphQLCollectionToSingleField(schemaProvider, collectionNode, graphQLNode, listExp.Item2!); - } - return graphQLNode; - } - - /// - /// Given a syntax of someCollection { fields, to, selection, from, object } - /// it will build a select assuming 'someCollection' is an IEnumerable - /// - private GraphQLListSelectionField BuildDynamicSelectOnCollection( - IField actualField, - Expression nodeExpression, - ISchemaType returnType, - string resultName, - IGraphQLNode context, - SelectionSetNode selection, - Dictionary? arguments - ) - { - if (context == null) - throw new EntityGraphQLCompilerException("context should not be null building select on collection"); - - var elementType = returnType.TypeDotnet; - var fieldParam = Expression.Parameter(elementType, $"p_{elementType.Name}"); - - var gqlNode = new GraphQLListSelectionField(schemaProvider, actualField, resultName, fieldParam, actualField.FieldParam ?? context.RootParameter, nodeExpression, context, arguments); - - // visit child fields. Will be more fields - base.VisitSelectionSet(selection, gqlNode); - return gqlNode; - } - - /// - /// Given a syntax of { fields, to, selection, from, object } with a context - /// it will build the correct select statement - /// - /// - /// - /// - private GraphQLObjectProjectionField BuildDynamicSelectForObjectGraph( - IField actualField, - Expression nodeExpression, - IGraphQLNode context, - string name, - SelectionSetNode selection, - Dictionary? arguments - ) - { - if (context == null) - throw new EntityGraphQLCompilerException("context should not be null visiting field"); - if (context.NextFieldContext == null && context.RootParameter == null) - throw new EntityGraphQLCompilerException("context.NextFieldContext and context.RootParameter should not be null visiting field"); - - var rootParam = context.NextFieldContext?.NodeType == ExpressionType.Parameter ? actualField.FieldParam : context.RootParameter!; - var graphQLNode = new GraphQLObjectProjectionField(schemaProvider, actualField, name, nodeExpression, rootParam ?? context.RootParameter!, context, arguments); - - base.VisitSelectionSet(selection, graphQLNode); - - return graphQLNode; - } - - private Dictionary ProcessArguments(IField field, IEnumerable queryArguments) - { - var args = new Dictionary(); - foreach (var arg in queryArguments) - { - var argName = arg.Name.Value; - if (!field.Arguments.ContainsKey(argName)) - { - throw new EntityGraphQLCompilerException($"No argument '{argName}' found on field '{field.Name}'"); - } - args.Add(argName, ParseArgument(argName, field, arg)); - } - return args; - } - - private object? ParseArgument(string argName, IField fieldArgumentContext, ArgumentNode argument) - { - if (Document == null) - throw new EntityGraphQLCompilerException("Document should not be null when visiting arguments"); - - var argType = fieldArgumentContext.GetArgumentType(argName); - var argVal = ProcessArgumentOrVariable(argName, schemaProvider, argument, argType.Type.TypeDotnet); - - return argVal; - } - - /// - /// Build the expression for the argument. A Variable ($name) will be a Expression.Parameter - /// A inline value will be a Expression.Constant - /// - private object? ProcessArgumentOrVariable(string argName, ISchemaProvider schema, ArgumentNode argument, Type argType) - { - if (currentOperation == null) - throw new EntityGraphQLCompilerException("currentOperation should not be null when visiting arguments"); - - if (argument.Value.Kind == SyntaxKind.Variable) - { - return Expression.PropertyOrField(currentOperation.OpVariableParameter!, ((VariableNode)argument.Value).Name.Value); - } - return QueryWalkerHelper.ProcessArgumentValue(schema, argument.Value, argName, argType); - } - - private List ProcessFieldDirectives(ExecutableDirectiveLocation location, IEnumerable directives) - { - var result = new List(directives.Count()); - foreach (var directive in directives) - { - var processor = schemaProvider.GetDirective(directive.Name.Value); - if (!processor.Location.Contains(location)) - throw new EntityGraphQLCompilerException($"Directive '{directive.Name.Value}' can not be used on '{location}'"); - var argTypes = processor.GetArguments(schemaProvider); - var args = new Dictionary(); - foreach (var arg in directive.Arguments) - { - var argVal = ProcessArgumentOrVariable(arg.Name.Value, schemaProvider, arg, argTypes[arg.Name.Value].RawType); - if (argVal != null) - args.Add(arg.Name.Value, argVal); - } - result.Add(new GraphQLDirective(directive.Name.Value, processor, args)); - } - return result; - } - - protected override void VisitFragmentDefinition(FragmentDefinitionNode node, IGraphQLNode? context) - { - if (Document == null) - throw new EntityGraphQLCompilerException("Document can not be null in VisitFragmentDefinition"); - // top level statement in GQL doc. Defines the fragment fields. - // Add to the fragments and return null - var typeName = node.TypeCondition.Name.Value; - - var fragParameter = Expression.Parameter(schemaProvider.Type(typeName).TypeDotnet, $"frag_{typeName}"); - var fragDef = new GraphQLFragmentStatement(schemaProvider, node.Name.Value, fragParameter, fragParameter); - if (node.Directives?.Any() == true) - { - foreach (var directive in ProcessFieldDirectives(ExecutableDirectiveLocation.FRAGMENT_DEFINITION, node.Directives)) - { - directive.VisitNode(ExecutableDirectiveLocation.FRAGMENT_DEFINITION, schemaProvider, fragDef, new Dictionary(), null, null); - } - } - - Document.Fragments.Add(fragDef.Name, fragDef); - - base.VisitFragmentDefinition(node, fragDef); - } - - protected override void VisitInlineFragment(InlineFragmentNode node, IGraphQLNode? context) - { - if (context == null) - throw new EntityGraphQLCompilerException("context should not be null visiting inline fragment"); - - if (node.TypeCondition is not null && context is not null) - { - var type = schemaProvider.GetSchemaType(node.TypeCondition.Name.Value, null); - if (type != null) - { - var fragParameter = Expression.Parameter(type.TypeDotnet, $"frag_{type.Name}"); - var newContext = new GraphQLInlineFragmentField(schemaProvider, type.Name, fragParameter, fragParameter, context); - - if (node.Directives?.Any() == true) - { - newContext.AddDirectives(ProcessFieldDirectives(ExecutableDirectiveLocation.INLINE_FRAGMENT, node.Directives)); - } - - base.VisitInlineFragment(node, newContext); - - context.AddField(newContext); - } - else - { - base.VisitInlineFragment(node, context); - } - } - } - - protected override void VisitFragmentSpread(FragmentSpreadNode node, IGraphQLNode? context) - { - if (context == null) - throw new EntityGraphQLCompilerException("Context is null in FragmentSpread"); - if (context.RootParameter == null) - throw new EntityGraphQLCompilerException("Fragment spread can only be used inside a selection set (context.RootParameter is null)"); - // later when executing we turn this field into the defined fragment (as the fragment may be defined after use) - // Just store the name to look up when needed - BaseGraphQLField? fragField = new GraphQLFragmentSpreadField(schemaProvider, node.Name.Value, null, context.RootParameter, context); - if (node.Directives?.Any() == true) - { - fragField.AddDirectives(ProcessFieldDirectives(ExecutableDirectiveLocation.FRAGMENT_SPREAD, node.Directives)); - } - if (fragField != null) - { - base.VisitFragmentSpread(node, fragField); - context.AddField(fragField); - } - } - - /// - /// Validates that fragment spreads do not form cycles according to GraphQL spec - /// - private void ValidateFragmentCycles() - { - if (Document?.Fragments == null || Document.Fragments.Count == 0) - return; - - // Build dependency graph with optimized allocation - var fragmentDependencies = new Dictionary>(Document.Fragments.Count); - foreach (var fragment in Document.Fragments.Values) - { - var dependencies = new HashSet(); - CollectFragmentDependencies(fragment, dependencies); - if (dependencies.Count > 0) // Only store if there are dependencies - { - fragmentDependencies[fragment.Name] = dependencies; - } - } - - // Early exit if no dependencies found - if (fragmentDependencies.Count == 0) - return; - - // Single-pass DFS cycle detection with reused data structures - var visited = new HashSet(fragmentDependencies.Count); - var recursionStack = new HashSet(); - - foreach (var fragmentName in fragmentDependencies.Keys) - { - if (!visited.Contains(fragmentName)) - { - if (HasCycleOptimized(fragmentName, fragmentDependencies, visited, recursionStack)) - { - throw new EntityGraphQLCompilerException($"Fragment spreads must not form cycles. Fragment '{fragmentName}' creates a cycle."); - } - } - } - } - - /// - /// Collects all fragment spread dependencies for a given fragment - /// - private static void CollectFragmentDependencies(GraphQLFragmentStatement fragment, HashSet dependencies) - { - foreach (var field in fragment.QueryFields) - { - CollectFragmentDependenciesFromField(field, dependencies); - } - } - - /// - /// Recursively collects fragment dependencies from fields - /// - private static void CollectFragmentDependenciesFromField(BaseGraphQLField field, HashSet dependencies) - { - if (field is GraphQLFragmentSpreadField fragmentSpread) - { - dependencies.Add(fragmentSpread.Name); - } - - // Recursively check nested fields - foreach (var subField in field.QueryFields) - { - CollectFragmentDependenciesFromField(subField, dependencies); - } - } - - /// - /// Uses optimized DFS to detect cycles in the fragment dependency graph - /// Reuses visited and recursion stack across multiple fragment traversals - /// - private static bool HasCycleOptimized(string fragmentName, Dictionary> dependencies, HashSet visited, HashSet recursionStack) - { - visited.Add(fragmentName); - recursionStack.Add(fragmentName); - - if (dependencies.TryGetValue(fragmentName, out var fragmentDeps)) - { - foreach (var dependency in fragmentDeps) - { - if (recursionStack.Contains(dependency)) - { - return true; // Back edge found - cycle detected (check this first for early exit) - } - - if (!visited.Contains(dependency)) - { - if (HasCycleOptimized(dependency, dependencies, visited, recursionStack)) - return true; - } - } - } - - recursionStack.Remove(fragmentName); - return false; - } -} diff --git a/src/EntityGraphQL/Compiler/EntityGraphQLValidationException.cs b/src/EntityGraphQL/Compiler/EntityGraphQLValidationException.cs deleted file mode 100644 index 191b9ddb..00000000 --- a/src/EntityGraphQL/Compiler/EntityGraphQLValidationException.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace EntityGraphQL.Compiler; - -public class EntityGraphQLValidationException : Exception -{ - public List ValidationErrors { get; } - - public EntityGraphQLValidationException(IEnumerable validationErrors) - { - ValidationErrors = validationErrors.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); - } - - public EntityGraphQLValidationException(string validationError) - { - ValidationErrors = [validationError]; - } -} diff --git a/src/EntityGraphQL/Compiler/EntityQuery/DefaultMethodImplementations.cs b/src/EntityGraphQL/Compiler/EntityQuery/DefaultMethodImplementations.cs index ac9c7b27..f35ceb2d 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/DefaultMethodImplementations.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/DefaultMethodImplementations.cs @@ -26,14 +26,14 @@ internal static Expression MakeStringStartsWithMethod(Expression context, Expres { ExpectArgsCount(1, args, methodName); var predicate = ConvertTypeIfWeCan(methodName, args[0], typeof(string)); - return Expression.Call(context, typeof(string).GetMethod(nameof(string.StartsWith), new[] { typeof(string) })!, predicate); + return Expression.Call(context, typeof(string).GetMethod(nameof(string.StartsWith), [typeof(string)])!, predicate); } internal static Expression MakeStringEndsWithMethod(Expression context, Expression argContext, string methodName, Expression[] args) { ExpectArgsCount(1, args, methodName); var predicate = ConvertTypeIfWeCan(methodName, args[0], typeof(string)); - return Expression.Call(context, typeof(string).GetMethod(nameof(string.EndsWith), new[] { typeof(string) })!, predicate); + return Expression.Call(context, typeof(string).GetMethod(nameof(string.EndsWith), [typeof(string)])!, predicate); } internal static Expression MakeStringToLowerMethod(Expression context, Expression argContext, string methodName, Expression[] args) @@ -57,7 +57,7 @@ internal static Expression MakeWhereMethod(Expression context, Expression argCon ExpectArgsCount(1, args, methodName); var predicate = ConvertTypeIfWeCan(methodName, args[0], typeof(bool)); var lambda = Expression.Lambda(predicate, (ParameterExpression)argContext); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Where), new[] { argContext.Type }, context, lambda); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Where), [argContext.Type], context, lambda); } internal static Expression MakeAnyMethod(Expression context, Expression argContext, string methodName, Expression[] args) @@ -65,7 +65,7 @@ internal static Expression MakeAnyMethod(Expression context, Expression argConte ExpectArgsCount(1, args, methodName); var predicate = ConvertTypeIfWeCan(methodName, args[0], typeof(bool)); var lambda = Expression.Lambda(predicate, (ParameterExpression)argContext); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Any), new[] { argContext.Type }, context, lambda); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Any), [argContext.Type], context, lambda); } internal static Expression MakeFirstMethod(Expression context, Expression argContext, string methodName, Expression[] args) => @@ -94,21 +94,21 @@ private static Expression MakeOptionalFilterArgumentCall(Expression context, Exp allArgs.Add(Expression.Lambda(predicate, (ParameterExpression)argContext)); } - return ExpressionUtil.MakeCallOnQueryable(actualMethodName, new[] { argContext.Type }, allArgs.ToArray()); + return ExpressionUtil.MakeCallOnQueryable(actualMethodName, [argContext.Type], allArgs.ToArray()); } internal static Expression MakeTakeMethod(Expression context, Expression argContext, string methodName, Expression[] args) { ExpectArgsCount(1, args, methodName); var amount = ConvertTypeIfWeCan(methodName, args[0], typeof(int)); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Take), new[] { argContext.Type }, context, amount); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Take), [argContext.Type], context, amount); } internal static Expression MakeSkipMethod(Expression context, Expression argContext, string methodName, Expression[] args) { ExpectArgsCount(1, args, methodName); var amount = ConvertTypeIfWeCan(methodName, args[0], typeof(int)); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Skip), new[] { argContext.Type }, context, amount); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Skip), [argContext.Type], context, amount); } internal static Expression MakeOrderByMethod(Expression context, Expression argContext, string methodName, Expression[] args) @@ -116,7 +116,7 @@ internal static Expression MakeOrderByMethod(Expression context, Expression argC ExpectArgsCount(1, args, methodName); var column = args[0]; var lambda = Expression.Lambda(column, (ParameterExpression)argContext); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.OrderBy), new[] { argContext.Type, column.Type }, context, lambda); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.OrderBy), [argContext.Type, column.Type], context, lambda); } internal static Expression MakeOrderByDescMethod(Expression context, Expression argContext, string methodName, Expression[] args) @@ -124,7 +124,22 @@ internal static Expression MakeOrderByDescMethod(Expression context, Expression ExpectArgsCount(1, args, methodName); var column = args[0]; var lambda = Expression.Lambda(column, (ParameterExpression)argContext); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.OrderByDescending), new[] { argContext.Type, column.Type }, context, lambda); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.OrderByDescending), [argContext.Type, column.Type], context, lambda); + } + + internal static Expression MakeSelectManyMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + ExpectArgsCount(1, args, methodName); + var selector = args[0]; + var lambda = Expression.Lambda(selector, (ParameterExpression)argContext); + + // Get the result type of the selector - should be an enumerable + var selectorResultType = selector.Type; + var elementType = + selectorResultType.GetEnumerableOrArrayType() + ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"selectMany selector must return an enumerable type, but got '{selectorResultType}'"); + + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.SelectMany), [argContext.Type, elementType], context, lambda); } #endregion @@ -135,20 +150,20 @@ internal static Expression MakeIsAnyMethod(Expression context, Expression argCon { ExpectArgsCount(1, args, methodName); var array = args[0]; - var arrayType = array.Type.GetEnumerableOrArrayType() ?? throw new EntityGraphQLCompilerException("Could not get element type from enumerable/array"); - var isQueryable = typeof(IQueryable).IsAssignableFrom(array.Type); + var arrayType = array.Type.GetEnumerableOrArrayType() ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "Could not get element type from enumerable/array"); + var isQueryable = array.Type.IsGenericTypeQueryable(); if (context.Type.IsNullableType()) { var call = isQueryable - ? ExpressionUtil.MakeCallOnQueryable(nameof(Enumerable.Contains), new[] { arrayType }, array, Expression.Convert(context, arrayType)) - : ExpressionUtil.MakeCallOnEnumerable(nameof(Enumerable.Contains), new[] { arrayType }, array, Expression.Convert(context, arrayType)); + ? ExpressionUtil.MakeCallOnQueryable(nameof(Enumerable.Contains), [arrayType], array, Expression.Convert(context, arrayType)) + : ExpressionUtil.MakeCallOnEnumerable(nameof(Enumerable.Contains), [arrayType], array, Expression.Convert(context, arrayType)); return Expression.Condition(Expression.Equal(context, Expression.Constant(null, context.Type)), Expression.Constant(false), call); } return isQueryable - ? ExpressionUtil.MakeCallOnQueryable(nameof(Enumerable.Contains), new[] { arrayType }, array, Expression.Convert(context, arrayType)) - : ExpressionUtil.MakeCallOnEnumerable(nameof(Enumerable.Contains), new[] { arrayType }, array, Expression.Convert(context, arrayType)); + ? ExpressionUtil.MakeCallOnQueryable(nameof(Enumerable.Contains), [arrayType], array, Expression.Convert(context, arrayType)) + : ExpressionUtil.MakeCallOnEnumerable(nameof(Enumerable.Contains), [arrayType], array, Expression.Convert(context, arrayType)); } internal static Expression MakeAllMethod(Expression context, Expression argContext, string methodName, Expression[] args) @@ -156,7 +171,7 @@ internal static Expression MakeAllMethod(Expression context, Expression argConte ExpectArgsCount(1, args, methodName); var condition = args[0]; var lambda = Expression.Lambda(condition, (ParameterExpression)argContext); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.All), new[] { argContext.Type }, context, lambda); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.All), [argContext.Type], context, lambda); } internal static Expression MakeSumMethod(Expression context, Expression argContext, string methodName, Expression[] args) @@ -164,18 +179,18 @@ internal static Expression MakeSumMethod(Expression context, Expression argConte if (args.Length == 0) { // Sum() - direct sum of numeric elements - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Sum), new[] { argContext.Type }, context); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Sum), [argContext.Type], context); } else if (args.Length == 1) { // Sum(selector) - sum with projection var selector = args[0]; var lambda = Expression.Lambda(selector, (ParameterExpression)argContext); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Sum), new[] { argContext.Type }, context, lambda); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Sum), [argContext.Type], context, lambda); } else { - throw new EntityGraphQLCompilerException($"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); } } @@ -183,17 +198,17 @@ internal static Expression MakeMinMethod(Expression context, Expression argConte { if (args.Length == 0) { - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Min), new[] { argContext.Type }, context); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Min), [argContext.Type], context); } else if (args.Length == 1) { var selector = args[0]; var lambda = Expression.Lambda(selector, (ParameterExpression)argContext); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Min), new[] { argContext.Type }, context, lambda); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Min), [argContext.Type], context, lambda); } else { - throw new EntityGraphQLCompilerException($"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); } } @@ -201,17 +216,17 @@ internal static Expression MakeMaxMethod(Expression context, Expression argConte { if (args.Length == 0) { - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Max), new[] { argContext.Type }, context); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Max), [argContext.Type], context); } else if (args.Length == 1) { var selector = args[0]; var lambda = Expression.Lambda(selector, (ParameterExpression)argContext); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Max), new[] { argContext.Type }, context, lambda); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Max), [argContext.Type], context, lambda); } else { - throw new EntityGraphQLCompilerException($"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); } } @@ -219,17 +234,17 @@ internal static Expression MakeAverageMethod(Expression context, Expression argC { if (args.Length == 0) { - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Average), new[] { argContext.Type }, context); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Average), [argContext.Type], context); } else if (args.Length == 1) { var selector = args[0]; var lambda = Expression.Lambda(selector, (ParameterExpression)argContext); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Average), new[] { argContext.Type }, context, lambda); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Average), [argContext.Type], context, lambda); } else { - throw new EntityGraphQLCompilerException($"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); } } @@ -237,7 +252,7 @@ internal static Expression GetContextFromEnumerable(Expression context) { if (context.Type.IsEnumerableOrArray()) { - Type type = context.Type.GetEnumerableOrArrayType() ?? throw new EntityGraphQLCompilerException("Could not get element type from enumerable/array"); + Type type = context.Type.GetEnumerableOrArrayType() ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "Could not get element type from enumerable/array"); return Expression.Parameter(type, $"p_{type.Name}"); } var t = context.Type.GetEnumerableOrArrayType(); @@ -251,13 +266,13 @@ internal static Expression GetContextFromEnumerable(Expression context) private static void ExpectArgsCount(int count, Expression[] args, string method) { if (args.Length != count) - throw new EntityGraphQLCompilerException($"Method '{method}' expects {count} argument(s) but {args.Length} were supplied"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{method}' expects {count} argument(s) but {args.Length} were supplied"); } private static void ExpectArgsCountBetween(int low, int high, Expression[] args, string method) { if (args.Length < low || args.Length > high) - throw new EntityGraphQLCompilerException($"Method '{method}' expects {low}-{high} argument(s) but {args.Length} were supplied"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{method}' expects {low}-{high} argument(s) but {args.Length} were supplied"); } private static Expression ConvertTypeIfWeCan(string methodName, Expression argExp, Type expected) @@ -271,7 +286,13 @@ private static Expression ConvertTypeIfWeCan(string methodName, Expression argEx } catch (Exception ex) { - throw new EntityGraphQLCompilerException($"Method '{methodName}' expects parameter that evaluates to a '{expected}' result but found result type '{argExp.Type}'", ex); + throw new EntityGraphQLException( + GraphQLErrorCategory.DocumentError, + $"Method '{methodName}' expects parameter that evaluates to a '{expected}' result but found result type '{argExp.Type}'", + null, + [methodName], + ex + ); } } diff --git a/src/EntityGraphQL/Compiler/EntityQuery/EntityQueryCompiler.cs b/src/EntityGraphQL/Compiler/EntityQuery/EntityQueryCompiler.cs index 65c82252..dca887aa 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/EntityQueryCompiler.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/EntityQueryCompiler.cs @@ -18,11 +18,14 @@ namespace EntityGraphQL.Compiler.EntityQuery; /// not(), ! public static class EntityQueryCompiler { - public static CompiledQueryResult Compile(string query, ExecutionOptions executionOptions) + public static CompiledQueryResult Compile(string query, EqlCompileContext compileContext) { - return Compile(query, null, executionOptions, null); + return Compile(query, null, compileContext, null); } + public static CompiledQueryResult Compile(string query, ISchemaProvider? schemaProvider, CompileContext compileContext, IMethodProvider? methodProvider = null) => + Compile(query, schemaProvider, new EqlCompileContext(compileContext), methodProvider); + /// /// Compile a query. /// @@ -30,7 +33,7 @@ public static CompiledQueryResult Compile(string query, ExecutionOptions executi /// /// /// - public static CompiledQueryResult Compile(string query, ISchemaProvider? schemaProvider, ExecutionOptions executionOptions, IMethodProvider? methodProvider = null) + public static CompiledQueryResult Compile(string query, ISchemaProvider? schemaProvider, EqlCompileContext compileContext, IMethodProvider? methodProvider = null) { #if NET8_0_OR_GREATER ArgumentNullException.ThrowIfNull(query, nameof(query)); @@ -46,7 +49,7 @@ public static CompiledQueryResult Compile(string query, ISchemaProvider? schemaP if (schemaProvider != null) contextParam = Expression.Parameter(schemaProvider.QueryContextType, $"cxt_{schemaProvider.QueryContextType.Name}"); - var expression = CompileQuery(query, contextParam, schemaProvider, new QueryRequestContext(null, null), methodProvider, executionOptions); + var expression = CompileQuery(query, contextParam, schemaProvider, new QueryRequestContext(null, null), methodProvider, compileContext); var contextParams = new List(); if (contextParam != null) @@ -59,12 +62,14 @@ public static CompiledQueryResult CompileWith( Expression context, ISchemaProvider schemaProvider, QueryRequestContext requestContext, - ExecutionOptions executionOptions, + EqlCompileContext compileContext, IMethodProvider? methodProvider = null ) { methodProvider ??= new EqlMethodProvider(); - var expression = CompileQuery(query, context, schemaProvider, requestContext, methodProvider, executionOptions) ?? throw new EntityGraphQLCompilerException("Failed to compile expression"); + var expression = + CompileQuery(query, context, schemaProvider, requestContext, methodProvider, compileContext) + ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "Failed to compile expression"); var parameters = expression.NodeType == ExpressionType.Lambda ? ((LambdaExpression)expression).Parameters.ToList() : []; return new CompiledQueryResult(expression, parameters); } @@ -75,10 +80,9 @@ private static Expression CompileQuery( ISchemaProvider? schemaProvider, QueryRequestContext requestContext, IMethodProvider methodProvider, - ExecutionOptions executionOptions + EqlCompileContext compileContext ) { - var compileContext = new CompileContext(executionOptions, null, requestContext); var expression = EntityQueryParser.Instance.Parse(query, context, schemaProvider, requestContext, methodProvider, compileContext); return expression; } diff --git a/src/EntityGraphQL/Compiler/EntityQuery/EqlCompileContext.cs b/src/EntityGraphQL/Compiler/EntityQuery/EqlCompileContext.cs new file mode 100644 index 00000000..693ff361 --- /dev/null +++ b/src/EntityGraphQL/Compiler/EntityQuery/EqlCompileContext.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using EntityGraphQL.Schema; + +namespace EntityGraphQL.Compiler.EntityQuery; + +public class EqlCompileContext : CompileContext +{ + public EqlCompileContext(CompileContext compileContext) + : base( + compileContext.ExecutionOptions, + compileContext.BulkData, + compileContext.RequestContext, + compileContext.DocumentVariablesParameter, + compileContext.DocumentVariables, + compileContext.CancellationToken + ) { } + + public List ServiceFieldDependencies { get; } = new(); + public Expression? OriginalContext { get; set; } +} diff --git a/src/EntityGraphQL/Compiler/EntityQuery/EqlMethodProvider.cs b/src/EntityGraphQL/Compiler/EntityQuery/EqlMethodProvider.cs index 9087ed24..860f3b31 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/EqlMethodProvider.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/EqlMethodProvider.cs @@ -14,10 +14,12 @@ namespace EntityGraphQL.Compiler.EntityQuery; public class EqlMethodProvider : IMethodProvider { private readonly Dictionary registeredMethods; + private readonly HashSet isAnySupportedTypes = new(); public EqlMethodProvider() { registeredMethods = new Dictionary(StringComparer.OrdinalIgnoreCase); + InitializeIsAnySupportedTypes(); RegisterDefaultMethods(); } @@ -146,14 +148,14 @@ public void ClearCustomMethods() public Expression MakeCall(Expression context, Expression argContext, string methodName, IEnumerable? args, Type type) { if (!registeredMethods.TryGetValue(methodName, out var method)) - throw new EntityGraphQLCompilerException($"Unsupported method {methodName}"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Unsupported method {methodName}"); if (!IsTypeCompatible(type, method)) - throw new EntityGraphQLCompilerException($"Method '{methodName}' cannot be called on type '{type}'. Expected '{method.MethodContextType}'"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' cannot be called on type '{type}'. Expected '{method.MethodContextType}'"); if (method.MakeCallFunc != null) return method.MakeCallFunc(context, argContext, method.MethodName, args?.ToArray() ?? Array.Empty()); - throw new EntityGraphQLCompilerException($"Method '{methodName}' does not have a MakeCallFunc defined"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' does not have a MakeCallFunc defined"); } #endregion @@ -187,14 +189,16 @@ private void RegisterDefaultMethods() RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "min", DefaultMethodImplementations.MakeMinMethod); RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "max", DefaultMethodImplementations.MakeMaxMethod); RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "average", DefaultMethodImplementations.MakeAverageMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "selectMany", DefaultMethodImplementations.MakeSelectManyMethod); // IsAny method with complex type predicate RegisterMethodWithTypePredicate(CreateIsAnyTypePredicate(), "isAny", DefaultMethodImplementations.MakeIsAnyMethod); } - private static Func CreateIsAnyTypePredicate() + private void InitializeIsAnySupportedTypes() { - var supportedTypes = new HashSet + isAnySupportedTypes.Clear(); + var defaults = new[] { typeof(string), typeof(long), @@ -226,8 +230,10 @@ private static Func CreateIsAnyTypePredicate() typeof(Guid), typeof(Guid?), typeof(DateTimeOffset), - typeof(DateTimeOffset?) -#if NET5_0_OR_GREATER + typeof(DateTimeOffset?), + typeof(TimeSpan), + typeof(TimeSpan?) +#if NET8_0_OR_GREATER , typeof(DateOnly), typeof(DateOnly?), @@ -235,8 +241,39 @@ private static Func CreateIsAnyTypePredicate() typeof(TimeOnly?) #endif }; + foreach (var t in defaults) + { + isAnySupportedTypes.Add(t); + } + } + + internal void ExtendIsAnySupportedTypes(params Type[] types) + { + foreach (var t in types) + { + // Always add the provided type + isAnySupportedTypes.Add(t); + + // If a nullable form is provided, also add its underlying type (non-nullable) + var underlying = Nullable.GetUnderlyingType(t); + if (underlying != null) + { + isAnySupportedTypes.Add(underlying); + continue; + } + + // If a non-nullable value type is provided, also add its nullable variant + if (t.IsValueType) + { + var nullable = typeof(Nullable<>).MakeGenericType(t); + isAnySupportedTypes.Add(nullable); + } + } + } - return supportedTypes.Contains; + private Func CreateIsAnyTypePredicate() + { + return t => isAnySupportedTypes.Contains(t); } #endregion diff --git a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/Binary.cs b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/Binary.cs index b92ba156..77a315a4 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/Binary.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/Binary.cs @@ -14,68 +14,83 @@ internal sealed class Binary(ExpressionType op, IExpression left, IExpression ri public IExpression Left { get; } = left; public IExpression Right { get; } = right; - public Expression Compile(Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) + public Expression Compile(Expression? context, EntityQueryParser parser, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) { - var left = Left.Compile(context, schema, requestContext, methodProvider); - var right = Right.Compile(context, schema, requestContext, methodProvider); + var left = Left.Compile(context, parser, schema, requestContext, methodProvider); + var right = Right.Compile(context, parser, schema, requestContext, methodProvider); if (left.Type != right.Type) { - // if (op == ExpressionType.Equal || op == ExpressionType.NotEqual) - // { - // var result = DoObjectComparisonOnDifferentTypes(op, left, right); - - // if (result != null) - // return result; - // } - return ConvertLeftOrRight(Op, left, right); + return ConvertLeftOrRight(Op, left, right, parser, schema); } return Expression.MakeBinary(Op, left, right); } - private static BinaryExpression ConvertLeftOrRight(ExpressionType op, Expression left, Expression right) + private static BinaryExpression ConvertLeftOrRight(ExpressionType op, Expression left, Expression right, EntityQueryParser parser, ISchemaProvider? schema) { - if (left.Type.IsNullableType() && !right.Type.IsNullableType()) - right = Expression.Convert(right, right.Type.GetNullableType()); - else if (right.Type.IsNullableType() && !left.Type.IsNullableType()) - left = Expression.Convert(left, left.Type.GetNullableType()); - else if (left.Type == typeof(int) && (right.Type == typeof(uint) || right.Type == typeof(short) || right.Type == typeof(long) || right.Type == typeof(ushort) || right.Type == typeof(ulong))) - right = Expression.Convert(right, left.Type); - else if (left.Type == typeof(uint) && (right.Type == typeof(int) || right.Type == typeof(short) || right.Type == typeof(long) || right.Type == typeof(ushort) || right.Type == typeof(ulong))) - left = Expression.Convert(left, right.Type); - + // Defer nullable promotion and numeric width alignment until after we attempt literal parsing and specific conversions if (left.Type != right.Type) { + // Try to use type converters for constant string expressions first + if (schema != null && right is ConstantExpression { Value: string str } && left.Type != typeof(string)) + { + if (schema.TryConvertCustom(str, left.Type, out var converted)) + { + right = Expression.Constant(converted, left.Type); + } + } + else if (schema != null && left is ConstantExpression { Value: string str2 } && right.Type != typeof(string)) + { + if (schema.TryConvertCustom(str2, right.Type, out var converted)) + { + left = Expression.Constant(converted, right.Type); + } + } + if (left.Type.IsEnum && right.Type.IsEnum) - throw new EntityGraphQLCompilerException($"Cannot compare enums of different types '{left.Type.Name}' and '{right.Type.Name}'"); - if (left.Type == typeof(Guid) || left.Type == typeof(Guid?) && right.Type == typeof(string)) - right = ConvertToGuid(right); - else if (right.Type == typeof(Guid) || right.Type == typeof(Guid?) && left.Type == typeof(string)) - left = ConvertToGuid(left); - else if (left.Type == typeof(DateTime) || left.Type == typeof(DateTime?) && right.Type == typeof(string)) - right = ConvertToDateTime(right); - else if (right.Type == typeof(DateTime) || right.Type == typeof(DateTime?) && left.Type == typeof(string)) - left = ConvertToDateTime(left); - else if (left.Type == typeof(DateTimeOffset) || left.Type == typeof(DateTimeOffset?) && right.Type == typeof(string)) - right = ConvertToDateTimeOffset(right); - else if (right.Type == typeof(DateTimeOffset) || right.Type == typeof(DateTimeOffset?) && left.Type == typeof(string)) - left = ConvertToDateTimeOffset(left); - else if (left.Type.IsEnum && right.Type == typeof(string)) - right = ConvertToEnum(right, left.Type); - else if (right.Type.IsEnum && left.Type == typeof(string)) - left = ConvertToEnum(left, right.Type); - // convert ints "up" to float/decimal - else if ( - (left.Type == typeof(int) || left.Type == typeof(uint) || left.Type == typeof(short) || left.Type == typeof(ushort) || left.Type == typeof(long) || left.Type == typeof(ulong)) - && (right.Type == typeof(float) || right.Type == typeof(double) || right.Type == typeof(decimal)) - ) - left = Expression.Convert(left, right.Type); - else if ( - (right.Type == typeof(int) || right.Type == typeof(uint) || right.Type == typeof(short) || right.Type == typeof(ushort) || right.Type == typeof(long) || right.Type == typeof(ulong)) - && (left.Type == typeof(float) || left.Type == typeof(double) || left.Type == typeof(decimal)) - ) - right = Expression.Convert(right, left.Type); - else // default try to make types match - left = Expression.Convert(left, right.Type); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Cannot compare enums of different types '{left.Type.Name}' and '{right.Type.Name}'"); + + // If types are now equal after literal parser, skip further specific conversions + if (left.Type != right.Type) + { + if (left.Type == typeof(Guid) || left.Type == typeof(Guid?) && right.Type == typeof(string)) + right = ConvertToGuid(right); + else if (right.Type == typeof(Guid) || right.Type == typeof(Guid?) && left.Type == typeof(string)) + left = ConvertToGuid(left); + else if (left.Type == typeof(DateTime) || left.Type == typeof(DateTime?) && right.Type == typeof(string)) + right = ConvertToDateTime(right); + else if (right.Type == typeof(DateTime) || right.Type == typeof(DateTime?) && left.Type == typeof(string)) + left = ConvertToDateTime(left); + else if (left.Type == typeof(DateTimeOffset) || left.Type == typeof(DateTimeOffset?) && right.Type == typeof(string)) + right = ConvertToDateTimeOffset(right); + else if (right.Type == typeof(DateTimeOffset) || right.Type == typeof(DateTimeOffset?) && left.Type == typeof(string)) + left = ConvertToDateTimeOffset(left); + else if (left.Type == typeof(TimeSpan) || left.Type == typeof(TimeSpan?) && right.Type == typeof(string)) + right = ConvertToTimeSpan(right); + else if (right.Type == typeof(TimeSpan) || right.Type == typeof(TimeSpan?) && left.Type == typeof(string)) + left = ConvertToTimeSpan(left); +#if NET8_0_OR_GREATER + else if (left.Type == typeof(DateOnly) || left.Type == typeof(DateOnly?) && right.Type == typeof(string)) + right = ConvertToDateOnly(right); + else if (right.Type == typeof(DateOnly) || right.Type == typeof(DateOnly?) && left.Type == typeof(string)) + left = ConvertToDateOnly(left); + else if (left.Type == typeof(TimeOnly) || left.Type == typeof(TimeOnly?) && right.Type == typeof(string)) + right = ConvertToTimeOnly(right); + else if (right.Type == typeof(TimeOnly) || right.Type == typeof(TimeOnly?) && left.Type == typeof(string)) + left = ConvertToTimeOnly(left); +#endif + else if (left.Type.IsEnum && right.Type == typeof(string)) + right = ConvertToEnum(right, left.Type); + else if (right.Type.IsEnum && left.Type == typeof(string)) + left = ConvertToEnum(left, right.Type); + // Convert integral types "up" to floating-point types (float/double/decimal) + else if (IsIntegralOrNullableIntegral(left.Type) && IsFloatingPointOrNullable(right.Type)) + left = Expression.Convert(left, right.Type); + else if (IsIntegralOrNullableIntegral(right.Type) && IsFloatingPointOrNullable(left.Type)) + right = Expression.Convert(right, left.Type); + // Align floating-point types (float/double/decimal) or integral types + else if (!AlignFloatingPointTypes(ref left, ref right) && !AlignIntegralTypes(ref left, ref right) && left.Type != right.Type) + left = Expression.Convert(left, right.Type); + } } if (left.Type.IsNullableType() && !right.Type.IsNullableType()) @@ -96,6 +111,23 @@ private static MethodCallExpression ConvertToDateTime(Expression expression) return Expression.Call(typeof(DateTime), nameof(DateTime.Parse), null, expression, Expression.Constant(CultureInfo.InvariantCulture)); } + private static MethodCallExpression ConvertToTimeSpan(Expression expression) + { + return Expression.Call(typeof(TimeSpan), nameof(TimeSpan.Parse), null, expression, Expression.Constant(CultureInfo.InvariantCulture)); + } + +#if NET8_0_OR_GREATER + private static MethodCallExpression ConvertToDateOnly(Expression expression) + { + return Expression.Call(typeof(DateOnly), nameof(DateOnly.Parse), null, expression, Expression.Constant(CultureInfo.InvariantCulture)); + } + + private static MethodCallExpression ConvertToTimeOnly(Expression expression) + { + return Expression.Call(typeof(TimeOnly), nameof(TimeOnly.Parse), null, expression, Expression.Constant(CultureInfo.InvariantCulture)); + } +#endif + private static MethodCallExpression ConvertToGuid(Expression expression) { return Expression.Call(typeof(Guid), nameof(Guid.Parse), null, Expression.Call(expression, typeof(object).GetMethod(nameof(ToString))!)); @@ -109,24 +141,106 @@ private static UnaryExpression ConvertToEnum(Expression expression, Type enumTyp ); } - // private static Expression? DoObjectComparisonOnDifferentTypes(ExpressionType op, Expression left, Expression right) - // { - // var convertedToSameTypes = false; - - // // leftGuid == 'asdasd' == null ? (Guid) null : new Guid('asdasdas'.ToString()) - // // leftGuid == null - // if (left.Type == typeof(Guid) && right.Type != typeof(Guid)) - // { - // right = ConvertToGuid(right); - // convertedToSameTypes = true; - // } - // else if (right.Type == typeof(Guid) && left.Type != typeof(Guid)) - // { - // left = ConvertToGuid(left); - // convertedToSameTypes = true; - // } - - // var result = convertedToSameTypes ? (Expression)Expression.MakeBinary(op, left, right) : null; - // return result; - // } + /// + /// Aligns integral numeric types between left and right expressions, including nullable types. + /// + private static bool AlignIntegralTypes(ref Expression left, ref Expression right) => AlignNumericTypes(ref left, ref right, IsIntegralType); + + /// + /// Aligns floating-point numeric types (float, double, decimal) between left and right expressions, including nullable types. + /// + private static bool AlignFloatingPointTypes(ref Expression left, ref Expression right) => AlignNumericTypes(ref left, ref right, IsFloatingPointType); + + /// + /// Aligns numeric types between left and right expressions, including nullable types. + /// Prioritizes non-constant expressions (e.g., field access) over constants to avoid database column casts. + /// Returns true if alignment was performed. + /// + /// The left expression (may be modified) + /// The right expression (may be modified) + /// Function to check if a type belongs to the numeric category being aligned + private static bool AlignNumericTypes(ref Expression left, ref Expression right, Func typeChecker) + { + // Get the underlying types (unwrap nullable if needed) + var leftType = Nullable.GetUnderlyingType(left.Type) ?? left.Type; + var rightType = Nullable.GetUnderlyingType(right.Type) ?? right.Type; + + // Check if both types belong to the same numeric category + if (!typeChecker(leftType) || !typeChecker(rightType)) + return false; + + // Determine which side to prioritize - prefer non-constant side (field access) over constant (literal). + // This avoids casting database columns which can prevent index usage. + // Truth table for prioritizeLeft: + // left=constant, right=constant -> true (default to left) + // left=constant, right=non-constant -> false (prioritize right, the field) + // left=non-constant, right=constant -> true (prioritize left, the field) + // left=non-constant, right=non-constant -> true (default to left) + var leftIsConstant = left is ConstantExpression; + var rightIsConstant = right is ConstantExpression; + var prioritizeLeft = !leftIsConstant || rightIsConstant; + + // Convert to match the prioritized side's type + if (leftType != rightType) + { + if (prioritizeLeft) + right = Expression.Convert(right, left.Type); + else + left = Expression.Convert(left, right.Type); + return true; + } + + // Types are the same underlying type but might differ in nullability + if (left.Type != right.Type) + { + if (prioritizeLeft) + right = Expression.Convert(right, left.Type); + else + left = Expression.Convert(left, right.Type); + return true; + } + + return false; + } + + /// + /// Checks if a type is an integral numeric type (int, uint, short, ushort, long, ulong, byte, sbyte) + /// + private static bool IsIntegralType(Type type) + { + return type == typeof(int) + || type == typeof(uint) + || type == typeof(short) + || type == typeof(ushort) + || type == typeof(long) + || type == typeof(ulong) + || type == typeof(byte) + || type == typeof(sbyte); + } + + /// + /// Checks if a type is a floating-point type (float, double, decimal) + /// + private static bool IsFloatingPointType(Type type) + { + return type == typeof(float) || type == typeof(double) || type == typeof(decimal); + } + + /// + /// Checks if a type is an integral type or nullable integral type + /// + private static bool IsIntegralOrNullableIntegral(Type type) + { + var underlyingType = Nullable.GetUnderlyingType(type) ?? type; + return IsIntegralType(underlyingType); + } + + /// + /// Checks if a type is a floating-point type (float, double, decimal) or nullable version + /// + private static bool IsFloatingPointOrNullable(Type type) + { + var underlyingType = Nullable.GetUnderlyingType(type) ?? type; + return underlyingType == typeof(float) || underlyingType == typeof(double) || underlyingType == typeof(decimal); + } } diff --git a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/CallExpression.cs b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/CallExpression.cs index a4244eb6..c14cac7e 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/CallExpression.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/CallExpression.cs @@ -12,5 +12,6 @@ internal sealed class CallExpression(string name, IReadOnlyList? ar public string Name { get; } = name; public IReadOnlyList? Arguments { get; } = arguments; - public Expression Compile(Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) => throw new NotImplementedException(); + public Expression Compile(Expression? context, EntityQueryParser parser, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) => + throw new NotImplementedException(); } diff --git a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/CallPath.cs b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/CallPath.cs index b2c38ac7..1d01ae31 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/CallPath.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/CallPath.cs @@ -2,30 +2,32 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Extensions; using EntityGraphQL.Schema; namespace EntityGraphQL.Compiler.EntityQuery.Grammar; -internal sealed class CallPath(IReadOnlyList parts, CompileContext compileContext) : IExpression +internal sealed class CallPath(IReadOnlyList parts, EqlCompileContext compileContext) : IExpression { private readonly IReadOnlyList parts = parts; - private readonly CompileContext compileContext = compileContext; + private readonly EqlCompileContext compileContext = compileContext; public Type Type => parts[-1].Type; - public Expression Compile(Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) + public Expression Compile(Expression? context, EntityQueryParser parser, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) { if (parts.Count == 1) { if (parts[0] is CallExpression ce) { - return MakeMethodCall(schema, methodProvider, ref context!, ce.Name, ce.Arguments, requestContext); + return MakeMethodCall(schema, parser, methodProvider, ref context!, ce.Name, ce.Arguments, requestContext); } if (parts[0] is IdentityExpression ie) { return IdentityExpression.MakePropertyCall(context!, schema, ie.Name, requestContext, compileContext); } - return parts[0].Compile(context, schema, requestContext, methodProvider); + return parts[0].Compile(context, parser, schema, requestContext, methodProvider); } var exp = parts.Aggregate( context!, @@ -33,13 +35,13 @@ public Expression Compile(Expression? context, ISchemaProvider? schema, QueryReq { if (next is CallExpression ce) { - return MakeMethodCall(schema, methodProvider, ref currentContext, ce.Name, ce.Arguments, requestContext); + return MakeMethodCall(schema, parser, methodProvider, ref currentContext, ce.Name, ce.Arguments, requestContext); } if (next is IdentityExpression ie) { return IdentityExpression.MakePropertyCall(currentContext!, schema, ie.Name, requestContext, compileContext); } - return next.Compile(context, schema, requestContext, methodProvider); + return next.Compile(context, parser, schema, requestContext, methodProvider); } ); return exp; @@ -47,6 +49,7 @@ public Expression Compile(Expression? context, ISchemaProvider? schema, QueryReq internal static Expression MakeMethodCall( ISchemaProvider? schema, + EntityQueryParser parser, IMethodProvider methodProvider, ref Expression currentContext, string name, @@ -55,12 +58,12 @@ QueryRequestContext requestContext ) { if (currentContext == null) - throw new EntityGraphQLCompilerException("CurrentContext is null"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "CurrentContext is null"); var method = name; if (!methodProvider.EntityTypeHasMethod(currentContext.Type, method)) { - throw new EntityGraphQLCompilerException($"Method '{method}' not found on current context '{currentContext.Type.Name}'"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{method}' not found on current context '{currentContext.Type.Name}'"); } // Keep the current context var outerContext = currentContext; @@ -70,7 +73,40 @@ QueryRequestContext requestContext // build our method call var localContext = currentContext; // Create a local variable to store the value of currentContext // Compile the arguments with the new context - var args = arguments?.Select(a => a.Compile(localContext, schema, requestContext, methodProvider))?.ToList(); + var args = arguments?.Select(a => a.Compile(localContext, parser, schema, requestContext, methodProvider))?.ToList(); + + // Special handling for isAny: if the provided array/list element type doesn't match the context type, + // convert the list elements to the context type using schema-aware converters first. + if (string.Equals(method, "isAny", StringComparison.OrdinalIgnoreCase) && args != null && args.Count == 1) + { + var array = args[0]; + var arrayEleType = array.Type.GetEnumerableOrArrayType(); + if (arrayEleType != null) + { + var ctxType = outerContext.Type; + // unwrap Nullable for comparison + var targetType = ctxType.IsNullableType() ? Nullable.GetUnderlyingType(ctxType)! : ctxType; + if (arrayEleType != targetType) + { + var p = Expression.Parameter(arrayEleType, "x"); + var convertCall = Expression.Call( + typeof(ExpressionUtil), + nameof(ExpressionUtil.ConvertObjectType), + Type.EmptyTypes, + Expression.Convert(p, typeof(object)), + Expression.Constant(targetType, typeof(Type)), + Expression.Constant(schema, typeof(ISchemaProvider)) + ); + var body = Expression.Convert(convertCall, targetType); + var lambda = Expression.Lambda(body, p); + + array = Expression.Call(array.Type.IsGenericTypeQueryable() ? typeof(Queryable) : typeof(Enumerable), nameof(Queryable.Select), [arrayEleType, targetType], array, lambda); + + args[0] = array; + } + } + } + var call = methodProvider.MakeCall(outerContext, methodArgContext, method, args, outerContext.Type); currentContext = call; return call; diff --git a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/ConditionExpression.cs b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/ConditionExpression.cs index da80e602..8fef3a2c 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/ConditionExpression.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/ConditionExpression.cs @@ -12,14 +12,14 @@ internal sealed class ConditionExpression(IExpression condition, IExpression ifT public Type Type => ifTrue.Type; - public Expression Compile(Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) + public Expression Compile(Expression? context, EntityQueryParser parser, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) { - var trueExp = ifTrue.Compile(context, schema, requestContext, methodProvider); - var falseExp = ifFalse.Compile(context, schema, requestContext, methodProvider); + var trueExp = ifTrue.Compile(context, parser, schema, requestContext, methodProvider); + var falseExp = ifFalse.Compile(context, parser, schema, requestContext, methodProvider); if (trueExp.Type != falseExp.Type) - throw new EntityGraphQLCompilerException($"Conditional result types mismatch. Types '{trueExp.Type.Name}' and '{falseExp.Type.Name}' must be the same."); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Conditional result types mismatch. Types '{trueExp.Type.Name}' and '{falseExp.Type.Name}' must be the same."); - return Expression.Condition(condition.Compile(context, schema, requestContext, methodProvider), trueExp, falseExp); + return Expression.Condition(condition.Compile(context, parser, schema, requestContext, methodProvider), trueExp, falseExp); } } diff --git a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/EntityQueryParser.cs b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/EntityQueryParser.cs index 075a93b6..7149c0ed 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/EntityQueryParser.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/EntityQueryParser.cs @@ -55,6 +55,7 @@ public sealed class EntityQueryParser private static readonly Parser comma = Terms.Char(','); private static readonly Parser questionMark = Terms.Char('?'); private static readonly Parser colon = Terms.Char(':'); + private static readonly Parser dollarSign = Terms.Char('$'); private static readonly Parser ifExp = Terms.Text("if"); private static readonly Parser thenExp = Terms.Text("then"); private static readonly Parser elseExp = Terms.Text("else"); @@ -63,7 +64,7 @@ public sealed class EntityQueryParser .Number(NumberOptions.AllowLeadingSign | NumberOptions.Float) .Then(static d => { -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER var scale = d.Scale; #else var bits = decimal.GetBits(d); @@ -75,6 +76,11 @@ public sealed class EntityQueryParser private static readonly Parser strExp = SkipWhiteSpace(new StringLiteral(StringLiteralQuotes.SingleOrDouble)) .Then(static s => new EqlExpression(Expression.Constant(s.ToString()))); + // Variable expression: $variableName + private static readonly Parser variableExp = dollarSign + .And(new Identifier()) + .Then(static (c, x) => new VariableExpression(x.Item2.ToString()!, ((EntityQueryParseContext)c).CompileContext)); + private EntityQueryParser() { // The Deferred helper creates a parser that can be referenced by others before it is defined @@ -86,12 +92,30 @@ private EntityQueryParser() var callArgs = openParen.And(Separated(comma, expression)).And(closeParen).Then(static x => x.Item2); var emptyCallArgs = openParen.And(closeParen).Then(static x => new List() as IReadOnlyList); - var identifier = SkipWhiteSpace(new Identifier()).And(Not(emptyCallArgs)).Then(static (c, x) => new IdentityExpression(x.Item1.ToString()!, ((EntityQueryParseContext)c).CompileContext)); + var identifier = SkipWhiteSpace(new Identifier()) + .And(Not(emptyCallArgs)) + .Then(static (c, x) => new IdentityExpression(x.Item1.ToString()!, ((EntityQueryParseContext)c).CompileContext)); var constArray = openArray .And(Separated(comma, expression)) .And(closeArray) - .Then(static (c, x) => new EqlExpression(Expression.NewArrayInit(x.Item2[0].Type, x.Item2.Select(e => e.Compile(((EntityQueryParseContext)c).Context, ((EntityQueryParseContext)c).Schema, ((EntityQueryParseContext)c).RequestContext, ((EntityQueryParseContext)c).MethodProvider))))); + .Then( + static (c, x) => + new EqlExpression( + Expression.NewArrayInit( + x.Item2[0].Type, + x.Item2.Select(e => + e.Compile( + ((EntityQueryParseContext)c).Context, + Instance, + ((EntityQueryParseContext)c).Schema, + ((EntityQueryParseContext)c).RequestContext, + ((EntityQueryParseContext)c).MethodProvider + ) + ) + ) + ) + ); var call = SkipWhiteSpace(new Identifier()).And(callArgs.Or(emptyCallArgs)).Then(static x => new CallExpression(x.Item1!.ToString()!, x.Item2)); @@ -102,12 +126,29 @@ private EntityQueryParser() var falseExp = Terms.Text("false").AndSkip(Not(identifier)).Then(static _ => new EqlExpression(Expression.Constant(false))); // primary => NUMBER | "(" expression ")"; - var primary = numberExp.Or(strExp).Or(trueExp).Or(falseExp).Or(nullExp).Or(callPath).Or(groupExpression).Or(constArray); + var primary = numberExp.Or(strExp).Or(trueExp).Or(falseExp).Or(nullExp).Or(variableExp).Or(callPath).Or(groupExpression).Or(constArray); // The Recursive helper allows to create parsers that depend on themselves. // ( "-" ) unary | primary; var unary = Recursive( - (u) => minus.And(u).Then(static (c, x) => new EqlExpression(Expression.Negate(x.Item2.Compile(((EntityQueryParseContext)c).Context, ((EntityQueryParseContext)c).Schema, ((EntityQueryParseContext)c).RequestContext, ((EntityQueryParseContext)c).MethodProvider)))).Or(primary) + (u) => + minus + .And(u) + .Then( + static (c, x) => + new EqlExpression( + Expression.Negate( + x.Item2.Compile( + ((EntityQueryParseContext)c).Context, + Instance, + ((EntityQueryParseContext)c).Schema, + ((EntityQueryParseContext)c).RequestContext, + ((EntityQueryParseContext)c).MethodProvider + ) + ) + ) + ) + .Or(primary) ); // factor => unary ( ( "*" | "/" | ... ) unary )* ; @@ -198,7 +239,8 @@ private static IExpression HandleBinary((IExpression, IReadOnlyList<(string, IEx private sealed class EntityQueryParseContext : ParseContext { - public EntityQueryParseContext(string query, Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider, CompileContext compileContext) : base(new Parlot.Scanner(query)) + public EntityQueryParseContext(string query, Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider, EqlCompileContext compileContext) + : base(new Parlot.Scanner(query)) { Context = context; Schema = schema; @@ -211,14 +253,14 @@ public EntityQueryParseContext(string query, Expression? context, ISchemaProvide public ISchemaProvider? Schema { get; } public QueryRequestContext RequestContext { get; } public IMethodProvider MethodProvider { get; } - public CompileContext CompileContext { get; } + public EqlCompileContext CompileContext { get; } } - public Expression Parse(string query, Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider, CompileContext compileContext) + public Expression Parse(string query, Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider, EqlCompileContext compileContext) { var parseContext = new EntityQueryParseContext(query, context, schema, requestContext, methodProvider, compileContext); - var result = grammar.Parse(parseContext) ?? throw new EntityGraphQLCompilerException("Failed to parse query"); - return result.Compile(parseContext.Context, parseContext.Schema, parseContext.RequestContext, parseContext.MethodProvider); + var result = grammar.Parse(parseContext) ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "Failed to parse query"); + return result.Compile(parseContext.Context, Instance, parseContext.Schema, parseContext.RequestContext, parseContext.MethodProvider); } } diff --git a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/IExpression.cs b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/IExpression.cs index fcc08b59..3886dfa3 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/IExpression.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/IExpression.cs @@ -7,7 +7,7 @@ namespace EntityGraphQL.Compiler.EntityQuery.Grammar; internal interface IExpression { Type Type { get; } - Expression Compile(Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider); + Expression Compile(Expression? context, EntityQueryParser parser, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider); } internal sealed class EqlExpression(Expression value) : IExpression @@ -16,7 +16,7 @@ internal sealed class EqlExpression(Expression value) : IExpression public Type Type => value.Type; - public Expression Compile(Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) + public Expression Compile(Expression? context, EntityQueryParser parser, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) { return value; } diff --git a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/IdentityExpression.cs b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/IdentityExpression.cs index 59add8f7..6bd28370 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/IdentityExpression.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/IdentityExpression.cs @@ -6,18 +6,18 @@ namespace EntityGraphQL.Compiler.EntityQuery.Grammar; -public class IdentityExpression(string name, CompileContext compileContext) : IExpression +public class IdentityExpression(string name, EqlCompileContext compileContext) : IExpression { public Type Type => throw new NotImplementedException(); public string Name { get; } = name; - public Expression Compile(Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) + public Expression Compile(Expression? context, EntityQueryParser parser, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) { return MakePropertyCall(context!, schema, Name, requestContext, compileContext); } - internal static Expression MakePropertyCall(Expression context, ISchemaProvider? schema, string name, QueryRequestContext requestContext, CompileContext compileContext) + internal static Expression MakePropertyCall(Expression context, ISchemaProvider? schema, string name, QueryRequestContext requestContext, EqlCompileContext compileContext) { if (schema == null) { @@ -43,7 +43,32 @@ internal static Expression MakePropertyCall(Expression context, ISchemaProvider? return Expression.Constant(Enum.Parse(schemaType.TypeDotnet, name)); } var gqlField = schemaType.GetField(name, requestContext); - (var exp, _) = gqlField.GetExpression(gqlField.ResolveExpression!, context, null, null, compileContext, new Dictionary(), null, null, [], false, new Util.ParameterReplacer()); + + var isServiceField = gqlField.Services.Count > 0; + + Expression? exp; + + if (isServiceField && compileContext.ExecutionOptions.ExecuteServiceFieldsSeparately) + // null context so the service expression param is not replaced and we can replace it in the filter extension + (exp, _) = gqlField.GetExpression(gqlField.ResolveExpression!, null, null, null, compileContext, new Dictionary(), null, null, [], false, new Util.ParameterReplacer()); + else + (exp, _) = gqlField.GetExpression(gqlField.ResolveExpression!, context, null, null, compileContext, new Dictionary(), null, null, [], false, new Util.ParameterReplacer()); + + // Track service-backed fields for later filter splitting and wrap with a marker only + // when we are executing in split mode (EF pass + services pass). + if (isServiceField && exp != null && compileContext.ExecutionOptions.ExecuteServiceFieldsSeparately) + { + // Create a unique key and store the extracted fields for this service field on the compile context + if (gqlField.ExtractedFieldsFromServices != null) + { + compileContext.ServiceFieldDependencies.Add(gqlField); + compileContext.OriginalContext = context; + } + + var marker = typeof(Util.ServiceExpressionMarker).GetMethod(nameof(Util.ServiceExpressionMarker.MarkService))!.MakeGenericMethod(exp.Type); + exp = Expression.Call(marker, exp); + } + return exp!; } @@ -65,6 +90,6 @@ private static Expression MakeConstantFromIdentity(Expression context, ISchemaPr } } - throw new EntityGraphQLCompilerException($"Field '{name}' not found on type '{schema?.GetSchemaType(context!.Type, false, null)?.Name ?? context!.Type.Name}'"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Field '{name}' not found on type '{schema?.GetSchemaType(context!.Type, false, null)?.Name ?? context!.Type.Name}'"); } } diff --git a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/VariableExpression.cs b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/VariableExpression.cs new file mode 100644 index 00000000..fdd409f1 --- /dev/null +++ b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/VariableExpression.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using EntityGraphQL.Schema; + +namespace EntityGraphQL.Compiler.EntityQuery.Grammar; + +/// +/// Expression that resolves a GraphQL variable ($variableName) during filter compilation +/// +public class VariableExpression : IExpression +{ + private readonly string variableName; + private readonly EqlCompileContext compileContext; + + public VariableExpression(string variableName, EqlCompileContext compileContext) + { + this.variableName = variableName ?? throw new ArgumentNullException(nameof(variableName)); + this.compileContext = compileContext ?? throw new ArgumentNullException(nameof(compileContext)); + } + + public Type Type => typeof(object); // Will be resolved at compile time + + public Expression Compile(Expression? context, EntityQueryParser parser, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider) + { + // Check if we have variable information in the compile context + if (compileContext.DocumentVariables == null) + { + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Variable ${variableName} not found in variables."); + } + + if (compileContext.DocumentVariablesParameter == null) + { + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Variable ${variableName} not found in variables."); + } + + // Check if the variable exists in the actual variables and get its value + var variableAccessExpression = Expression.PropertyOrField(compileContext.DocumentVariablesParameter, variableName); + if (variableAccessExpression == null) + { + var availableVars = string.Join(", ", compileContext.DocumentVariablesParameter.Type.GetFields().Select(f => f.Name)); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Variable ${variableName} is not defined in the query variables. Available: [{availableVars}]"); + } + + var val = Expression.Lambda(variableAccessExpression, compileContext.DocumentVariablesParameter!).Compile().DynamicInvoke(compileContext.DocumentVariables); + return Expression.Constant(val, val?.GetType() ?? typeof(object)); + } +} diff --git a/src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLField.cs b/src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLField.cs index b61119f5..a3a029ac 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLField.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLField.cs @@ -23,9 +23,9 @@ namespace EntityGraphQL.Compiler; /// public abstract class BaseGraphQLField : IGraphQLNode, IFieldKey { - public ExecutableDirectiveLocation LocationForDirectives { get; protected set; } = ExecutableDirectiveLocation.FIELD; + public ExecutableDirectiveLocation LocationForDirectives { get; protected set; } = ExecutableDirectiveLocation.Field; public ISchemaProvider Schema { get; protected set; } - protected List Directives { get; set; } = []; + public List Directives { get; set; } = []; protected string? OpName { get; set; } public virtual bool IsRootField { get; set; } @@ -54,7 +54,7 @@ public abstract class BaseGraphQLField : IGraphQLNode, IFieldKey /// public ISchemaType? FromType => Field?.FromType; public IField? Field { get; } - public List QueryFields { get; } = []; + public virtual List QueryFields { get; } = []; public Expression? NextFieldContext { get; } public IGraphQLNode? ParentNode { get; set; } @@ -68,7 +68,7 @@ public abstract class BaseGraphQLField : IGraphQLNode, IFieldKey /// /// True if this field directly has services /// - public bool HasServices => Field?.Services.Count > 0; + public bool HasServices => Field?.Services.Count > 0 || Field?.ExecuteAsService == true; public BaseGraphQLField( ISchemaProvider schema, @@ -84,8 +84,8 @@ public BaseGraphQLField( NextFieldContext = nextFieldContext; RootParameter = rootParameter; ParentNode = parentNode; - this.Arguments = arguments ?? new Dictionary(); - this.Schema = schema; + Arguments = arguments ?? new Dictionary(); + Schema = schema; Field = field; } @@ -112,7 +112,7 @@ public BaseGraphQLField(BaseGraphQLField context, Expression? nextFieldContext) /// public virtual bool HasServicesAtOrBelow(IReadOnlyDictionary fragments) { - return Field?.Services.Count > 0 || QueryFields.Any(f => f.HasServicesAtOrBelow(fragments)); + return Field?.Services.Count > 0 || Field?.ExecuteAsService == true || QueryFields.Any(f => f.HasServicesAtOrBelow(fragments)); } /// @@ -233,7 +233,7 @@ internal virtual IEnumerable ExpandFromServices(bool withoutSe return withoutServiceFields && HasServices ? [] : new List { field ?? this }; } - public void AddField(BaseGraphQLField field) + public virtual void AddField(BaseGraphQLField field) { QueryFields.Add(field); } @@ -373,7 +373,10 @@ public static void HandleBeforeRootFieldExpressionBuild(CompileContext compileCo var currentReturnType = expression.Type; expression = compileContext.ExecutionOptions.BeforeRootFieldExpressionBuild(expression, opName, fieldName); if (expression.Type != currentReturnType && !expression.Type.IsAssignableFrom(currentReturnType)) - throw new EntityGraphQLCompilerException($"BeforeExpressionBuild changed the return type from {currentReturnType} to {expression.Type}"); + throw new EntityGraphQLException( + GraphQLErrorCategory.ExecutionError, + $"Field '{fieldName}' - BeforeExpressionBuild changed the return type from {currentReturnType} to {expression.Type}" + ); } } @@ -407,6 +410,15 @@ internal static void CheckFieldAccess(ISchemaProvider schema, IField? fieldNode, schema.CheckTypeAccess(field.ReturnType.SchemaType, requestContext); } } + + public IEnumerable BuildPath() + { + var path = new List(); + if (ParentNode is IGraphQLNode parentField && ParentNode is not ExecutableGraphQLStatement) + path.AddRange(parentField.BuildPath()); + path.Add(Name); + return path; + } } public interface IFieldKey diff --git a/src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLQueryField.cs b/src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLQueryField.cs index bab193ca..a96871cc 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLQueryField.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLQueryField.cs @@ -71,55 +71,62 @@ ParameterReplacer replacer // or a service field that we expand into the required fields for input foreach (var subField in field.Expand(compileContext, fragments, withoutServiceFields, nextFieldContext, docParam, docVariables)) { - // fragments might be fragments on the actually type whereas the context is a interface - // we do not need to change the context in this case - var actualNextFieldContext = nextFieldContext; - if ( - !contextChanged - && subField.RootParameter != null - && actualNextFieldContext.Type != subField.RootParameter.Type - && (field is GraphQLInlineFragmentField || field is GraphQLFragmentSpreadField) - && (subField.FromType?.BaseTypesReadOnly.Any() == true || Field?.ReturnType.SchemaType.GqlType == GqlTypes.Union) - ) + try { - // we can do the convert here and avoid have to do a replace later - actualNextFieldContext = Expression.Convert(actualNextFieldContext, subField.RootParameter.Type)!; - } - - var fieldExp = subField.GetNodeExpression( - compileContext, - serviceProvider, - fragments, - docParam, - docVariables, - schemaContext, - withoutServiceFields, - actualNextFieldContext, - PossibleNextContextTypes, - contextChanged, - replacer - ); - if (fieldExp == null) - continue; - - var potentialMatch = selectionFields.Keys.FirstOrDefault(f => f.Name == subField.Name); - if (potentialMatch != null && subField.FromType != null) - { - // if we have a match, we need to check if the types are the same - // if they are, we can just use the existing field - if (potentialMatch.FromType?.BaseTypesReadOnly.Contains(subField.FromType) == true) + // fragments might be fragments on the actually type whereas the context is a interface + // we do not need to change the context in this case + var actualNextFieldContext = nextFieldContext; + if ( + !contextChanged + && subField.RootParameter != null + && actualNextFieldContext.Type != subField.RootParameter.Type + && (field is GraphQLInlineFragmentField || field is GraphQLFragmentSpreadField) + && (subField.FromType?.BaseTypesReadOnly.Any() == true || Field?.ReturnType.SchemaType.GqlType == GqlTypes.Union) + ) { - continue; + // we can do the convert here and avoid have to do a replace later + actualNextFieldContext = Expression.Convert(actualNextFieldContext, subField.RootParameter.Type)!; } - if (potentialMatch.FromType != null && subField.FromType.BaseTypesReadOnly.Contains(potentialMatch.FromType)) - { - // replace - use the non-base type field - selectionFields.Remove(potentialMatch); - selectionFields[subField] = new CompiledField(subField, fieldExp); + + var fieldExp = subField.GetNodeExpression( + compileContext, + serviceProvider, + fragments, + docParam, + docVariables, + schemaContext, + withoutServiceFields, + actualNextFieldContext, + PossibleNextContextTypes, + contextChanged, + replacer + ); + if (fieldExp == null) continue; + + var potentialMatch = selectionFields.Keys.FirstOrDefault(f => f.Name == subField.Name); + if (potentialMatch != null && subField.FromType != null) + { + // if we have a match, we need to check if the types are the same + // if they are, we can just use the existing field + if (potentialMatch.FromType?.BaseTypesReadOnly.Contains(subField.FromType) == true) + { + continue; + } + if (potentialMatch.FromType != null && subField.FromType.BaseTypesReadOnly.Contains(potentialMatch.FromType)) + { + // replace - use the non-base type field + selectionFields.Remove(potentialMatch); + selectionFields[subField] = new CompiledField(subField, fieldExp); + continue; + } } + selectionFields[subField] = new CompiledField(subField, fieldExp); + } + catch (EntityGraphQLFieldException ex) + { + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Field '{Name}' - {ex.Message}", null, BuildPath(), ex); } - selectionFields[subField] = new CompiledField(subField, fieldExp); } } @@ -153,7 +160,7 @@ ParameterReplacer replacer { // Need the args that may be used in the bulk resolver expression var argumentValue = default(object); - var validationErrors = new List(); + var validationErrors = new HashSet(); var bulkFieldArgParam = bulkResolver.BulkArgParam; var newArgParam = bulkFieldArgParam != null ? Expression.Parameter(bulkFieldArgParam!.Type, $"{bulkFieldArgParam.Name}_exec") : null; compileContext.AddArgsToCompileContext(field.Field!, field.Arguments, docParam, docVariables, ref argumentValue, validationErrors, newArgParam); diff --git a/src/EntityGraphQL/Compiler/GqlNodes/CompileContext.cs b/src/EntityGraphQL/Compiler/GqlNodes/CompileContext.cs index 714b8929..7c46a74d 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/CompileContext.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/CompileContext.cs @@ -15,13 +15,23 @@ public class CompileContext private readonly Dictionary constantParameters = []; private readonly Dictionary constantParametersForField = []; - public CompileContext(ExecutionOptions options, Dictionary? bulkData, QueryRequestContext requestContext, CancellationToken cancellationToken = default) + public CompileContext( + ExecutionOptions options, + Dictionary? bulkData, + QueryRequestContext requestContext, + ParameterExpression? docParam, + IArgumentsTracker? docVariables, + CancellationToken cancellationToken = default + ) { BulkData = bulkData; BulkParameter = bulkData != null ? Expression.Parameter(bulkData.GetType(), "bulkData") : null; ExecutionOptions = options; RequestContext = requestContext; CancellationToken = cancellationToken; + // Store document variables for access in field extensions and EQL compilation + DocumentVariablesParameter = docParam; + DocumentVariables = docVariables; } public List Services { get; } = []; @@ -32,6 +42,8 @@ public CompileContext(ExecutionOptions options, Dictionary? bulk public ExecutionOptions ExecutionOptions { get; } public QueryRequestContext RequestContext { get; } public CancellationToken CancellationToken { get; } + public ParameterExpression? DocumentVariablesParameter { get; } + public IArgumentsTracker? DocumentVariables { get; } public ConcurrencyLimiterRegistry ConcurrencyLimiterRegistry { get; } = new ConcurrencyLimiterRegistry(); public void AddServices(IEnumerable services) @@ -88,7 +100,7 @@ public void AddArgsToCompileContext( ParameterExpression? docParam, IArgumentsTracker? docVariables, ref object? argumentValue, - List validationErrors, + HashSet validationErrors, ParameterExpression? newArgParam ) { diff --git a/src/EntityGraphQL/Compiler/GqlNodes/ExecutableGraphQLStatement.cs b/src/EntityGraphQL/Compiler/GqlNodes/ExecutableGraphQLStatement.cs index e586a68d..942183f3 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/ExecutableGraphQLStatement.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/ExecutableGraphQLStatement.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Directives; using EntityGraphQL.Extensions; using EntityGraphQL.Schema; using EntityGraphQL.Schema.FieldExtensions; @@ -29,13 +30,13 @@ public abstract class ExecutableGraphQLStatement : IGraphQLNode /// /// Variables that are expected to be passed in to execute this query /// - protected Dictionary OpDefinedVariables { get; set; } = []; + protected IReadOnlyDictionary OpDefinedVariables { get; set; } public ISchemaProvider Schema { get; protected set; } public ParameterExpression? OpVariableParameter { get; } public IField? Field { get; } - public bool HasServices => Field?.Services.Count > 0; + public bool HasServices => Field?.Services.Count > 0 || Field?.ExecuteAsService == true; public IReadOnlyDictionary Arguments { get; } @@ -49,13 +50,23 @@ public abstract class ExecutableGraphQLStatement : IGraphQLNode /// public bool IsRootField => false; - public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expression nodeExpression, ParameterExpression rootParameter, Dictionary opVariables) + /// + /// The executable directive location for this operation type + /// + protected abstract ExecutableDirectiveLocation DirectiveLocation { get; } + + /// + /// The schema type for this operation + /// + protected abstract ISchemaType SchemaType { get; } + + public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expression nodeExpression, ParameterExpression rootParameter, IReadOnlyDictionary opVariables) { Name = name; NextFieldContext = nodeExpression; RootParameter = rootParameter; OpDefinedVariables = opVariables; - this.Schema = schema; + Schema = schema; Arguments = new Dictionary(); if (OpDefinedVariables.Count > 0) { @@ -66,11 +77,22 @@ public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expressi } } - public virtual async Task> ExecuteAsync( + /// + /// Abstract method that each operation type must implement to execute their specific field type + /// + protected abstract Task<(object? data, bool didExecute, List errors)> ExecuteOperationField( + CompileContext compileContext, + BaseGraphQLField field, + TContext context, + IServiceProvider? serviceProvider, + IReadOnlyDictionary fragments, + IArgumentsTracker? docVariables + ); + + public virtual async Task<(ConcurrentDictionary data, List errors)> ExecuteAsync( TContext? context, IServiceProvider? serviceProvider, IReadOnlyDictionary fragments, - Func fieldNamer, ExecutionOptions options, QueryVariables? variables, QueryRequestContext requestContext, @@ -78,7 +100,9 @@ public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expressi ) { if (context == null && serviceProvider == null) - throw new EntityGraphQLCompilerException("Either context or serviceProvider must be provided."); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "Either context or serviceProvider must be provided."); + + Schema.CheckTypeAccess(SchemaType, requestContext); // build separate expression for all root level nodes in the op e.g. op is // query Op1 { @@ -87,69 +111,129 @@ public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expressi // } // people & movies will be the 2 fields that will be 2 separate expressions var result = new ConcurrentDictionary(); + var allErrors = new List(); - IArgumentsTracker? docVariables = BuildDocumentVariables(ref variables); - - foreach (var fieldNode in QueryFields) + // pass to directives + foreach (var directive in Directives) + { + if (directive.VisitNode(DirectiveLocation, Schema, this, Arguments, null, null) == null) + return (result, allErrors); + } + try { - cancellationToken.ThrowIfCancellationRequested(); + IArgumentsTracker? docVariables = BuildDocumentVariables(ref variables); + CompileContext compileContext = new(options, null, requestContext, OpVariableParameter, docVariables, cancellationToken); - try + foreach (var fieldNode in QueryFields) { -#if DEBUG - Stopwatch? timer = null; - if (options.IncludeDebugInfo) + cancellationToken.ThrowIfCancellationRequested(); + + try { - timer = new Stopwatch(); - timer.Start(); - } + var contextToUse = GetContextToUse(context, serviceProvider!, fieldNode)!; + + var expandedFields = fieldNode.Expand(compileContext, fragments, false, NextFieldContext!, OpVariableParameter, docVariables).Cast(); + if (!expandedFields.Any()) + continue; + + foreach (var expandedField in expandedFields) + { + try + { +#if DEBUG + Stopwatch? timer = null; + if (options.IncludeDebugInfo) + { + timer = new Stopwatch(); + timer.Start(); + } #endif - var contextToUse = GetContextToUse(context, serviceProvider!, fieldNode)!; - (var data, var didExecute) = await CompileAndExecuteNodeAsync( - new CompileContext(options, null, requestContext, cancellationToken), - contextToUse, - serviceProvider, - fragments, - fieldNode, - docVariables - ); + var (data, didExecute, fieldErrors) = await ExecuteOperationField(compileContext, expandedField, contextToUse, serviceProvider, fragments, docVariables); + #if DEBUG - if (options.IncludeDebugInfo) - { - timer?.Stop(); - result[$"__{fieldNode.Name}_timeMs"] = timer?.ElapsedMilliseconds; - } + if (options.IncludeDebugInfo) + { + timer?.Stop(); + result[$"__{expandedField.Name}_timeMs"] = timer?.ElapsedMilliseconds; + } #endif - // often use return null if mutation failed and added errors to validation - // don't include it if it is not a nullable field - if (data == null && fieldNode.Field?.ReturnType.TypeNotNullable == true) - continue; + if (fieldErrors.Count > 0) + { + // if the type is nullable the error bubbles up + // should be be on the inner field but the way we resolve full expression trees we don't get the error at that level + if (expandedField.Field?.ReturnType.TypeNotNullable == false) + result[expandedField.Name] = null; - if (didExecute) - result[fieldNode.Name] = data; - } - catch (EntityGraphQLValidationException) - { - throw; - } - catch (EntityGraphQLFieldException) - { - throw; - } - catch (Exception ex) - { - throw new EntityGraphQLFieldException(fieldNode.Name, ex); + allErrors.AddRange(fieldErrors); + } + + // often use return null if mutation failed and added errors to validation + // don't include it if it is not a nullable field + if (data == null && expandedField.Field?.ReturnType.TypeNotNullable == true) + continue; + + if (didExecute) + result[expandedField.Name] = data; + } + catch (EntityGraphQLFieldException fe) + { + allErrors.Add(new GraphQLError(Schema.AllowedExceptionMessage(fe), expandedField.BuildPath(), null)); + } + catch (EntityGraphQLException ve) + { + allErrors.AddRange(Schema.GenerateErrors(ve)); + if (expandedField.Field?.ReturnType.TypeNotNullable == false) + result[expandedField.Name] = null; + } + catch (TargetInvocationException tie) when (tie.InnerException != null) + { + allErrors.AddRange(Schema.GenerateErrors(tie.InnerException, expandedField.Name)); + if (expandedField.Field?.ReturnType.TypeNotNullable == false) + result[expandedField.Name] = null; + } + catch (Exception ex) + { + allErrors.AddRange( + Schema.GenerateErrors( + new EntityGraphQLException( + GraphQLErrorCategory.ExecutionError, + $"Field '{expandedField.Name}' - {Schema.AllowedExceptionMessage(ex)}", + null, + expandedField.BuildPath(), + ex + ) + ) + ); + if (expandedField.Field?.ReturnType.TypeNotNullable == false) + result[expandedField.Name] = null; + } + } + } + catch (Exception ex) + { + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Error executing field {fieldNode.Name}", null, fieldNode.BuildPath(), ex); + } } } - return result; + // building the variables could cause this + catch (EntityGraphQLException ce) + { + allErrors.AddRange(Schema.GenerateErrors(ce)); + } + + if (allErrors.Count > 0 && result.IsEmpty) + result = null!; // if we have errors and no data, return null for data + + return (result, allErrors); } protected static TContext GetContextToUse(TContext? context, IServiceProvider serviceProvider, BaseGraphQLField fieldNode) { if (context == null) - return serviceProvider.GetService()! ?? throw new EntityGraphQLCompilerException($"Could not find service of type {typeof(TContext).Name} to execute field {fieldNode.Name}"); + return serviceProvider.GetService()! + ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Could not find service of type {typeof(TContext).Name} to execute field {fieldNode.Name}"); return context; } @@ -170,22 +254,23 @@ protected static TContext GetContextToUse(TContext? context, IServiceP object? argValue = null; if (variables.ContainsKey(name) || argType.DefaultValue.IsSet) { - argValue = ExpressionUtil.ConvertObjectType(variables.GetValueOrDefault(name) ?? argType.DefaultValue.Value, argType.RawType, Schema, null); + argValue = ExpressionUtil.ConvertObjectType(variables.GetValueOrDefault(name) ?? argType.DefaultValue.Value, argType.RawType, Schema); variablesToUse!.MarkAsSet(name); } if (argValue == null && argType.IsRequired) - throw new EntityGraphQLCompilerException( + throw new EntityGraphQLException( + GraphQLErrorCategory.DocumentError, $"Supplied variable '{name}' is null while the variable definition is non-null. Please update query document or supply a non-null value." ); OpVariableParameter.Type.GetField(name)!.SetValue(variablesToUse, argValue); } - catch (EntityGraphQLCompilerException) + catch (EntityGraphQLException) { throw; } catch (Exception ex) { - throw new EntityGraphQLCompilerException($"Supplied variable '{name}' can not be applied to defined variable type '{argType.Type}'", ex); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Supplied variable '{name}' can not be applied to defined variable type '{argType.Type}'", null, null, ex); } } } @@ -202,72 +287,87 @@ protected static TContext GetContextToUse(TContext? context, IServiceP IArgumentsTracker? docVariables ) { - object? runningContext = context; - - var replacer = new ParameterReplacer(); - // For root/top level fields we need to first select the whole graph without fields that require services - // so that EF Core can run and optimize the query against the DB - // We then select the full graph from that context - - if (node.RootParameter == null) - throw new EntityGraphQLCompilerException($"Root parameter not set for {node.Name}"); - - Expression? expression = null; - var contextParam = node.RootParameter; - bool isSecondExec = false; - - if (node.HasServicesAtOrBelow(fragments) && compileContext.ExecutionOptions.ExecuteServiceFieldsSeparately) + try { - // build this first as NodeExpression may modify ConstantParameters - // this is without fields that require services - expression = node.GetNodeExpression(compileContext, serviceProvider, fragments, OpVariableParameter, docVariables, contextParam, withoutServiceFields: true, null, null, false, replacer); - if (expression != null) - { - // execute expression now and get a result that we will then perform a full select over - // This part is happening via EntityFramework if you use it - (runningContext, _) = await ExecuteExpressionAsync(expression, runningContext!, contextParam, serviceProvider, replacer, compileContext, node, false, fragments, false); - if (runningContext == null) - return (null, true); + object? runningContext = context; - // the full selection is now on the anonymous type returned by the selection without fields. We don't know the type until now - var newContextType = Expression.Parameter(runningContext.GetType(), "ctx_no_srv"); + var replacer = new ParameterReplacer(); + // For root/top level fields we need to first select the whole graph without fields that require services + // so that EF Core can run and optimize the query against the DB + // We then select the full graph from that context - // core context data is fetched. Now fetch all the bulk resolvers - var bulkData = await ResolveBulkLoadersAsync(compileContext, serviceProvider, node, runningContext, replacer, newContextType); + if (node.RootParameter == null) + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Root parameter not set for {node.Name}"); - // new context - compileContext = new(compileContext.ExecutionOptions, bulkData, compileContext.RequestContext, compileContext.CancellationToken); + Expression? expression = null; + var contextParam = node.RootParameter; + bool isSecondExec = false; - // we now know the selection type without services and need to build the full select on that type - // need to rebuild the full query + bool hasServicesAtOrBelow = node.HasServicesAtOrBelow(fragments); + if (hasServicesAtOrBelow && compileContext.ExecutionOptions.ExecuteServiceFieldsSeparately) + { + // build this first as NodeExpression may modify ConstantParameters + // this is without fields that require services expression = node.GetNodeExpression( compileContext, serviceProvider, fragments, OpVariableParameter, docVariables, - newContextType, - false, - replacementNextFieldContext: newContextType, + contextParam, + withoutServiceFields: true, null, - contextChanged: true, + null, + false, replacer ); - contextParam = newContextType; - isSecondExec = true; + if (expression != null) + { + // execute expression now and get a result that we will then perform a full select over + // This part is happening via EntityFramework if you use it + (runningContext, _) = await ExecuteExpressionAsync(expression, runningContext!, contextParam, serviceProvider, replacer, compileContext, node, false, fragments, false); + if (runningContext == null) + return (null, true); + + // the full selection is now on the anonymous type returned by the selection without fields. We don't know the type until now + var newContextType = Expression.Parameter(runningContext.GetType(), "ctx_no_srv"); + + // core context data is fetched. Now fetch all the bulk resolvers + var bulkData = await ResolveBulkLoadersAsync(compileContext, serviceProvider, node, runningContext, replacer, newContextType); + + // new context + compileContext = new(compileContext.ExecutionOptions, bulkData, compileContext.RequestContext, OpVariableParameter, docVariables, compileContext.CancellationToken); + + // we now know the selection type without services and need to build the full select on that type + // need to rebuild the full query + expression = node.GetNodeExpression( + compileContext, + serviceProvider, + fragments, + OpVariableParameter, + docVariables, + newContextType, + false, + replacementNextFieldContext: newContextType, + null, + contextChanged: true, + replacer + ); + contextParam = newContextType; + isSecondExec = true; + } } - } -#pragma warning disable IDE0074 // Use compound assignment - if (expression == null) + // no services, or not doing it in 2 steps, build full expression now + expression ??= node.GetNodeExpression(compileContext, serviceProvider, fragments, OpVariableParameter, docVariables, contextParam, false, null, null, contextChanged: false, replacer); + + var data = await ExecuteExpressionAsync(expression, runningContext, contextParam, serviceProvider, replacer, compileContext, node, true, fragments, isSecondExec); + return data; + } + catch (EntityGraphQLFieldException fe) { - // just do things normally - expression = node.GetNodeExpression(compileContext, serviceProvider, fragments, OpVariableParameter, docVariables, contextParam, false, null, null, contextChanged: false, replacer); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Field '{node.Name}' - {fe.Message}"); } -#pragma warning restore IDE0074 // Use compound assignment - - var data = await ExecuteExpressionAsync(expression, runningContext, contextParam, serviceProvider, replacer, compileContext, node, true, fragments, isSecondExec); - return data; } private static async Task> ResolveBulkLoadersAsync( @@ -470,7 +570,7 @@ bool isSecondExec return (null, false); #endif object? res = null; - if (lambdaExpression.ReturnType.IsGenericType && lambdaExpression.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) + if (lambdaExpression.ReturnType.IsGenericType && lambdaExpression.ReturnType.IsAsyncGenericType()) res = await (dynamic?)lambdaExpression.Compile().DynamicInvoke(allArgs.ToArray()); else res = lambdaExpression.Compile().DynamicInvoke(allArgs.ToArray()); @@ -844,4 +944,11 @@ internal static async Task BufferAsyncEnumerable(object asyncEnumerableO return typedList; } + + public IEnumerable BuildPath() + { + if (Name == null) + return []; + return [Name]; + } } diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLCollectionToSingleField.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLCollectionToSingleField.cs index 3e6c1733..78424bdf 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLCollectionToSingleField.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLCollectionToSingleField.cs @@ -62,9 +62,7 @@ public GraphQLCollectionToSingleField(ISchemaProvider schema, GraphQLListSelecti public override bool HasServicesAtOrBelow(IReadOnlyDictionary fragments) { - return CollectionSelectionNode.HasServicesAtOrBelow(fragments) - || ObjectProjectionNode.HasServicesAtOrBelow(fragments) - || ObjectProjectionNode.QueryFields?.Any(f => f.HasServicesAtOrBelow(fragments)) == true; + return CollectionSelectionNode.HasServicesAtOrBelow(fragments) || ObjectProjectionNode.HasServicesAtOrBelow(fragments); } protected override Expression? GetFieldExpression( @@ -160,4 +158,16 @@ ParameterReplacer replacer exp = ExpressionUtil.MakeCallOnQueryable(capMethod, [genericType], result); return exp; } + + public override List QueryFields => CollectionSelectionNode.QueryFields; + + public override void AddField(BaseGraphQLField field) + { + // both need the fields so we can build the right expression + // Update the parent node to be the collection node + // This ensures child fields use the correct parameter context + field.ParentNode = CollectionSelectionNode; + CollectionSelectionNode.QueryFields.Add(field); + ObjectProjectionNode.QueryFields.Add(field); + } } diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLDirective.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLDirective.cs index 5ab58d9f..a2eb6844 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLDirective.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLDirective.cs @@ -28,7 +28,7 @@ public GraphQLDirective(string name, IDirectiveProcessor processor, Dictionary(); + var validationErrors = new HashSet(); var arguments = ArgumentUtil.BuildArgumentsObject( schema, name, @@ -43,7 +43,7 @@ public GraphQLDirective(string name, IDirectiveProcessor processor, Dictionary 0) { - throw new EntityGraphQLValidationException(validationErrors); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, validationErrors); } return processor.VisitNode(location, node, arguments); diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLDocument.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLDocument.cs index 0c4ed174..81d92182 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLDocument.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLDocument.cs @@ -5,7 +5,6 @@ using System.Threading; using System.Threading.Tasks; using EntityGraphQL.Schema; -using Microsoft.Extensions.DependencyInjection; namespace EntityGraphQL.Compiler; @@ -56,7 +55,7 @@ public GraphQLDocument(ISchemaProvider schema) public string Name => "Query Request Root"; public IField? Field { get; } - public bool HasServices => Field?.Services.Count > 0; + public bool HasServices => Field?.Services.Count > 0 || Field?.ExecuteAsService == true; public IReadOnlyDictionary Arguments { get; } @@ -97,27 +96,24 @@ public async Task ExecuteQueryAsync( { // check operation names if (Operations.Count > 1 && Operations.Any(o => string.IsNullOrEmpty(o.Name))) - throw new EntityGraphQLExecutionException("An operation name must be defined for all operations if there are multiple operations in the request"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "An operation name must be defined for all operations if there are multiple operations in the request"); var result = new QueryResult(); - IGraphQLValidator? validator = serviceProvider?.GetService(); var op = string.IsNullOrEmpty(operationName) ? Operations.First() : Operations.First(o => o.Name == operationName); // execute the selected operation options ??= new ExecutionOptions(); // defaults - result.SetData( - await op.ExecuteAsync( - overwriteContext, - serviceProvider, - Fragments, - Schema.SchemaFieldNamer, - options, - variables, - requestContext ?? new QueryRequestContext(Schema.AuthorizationService, null), - cancellationToken - ) + var (data, errors) = await op.ExecuteAsync( + overwriteContext, + serviceProvider, + Fragments, + options, + variables, + requestContext ?? new QueryRequestContext(Schema.AuthorizationService, null), + cancellationToken ); + result.SetData(data); // Add query information if requested if (options.IncludeQueryInfo) @@ -126,10 +122,11 @@ await op.ExecuteAsync( result.SetQueryInfo(queryInfo); } - if (validator?.Errors.Count > 0) - result.AddErrors(validator.Errors); + if (errors.Count > 0) + result.AddErrors(errors); - if (result.Data?.Count == 0 && result.HasErrorKey()) + // If we have no data keys & no have execution errors, we must have a request error and remove the data key. + if (result.Data?.Count == 0 && result.HasErrorKey() && !errors.Where(e => e.IsExecutionError).Any()) result.RemoveDataKey(); return result; @@ -144,4 +141,9 @@ public void AddDirectives(IEnumerable graphQLDirectives) { throw new NotImplementedException(); } + + public IEnumerable BuildPath() + { + return [Name]; + } } diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLExtractedField.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLExtractedField.cs index ae01b49a..91fd1b8b 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLExtractedField.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLExtractedField.cs @@ -64,6 +64,6 @@ public Expression GetNodeExpression(Expression replacementNextFieldContext, List } } } - throw new EntityGraphQLCompilerException($"Could not find field {Name} on type {replacementNextFieldContext.Type.Name}"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Could not find field {Name} on type {replacementNextFieldContext.Type.Name}"); } } diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLFragmentSpreadField.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLFragmentSpreadField.cs index 146d727f..62d74ccb 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLFragmentSpreadField.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLFragmentSpreadField.cs @@ -13,12 +13,12 @@ public class GraphQLFragmentSpreadField : BaseGraphQLField public GraphQLFragmentSpreadField(ISchemaProvider schema, string name, Expression? nodeExpression, ParameterExpression rootParameter, IGraphQLNode parentNode) : base(schema, null, name, nodeExpression, rootParameter, parentNode, null) { - LocationForDirectives = ExecutableDirectiveLocation.FRAGMENT_SPREAD; + LocationForDirectives = ExecutableDirectiveLocation.FragmentSpread; } public override bool HasServicesAtOrBelow(IReadOnlyDictionary fragments) { - var fragment = fragments.GetValueOrDefault(Name) ?? throw new EntityGraphQLCompilerException($"Fragment {Name} not found in query document"); + var fragment = fragments.GetValueOrDefault(Name) ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Fragment {Name} not found in query document"); return fragment.QueryFields.Any(f => f.HasServicesAtOrBelow(fragments)); } @@ -32,7 +32,7 @@ protected override IEnumerable ExpandField( IArgumentsTracker? docVariables ) { - var fragment = fragments.GetValueOrDefault(Name) ?? throw new EntityGraphQLCompilerException($"Fragment {Name} not found in query document"); + var fragment = fragments.GetValueOrDefault(Name) ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Fragment {Name} not found in query document"); var fields = fragment.QueryFields.SelectMany(f => f.Expand(compileContext, fragments, withoutServiceFields, fieldContext, docParam, docVariables)); // the current op did not know about services in the fragment as the fragment definition may be after the operation in the query // we now know if there are services we need to know about for executing @@ -83,6 +83,6 @@ private static void GetServices(CompileContext compileContext, BaseGraphQLField ParameterReplacer replacer ) { - throw new EntityGraphQLCompilerException($"Fragment should have expanded out into non-fragment fields"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Fragment should have expanded out into non-fragment fields"); } } diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLFragmentStatement.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLFragmentStatement.cs index 2a5ffe72..1a3c82b1 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLFragmentStatement.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLFragmentStatement.cs @@ -12,7 +12,7 @@ public class GraphQLFragmentStatement : IGraphQLNode public ParameterExpression? RootParameter { get; } public IField? Field { get; } - public bool HasServices => Field?.Services.Count > 0; + public bool HasServices => Field?.Services.Count > 0 || Field?.ExecuteAsService == true; public IReadOnlyDictionary Arguments { get; } @@ -41,4 +41,9 @@ public void AddDirectives(IEnumerable graphQLDirectives) { throw new NotImplementedException(); } + + public IEnumerable BuildPath() + { + return [Name]; + } } diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLInlineFragmentField.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLInlineFragmentField.cs index a5a902be..6423d43f 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLInlineFragmentField.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLInlineFragmentField.cs @@ -13,7 +13,7 @@ public class GraphQLInlineFragmentField : BaseGraphQLField public GraphQLInlineFragmentField(ISchemaProvider schema, string name, Expression? nodeExpression, ParameterExpression rootParameter, IGraphQLNode parentNode) : base(schema, null, name, nodeExpression, rootParameter, parentNode, null) { - LocationForDirectives = ExecutableDirectiveLocation.INLINE_FRAGMENT; + LocationForDirectives = ExecutableDirectiveLocation.InlineFragment; } public override bool HasServicesAtOrBelow(IReadOnlyDictionary fragments) @@ -47,6 +47,6 @@ protected override IEnumerable ExpandField( ParameterReplacer replacer ) { - throw new EntityGraphQLCompilerException($"Inline fragment should have expanded out into non-fragment fields"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Inline fragment should have expanded out into non-fragment fields"); } } diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLListSelectionField.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLListSelectionField.cs index 95cd4db1..7ed02279 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLListSelectionField.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLListSelectionField.cs @@ -88,7 +88,7 @@ ParameterReplacer replacer nextFieldContext = Expression.Parameter(listContext.Type.GetEnumerableOrArrayType()!, $"{nextFieldContext.Name}2"); } (listContext, var argumentParams) = - Field?.GetExpression(listContext!, replacementNextFieldContext, ParentNode!, schemaContext, compileContext, Arguments, docParam, docVariables, Directives, contextChanged, replacer) + Field?.GetExpression(listContext!, replacementNextFieldContext, this, schemaContext, compileContext, Arguments, docParam, docVariables, Directives, contextChanged, replacer) ?? (ListExpression, null); if (listContext == null) return null; @@ -101,6 +101,9 @@ ParameterReplacer replacer var selectionFields = GetSelectionFields(compileContext, serviceProvider, fragments, docParam, docVariables, withoutServiceFields, nextFieldContext, schemaContext, contextChanged, replacer); + if (HasServices) + compileContext.AddServices(Field!.Services); + if (selectionFields == null || selectionFields.Count == 0) { if (withoutServiceFields && HasServices) @@ -110,24 +113,10 @@ ParameterReplacer replacer (listContext, selectionFields, nextFieldContext) = ProcessExtensionsSelection(listContext, selectionFields, nextFieldContext, argumentParams, contextChanged, replacer); - if (HasServices) - compileContext.AddServices(Field!.Services); - Expression? resultExpression = null; - if (!withoutServiceFields) - { - bool needsServiceWrap = NeedsServiceWrap(withoutServiceFields); - if (needsServiceWrap) - { - // To support a common use case where we are coming from a service result to another service field where the - // service is the Query Context. Which we are assuming is likely an EF context and we don't need the null check - // Use ExecutionOptions.ExecuteServiceFieldsSeparately = false to disable this behavior - var nullCheck = Field!.Services.Any(s => s.Type != Field.Schema.QueryContextType); - (resultExpression, PossibleNextContextTypes) = ExpressionUtil.MakeSelectWithDynamicType(this, nextFieldContext!, listContext, selectionFields, nullCheck, withoutServiceFields); - } - } - - var useNullCheckMethods = contextChanged || !compileContext.ExecutionOptions.ExecuteServiceFieldsSeparately || HasServices; + var isAsync = Field?.IsAsync == true; + var useNullCheckMethods = + contextChanged || !compileContext.ExecutionOptions.ExecuteServiceFieldsSeparately || HasServices || Field?.Services.Any(s => s.Type != Field.Schema.QueryContextType) == true; // have this return both the dynamic types so we can use them next, post-service if (resultExpression == null) (resultExpression, PossibleNextContextTypes) = ExpressionUtil.MakeSelectWithDynamicType( @@ -136,13 +125,14 @@ ParameterReplacer replacer listContext, selectionFields, useNullCheckMethods, + isAsync, withoutServiceFields || !contextChanged ); var resultElementType = resultExpression.Type.GetEnumerableOrArrayType()!; // Make sure lists are evaluated and not deferred otherwise the second pass with services will fail if it needs to wrap for null check above - if (AllowToList && resultExpression.Type.IsEnumerableOrArray() && !resultExpression.Type.IsDictionary()) + if (AllowToList && Field?.IsAsync == false && resultExpression.Type.IsEnumerableOrArray() && !resultExpression.Type.IsDictionary()) resultExpression = useNullCheckMethods ? Expression.Call( typeof(EnumerableExtensions), diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLMutationField.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLMutationField.cs index 38e5680d..361aefdf 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLMutationField.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLMutationField.cs @@ -17,7 +17,7 @@ public GraphQLMutationField( ISchemaProvider schema, string name, MutationField mutationField, - Dictionary? args, + IReadOnlyDictionary? args, Expression nextFieldContext, ParameterExpression rootParameter, IGraphQLNode parentNode @@ -27,21 +27,25 @@ IGraphQLNode parentNode this.MutationField = mutationField; } - public Task ExecuteMutationAsync( + public async Task<(object? data, IGraphQLValidator? methodValidator)> ExecuteMutationAsync( TContext context, IServiceProvider? serviceProvider, ParameterExpression? variableParameter, IArgumentsTracker? variablesToUse, - ExecutionOptions executionOptions + CompileContext compileContext ) { try { - return MutationField.CallAsync(context, Arguments, serviceProvider, variableParameter, variablesToUse, executionOptions); + return await MutationField.CallAsync(context, Arguments, serviceProvider, variableParameter, variablesToUse, compileContext); } - catch (EntityQuerySchemaException e) + catch (EntityGraphQLException ex) { - throw new EntityQuerySchemaException($"Error applying mutation: {e.Message}", e); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, ex.Messages, null, BuildPath(), ex); + } + catch (Exception ex) + { + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, Schema.AllowedExceptionMessage(ex), null, BuildPath(), ex); } } @@ -60,7 +64,7 @@ ParameterReplacer replacer ) { if (ResultSelection == null) - throw new EntityGraphQLCompilerException($"Mutation {Name} should have a result selection"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Mutation {Name} should have a result selection"); return ResultSelection.GetNodeExpression( compileContext, diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLMutationStatement.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLMutationStatement.cs index c922c9e4..9974918b 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLMutationStatement.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLMutationStatement.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Linq.Expressions; -using System.Threading; using System.Threading.Tasks; using EntityGraphQL.Compiler.Util; using EntityGraphQL.Directives; @@ -21,118 +18,77 @@ public class GraphQLMutationStatement : ExecutableGraphQLStatement public GraphQLMutationStatement(ISchemaProvider schema, string? name, Expression nodeExpression, ParameterExpression rootParameter, Dictionary variables) : base(schema, name, nodeExpression, rootParameter, variables) { } - public override async Task> ExecuteAsync( - TContext? context, + protected override ExecutableDirectiveLocation DirectiveLocation => ExecutableDirectiveLocation.Mutation; + protected override ISchemaType SchemaType => Schema.GetSchemaType(Schema.MutationType, false, null)!; + + protected override async Task<(object? data, bool didExecute, List errors)> ExecuteOperationField( + CompileContext compileContext, + BaseGraphQLField field, + TContext context, IServiceProvider? serviceProvider, IReadOnlyDictionary fragments, - Func fieldNamer, - ExecutionOptions options, - QueryVariables? variables, - QueryRequestContext requestContext, - CancellationToken cancellationToken = default + IArgumentsTracker? docVariables ) - where TContext : default { - if (context == null && serviceProvider == null) - throw new EntityGraphQLCompilerException("Either context or serviceProvider must be provided."); + if (field is not GraphQLMutationField node) + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Expected a mutation field but got {field.GetType().Name}"); - Schema.CheckTypeAccess(Schema.GetSchemaType(Schema.MutationType, false, null), requestContext); + var errors = new List(); - var result = new ConcurrentDictionary(); - // pass to directives - foreach (var directive in Directives) - { - if (directive.VisitNode(ExecutableDirectiveLocation.MUTATION, Schema, this, Arguments, null, null) == null) - return result; - } + // For mutations, we need to expand and execute each mutation field individually + var (data, didExecute, methodValidator) = await ExecuteAsync(compileContext, (GraphQLMutationField)field, context, serviceProvider, fragments, docVariables); - // Mutation fields don't directly have services to collect. This is handled after the mutation is executed. - // When we are building/executing the selection on the mutation result services are handled - CompileContext compileContext = new(options, null, requestContext, cancellationToken); - foreach (var field in QueryFields) + if (methodValidator?.HasErrors == true) { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - IArgumentsTracker? docVariables = BuildDocumentVariables(ref variables); - foreach (var node in field.Expand(compileContext, fragments, false, NextFieldContext!, OpVariableParameter, docVariables).Cast()) - { -#if DEBUG - Stopwatch? timer = null; - if (options.IncludeDebugInfo) - { - timer = new Stopwatch(); - timer.Start(); - } -#endif - - var contextToUse = GetContextToUse(context, serviceProvider!, field)!; - var data = await ExecuteAsync(compileContext, node, contextToUse, serviceProvider, fragments, options, docVariables); -#if DEBUG - if (options.IncludeDebugInfo) - { - timer?.Stop(); - result[$"__{node.Name}_timeMs"] = timer?.ElapsedMilliseconds; - } -#endif - // often use return null if mutation failed and added errors to validation - // don't include it if it is not a nullable field - if (data == null && node.Field!.ReturnType.TypeNotNullable) - continue; - result[node.Name] = data; - } - } - catch (EntityGraphQLValidationException) - { - throw; - } - catch (EntityGraphQLFieldException) - { - throw; - } - catch (Exception ex) - { - throw new EntityGraphQLFieldException(field.Name, ex); - } + foreach (var error in methodValidator.Errors) + error.Path = [node.Name]; + errors.AddRange(methodValidator.Errors); } - return result; + + return (data, didExecute, errors); } /// /// Execute the current mutation /// + /// The compile context /// The mutation field to execute /// The context instance that will be used /// A service provider to look up any dependencies /// - /// Execution options /// Resolved values of variables pass in request /// /// - private async Task ExecuteAsync( + private async Task<(object? data, bool didExecute, IGraphQLValidator? methodValidator)> ExecuteAsync( CompileContext compileContext, GraphQLMutationField node, TContext context, IServiceProvider? serviceProvider, IReadOnlyDictionary fragments, - ExecutionOptions options, IArgumentsTracker? docVariables ) { BaseGraphQLField.CheckFieldAccess(Schema, node.Field, compileContext.RequestContext); if (context == null) - return null; + return (null, false, null); + + // apply directives + foreach (var directive in node.Directives) + { + if (directive.VisitNode(ExecutableDirectiveLocation.Field, Schema, node, Arguments, null, null) == null) + return (null, false, null); + } + // run the mutation to get the context for the query select - var result = await node.ExecuteMutationAsync(context, serviceProvider, OpVariableParameter, docVariables, options); + var (data, validatorErrors) = await node.ExecuteMutationAsync(context, serviceProvider, OpVariableParameter, docVariables, compileContext); if ( - result == null + data == null || // result is null and don't need to do anything more node.ResultSelection == null ) // mutation must return a scalar type - return result; - return await MakeSelectionFromResultAsync(compileContext, node, node.ResultSelection!, context, serviceProvider, fragments, docVariables, result); + return (data, true, validatorErrors); + return (await MakeSelectionFromResultAsync(compileContext, node, node.ResultSelection!, context, serviceProvider, fragments, docVariables, data), true, validatorErrors); } protected async Task MakeSelectionFromResultAsync( diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLObjectProjectionField.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLObjectProjectionField.cs index afb46f0f..eb887381 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLObjectProjectionField.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLObjectProjectionField.cs @@ -89,37 +89,56 @@ ParameterReplacer replacer nextFieldContext = ReplaceContext(replacementNextFieldContext!, replacer, nextFieldContext!, possibleNextContextTypes); } (nextFieldContext, var argumentParam) = - Field?.GetExpression(nextFieldContext!, replacementNextFieldContext, ParentNode!, schemaContext, compileContext, Arguments, docParam, docVariables, Directives, contextChanged, replacer) + Field?.GetExpression(nextFieldContext!, replacementNextFieldContext, this, schemaContext, compileContext, Arguments, docParam, docVariables, Directives, contextChanged, replacer) ?? (nextFieldContext, null); if (nextFieldContext == null) return null; HandleBeforeRootFieldExpressionBuild(compileContext, GetOperationName(this), Name, contextChanged, IsRootField, ref nextFieldContext); - bool needsServiceWrap = NeedsServiceWrap(withoutServiceFields); - (nextFieldContext, _) = ProcessExtensionsPreSelection(nextFieldContext, null, replacer); // if we have services and they don't want service fields, return the expression only for extraction if (withoutServiceFields && HasServices && !IsRootField) return nextFieldContext; - var selectionFields = GetSelectionFields(compileContext, serviceProvider, fragments, docParam, docVariables, withoutServiceFields, nextFieldContext, schemaContext, contextChanged, replacer); + var selectionContext = nextFieldContext; + bool needsServiceWrap = NeedsServiceWrap(withoutServiceFields) || ((nextFieldContext.NodeType == ExpressionType.MemberInit || nextFieldContext.NodeType == ExpressionType.New) && IsRootField); + + if (Field?.IsAsync == true && !contextChanged) + { + // for async fields we need to build the selection on the result of the task + var resultType = nextFieldContext.Type.GetGenericArguments()[0]; + selectionContext = Expression.Parameter(resultType, $"{Name}_result"); + } + else if (needsServiceWrap) + { + // we need to build the selection on a parameter of the result type + selectionContext = Expression.Parameter(nextFieldContext.Type, $"{Name}_result"); + } + var selectionFields = GetSelectionFields(compileContext, serviceProvider, fragments, docParam, docVariables, withoutServiceFields, selectionContext, schemaContext, contextChanged, replacer); if (selectionFields == null || selectionFields.Count == 0) return null; if (HasServices) compileContext.AddServices(Field!.Services); - if (needsServiceWrap || ((nextFieldContext.NodeType == ExpressionType.MemberInit || nextFieldContext.NodeType == ExpressionType.New) && IsRootField)) + // build a new {...} - returning a single object {} + (nextFieldContext, selectionFields, _) = ProcessExtensionsSelection(nextFieldContext, selectionFields, null, argumentParam, contextChanged, replacer); + var newExp = ExpressionUtil.CreateNewExpressionWithInterfaceOrUnionCheck(Name, nextFieldContext, Field!.ReturnType, selectionFields, out Type anonType)!; + + if (needsServiceWrap || Field?.IsAsync == true) { - nextFieldContext = WrapWithNullCheck(compileContext, selectionFields, serviceProvider, nextFieldContext, schemaContext, argumentParam, contextChanged, replacer); + nextFieldContext = Expression.Call( + typeof(EnumerableExtensions), + nameof(EnumerableExtensions.ProjectWithNullCheck), + [selectionContext.Type, anonType], + nextFieldContext, + Expression.Lambda(newExp, (ParameterExpression)selectionContext) + ); } else { - (nextFieldContext, selectionFields, _) = ProcessExtensionsSelection(nextFieldContext, selectionFields, null, argumentParam, contextChanged, replacer); - // build a new {...} - returning a single object {} - var newExp = ExpressionUtil.CreateNewExpressionWithInterfaceOrUnionCheck(Name, nextFieldContext, Field!.ReturnType, selectionFields, out Type anonType); var isNullable = !nextFieldContext.Type.IsValueType || nextFieldContext.Type.IsNullableType(); if (isNullable && nextFieldContext.NodeType != ExpressionType.MemberInit && nextFieldContext.NodeType != ExpressionType.New) { @@ -140,83 +159,6 @@ ParameterReplacer replacer return nextFieldContext; } - /// - /// These expression will be built on the element type - /// we might be using a service i.e. ctx => WithService((T r) => r.DoSomething(ctx.Entities.Select(f => f.Id).ToList())) - /// if we can we want to avoid calling that multiple times with a expression like - /// r.DoSomething(ctx.Entities.Select(f => f.Id).ToList()) == null ? null : new { - /// Field = r.DoSomething(ctx.Entities.Select(f => f.Id).ToList()).Blah - /// } - /// by wrapping the whole thing in a method that does the null check once. - /// This means we build the fieldExpressions on a parameter of the result type - /// - /// Fields to select once we know if this result is null or not - /// - /// The expression that the selection fields will be built from - /// - /// Has the context changes (second pass with services) - /// - /// - private Expression WrapWithNullCheck( - CompileContext compileContext, - Dictionary selectionFields, - IServiceProvider? serviceProvider, - Expression nextFieldContext, - ParameterExpression schemaContext, - ParameterExpression? argumentParam, - bool contextChanged, - ParameterReplacer replacer - ) - { - // selectionFields is set up but we need to wrap - // we wrap here as we have access to the values and services etc - var fieldParamValues = new List(compileContext.ConstantParameters.Values); - var fieldParams = new List(compileContext.ConstantParameters.Keys); - - // do not need to inject services here as this expression is used in the call arguments - var updatedExpression = nextFieldContext; - // replace with null_wrap - // this is the parameter used in the null wrap. We pass it to the wrap function which has the value to match - var nullWrapParam = Expression.Parameter(updatedExpression.Type, "nullwrap"); - - if (contextChanged) - { - HashSet propsOrFields = [.. nullWrapParam.Type.GetProperties().Select(i => i.Name), .. nullWrapParam.Type.GetFields().Select(i => i.Name)]; - foreach (var item in selectionFields) - { - if (item.Value.Field.HasServices || item.Key.Name == "__typename") - item.Value.Expression = replacer.ReplaceByType(item.Value.Expression, nextFieldContext.Type, nullWrapParam); - else - { - // if we can just use Expression.PropertyOrField that is faster - if (propsOrFields.Contains(item.Key.Name, StringComparer.OrdinalIgnoreCase)) - item.Value.Expression = Expression.PropertyOrField(nullWrapParam, item.Key.Name); - else - // selecting from a dotnet type (not a anonymous result type) we need to replace the base call not use the field name (item.Key.Name) - // e.g. if we have renamed the field schema.Type().AddField("username", u => u.Name) - item.Value.Expression = replacer.Replace(item.Value.Expression, nextFieldContext, nullWrapParam); - } - } - } - else - { - foreach (var item in selectionFields) - { - item.Value.Expression = replacer.ReplaceByType(item.Value.Expression, nextFieldContext.Type, nullWrapParam); - } - } - - (updatedExpression, selectionFields, _) = ProcessExtensionsSelection(updatedExpression, selectionFields, null, argumentParam, contextChanged, replacer); - // we need to make sure the wrap can resolve any services in the select - var selectionExpressions = selectionFields.ToDictionary( - f => f.Key.Name, - f => GraphQLHelper.InjectServices(serviceProvider!, compileContext.Services, fieldParamValues, f.Value.Expression, fieldParams, replacer, compileContext.CancellationToken) - ); - - updatedExpression = ExpressionUtil.WrapObjectProjectionFieldForNullCheck(Name, updatedExpression, fieldParams, selectionExpressions, fieldParamValues, nullWrapParam, schemaContext); - return updatedExpression; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void HandleBulkResolverForField( CompileContext compileContext, diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLQueryStatement.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLQueryStatement.cs index 2dbbcd33..0450eb25 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLQueryStatement.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLQueryStatement.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq.Expressions; -using System.Threading; using System.Threading.Tasks; using EntityGraphQL.Directives; using EntityGraphQL.Schema; @@ -11,30 +9,29 @@ namespace EntityGraphQL.Compiler; public class GraphQLQueryStatement : ExecutableGraphQLStatement { - public GraphQLQueryStatement(ISchemaProvider schema, string? name, Expression nodeExpression, ParameterExpression rootParameter, Dictionary variables) + public GraphQLQueryStatement(ISchemaProvider schema, string? name, Expression nodeExpression, ParameterExpression rootParameter, IReadOnlyDictionary variables) : base(schema, name, nodeExpression, rootParameter, variables) { } - public override Task> ExecuteAsync( - TContext? context, + protected override ExecutableDirectiveLocation DirectiveLocation => ExecutableDirectiveLocation.Query; + protected override ISchemaType SchemaType => Schema.GetSchemaType(Schema.QueryContextType, false, null)!; + + protected override async Task<(object? data, bool didExecute, List errors)> ExecuteOperationField( + CompileContext compileContext, + BaseGraphQLField field, + TContext context, IServiceProvider? serviceProvider, IReadOnlyDictionary fragments, - Func fieldNamer, - ExecutionOptions options, - QueryVariables? variables, - QueryRequestContext requestContext, - CancellationToken cancellationToken = default + IArgumentsTracker? docVariables ) - where TContext : default { - Schema.CheckTypeAccess(Schema.GetSchemaType(Schema.QueryContextType, false, null), requestContext); - - var result = new ConcurrentDictionary(); - // pass to directives - foreach (var directive in Directives) + // apply directives + foreach (var directive in field.Directives) { - if (directive.VisitNode(ExecutableDirectiveLocation.QUERY, Schema, this, Arguments, null, null) == null) - return Task.FromResult(result); + if (directive.VisitNode(ExecutableDirectiveLocation.Field, Schema, field, Arguments, null, null) == null) + return (null, false, []); } - return base.ExecuteAsync(context, serviceProvider, fragments, fieldNamer, options, variables, requestContext, cancellationToken); + (var data, var didExecute) = await CompileAndExecuteNodeAsync(compileContext, context!, serviceProvider, fragments, field, docVariables); + + return (data, didExecute, new List()); } } diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLScalarField.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLScalarField.cs index ceca1af8..fca1fc43 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLScalarField.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLScalarField.cs @@ -19,11 +19,6 @@ public GraphQLScalarField( ) : base(schema, field, name, nextFieldContext, rootParameter, parentNode, arguments) { } - public override bool HasServicesAtOrBelow(IReadOnlyDictionary fragments) - { - return Field?.Services.Count > 0; - } - protected override Expression? GetFieldExpression( CompileContext compileContext, IServiceProvider? serviceProvider, @@ -59,7 +54,7 @@ ParameterReplacer replacer (var result, _) = Field!.GetExpression( nextFieldContext, replacementNextFieldContext, - ParentNode!, + this, schemaContext, compileContext, Arguments, diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLSubscriptionField.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLSubscriptionField.cs index e0c78dc3..6961d86e 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLSubscriptionField.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLSubscriptionField.cs @@ -17,7 +17,7 @@ public GraphQLSubscriptionField( ISchemaProvider schema, string name, SubscriptionField subscriptionField, - Dictionary? args, + IReadOnlyDictionary? args, Expression nextFieldContext, ParameterExpression rootParameter, IGraphQLNode parentNode @@ -27,21 +27,21 @@ IGraphQLNode parentNode this.SubscriptionField = subscriptionField; } - public Task ExecuteSubscriptionAsync( + public Task<(object? data, IGraphQLValidator? methodValidator)> ExecuteSubscriptionAsync( TContext context, IServiceProvider? serviceProvider, ParameterExpression? variableParameter, IArgumentsTracker? variablesToUse, - ExecutionOptions executionOptions + CompileContext compileContext ) { try { - return SubscriptionField.CallAsync(context, Arguments, serviceProvider, variableParameter, variablesToUse, executionOptions); + return SubscriptionField.CallAsync(context, Arguments, serviceProvider, variableParameter, variablesToUse, compileContext); } - catch (EntityQuerySchemaException e) + catch (EntityGraphQLException ex) { - throw new EntityQuerySchemaException($"Error registering subscription: {e.Message}", e); + throw new EntityGraphQLException(ex.Category, ex.Message, null, ex.Path ?? BuildPath(), ex); } } @@ -60,7 +60,7 @@ ParameterReplacer replacer ) { if (ResultSelection == null) - throw new EntityGraphQLCompilerException($"Subscription {Name} should have a result selection"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Subscription {Name} should have a result selection"); return ResultSelection.GetNodeExpression( compileContext, diff --git a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLSubscriptionStatement.cs b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLSubscriptionStatement.cs index c2ad5a36..9d392cb6 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLSubscriptionStatement.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLSubscriptionStatement.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Threading; @@ -26,11 +25,13 @@ public class GraphQLSubscriptionStatement : GraphQLMutationStatement public GraphQLSubscriptionStatement(ISchemaProvider schema, string? name, ParameterExpression rootParameter, Dictionary variables) : base(schema, name, rootParameter, rootParameter, variables) { } - public override async Task> ExecuteAsync( + protected override ExecutableDirectiveLocation DirectiveLocation => ExecutableDirectiveLocation.Subscription; + protected override ISchemaType SchemaType => Schema.GetSchemaType(Schema.SubscriptionType, false, null)!; + + public override async Task<(ConcurrentDictionary data, List errors)> ExecuteAsync( TContext? context, IServiceProvider? serviceProvider, IReadOnlyDictionary fragments, - Func fieldNamer, ExecutionOptions options, QueryVariables? variables, QueryRequestContext requestContext, @@ -38,72 +39,35 @@ public GraphQLSubscriptionStatement(ISchemaProvider schema, string? name, Parame ) where TContext : default { - if (context == null && serviceProvider == null) - throw new EntityGraphQLCompilerException("Either context or serviceProvider must be provided."); - - Schema.CheckTypeAccess(Schema.GetSchemaType(Schema.SubscriptionType, false, null), requestContext); - + // Store these for later use in subscription event execution this.fragments = fragments.ToDictionary(f => f.Key, f => f.Value); this.options = options; this.docVariables = BuildDocumentVariables(ref variables); - var result = new ConcurrentDictionary(); - // pass to directives - foreach (var directive in Directives) - { - if (directive.VisitNode(ExecutableDirectiveLocation.SUBSCRIPTION, Schema, this, Arguments, null, null) == null) - return result; - } + return await base.ExecuteAsync(context, serviceProvider, fragments, options, variables, requestContext, cancellationToken); + } + + protected override async Task<(object? data, bool didExecute, List errors)> ExecuteOperationField( + CompileContext compileContext, + BaseGraphQLField field, + TContext context, + IServiceProvider? serviceProvider, + IReadOnlyDictionary fragments, + IArgumentsTracker? docVariables + ) + { + if (field is not GraphQLSubscriptionField) + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Expected a subscription field but got {field.GetType().Name}"); - CompileContext compileContext = new(options, null, requestContext); - foreach (var field in QueryFields) + foreach (var directive in field.Directives) { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - foreach (var node in field.Expand(compileContext, fragments, false, NextFieldContext!, OpVariableParameter, docVariables).Cast()) - { -#if DEBUG - Stopwatch? timer = null; - if (options.IncludeDebugInfo) - { - timer = new Stopwatch(); - timer.Start(); - } -#endif - var contextToUse = GetContextToUse(context, serviceProvider!, node)!; - var data = await ExecuteAsync(node, contextToUse, serviceProvider, docVariables, options, requestContext, cancellationToken); -#if DEBUG - if (options.IncludeDebugInfo) - { - timer?.Stop(); - result[$"__{node.Name}_timeMs"] = timer?.ElapsedMilliseconds; - } -#endif - - // often use return null if mutation failed and added errors to validation - // don't include it if it is not a nullable field - if (data == null && node.Field!.ReturnType.TypeNotNullable) - continue; - - result[node.Name] = data; - } - } - catch (EntityGraphQLValidationException) - { - throw; - } - catch (EntityGraphQLFieldException) - { - throw; - } - catch (Exception ex) - { - throw new EntityGraphQLFieldException(field.Name, ex); - } + if (directive.VisitNode(ExecutableDirectiveLocation.Field, Schema, field, Arguments, null, null) == null) + return (null, false, []); } - return result; + + // For subscriptions, we need to expand and execute each subscription field individually + var data = await ExecuteAsync((GraphQLSubscriptionField)field, context, serviceProvider, docVariables, compileContext); + return (data, true, new List()); } private async Task ExecuteAsync( @@ -111,25 +75,24 @@ public GraphQLSubscriptionStatement(ISchemaProvider schema, string? name, Parame TContext context, IServiceProvider? serviceProvider, IArgumentsTracker? docVariables, - ExecutionOptions executionOptions, - QueryRequestContext requestContext, - CancellationToken cancellationToken = default + CompileContext compileContext ) { if (context == null) return null; - BaseGraphQLField.CheckFieldAccess(Schema, node.Field, requestContext); + BaseGraphQLField.CheckFieldAccess(Schema, node.Field, compileContext.RequestContext); // execute the subscription set up method. It returns in IObservable - var result = await node.ExecuteSubscriptionAsync(context, serviceProvider, OpVariableParameter, docVariables, executionOptions); + var (result, _) = await node.ExecuteSubscriptionAsync(context, serviceProvider, OpVariableParameter, docVariables, compileContext); if (result == null || node.ResultSelection == null) - throw new EntityGraphQLExecutionException($"Subscription {node.Name} returned null. It must return an IObservable"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Subscription {node.Name} returned null. It must return an IObservable"); // result == IObservable var returnType = - result.GetType().GetGenericArgument(typeof(IObservable<>)) ?? throw new EntityGraphQLExecutionException($"Subscription {node.Name} return type does not implement IObservable"); + result.GetType().GetGenericArgument(typeof(IObservable<>)) + ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Subscription {node.Name} return type does not implement IObservable"); return new GraphQLSubscribeResult(returnType, result, this, node); } @@ -142,14 +105,26 @@ public GraphQLSubscriptionStatement(ISchemaProvider schema, string? name, Parame { var context = (TQueryContext)serviceProvider.GetRequiredService(typeof(TQueryContext)); - var result = MakeSelectionFromResultAsync(new CompileContext(options!, null, requestContext), node, node.ResultSelection!, context, serviceProvider, fragments!, docVariables, eventValue); + var result = MakeSelectionFromResultAsync( + new CompileContext(options!, null, requestContext, OpVariableParameter, docVariables), + node, + node.ResultSelection!, + context, + serviceProvider, + fragments!, + docVariables, + eventValue + ); return result; } public override void AddField(BaseGraphQLField field) { if (QueryFields.Count > 0) - throw new EntityGraphQLCompilerException($"Subscription operations may only have a single root field. Field '{field.Name}' should be used in another operation."); + throw new EntityGraphQLException( + GraphQLErrorCategory.DocumentError, + $"Subscription operations may only have a single root field. Field '{field.Name}' should be used in another operation." + ); field.IsRootField = true; QueryFields.Add(field); } diff --git a/src/EntityGraphQL/Compiler/GqlNodes/IGraphQLNode.cs b/src/EntityGraphQL/Compiler/GqlNodes/IGraphQLNode.cs index f456fba1..1ff51299 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/IGraphQLNode.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/IGraphQLNode.cs @@ -35,4 +35,5 @@ public interface IGraphQLNode /// true if the node is a root level field selection (on query, mutation or subscription type) /// bool IsRootField { get; } + IEnumerable BuildPath(); } diff --git a/src/EntityGraphQL/Compiler/GraphQLCompiler.cs b/src/EntityGraphQL/Compiler/GraphQLCompiler.cs deleted file mode 100644 index bc02fc8b..00000000 --- a/src/EntityGraphQL/Compiler/GraphQLCompiler.cs +++ /dev/null @@ -1,52 +0,0 @@ -using EntityGraphQL.Schema; -using HotChocolate.Language; - -namespace EntityGraphQL.Compiler; - -/// -/// Compiles a Graph QL query document string into an AST for processing. -/// -public class GraphQLCompiler -{ - private readonly ISchemaProvider schemaProvider; - - public GraphQLCompiler(ISchemaProvider schemaProvider) - { - this.schemaProvider = schemaProvider; - } - - /// Parses a GraphQL-like query syntax into a tree representing the requested object graph. E.g. - /// { - /// entity/query { - /// field1, - /// field2, - /// relation { field } - /// }, - /// ... - /// } - /// - /// The returned DataQueryNode is a root node, it's Fields are the top level data queries - public GraphQLDocument Compile(string query, QueryVariables? variables = null) - { - variables ??= []; - return Compile(new QueryRequest { Query = query, Variables = variables }); - } - - public GraphQLDocument Compile(string query) - { - return Compile(new QueryRequest { Query = query }); - } - - public GraphQLDocument Compile(QueryRequest query) - { - if (query.Query == null) - throw new EntityGraphQLCompilerException($"GraphQL Query can not be null"); - - DocumentNode document = Utf8GraphQLParser.Parse(query.Query, ParserOptions.Default); - var walker = new EntityGraphQLQueryWalker(schemaProvider, query.Variables); - walker.Visit(document, null); - if (walker.Document == null) - throw new EntityGraphQLCompilerException($"Error compiling query: {query.Query}"); - return walker.Document; - } -} diff --git a/src/EntityGraphQL/Compiler/GraphQLParser.cs b/src/EntityGraphQL/Compiler/GraphQLParser.cs new file mode 100644 index 00000000..614af63f --- /dev/null +++ b/src/EntityGraphQL/Compiler/GraphQLParser.cs @@ -0,0 +1,1568 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using System.Text; +using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Directives; +using EntityGraphQL.Extensions; +using EntityGraphQL.Schema; + +namespace EntityGraphQL.Compiler; + +public static class GraphQLParser +{ + private static readonly Dictionary EmptyArguments = new(); + private static readonly Dictionary EmptyVariableDefinitions = new(); + + public static GraphQLDocument Parse(QueryRequest request, ISchemaProvider schemaProvide) + { + return Parse(request.Query, schemaProvide, request.Variables ?? new QueryVariables()); + } + + public static GraphQLDocument Parse(string? query, ISchemaProvider schemaProvider) + { + return Parse(query, schemaProvider, new QueryVariables()); + } + + public static GraphQLDocument Parse(string? query, ISchemaProvider schemaProvider, QueryVariables queryVariables) + { + if (query == null) + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"GraphQL Query can not be null"); + + query ??= string.Empty; + var parseContext = new GraphQLParseContext(query, queryVariables); + var reader = new SpanReader(query); + + var document = new GraphQLDocument(schemaProvider); + reader.SkipIgnored(); + while (!reader.End) + { + ParseDefinition(parseContext, document, ref reader); + reader.SkipIgnored(); + } + + ValidateFragmentCycles(document); + return document; + } + + private static void ParseDefinition(GraphQLParseContext parseContext, GraphQLDocument document, ref SpanReader reader) + { + reader.SkipIgnored(); + if (reader.End) + throw CreateParseException(parseContext, "Unexpected end of document.", reader.Position); + + SkipDescription(parseContext, ref reader); + + if (reader.TryConsumeKeyword("fragment")) + ParseFragmentDefinition(parseContext, document, ref reader, document); + else if (reader.TryPeek('{')) + { + var operation = CreateOperation(parseContext, "query", null, EmptyVariableDefinitions, document, ref reader); + parseContext.CurrentOperation = operation; + ParseSelectionSet(parseContext, operation, ref reader); + parseContext.CurrentOperation = null; + document.Operations.Add(operation); + } + else if (reader.TryConsumeKeyword("query")) + document.Operations.Add(ParseOperationDefinition(parseContext, document, ref reader, "query")); + else if (reader.TryConsumeKeyword("mutation")) + document.Operations.Add(ParseOperationDefinition(parseContext, document, ref reader, "mutation")); + else if (reader.TryConsumeKeyword("subscription")) + document.Operations.Add(ParseOperationDefinition(parseContext, document, ref reader, "subscription")); + else + { + var unexpected = reader.Peek(); + var token = unexpected == '\0' ? "EOF" : unexpected.ToString(); + throw CreateParseException(parseContext, $"Unexpected token '{token}' while parsing document.", reader.Position); + } + } + + private static void SkipDescription(GraphQLParseContext parseContext, ref SpanReader reader) + { + if (reader.TryPeek('"')) + { + // could store the description if we have a use for it one day + ParseStringValue(parseContext, ref reader); + reader.SkipIgnored(); + } + } + + private static ExecutableGraphQLStatement ParseOperationDefinition(GraphQLParseContext parseContext, IGraphQLNode node, ref SpanReader reader, string operationType) + { + string? operationName = null; + reader.SkipIgnored(); + if (!reader.End && IsNameStart(reader.Peek())) + { + operationName = ReadName(parseContext, ref reader, skipIgnored: false); + } + + reader.SkipIgnored(); + Dictionary? variables = null; + if (reader.TryConsume('(')) + { + variables = ParseVariableDefinitions(parseContext, ref reader, operationName, node); + } + + var operation = CreateOperation(parseContext, operationType, operationName, variables ?? EmptyVariableDefinitions, node, ref reader); + parseContext.CurrentOperation = operation; + ParseSelectionSet(parseContext, operation, ref reader); + parseContext.CurrentOperation = null; + + return operation; + } + + private static void ParseFragmentDefinition(GraphQLParseContext parseContext, GraphQLDocument document, ref SpanReader reader, IGraphQLNode node) + { + reader.SkipIgnored(); + var fragmentName = ReadName(parseContext, ref reader); + reader.SkipIgnored(); + + if (!reader.TryConsumeKeyword("on")) + throw CreateParseException(parseContext, "Expected 'on' in fragment definition.", reader.Position); + + reader.SkipIgnored(); + var typeName = ReadName(parseContext, ref reader); + var directives = ParseDirectives(parseContext, ref reader, ExecutableDirectiveLocation.FragmentDefinition, node); + + var schemaType = + document.Schema.GetSchemaType(typeName, null) ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Unknown type '{typeName}' in fragment '{fragmentName}'"); + var fragParameter = Expression.Parameter(schemaType.TypeDotnet, $"frag_{typeName}"); + var fragStatement = new GraphQLFragmentStatement(document.Schema, fragmentName, fragParameter, fragParameter); + + parseContext.InFragment = true; + + ParseSelectionSet(parseContext, fragStatement, ref reader); + + if (directives?.Count > 0) + { + foreach (var directive in directives) + { + // TODO args all of these + directive.VisitNode(ExecutableDirectiveLocation.FragmentDefinition, document.Schema, fragStatement, new Dictionary(), null, null); + } + } + + parseContext.InFragment = false; + + document.Fragments.Add(fragmentName, fragStatement); + } + + private static Dictionary ParseVariableDefinitions(GraphQLParseContext parseContext, ref SpanReader reader, string? operationName, IGraphQLNode node) + { + var variables = new Dictionary(); + reader.SkipIgnored(); + + SkipDescription(parseContext, ref reader); + + while (true) + { + if (reader.TryConsume(')')) + break; + + if (!reader.TryConsume('$')) + throw CreateParseException(parseContext, "Expected '$' to start variable definition.", reader.Position); + + var name = ReadName(parseContext, ref reader, skipIgnored: false); + reader.SkipIgnored(); + reader.Expect(':', parseContext, "Expected ':' after variable name."); + + var type = ParseVariableType(parseContext, ref reader); + reader.SkipIgnored(); + + object? defaultValue = null; + if (reader.TryConsume('=')) + { + defaultValue = ParseValue(parseContext, ref reader); + reader.SkipIgnored(); + } + + var directives = ParseDirectives(parseContext, ref reader, ExecutableDirectiveLocation.VariableDefinition, node); + var isRequired = type.OuterNonNull; + if (isRequired && !parseContext.QueryVariables.ContainsKey(name)) + { + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Missing required variable '{name}' on operation '{operationName}'"); + } + + var schemaType = node.Schema.GetSchemaType(type.TypeName, null); + var dotnetType = schemaType.TypeDotnet; + + if (type.IsList) + { + dotnetType = typeof(List<>).MakeGenericType(dotnetType); + } + + if (!isRequired && dotnetType.IsValueType) + { + dotnetType = typeof(Nullable<>).MakeGenericType(dotnetType); + } + + var gqlTypeInfo = new GqlTypeInfo(() => schemaType, dotnetType) + { + TypeNotNullable = isRequired, + ElementTypeNullable = type.IsList && !type.InnerNonNull, + IsList = type.IsList, + }; + + var argType = new ArgType(type.TypeName, dotnetType.Name, gqlTypeInfo, dotnetType) { DefaultValue = new DefaultArgValue(defaultValue != null, defaultValue), IsRequired = isRequired }; + + if (directives?.Count > 0) + { + foreach (var directive in directives) + { + directive.VisitNode(ExecutableDirectiveLocation.VariableDefinition, node.Schema, null, new Dictionary(), null, null); + } + } + + variables.Add(name, argType); + reader.SkipIgnored(); + } + + return variables; + } + + private static GraphQLVariableType ParseVariableType(GraphQLParseContext parseContext, ref SpanReader reader) + { + reader.SkipIgnored(); + + if (reader.TryConsume('[')) + { + var typeName = ReadName(parseContext, ref reader); + reader.SkipIgnored(); + var innerNonNull = reader.TryConsume('!'); + reader.SkipIgnored(); + reader.Expect(']', parseContext, "Expected ']' to close list type."); + reader.SkipIgnored(); + var outerNonNull = reader.TryConsume('!'); + return new GraphQLVariableType(typeName, true, innerNonNull, outerNonNull); + } + + var namedType = ReadName(parseContext, ref reader); + reader.SkipIgnored(); + var nonNull = reader.TryConsume('!'); + return new GraphQLVariableType(namedType, false, false, nonNull); + } + + private static void ParseSelectionSet(GraphQLParseContext parseContext, IGraphQLNode node, ref SpanReader reader) + { + reader.SkipIgnored(); + reader.Expect('{', parseContext, "Expected '{' to start selection set."); + + reader.SkipIgnored(); + + while (!reader.TryPeek('}')) + { + var field = ParseSelection(parseContext, node, ref reader); + node.AddField(field); + reader.SkipIgnored(); + + if (field.Field?.ReturnType.SchemaType.RequiresSelection == true) + { + if ( + (field is GraphQLMutationField mutField && mutField.ResultSelection == null) + || (field is GraphQLSubscriptionField subField && subField.ResultSelection == null) + || (field is not GraphQLMutationField && field is not GraphQLSubscriptionField && field.QueryFields.Count == 0) + ) + throw new EntityGraphQLException($"Field '{field.Name}' requires a selection set defining the fields you would like to select."); + } + } + + reader.Expect('}', parseContext, "Expected '}' to close selection set."); + } + + private static BaseGraphQLField ParseSelection(GraphQLParseContext parseContext, IGraphQLNode node, ref SpanReader reader) + { + reader.SkipIgnored(); + + if (TryConsumeSpread(ref reader)) + { + reader.SkipIgnored(); + if (reader.TryConsumeKeyword("on")) + { + reader.SkipIgnored(); + var typeName = ReadName(parseContext, ref reader); + var directives = ParseDirectives(parseContext, ref reader, ExecutableDirectiveLocation.InlineFragment, node); + + var schemaType = node.Schema.GetSchemaType(typeName, null) ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Unknown type '{typeName}' in inline fragment"); + var fragParameter = Expression.Parameter(schemaType.TypeDotnet, $"frag_{typeName}"); + var inlineFragField = new GraphQLInlineFragmentField(node.Schema, typeName, fragParameter, fragParameter, node.ParentNode!); + ParseSelectionSet(parseContext, inlineFragField, ref reader); + + if (directives != null && directives.Count > 0) + { + inlineFragField.AddDirectives(directives); + foreach (var directive in directives) + { + directive.VisitNode(ExecutableDirectiveLocation.InlineFragment, node.Schema, inlineFragField, new Dictionary(), null, null); + } + } + + return inlineFragField; + } + else + { + var fragmentName = ReadName(parseContext, ref reader); + var directives = ParseDirectives(parseContext, ref reader, ExecutableDirectiveLocation.FragmentSpread, node); + var fragField = new GraphQLFragmentSpreadField(node.Schema, fragmentName, null, node.RootParameter!, node.ParentNode!); + if (directives != null && directives.Count > 0) + { + fragField.AddDirectives(directives); + } + return fragField; + } + } + + return ParseField(parseContext, node, ref reader); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryConsumeSpread(ref SpanReader reader) + { + if (reader.Matches("...")) + { + reader.Advance(3); + return true; + } + + return false; + } + + private static BaseGraphQLField ParseField(GraphQLParseContext parseContext, IGraphQLNode node, ref SpanReader reader) + { + SkipDescription(parseContext, ref reader); + + var aliasOrName = ReadName(parseContext, ref reader); + reader.SkipIgnored(); + + string? alias = null; + var fieldName = aliasOrName; + + if (reader.TryConsume(':')) + { + reader.SkipIgnored(); + fieldName = ReadName(parseContext, ref reader); + alias = aliasOrName; + } + + var arguments = ParseArguments(parseContext, ref reader); + var directives = ParseDirectives(parseContext, ref reader, ExecutableDirectiveLocation.Field, node); + var field = CreateField(parseContext, fieldName, alias, arguments, directives, node); + + if (reader.TryPeek('{')) + { + // For subscription/mutation fields, we need to create and populate the ResultSelection + if (field is GraphQLSubscriptionField subscriptionField) + { + var actualField = subscriptionField.Field!; + var nextContextParam = (ParameterExpression)subscriptionField.NextFieldContext!; + BaseGraphQLQueryField resultSelection; + + if (actualField.ReturnType.IsList) + { + var elementType = actualField.ReturnType.SchemaType.TypeDotnet; + var elementParam = Expression.Parameter(elementType, $"p_{elementType.Name}"); + resultSelection = new GraphQLListSelectionField( + node.Schema, + actualField, + aliasOrName, + elementParam, + field.RootParameter!, + nextContextParam, + node, + subscriptionField.Arguments as Dictionary + ); + } + else + { + resultSelection = new GraphQLObjectProjectionField( + node.Schema, + actualField, + aliasOrName, + nextContextParam, + field.RootParameter!, + subscriptionField, + subscriptionField.Arguments as Dictionary + ); + } + + ParseSelectionSet(parseContext, resultSelection, ref reader); + subscriptionField.ResultSelection = resultSelection; + } + else if (field is GraphQLMutationField mutationField) + { + var actualField = mutationField.Field!; + var nextContextParam = (ParameterExpression)mutationField.NextFieldContext!; + BaseGraphQLQueryField resultSelection; + + if (actualField.ReturnType.IsList) + { + var elementType = actualField.ReturnType.SchemaType.TypeDotnet; + var elementParam = Expression.Parameter(elementType, $"p_{elementType.Name}"); + resultSelection = new GraphQLListSelectionField( + node.Schema, + actualField, + aliasOrName, + elementParam, + field.RootParameter!, + nextContextParam, + node, + mutationField.Arguments as Dictionary + ); + } + else + { + resultSelection = new GraphQLObjectProjectionField( + node.Schema, + actualField, + aliasOrName, + nextContextParam, + field.RootParameter!, + mutationField, + mutationField.Arguments as Dictionary + ); + } + + ParseSelectionSet(parseContext, resultSelection, ref reader); + mutationField.ResultSelection = resultSelection; + } + else + { + ParseSelectionSet(parseContext, field, ref reader); + } + } + + return field; + } + + private static Dictionary ParseArguments(GraphQLParseContext parseContext, ref SpanReader reader) + { + reader.SkipIgnored(); + if (!reader.TryConsume('(')) + return EmptyArguments; + + Dictionary? arguments = null; + reader.SkipIgnored(); + + while (!reader.TryPeek(')')) + { + var argName = ReadName(parseContext, ref reader); + reader.SkipIgnored(); + reader.Expect(':', parseContext, $"Expected ':' after argument name '{argName}'."); + reader.SkipIgnored(); + var value = ParseValue(parseContext, ref reader); + (arguments ??= new Dictionary())[argName] = value; + reader.SkipIgnored(); + } + + reader.Expect(')', parseContext, "Expected ')' to close argument list."); + return arguments ?? EmptyArguments; + } + + private static List? ParseDirectives(GraphQLParseContext parseContext, ref SpanReader reader, ExecutableDirectiveLocation location, IGraphQLNode node) + { + reader.SkipIgnored(); + List? directives = null; + + while (reader.TryConsume('@')) + { + var name = ReadName(parseContext, ref reader, skipIgnored: false); + var arguments = ParseArguments(parseContext, ref reader); + directives ??= new List(); + var processor = node.Schema.GetDirective(name); + if (!processor.Location.Contains(location)) + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Directive '{name}' can not be used on '{location}'"); + + var processedArgs = arguments; + if (processedArgs.Count > 0) + { + foreach (var arg in processedArgs) + { + if (arg.Value is Expression) + { + processedArgs[arg.Key] = arg.Value; + } + } + } + + directives.Add(new GraphQLDirective(name, processor, processedArgs)); + reader.SkipIgnored(); + } + + return directives; + } + + private static object? ParseValue(GraphQLParseContext parseContext, ref SpanReader reader) + { + reader.SkipIgnored(); + if (reader.End) + throw CreateParseException(parseContext, "Unexpected end of document while reading value.", reader.Position); + + var ch = reader.Peek(); + switch (ch) + { + case '$': + reader.Advance(); + if (parseContext.CurrentOperation == null && !parseContext.InFragment) + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "Variable used but no current operation found"); + var variableName = ReadName(parseContext, ref reader, skipIgnored: false); + // If we're in a fragment, we can't resolve the variable yet since fragments don't have operation context + // We'll resolve it later when the fragment is expanded into an operation + if (parseContext.InFragment) + return new VariableReference(variableName); + if (parseContext.CurrentOperation == null) + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "Variable used but no current operation found"); + var variableExpression = Expression.PropertyOrField(parseContext.CurrentOperation.OpVariableParameter!, variableName); + return variableExpression; + case '"': + return ParseStringValue(parseContext, ref reader); + case '[': + return ParseListValue(parseContext, ref reader); + case '{': + return ParseObjectValue(parseContext, ref reader); + case '-': + return ParseNumberValue(parseContext, ref reader); + default: + if (char.IsDigit(ch)) + return ParseNumberValue(parseContext, ref reader); + if (reader.TryConsumeKeyword("true")) + return true; + if (reader.TryConsumeKeyword("false")) + return false; + if (reader.TryConsumeKeyword("null")) + return null; + if (IsNameStart(ch)) + return ReadName(parseContext, ref reader, skipIgnored: false); + + throw CreateParseException(parseContext, $"Unexpected token '{ch}' while parsing value.", reader.Position); + } + } + + private static List ParseListValue(GraphQLParseContext parseContext, ref SpanReader reader) + { + reader.SkipIgnored(); + reader.Expect('[', parseContext, "Expected '[' to start list value."); + var list = new List(); + reader.SkipIgnored(); + + while (!reader.TryPeek(']')) + { + var value = ParseValue(parseContext, ref reader); + list.Add(value); + reader.SkipIgnored(); + } + + reader.Expect(']', parseContext, "Expected ']' to close list value."); + return list; + } + + private static Dictionary ParseObjectValue(GraphQLParseContext parseContext, ref SpanReader reader) + { + reader.SkipIgnored(); + reader.Expect('{', parseContext, "Expected '{' to start object value."); + + var obj = new Dictionary(); + reader.SkipIgnored(); + + while (!reader.TryPeek('}')) + { + var fieldName = ReadName(parseContext, ref reader); + reader.SkipIgnored(); + reader.Expect(':', parseContext, $"Expected ':' after object field '{fieldName}'."); + reader.SkipIgnored(); + var value = ParseValue(parseContext, ref reader); + obj[fieldName] = value; + reader.SkipIgnored(); + } + + reader.Expect('}', parseContext, "Expected '}' to close object value."); + return obj; + } + + private static object ParseNumberValue(GraphQLParseContext parseContext, ref SpanReader reader) + { + var start = reader.Position; + + if (reader.TryConsume('-')) + { + if (reader.End || !char.IsDigit(reader.Peek())) + throw CreateParseException(parseContext, "Invalid number literal.", reader.Position); + } + + if (!reader.End && reader.Peek() == '0') + { + reader.Advance(); + if (!reader.End && char.IsDigit(reader.Peek())) + throw CreateParseException(parseContext, "Invalid number literal with leading zero.", reader.Position); + } + else + { + while (!reader.End && char.IsDigit(reader.Peek())) + reader.Advance(); + } + + var isFloat = false; + + if (!reader.End && reader.Peek() == '.') + { + isFloat = true; + reader.Advance(); + if (reader.End || !char.IsDigit(reader.Peek())) + throw CreateParseException(parseContext, "Invalid float literal.", reader.Position); + while (!reader.End && char.IsDigit(reader.Peek())) + reader.Advance(); + } + + if (!reader.End) + { + var next = reader.Peek(); + if (next == 'e' || next == 'E') + { + isFloat = true; + reader.Advance(); + if (!reader.End && (reader.Peek() == '+' || reader.Peek() == '-')) + reader.Advance(); + if (reader.End || !char.IsDigit(reader.Peek())) + throw CreateParseException(parseContext, "Invalid float literal exponent.", reader.Position); + while (!reader.End && char.IsDigit(reader.Peek())) + reader.Advance(); + } + } + + var span = reader.Slice(start, reader.Position - start); + if (!isFloat && long.TryParse(span, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var longValue)) + return longValue; + + var decimalValue = decimal.Parse(span, NumberStyles.Float, CultureInfo.InvariantCulture); + return decimalValue; + } + + private static string ParseStringValue(GraphQLParseContext parseContext, ref SpanReader reader) + { + reader.SkipIgnored(); + if (reader.Matches("\"\"\"")) + { + reader.Advance(3); + return ParseBlockStringValue(parseContext, ref reader); + } + + reader.Expect('"', parseContext, "Expected '\"' to start string literal."); + var start = reader.Position; + + while (!reader.End) + { + var ch = reader.Peek(); + if (ch == '"') + { + var result = reader.GetString(start, reader.Position - start); + reader.Advance(); + return result; + } + + if (ch == '\\') + { + // Found an escape sequence, fall back to StringBuilder path + return ParseEscapedStringValue(parseContext, ref reader, start); + } + + if (ch < 0x20) + throw CreateParseException(parseContext, "Invalid control character in string literal.", reader.Position); + + reader.Advance(); + } + + throw CreateParseException(parseContext, "Unterminated string literal.", reader.Position); + } + + private static string ParseEscapedStringValue(GraphQLParseContext parseContext, ref SpanReader reader, int start) + { + var sb = new StringBuilder(); + sb.Append(reader.Slice(start, reader.Position - start)); + + while (!reader.End) + { + var ch = reader.Peek(); + if (ch == '"') + { + reader.Advance(); + return sb.ToString(); + } + + if (ch == '\\') + { + reader.Advance(); // consume '\' + if (reader.End) + throw CreateParseException(parseContext, "Unterminated escape sequence in string literal.", reader.Position); + + var escape = reader.Peek(); + reader.Advance(); // consume escape char + sb.Append(ParseEscapedSequence(parseContext, ref reader, escape)); + continue; + } + + if (ch < 0x20) + throw CreateParseException(parseContext, "Invalid control character in string literal.", reader.Position); + + sb.Append(ch); + reader.Advance(); + } + + throw CreateParseException(parseContext, "Unterminated string literal.", reader.Position); + } + + private static string ParseEscapedSequence(GraphQLParseContext parseContext, ref SpanReader reader, char escape) + { + return escape switch + { + '"' => "\"", + '/' => "/", + '\\' => "\\", + 'b' => "\b", + 'f' => "\f", + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'u' => ParseUnicodeEscape(parseContext, ref reader), + _ => throw CreateParseException(parseContext, $"Invalid escape sequence '\\{escape}'.", reader.Position), + }; + } + + private static string ParseUnicodeEscape(GraphQLParseContext parseContext, ref SpanReader reader) + { + // Check for variable-width unicode escape \u{...} (GraphQL Sept 2025 spec) + if (!reader.End && reader.Peek() == '{') + { + reader.Advance(); // consume '{' + + var value = 0; + var digitCount = 0; + + while (!reader.End && reader.Peek() != '}') + { + var ch = reader.Peek(); + if (!IsHexDigit(ch)) + throw CreateParseException(parseContext, "Invalid hex digit in unicode escape sequence.", reader.Position); + + reader.Advance(); + value = (value << 4) + HexValue(ch); + digitCount++; + + // Unicode code points are up to 6 hex digits (0x10FFFF) + if (digitCount > 6) + throw CreateParseException(parseContext, "Unicode escape sequence too long.", reader.Position); + } + + if (reader.End) + throw CreateParseException(parseContext, "Unterminated unicode escape sequence.", reader.Position); + + if (digitCount == 0) + throw CreateParseException(parseContext, "Empty unicode escape sequence.", reader.Position); + + reader.Expect('}', parseContext, "Expected '}' to close unicode escape sequence."); + + // Validate code point range (0x0000 to 0x10FFFF) + if (value > 0x10FFFF) + throw CreateParseException(parseContext, "Unicode code point out of range.", reader.Position); + + // Validate not a surrogate (0xD800-0xDFFF are reserved for UTF-16 surrogates) + if (value >= 0xD800 && value <= 0xDFFF) + throw CreateParseException(parseContext, "Unicode escape sequence cannot specify surrogate code points.", reader.Position); + + return char.ConvertFromUtf32(value); + } + + // Fixed-width 4-digit unicode escape \uXXXX (original format) + if (reader.Remaining < 4) + throw CreateParseException(parseContext, "Incomplete unicode escape sequence.", reader.Position); + + var fixedValue = 0; + for (var i = 0; i < 4; i++) + { + var ch = reader.Peek(); + reader.Advance(); + if (!IsHexDigit(ch)) + throw CreateParseException(parseContext, "Invalid unicode escape sequence.", reader.Position); + + fixedValue = (fixedValue << 4) + HexValue(ch); + } + + // For 4-digit escapes, allow surrogates as they may be part of a surrogate pair + // The spec allows legacy surrogate pair format: \uD83D\uDCA9 + // If it's a surrogate, return it as a char (will be combined with pair in string) + if (fixedValue >= 0xD800 && fixedValue <= 0xDFFF) + { + // Return the surrogate as-is, it will be paired up in the string + return ((char)fixedValue).ToString(); + } + + // Valid standalone code point + try + { + return char.ConvertFromUtf32(fixedValue); + } + catch (ArgumentOutOfRangeException) + { + throw CreateParseException(parseContext, $"Invalid unicode code point: 0x{fixedValue:X4}.", reader.Position); + } + } + + private static string ParseBlockStringValue(GraphQLParseContext parseContext, ref SpanReader reader) + { + var sb = (StringBuilder?)null; + var start = reader.Position; + + while (!reader.End) + { + if (reader.Matches("\"\"\"") && !reader.HasOddNumberOfPrecedingBackslashes()) + { + var length = reader.Position - start; + string raw; + if (sb == null) + { + raw = reader.GetString(start, length); + } + else + { + if (length > 0) + sb.Append(reader.Slice(start, length)); + raw = sb.ToString(); + } + + reader.Advance(3); + return NormalizeBlockString(raw); + } + + var ch = reader.Peek(); + if (ch == '\\') + { + if (sb == null) + sb = new StringBuilder(reader.Position - start + 16); + if (reader.Position > start) + sb.Append(reader.Slice(start, reader.Position - start)); + + reader.Advance(); + if (reader.End) + throw CreateParseException(parseContext, "Unterminated escape sequence in block string literal.", reader.Position); + + var escape = reader.Peek(); + reader.Advance(); + sb.Append(ParseEscapedSequence(parseContext, ref reader, escape)); + start = reader.Position; + continue; + } + + reader.Advance(); + } + + throw CreateParseException(parseContext, "Unterminated block string literal.", reader.Position); + } + + private static ReadOnlySpan TrimCarriageReturn(ReadOnlySpan value) + { + if (value.Length > 0 && value[^1] == '\r') + return value[..^1]; + return value; + } + + private static bool IsWhitespaceLine(ReadOnlySpan source, (int Start, int Length) line) + { + var slice = TrimCarriageReturn(source.Slice(line.Start, line.Length)); + for (int i = 0; i < slice.Length; i++) + { + if (!char.IsWhiteSpace(slice[i])) + return false; + } + return true; + } + + private static string NormalizeBlockString(string raw) + { + if (string.IsNullOrEmpty(raw)) + return string.Empty; + + ReadOnlySpan span = raw.AsSpan(); + int estimatedLineCount = 1; + for (int i = 0; i < span.Length; i++) + { + if (span[i] == '\n') + estimatedLineCount++; + } + + var lines = new List<(int Start, int Length)>(estimatedLineCount); + + int position = 0; + while (position < span.Length) + { + int relativeIndex = span[position..].IndexOf('\n'); + if (relativeIndex >= 0) + { + lines.Add((position, relativeIndex)); + position += relativeIndex + 1; + } + else + { + lines.Add((position, span.Length - position)); + position = span.Length; + } + } + + if (raw.Length > 0 && raw[^1] == '\n') + { + lines.Add((span.Length, 0)); + } + + int startLine = 0; + while (startLine < lines.Count && IsWhitespaceLine(span, lines[startLine])) + startLine++; + + int endLine = lines.Count - 1; + while (endLine >= startLine && IsWhitespaceLine(span, lines[endLine])) + endLine--; + + if (startLine > endLine) + return string.Empty; + + int commonIndent = int.MaxValue; + for (int i = startLine; i <= endLine; i++) + { + var line = TrimCarriageReturn(span.Slice(lines[i].Start, lines[i].Length)); + if (line.Length == 0) + continue; + + int indent = 0; + while (indent < line.Length && (line[indent] == ' ' || line[indent] == '\t')) + { + indent++; + } + + if (indent < line.Length) + commonIndent = Math.Min(commonIndent, indent); + } + + if (commonIndent == int.MaxValue) + commonIndent = 0; + + var builder = new StringBuilder(span.Length); + for (int i = startLine; i <= endLine; i++) + { + var line = TrimCarriageReturn(span.Slice(lines[i].Start, lines[i].Length)); + if (line.Length > commonIndent) + { + builder.Append(line[commonIndent..]); + } + + if (i < endLine) + builder.Append('\n'); + } + + return builder.ToString(); + } + + private static string ReadName(GraphQLParseContext parseContext, ref SpanReader reader, bool skipIgnored = true) + { + if (skipIgnored) + reader.SkipIgnored(); + + if (reader.End) + throw CreateParseException(parseContext, "Unexpected end of document while reading name.", reader.Position); + + var ch = reader.Peek(); + if (!IsNameStart(ch)) + throw CreateParseException(parseContext, $"Invalid name start character '{ch}'.", reader.Position); + + var start = reader.Position; + reader.Advance(); + + while (!reader.End && IsNameContinue(reader.Peek())) + reader.Advance(); + + return reader.GetString(start, reader.Position - start); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static EntityGraphQLException CreateParseException(GraphQLParseContext parseContext, string message, int position) + { + var (line, column) = GetLineAndColumn(parseContext.Source, position); + return new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"{message} (line {line}, column {column})"); + } + + private static (int line, int column) GetLineAndColumn(ReadOnlySpan source, int position) + { + var line = 1; + var column = 1; + var length = Math.Min(position, source.Length); + var span = source[0..length]; + + for (var i = 0; i < span.Length; i++) + { + if (span[i] == '\n') + { + line++; + column = 1; + } + else + { + column++; + } + } + + return (line, column); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsNameStart(char ch) => ch == '_' || char.IsLetter(ch); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsNameContinue(char ch) => ch == '_' || char.IsLetterOrDigit(ch); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHexDigit(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int HexValue(char ch) => + ch switch + { + >= '0' and <= '9' => ch - '0', + >= 'A' and <= 'F' => ch - 'A' + 10, + >= 'a' and <= 'f' => ch - 'a' + 10, + _ => 0, + }; + + private ref struct SpanReader + { + private readonly ReadOnlySpan buffer; + private int position; + + public SpanReader(ReadOnlySpan source) + { + buffer = source; + position = 0; + } + + public readonly bool End => position >= buffer.Length; + public readonly int Position => position; + public readonly int Remaining => buffer.Length - position; + + public void SkipIgnored() + { + while (!End) + { + var ch = buffer[position]; + if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == ',' || ch == '\ufeff') + { + position++; + continue; + } + + if (ch == '#') + { + position++; + while (!End) + { + ch = buffer[position]; + if (ch == '\n' || ch == '\r') + break; + position++; + } + + continue; + } + + break; + } + } + + public bool TryConsume(char ch) + { + if (!End && buffer[position] == ch) + { + position++; + return true; + } + + return false; + } + + public readonly bool TryPeek(char ch) => !End && buffer[position] == ch; + + public readonly char Peek() => End ? '\0' : buffer[position]; + + public void Advance(int count = 1) => position += count; + + public readonly bool Matches(string value) + { + if (position + value.Length > buffer.Length) + return false; + + return buffer.Slice(position, value.Length).SequenceEqual(value); + } + + public bool TryConsumeKeyword(string keyword) + { + if (!Matches(keyword)) + return false; + + var nextIndex = position + keyword.Length; + if (nextIndex < buffer.Length && IsNameContinue(buffer[nextIndex])) + return false; + + position = nextIndex; + return true; + } + + public readonly ReadOnlySpan Slice(int start, int length) => buffer.Slice(start, length); + + public readonly string GetString(int start, int length) + { + if (length <= 0) + return string.Empty; + + return new string(buffer.Slice(start, length)); + } + + public void Expect(char ch, GraphQLParseContext parseContext, string message) + { + if (!TryConsume(ch)) + throw CreateParseException(parseContext, message, position); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool HasOddNumberOfPrecedingBackslashes() + { + var count = 0; + var i = position - 1; + while (i >= 0 && buffer[i] == '\\') + { + count++; + i--; + } + + return (count & 1) == 1; + } + } + + /// + /// Validates that fragment spreads do not form cycles according to GraphQL spec + /// + private static void ValidateFragmentCycles(GraphQLDocument document) + { + if (document.Fragments.Count == 0) + return; + + var fragmentDependencies = new Dictionary>(document.Fragments.Count); + foreach (var fragment in document.Fragments.Values) + { + var dependencies = new HashSet(); + CollectFragmentDependencies(fragment, dependencies); + if (dependencies.Count > 0) + { + fragmentDependencies[fragment.Name] = dependencies; + } + } + + if (fragmentDependencies.Count == 0) + return; + + var visited = new HashSet(fragmentDependencies.Count); + var recursionStack = new HashSet(); + + foreach (var fragmentName in fragmentDependencies.Keys) + { + if (!visited.Contains(fragmentName)) + { + if (HasFragmentCycle(fragmentName, fragmentDependencies, visited, recursionStack)) + { + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Fragment spreads must not form cycles. Fragment '{fragmentName}' creates a cycle."); + } + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CollectFragmentDependencies(GraphQLFragmentStatement fragment, HashSet dependencies) + { + foreach (var field in fragment.QueryFields) + { + CollectFragmentDependenciesFromField(field, dependencies); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CollectFragmentDependenciesFromField(BaseGraphQLField field, HashSet dependencies) + { + if (field is GraphQLFragmentSpreadField fragmentSpread) + { + dependencies.Add(fragmentSpread.Name); + } + + foreach (var subField in field.QueryFields) + { + CollectFragmentDependenciesFromField(subField, dependencies); + } + } + + private static bool HasFragmentCycle(string fragmentName, Dictionary> dependencies, HashSet visited, HashSet recursionStack) + { + visited.Add(fragmentName); + recursionStack.Add(fragmentName); + + if (dependencies.TryGetValue(fragmentName, out var fragmentDeps)) + { + foreach (var dependency in fragmentDeps) + { + if (recursionStack.Contains(dependency)) + return true; + + if (!visited.Contains(dependency) && HasFragmentCycle(dependency, dependencies, visited, recursionStack)) + return true; + } + } + + recursionStack.Remove(fragmentName); + return false; + } + + private static ExecutableGraphQLStatement CreateOperation( + GraphQLParseContext parseContext, + string operationType, + string? operationName, + Dictionary variables, + IGraphQLNode node, + ref SpanReader reader + ) + { + ExecutableGraphQLStatement operation; + switch (operationType) + { + case "query": + { + var queryParam = Expression.Parameter(node.Schema.QueryContextType, "query_ctx"); + operation = new GraphQLQueryStatement(node.Schema, operationName, queryParam, queryParam, variables); + break; + } + case "mutation": + { + var mutationParam = Expression.Parameter(node.Schema.MutationType, "mut_ctx"); + operation = new GraphQLMutationStatement(node.Schema, operationName, mutationParam, mutationParam, variables); + break; + } + case "subscription": + { + var subscriptionParam = Expression.Parameter(node.Schema.SubscriptionType, "sub_ctx"); + operation = new GraphQLSubscriptionStatement(node.Schema, operationName, subscriptionParam, variables); + break; + } + default: + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Unknown operation type {operationType}"); + } + + parseContext.CurrentOperation = operation; + + var location = operationType switch + { + "query" => ExecutableDirectiveLocation.Query, + "mutation" => ExecutableDirectiveLocation.Mutation, + "subscription" => ExecutableDirectiveLocation.Subscription, + _ => throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Unknown operation type {operationType}"), + }; + var directives = ParseDirectives(parseContext, ref reader, location, node); + + if (directives != null) + { + operation.AddDirectives(directives); + } + + parseContext.CurrentOperation = null; + + return operation; + } + + private static Dictionary ProcessArguments(GraphQLParseContext parseContext, IField field, Dictionary arguments, IGraphQLNode node) + { + if (arguments.Count == 0) + return arguments; + + foreach (var arg in arguments) + { + var argName = arg.Key; + if (!field.Arguments.ContainsKey(argName)) + { + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"No argument '{argName}' found on field '{field.Name}'"); + } + + var argType = field.GetArgumentType(argName); + + if (arg.Value is Expression or VariableReference) + { + // Keep Expression and VariableReference as-is, they'll be resolved later during compilation + arguments[argName] = arg.Value; + } + else + { + var processedValue = ConvertArgumentValue(node.Schema, arg.Value, argType.Type.TypeDotnet); + arguments[argName] = processedValue; + } + } + + return arguments; + } + + internal static object? ConvertArgumentValue(ISchemaProvider schema, object? value, Type targetType) + { + if (value == null) + return null; + + if (value is Dictionary dict) + { + return ConvertObjectArgument(schema, dict, targetType); + } + + if (value is List list) + { + return ConvertListArgument(schema, list, targetType); + } + + if (targetType.IsEnum && value is string enumStr) + { + return Enum.Parse(targetType, enumStr, true); + } + + var underlyingType = Nullable.GetUnderlyingType(targetType); + if (underlyingType != null && underlyingType.IsEnum && value is string enumStrNullable) + { + return Enum.Parse(underlyingType, enumStrNullable, true); + } + + if (value is Expression or VariableReference) + { + // Keep Expression and VariableReference as-is, they'll be resolved later during compilation + return value; + } + + return ExpressionUtil.ConvertObjectType(value, targetType, schema); + } + + private static object ConvertObjectArgument(ISchemaProvider schema, Dictionary dict, Type targetType) + { + var constructors = targetType.GetConstructors(); + var constructor = constructors.FirstOrDefault(c => c.GetParameters().Length == 0 || c.GetParameters().Length == dict.Count); + + if (constructor == null) + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"No suitable constructor found for type '{targetType.Name}'"); + + var constructorParameters = constructor.GetParameters(); + if (constructorParameters.Length > 0) + { + object[] constructorArgs = new object[constructorParameters.Length]; + + for (int i = 0; i < constructorParameters.Length; i++) + { + var paramName = constructorParameters[i].Name!; + if (!dict.TryGetValue(paramName, out var paramValue)) + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Field '{paramName}' not found in argument object"); + + constructorArgs[i] = + ConvertArgumentValue(schema, paramValue, constructorParameters[i].ParameterType) + ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Field '{paramName}' is null in argument object"); + } + + return constructor.Invoke(constructorArgs); + } + + var obj = Activator.CreateInstance(targetType)!; + var schemaType = schema.GetSchemaType(targetType, true, null); + var propTracker = obj is IArgumentsTracker tracker ? tracker : null; + + foreach (var kvp in dict) + { + if (!schemaType.HasField(kvp.Key, null)) + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Field '{kvp.Key}' not found of type '{schemaType.Name}'"); + + var schemaField = schemaType.GetField(kvp.Key, null); + if (schemaField.ResolveExpression == null) + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Field '{kvp.Key}' on type '{schemaType.Name}' has no resolve expression"); + + var nameFromType = ((MemberExpression)schemaField.ResolveExpression).Member.Name; + var prop = targetType.GetProperty(nameFromType); + + if (prop == null) + { + var fieldInfo = targetType.GetField(nameFromType); + if (fieldInfo != null) + { + fieldInfo.SetValue(obj, ConvertArgumentValue(schema, kvp.Value, fieldInfo.FieldType)); + propTracker?.MarkAsSet(fieldInfo.Name); + } + } + else + { + prop.SetValue(obj, ConvertArgumentValue(schema, kvp.Value, prop.PropertyType)); + propTracker?.MarkAsSet(prop.Name); + } + } + + return obj; + } + + private static object ConvertListArgument(ISchemaProvider schema, List list, Type targetType) + { + var elementType = targetType.GetEnumerableOrArrayType() ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "Could not determine list element type"); + + if (targetType.IsArray) + { + var array = Array.CreateInstance(elementType, list.Count); + for (int i = 0; i < list.Count; i++) + { + var convertedValue = ConvertArgumentValue(schema, list[i], elementType); + array.SetValue(convertedValue, i); + } + return array; + } + + IList result; + if (targetType.IsInterface && targetType.IsGenericType && targetType.IsGenericTypeEnumerable()) + { + result = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType), list.Count)!; + } + else + { + try + { + result = (IList)Activator.CreateInstance(targetType, list.Count)!; + } + catch + { + result = (IList)Activator.CreateInstance(targetType)!; + } + } + + for (int i = 0; i < list.Count; i++) + { + var convertedValue = ConvertArgumentValue(schema, list[i], elementType); + result.Add(convertedValue); + } + + return result; + } + + /// + /// Creates a field. Selection fields are added after creation + /// + private static BaseGraphQLField CreateField( + GraphQLParseContext parseContext, + string name, + string? alias, + Dictionary arguments, + List? directives, + IGraphQLNode node + ) + { + var schemaType = node.Field?.ReturnType.SchemaType ?? node.Schema.GetSchemaType(node.NextFieldContext?.Type ?? node.Schema.QueryContextType, false, null); + var schemaField = schemaType.GetField(name, null); + var resultName = alias ?? schemaField.Name; + var processedArguments = ProcessArguments(parseContext, schemaField, arguments, node); + + BaseGraphQLField field; + if (schemaField is SubscriptionField subscriptionField) + { + var nextContextParam = Expression.Parameter(schemaField.ReturnType.TypeDotnet, $"sub_{schemaField.Name}"); + field = new GraphQLSubscriptionField(node.Schema, resultName, subscriptionField, processedArguments, nextContextParam, nextContextParam, node); + } + else if (schemaField is MutationField mutationField) + { + var nextContextParam = Expression.Parameter(schemaField.ReturnType.TypeDotnet, $"mut_{schemaField.Name}"); + field = new GraphQLMutationField(node.Schema, resultName, mutationField, processedArguments, nextContextParam, nextContextParam, node); + } + else if (schemaField.ReturnType.IsList) + { + var elementType = schemaField.ReturnType.SchemaType.TypeDotnet; + var fieldParam = Expression.Parameter(elementType, $"p_{elementType.Name}"); + field = new GraphQLListSelectionField( + node.Schema, + schemaField, + resultName, + fieldParam, + schemaField.FieldParam ?? node.RootParameter, + schemaField.ResolveExpression!, + node, + processedArguments + ); + } + else if (schemaField.ReturnType.SchemaType.RequiresSelection) + { + var rootParam = schemaField.FieldParam ?? node.RootParameter!; + field = new GraphQLObjectProjectionField(node.Schema, schemaField, resultName, schemaField.ResolveExpression!, rootParam, node, processedArguments); + + var listExp = ExpressionUtil.FindEnumerable(schemaField.ResolveExpression!); + if (listExp.Item1 != null && listExp.Item2 != null) + { + var returnType = node.Schema.GetSchemaType(listExp.Item1.Type.GetEnumerableOrArrayType()!, node.Field?.FromType.GqlType == GqlTypes.InputObject, null); + var elementType = returnType.TypeDotnet; + var listFieldParam = Expression.Parameter(elementType, $"p_{elementType.Name}"); + var listField = new GraphQLListSelectionField( + node.Schema, + schemaField, + resultName, + listFieldParam, + schemaField.FieldParam ?? node.RootParameter, + listExp.Item1, + node, + processedArguments + ); + + field = new GraphQLCollectionToSingleField(node.Schema, listField, (GraphQLObjectProjectionField)field, listExp.Item2); + } + } + else if (schemaField.ReturnType.SchemaType.IsScalar || schemaField.ReturnType.SchemaType.IsEnum) + { + var rootParam = schemaField.FieldParam ?? node.RootParameter; + field = new GraphQLScalarField(node.Schema, schemaField, resultName, schemaField.ResolveExpression!, rootParam, node, processedArguments); + } + else + { + throw new EntityGraphQLException( + GraphQLErrorCategory.DocumentError, + $"Field '{schemaField.Name}' of type '{schemaField.ReturnType.SchemaType.Name}' can not be queried directly. It is not a scalar and does not require a selection set." + ); + } + + if (directives != null && directives.Count > 0) + { + field.AddDirectives(directives); + } + + return field; + } +} + +internal sealed class GraphQLVariableType +{ + public string TypeName { get; } + public bool IsList { get; } + public bool InnerNonNull { get; } + public bool OuterNonNull { get; } + + public GraphQLVariableType(string typeName, bool isList, bool innerNonNull, bool outerNonNull) + { + TypeName = typeName; + IsList = isList; + InnerNonNull = innerNonNull; + OuterNonNull = outerNonNull; + } +} + +internal ref struct GraphQLParseContext +{ + public ReadOnlySpan Source { get; } + public QueryVariables QueryVariables { get; } + public ExecutableGraphQLStatement? CurrentOperation { get; set; } + public bool InFragment { get; set; } + + public GraphQLParseContext(ReadOnlySpan query, QueryVariables queryVariables) + { + Source = query; + QueryVariables = queryVariables; + } +} diff --git a/src/EntityGraphQL/Compiler/QueryWalkerHelper.cs b/src/EntityGraphQL/Compiler/QueryWalkerHelper.cs deleted file mode 100644 index c89d2501..00000000 --- a/src/EntityGraphQL/Compiler/QueryWalkerHelper.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Text.RegularExpressions; -using EntityGraphQL.Compiler.Util; -using EntityGraphQL.Extensions; -using EntityGraphQL.Schema; -using HotChocolate.Language; - -namespace EntityGraphQL.Compiler; - -public static class QueryWalkerHelper -{ - public static readonly Regex GuidRegex = new(@"^[0-9A-F]{8}[-]?([0-9A-F]{4}[-]?){3}[0-9A-F]{12}$", RegexOptions.IgnoreCase); - - public static object? ProcessArgumentValue(ISchemaProvider schema, IValueNode argumentValue, string argName, Type argType) - { - object? argValue = null; - if (argumentValue.Value != null) - { - switch (argumentValue.Kind) - { - case SyntaxKind.IntValue: - argValue = argType switch - { - _ when argType == typeof(short) || argType == typeof(short?) => short.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ when argType == typeof(ushort) || argType == typeof(ushort?) => ushort.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ when argType == typeof(int) || argType == typeof(int?) => int.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ when argType == typeof(uint) || argType == typeof(uint?) => uint.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ when argType == typeof(long) || argType == typeof(long?) => long.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ when argType == typeof(ulong) || argType == typeof(ulong?) => ulong.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ when argType == typeof(float) || argType == typeof(float?) => float.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ when argType == typeof(decimal) || argType == typeof(decimal?) => decimal.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ when argType == typeof(double) || argType == typeof(double?) => double.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ => argValue, - }; - break; - // these ones are the correct type - case SyntaxKind.StringValue: - argValue = (string)argumentValue.Value; - break; - case SyntaxKind.BooleanValue: - argValue = argumentValue.Value; - break; - case SyntaxKind.NullValue: - argValue = null; - break; - case SyntaxKind.EnumValue: - argValue = (string)argumentValue.Value; - break; - case SyntaxKind.ListValue: - argValue = ProcessListArgument(schema, (List)argumentValue.Value, argName, argType); - break; - case SyntaxKind.ObjectValue: - argValue = ProcessObjectValue(schema, argumentValue, argName, argType); - break; - case SyntaxKind.FloatValue: - argValue = argType switch - { - _ when argType == typeof(float) || argType == typeof(float?) => float.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ when argType == typeof(decimal) || argType == typeof(decimal?) => decimal.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ when argType == typeof(double) || argType == typeof(double?) => double.Parse(argumentValue.Value.ToString()!, CultureInfo.InvariantCulture), - _ => argValue, - }; - break; - } - } - - return ExpressionUtil.ConvertObjectType(argValue, argType, schema, null); - } - - private static object ProcessObjectValue(ISchemaProvider schema, IValueNode argumentValue, string argName, Type argType) - { - // this should be an Input type - // see if it has an empty constructor or a constructor that matches the fields - if (argumentValue.Value is not List objectValues) - throw new EntityGraphQLCompilerException($"Argument {argName} is not an object"); - - var constructor = - argType.GetConstructors().FirstOrDefault(c => c.GetParameters().Length == 0 || c.GetParameters().Length == objectValues.Count) - ?? throw new EntityGraphQLCompilerException($"No constructor found for object argument {argName}"); - var constructorParameters = constructor.GetParameters(); - if (constructorParameters.Length > 0) - { - object[] constructorArgs = new object[constructorParameters.Length]; - - // objectValue.Fields can be looked up by the constructor parameter name - for (int i = 0; i < constructorParameters.Length; i++) - { - var field = - objectValues.FirstOrDefault(f => f.Name.Value == constructorParameters[i].Name) - ?? throw new EntityGraphQLCompilerException($"Field '{constructorParameters[i].Name}' not found in argument object {argName}"); - constructorArgs[i] = - ProcessArgumentValue(schema, field.Value, argName, constructorParameters[i].ParameterType) - ?? throw new EntityGraphQLCompilerException($"Field '{constructorParameters[i].Name}' is null in argument object {argName}"); - } - - // Create the object using the specific constructor - var argObj = constructor.Invoke(constructorArgs); - return argObj; - } - - var obj = Activator.CreateInstance(argType)!; - var propTracker = obj is IArgumentsTracker p ? p : null; - - object argValue; - var schemaType = schema.GetSchemaType(argType, true, null); - foreach (var item in objectValues) - { - if (!schemaType.HasField(item.Name.Value, null)) - throw new EntityGraphQLCompilerException($"Field '{item.Name.Value}' not found of type '{schemaType.Name}'"); - var schemaField = schemaType.GetField(item.Name.Value, null); - - if (schemaField.ResolveExpression == null) - throw new EntityGraphQLCompilerException($"Field '{item.Name.Value}' on type '{schemaType.Name}' has no resolve expression"); - - var nameFromType = ((MemberExpression)schemaField.ResolveExpression).Member.Name; - var prop = argType.GetProperty(nameFromType); - - if (prop == null) - { - var field = argType.GetField(nameFromType) ?? throw new EntityGraphQLCompilerException($"Field '{item.Name.Value}' not found on object argument"); - field.SetValue(obj, ProcessArgumentValue(schema, item.Value, argName, field.FieldType)); - propTracker?.MarkAsSet(field.Name); - } - else - { - prop.SetValue(obj, ProcessArgumentValue(schema, item.Value, argName, prop.PropertyType)); - propTracker?.MarkAsSet(prop.Name); - } - } - argValue = obj; - return argValue; - } - - public static object ProcessListArgument(ISchemaProvider schema, List values, string argName, Type fieldArgType) - { - IList list; - if (fieldArgType.IsInterface && fieldArgType.IsGenericType && fieldArgType.IsGenericTypeEnumerable()) - list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(fieldArgType.GetEnumerableOrArrayType()!))!; - else - list = (IList)Activator.CreateInstance(fieldArgType, values.Count)!; - - var listType = list.GetType().GetEnumerableOrArrayType() ?? throw new EntityGraphQLCompilerException($"Argument {argName} is not a list"); - for (int i = 0; i < values.Count; i++) - { - IValueNode? item = values[i]; - if (fieldArgType.IsArray) - list[i] = ProcessArgumentValue(schema, item, argName, listType); - else - list.Add(ProcessArgumentValue(schema, item, argName, listType)); - } - - return list; - } -} diff --git a/src/EntityGraphQL/Compiler/Util/ArgumentUtil.cs b/src/EntityGraphQL/Compiler/Util/ArgumentUtil.cs index 462594bb..e3d0ff95 100644 --- a/src/EntityGraphQL/Compiler/Util/ArgumentUtil.cs +++ b/src/EntityGraphQL/Compiler/Util/ArgumentUtil.cs @@ -19,7 +19,7 @@ public static class ArgumentUtil Type? argumentsType, ParameterExpression? docParam, IArgumentsTracker? docVariables, - List validationErrors + HashSet validationErrors ) { if (argumentsType == null) @@ -38,7 +38,30 @@ List validationErrors object? val; try { - if (args.ContainsKey(argField.Name) && args[argField.Name] is Expression argExpression) + // Handle VariableReference from fragments - convert to Expression first + if (args.ContainsKey(argField.Name) && args[argField.Name] is VariableReference varRef) + { + // Resolve the VariableReference to an Expression now that we have the operation context + if (docParam == null) + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Variable '{varRef.VariableName}' used but no operation context available"); + + var argExpression = Expression.PropertyOrField(docParam, varRef.VariableName); + + // Process it like a regular Expression + if (docVariables != null) + { + val = Expression.Lambda(argExpression, docParam).Compile().DynamicInvoke([docVariables]); + if (docVariables.IsSet(varRef.VariableName)) + setValues.Add(argField.Name); + } + else + { + val = argExpression; + setValues.Add(argField.Name); + } + values.Add(argField.Name, ExpressionUtil.ConvertObjectType(val, argField.RawType, schema)); + } + else if (args.ContainsKey(argField.Name) && args[argField.Name] is Expression argExpression) { // this value comes from the variables from the query document if (docVariables != null) @@ -53,23 +76,20 @@ List validationErrors val = argExpression; setValues.Add(argField.Name); } - values.Add(argField.Name, ExpressionUtil.ConvertObjectType(val, argField.RawType, schema, null)); + values.Add(argField.Name, ExpressionUtil.ConvertObjectType(val, argField.RawType, schema)); } else { (var isSet, val) = BuildArgumentFromMember(schema, args, argField.Name, argField.RawType, argField.DefaultValue, validationErrors); // this could be int to RequiredField if (val != null && val.GetType() != argField.RawType) - val = ExpressionUtil.ConvertObjectType(val, argField.RawType, schema, null); + val = ExpressionUtil.ConvertObjectType(val, argField.RawType, schema); values.Add(argField.Name, val); if (val != null || argField.DefaultValue.IsSet) setValues.Add(argField.Name); } - argField.Validate(val, fieldName, validationErrors); - } - catch (EntityGraphQLValidationException) - { - throw; + if (field != null) + argField.ValidateAsync(val, field, validationErrors).GetAwaiter().GetResult(); } catch (Exception) { @@ -78,7 +98,9 @@ List validationErrors } // Build our object - var con = argumentsType!.GetConstructors()?.FirstOrDefault() ?? throw new EntityGraphQLCompilerException($"Could not find constructor for arguments type {argumentsType.Name}"); + var con = + argumentsType!.GetConstructors()?.FirstOrDefault() + ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Could not find constructor for arguments type {argumentsType.Name}"); var parameters = con.GetParameters().Select(x => values!.GetValueOrDefault(x.Name)).ToArray(); // anonymous objects will have a constructor with taking the properties as arguments @@ -116,7 +138,7 @@ List validationErrors validator.Validator.ValidateAsync(context).GetAwaiter().GetResult(); argumentValues = context.Arguments; - validationErrors.AddRange(context.Errors); + validationErrors.UnionWith(context.Errors); } } } @@ -145,7 +167,7 @@ internal static (bool isSet, object? value) BuildArgumentFromMember( string memberName, Type memberType, DefaultArgValue defaultValue, - IList validationErrors + HashSet validationErrors ) { string argName = memberName; @@ -171,7 +193,7 @@ IList validationErrors var parameters = c.GetParameters(); if (parameters.Length == 1) { - item = ExpressionUtil.ConvertObjectType(item, parameters[0].ParameterType, schema, null); + item = ExpressionUtil.ConvertObjectType(item, parameters[0].ParameterType, schema); constructor = memberType.GetConstructor(new[] { item!.GetType() }); break; } @@ -187,7 +209,7 @@ IList validationErrors var typedVal = constructor.Invoke([item]); return (true, typedVal); } - else if (defaultValue.IsSet && defaultValue.Value != null && defaultValue.GetType().IsConstructedGenericType && defaultValue.GetType().GetGenericTypeDefinition() == typeof(EntityQueryType<>)) + else if (defaultValue.IsSet && defaultValue.Value != null && defaultValue.GetType() == typeof(EntityQueryType)) { return (true, args != null && args.ContainsKey(argName) ? args[argName] : Activator.CreateInstance(memberType)); } diff --git a/src/EntityGraphQL/Compiler/Util/CompileHelper.cs b/src/EntityGraphQL/Compiler/Util/CompileHelper.cs index 66d6b5a5..ad6a6fc8 100644 --- a/src/EntityGraphQL/Compiler/Util/CompileHelper.cs +++ b/src/EntityGraphQL/Compiler/Util/CompileHelper.cs @@ -35,7 +35,9 @@ public static Expression InjectServices( } else { - var service = serviceProvider.GetService(serviceParam.Type) ?? throw new EntityGraphQLExecutionException($"Service {serviceParam.Type.Name} not found in service provider"); + var service = + serviceProvider.GetService(serviceParam.Type) + ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Service {serviceParam.Type.Name} not found in service provider"); allArgs.Add(service); } } @@ -53,7 +55,7 @@ public static void ValidateAndReplaceFieldArgs( ParameterReplacer replacer, ref object? argumentValue, ref Expression result, - List validationErrors, + HashSet validationErrors, ParameterExpression? newArgParam ) { @@ -72,12 +74,12 @@ public static void ValidateAndReplaceFieldArgs( argumentValue = invokeContext.Arguments; } - validationErrors.AddRange(invokeContext.Errors); + validationErrors.UnionWith(invokeContext.Errors); } if (validationErrors.Count > 0) { - throw new EntityGraphQLValidationException(validationErrors); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, validationErrors); } } } diff --git a/src/EntityGraphQL/Compiler/Util/ExpressionExtractor.cs b/src/EntityGraphQL/Compiler/Util/ExpressionExtractor.cs index 0df2f72b..d8640d73 100644 --- a/src/EntityGraphQL/Compiler/Util/ExpressionExtractor.cs +++ b/src/EntityGraphQL/Compiler/Util/ExpressionExtractor.cs @@ -15,9 +15,14 @@ namespace EntityGraphQL.Compiler.Util; /// ctx.Field1 /// ctx.Child.Field2 /// -public class ExpressionExtractor : ExpressionVisitor +public partial class ExpressionExtractor : ExpressionVisitor { - private readonly Regex pattern = new("[\\.\\(\\)\\!]"); +#if NETSTANDARD2_1 + private readonly Regex replacePattern = new("[\\.\\(\\)\\!]"); +#endif +#if NET8_0_OR_GREATER + private readonly Regex replacePattern = ReplacePattern(); +#endif private Expression? rootContext; @@ -43,13 +48,13 @@ public class ExpressionExtractor : ExpressionVisitor protected override Expression VisitParameter(ParameterExpression node) { if (rootContext == null) - throw new EntityGraphQLCompilerException("Root context not set for ExpressionExtractor"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "Root context not set for ExpressionExtractor"); if ((rootContext == node || (matchByType && rootContext.Type == node.Type)) && currentExpression.Count > 0) { var expressionItem = currentExpression.Peek(); // use the expression as the extracted field name as it will be unique - var name = "egql__" + pattern.Replace(expressionItem.ToString(), "_"); + var name = "egql__" + replacePattern.Replace(expressionItem.ToString(), "_"); if (extractedExpressions!.TryGetValue(name, out var existing)) { existing.Add(expressionItem); @@ -130,9 +135,14 @@ protected override Expression VisitMethodCall(MethodCallExpression node) } for (int i = startAt; i < node.Arguments.Count; i++) { - // each arg might be extractable but we should end up back in a acll or member access again + // each arg might be extractable but we should end up back in a call or member access again ProcessPotentialLeaf(node.Arguments[i]); } return node; } + +#if NET8_0_OR_GREATER + [GeneratedRegex("[\\.\\(\\)\\!]")] + private static partial Regex ReplacePattern(); +#endif } diff --git a/src/EntityGraphQL/Compiler/Util/ExpressionUtil.cs b/src/EntityGraphQL/Compiler/Util/ExpressionUtil.cs index bc26e9e2..181ff780 100644 --- a/src/EntityGraphQL/Compiler/Util/ExpressionUtil.cs +++ b/src/EntityGraphQL/Compiler/Util/ExpressionUtil.cs @@ -5,14 +5,22 @@ using System.Linq; using System.Linq.Expressions; using System.Text.Json; +using System.Text.RegularExpressions; using EntityGraphQL.Compiler.EntityQuery; using EntityGraphQL.Extensions; using EntityGraphQL.Schema; namespace EntityGraphQL.Compiler.Util; -public static class ExpressionUtil +public static partial class ExpressionUtil { +#if NETSTANDARD2_1 + public static readonly Regex GuidRegex = new(@"^[0-9A-F]{8}[-]?([0-9A-F]{4}[-]?){3}[0-9A-F]{12}$", RegexOptions.IgnoreCase); +#endif +#if NET8_0_OR_GREATER + public static readonly Regex GuidRegex = GuidRegexImpl(); +#endif + /// /// List of methods that take a list and return a single item. We need to handle these differently as we need to /// add a Select() before the method call to optimize EF queries. @@ -37,14 +45,14 @@ public static class ExpressionUtil public static Expression MakeCallOnQueryable(string methodName, Type[] genericTypes, params Expression[] parameters) { - var type = typeof(IQueryable).IsAssignableFrom(parameters.First().Type) ? typeof(Queryable) : typeof(Enumerable); + var type = parameters.First().Type.IsGenericTypeQueryable() ? typeof(Queryable) : typeof(Enumerable); try { return Expression.Call(type, methodName, genericTypes, parameters); } catch (InvalidOperationException ex) { - throw new EntityGraphQLCompilerException($"Could not find extension method {methodName} on types {type}", ex); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Could not find extension method {methodName} on types {type}", null, null, ex); } } @@ -56,7 +64,7 @@ public static Expression MakeCallOnEnumerable(string methodName, Type[] genericT } catch (InvalidOperationException ex) { - throw new EntityGraphQLCompilerException($"Could not find extension method {methodName} on types {typeof(Enumerable)}", ex); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Could not find extension method {methodName} on types {typeof(Enumerable)}", null, null, ex); } } @@ -71,11 +79,11 @@ public static MemberExpression CheckAndGetMemberExpression(E return (MemberExpression)exp; } - public static object? ConvertObjectType(object? value, Type toType, ISchemaProvider? schema, ExecutionOptions? executionOptions = null) + public static object? ConvertObjectType(object? value, Type toType, ISchemaProvider? schema) { if (value == null) { - if (toType.IsConstructedGenericType && toType.GetGenericTypeDefinition() == typeof(EntityQueryType<>)) + if (toType == typeof(EntityQueryType)) { // we don't want a null value. We want an empty EntityQueryType var entityQuery = Activator.CreateInstance(toType); @@ -109,13 +117,13 @@ public static MemberExpression CheckAndGetMemberExpression(E var prop = toType.GetProperties().FirstOrDefault(p => p.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase)); if (prop != null) { - prop.SetValue(value, ConvertObjectType(item.Value, prop.PropertyType, schema, executionOptions)); + prop.SetValue(value, ConvertObjectType(item.Value, prop.PropertyType, schema)); propSet?.MarkAsSet(item.Name); } else { var field = toType.GetFields().FirstOrDefault(p => p.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase)); - field?.SetValue(value, ConvertObjectType(item.Value, field.FieldType, schema, executionOptions)); + field?.SetValue(value, ConvertObjectType(item.Value, field.FieldType, schema)); propSet?.MarkAsSet(item.Name); } } @@ -124,9 +132,11 @@ public static MemberExpression CheckAndGetMemberExpression(E if (jsonEle.ValueKind == JsonValueKind.Array) { var eleType = toType.GetEnumerableOrArrayType()!; - var list = (IList?)Activator.CreateInstance(typeof(List<>).MakeGenericType(eleType)) ?? throw new EntityGraphQLCompilerException($"Could not create list of type {eleType}"); + var list = + (IList?)Activator.CreateInstance(typeof(List<>).MakeGenericType(eleType)) + ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Could not create list of type {eleType}"); foreach (var item in jsonEle.EnumerateArray()) - list.Add(ConvertObjectType(item, eleType, schema, executionOptions)); + list.Add(ConvertObjectType(item, eleType, schema)); return list; } value = jsonEle.ToString(); @@ -136,14 +146,10 @@ public static MemberExpression CheckAndGetMemberExpression(E return null; } - // custom type converters after we unwind JSON elements - if (schema?.TypeConverters.TryGetValue(fromType, out var converter) == true) + // custom type converters: (from,to) -> to-only -> from-only + if (schema != null && schema.TryConvertCustom(value, toType, out var converted)) { - value = converter.ChangeType(value, toType, schema); - fromType = value?.GetType()!; - - if (value == null || fromType == toType) - return value; + return converted; } if (toType != typeof(string) && fromType == typeof(string)) @@ -164,6 +170,14 @@ public static MemberExpression CheckAndGetMemberExpression(E return DateTime.Parse((string)value, CultureInfo.InvariantCulture); if (toType == typeof(DateTimeOffset) || toType == typeof(DateTimeOffset?)) return DateTimeOffset.Parse((string)value, CultureInfo.InvariantCulture); + if (toType == typeof(TimeSpan) || toType == typeof(TimeSpan?)) + return TimeSpan.Parse((string)value, CultureInfo.InvariantCulture); +#if NET8_0_OR_GREATER + if (toType == typeof(DateOnly) || toType == typeof(DateOnly?)) + return DateOnly.Parse((string)value, CultureInfo.InvariantCulture); + if (toType == typeof(TimeOnly) || toType == typeof(TimeOnly?)) + return TimeOnly.Parse((string)value, CultureInfo.InvariantCulture); +#endif } else if (toType != typeof(long) && fromType == typeof(long)) { @@ -171,6 +185,14 @@ public static MemberExpression CheckAndGetMemberExpression(E return new DateTime((long)value); if (toType == typeof(DateTimeOffset) || toType == typeof(DateTimeOffset?)) return new DateTimeOffset((long)value, TimeSpan.Zero); + if (toType == typeof(TimeSpan) || toType == typeof(TimeSpan?)) + return new TimeSpan((long)value); +#if NET8_0_OR_GREATER + if (toType == typeof(DateOnly) || toType == typeof(DateOnly?)) + return DateOnly.FromDateTime(new DateTime((long)value)); + if (toType == typeof(TimeOnly) || toType == typeof(TimeOnly?)) + return TimeOnly.FromTimeSpan(new TimeSpan((long)value)); +#endif } var argumentNonNullType = toType.IsNullableType() ? Nullable.GetUnderlyingType(toType)! : toType; @@ -184,7 +206,7 @@ public static MemberExpression CheckAndGetMemberExpression(E var interfaceType = fromType.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>)); // handle dictionary of dictionary representing the objects if (interfaceType.GetGenericArguments()[0] != typeof(string)) - throw new EntityGraphQLCompilerException($"Dictionary key type must be string. Got {fromType.GetGenericArguments()[0]}"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Dictionary key type must be string. Got {fromType.GetGenericArguments()[0]}"); var newValue = Activator.CreateInstance(toType); var prop = newValue is IArgumentsTracker p ? p : null; @@ -193,7 +215,7 @@ public static MemberExpression CheckAndGetMemberExpression(E var toProp = toType.GetProperties().FirstOrDefault(p => p.Name.Equals(key, StringComparison.OrdinalIgnoreCase)); if (toProp != null) { - toProp.SetValue(newValue, ConvertObjectType(((IDictionary)value)[key], toProp.PropertyType, schema, executionOptions)); + toProp.SetValue(newValue, ConvertObjectType(((IDictionary)value)[key], toProp.PropertyType, schema)); prop?.MarkAsSet(toProp.Name); } else @@ -201,7 +223,7 @@ public static MemberExpression CheckAndGetMemberExpression(E var toField = toType.GetFields().FirstOrDefault(p => p.Name.Equals(key, StringComparison.OrdinalIgnoreCase)); if (toField is not null) { - toField.SetValue(newValue, ConvertObjectType(((IDictionary)value)[key], toField.FieldType, schema, executionOptions)); + toField.SetValue(newValue, ConvertObjectType(((IDictionary)value)[key], toField.FieldType, schema)); prop?.MarkAsSet(toField.Name); } } @@ -211,13 +233,15 @@ public static MemberExpression CheckAndGetMemberExpression(E if (toType.IsEnumerableOrArray()) { var eleType = toType.GetEnumerableOrArrayType()!; - var list = (IList?)Activator.CreateInstance(typeof(List<>).MakeGenericType(eleType)) ?? throw new EntityGraphQLCompilerException($"Could not create list of type {eleType}"); + var list = + (IList?)Activator.CreateInstance(typeof(List<>).MakeGenericType(eleType)) + ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Could not create list of type {eleType}"); foreach (var item in (IEnumerable)value) - list.Add(ConvertObjectType(item, eleType, schema, executionOptions)); + list.Add(ConvertObjectType(item, eleType, schema)); if (toType.IsArray) { // if toType is array [] we can't use a List<> - var result = Expression.Lambda(Expression.Call(typeof(Enumerable), "ToArray", new[] { eleType }, Expression.Constant(list))).Compile().DynamicInvoke(); + var result = Expression.Lambda(Expression.Call(typeof(Enumerable), "ToArray", [eleType], Expression.Constant(list))).Compile().DynamicInvoke(); return result; } return list; @@ -225,7 +249,7 @@ public static MemberExpression CheckAndGetMemberExpression(E if ( (argumentNonNullType == typeof(Guid) || argumentNonNullType == typeof(Guid?) || argumentNonNullType == typeof(RequiredField) || argumentNonNullType == typeof(RequiredField)) && fromType == typeof(string) - && QueryWalkerHelper.GuidRegex.IsMatch(value.ToString()!) + && GuidRegex.IsMatch(value.ToString()!) ) { return Guid.Parse(value!.ToString()!); @@ -235,18 +259,18 @@ public static MemberExpression CheckAndGetMemberExpression(E if (fromType == toType.GetGenericArguments()[0]) return Activator.CreateInstance(toType, value); else if (toType.IsGenericType && toType.GetGenericTypeDefinition() == typeof(RequiredField<>)) - return Activator.CreateInstance(toType, ConvertObjectType(value, toType.GetGenericArguments()[0], schema, executionOptions)); + return Activator.CreateInstance(toType, ConvertObjectType(value, toType.GetGenericArguments()[0], schema)); } if (argumentNonNullType.IsClass && typeof(string) != argumentNonNullType && !fromType.IsEnumerableOrArray()) { - return ConvertObjectType(schema, value, toType, fromType, executionOptions); + return ConvertObjectType(schema, value, toType, fromType); } if (argumentNonNullType != valueNonNullType) { - var implicitCastOperator = argumentNonNullType.GetMethod("op_Implicit", new[] { valueNonNullType }); + var implicitCastOperator = argumentNonNullType.GetMethod("op_Implicit", [valueNonNullType]); if (implicitCastOperator != null) { - return implicitCastOperator.Invoke(null, new[] { value }); + return implicitCastOperator.Invoke(null, [value]); } var newVal = Convert.ChangeType(value, argumentNonNullType, CultureInfo.InvariantCulture); @@ -256,42 +280,42 @@ public static MemberExpression CheckAndGetMemberExpression(E return value; } - private static object? ConvertObjectType(ISchemaProvider? schema, object? value, Type toType, Type valueObjType, ExecutionOptions? executionOptions) + private static object? ConvertObjectType(ISchemaProvider? schema, object? value, Type toType, Type valueObjType) { var newValue = Activator.CreateInstance(toType); foreach (var toField in toType.GetFields()) { var fromProp = valueObjType.GetProperties().FirstOrDefault(p => p.Name.Equals(toField.Name, StringComparison.OrdinalIgnoreCase)); if (fromProp != null) - toField.SetValue(newValue, ConvertObjectType(fromProp.GetValue(value), toField.FieldType, schema, executionOptions)); + toField.SetValue(newValue, ConvertObjectType(fromProp.GetValue(value), toField.FieldType, schema)); else { var fromField = valueObjType.GetFields().FirstOrDefault(p => p.Name.Equals(toField.Name, StringComparison.OrdinalIgnoreCase)); if (fromField != null) - toField.SetValue(newValue, ConvertObjectType(fromField.GetValue(value), toField.FieldType, schema, executionOptions)); + toField.SetValue(newValue, ConvertObjectType(fromField.GetValue(value), toField.FieldType, schema)); } } foreach (var toProperty in toType.GetProperties()) { var fromProp = valueObjType.GetProperties().FirstOrDefault(p => p.Name.Equals(toProperty.Name, StringComparison.OrdinalIgnoreCase)); if (fromProp != null) - toProperty.SetValue(newValue, ConvertObjectType(fromProp.GetValue(value), toProperty.PropertyType, schema, executionOptions)); + toProperty.SetValue(newValue, ConvertObjectType(fromProp.GetValue(value), toProperty.PropertyType, schema)); else { var fromField = valueObjType.GetFields().FirstOrDefault(p => p.Name.Equals(toProperty.Name, StringComparison.OrdinalIgnoreCase)); if (fromField != null) - toProperty.SetValue(newValue, ConvertObjectType(fromField.GetValue(value), toProperty.PropertyType, schema, executionOptions)); + toProperty.SetValue(newValue, ConvertObjectType(fromField.GetValue(value), toProperty.PropertyType, schema)); } } // Handle converting a string to EntityQueryType - if (schema != null && toType.IsConstructedGenericType && toType.GetGenericTypeDefinition() == typeof(EntityQueryType<>)) + if (schema != null && toType == typeof(EntityQueryType)) { if (value != null && !string.IsNullOrWhiteSpace((string)value)) { - var expression = BuildEntityQueryExpression(schema, toType.GetGenericArguments()[0], (string)value); - var genericProp = toType.GetProperty("Query")!; - genericProp.SetValue(newValue, expression); + // Defer compilation unless a CompileContext-aware overload is used + var textProp = toType.GetProperty(nameof(EntityQueryType.Text)); + textProp?.SetValue(newValue, (string)value); } } return newValue; @@ -371,7 +395,7 @@ public static (string? capMethod, GraphQLListSelectionField listSelection) Updat // this is a ctx.Something.First(f => ...) // move the filter to a Where call so we can use .Select() to get the fields requested var filter = call.Arguments.ElementAt(1); - var isQueryable = typeof(IQueryable).IsAssignableFrom(listExpression.Type); + var isQueryable = listExpression.Type.IsGenericTypeQueryable(); listExpression = isQueryable ? MakeCallOnQueryable(nameof(Queryable.Where), [combineExpression.Type], listExpression, filter) : MakeCallOnEnumerable(nameof(Enumerable.Where), [combineExpression.Type], listExpression, filter); @@ -419,7 +443,7 @@ public static Expression CombineExpressions(Expression baseExp, Expression nextE return Expression.Call(baseExp, mc.Method, mc.Arguments); } default: - throw new EntityGraphQLCompilerException($"Could not join expressions '{baseExp.NodeType} and '{nextExp.NodeType}'"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Could not join expressions '{baseExp.NodeType} and '{nextExp.NodeType}'"); } } @@ -488,6 +512,7 @@ public static (Expression expression, List? dynamicTypes) MakeSelectWithDy Expression baseExp, IDictionary fieldExpressions, bool nullCheck, + bool isAsync, bool finalExecution ) { @@ -535,11 +560,17 @@ bool finalExecution ); var selector = Expression.Lambda(previous!, currentContextParam); - var isQueryable = typeof(IQueryable).IsAssignableFrom(baseExp.Type); + var isQueryable = baseExp.Type.IsGenericTypeQueryable(); Expression call; - if (nullCheck) - call = Expression.Call(typeof(EnumerableExtensions), nameof(EnumerableExtensions.SelectWithNullCheck), [currentContextParam.Type, baseDynamicType], baseExp, selector); + if (nullCheck || isAsync) + call = Expression.Call( + isQueryable ? typeof(QueryableExtensions) : typeof(EnumerableExtensions), + isQueryable ? nameof(QueryableExtensions.SelectWithNullCheck) : nameof(EnumerableExtensions.SelectWithNullCheck), + [currentContextParam.Type, baseDynamicType], + baseExp, + selector + ); else call = isQueryable ? MakeCallOnQueryable(nameof(Enumerable.Select), [currentContextParam.Type, baseDynamicType], baseExp, selector) @@ -553,13 +584,13 @@ bool finalExecution if (memberInit == null || dynamicType == null) // nothing to select return (baseExp, null); var selector = Expression.Lambda(memberInit, currentContextParam); - var isQueryable = typeof(IQueryable).IsAssignableFrom(baseExp.Type); + var isQueryable = baseExp.Type.IsGenericTypeQueryable(); Expression call; - if (nullCheck) + if (nullCheck || isAsync) call = Expression.Call( isQueryable ? typeof(QueryableExtensions) : typeof(EnumerableExtensions), isQueryable ? nameof(QueryableExtensions.SelectWithNullCheck) : nameof(EnumerableExtensions.SelectWithNullCheck), - new Type[] { currentContextParam.Type, dynamicType }, + [currentContextParam.Type, dynamicType], baseExp, selector ); @@ -582,7 +613,7 @@ private static bool CheckFieldType(ParameterExpression currentContextParam, Comp var bindings = type.GetFields().Select(p => Expression.Bind(p, fieldExpressionsByName[p.Name])).OfType().ToList(); if (includeProperties) bindings.AddRange(type.GetProperties().Select(p => Expression.Bind(p, fieldExpressionsByName[p.Name])).OfType()); - var constructor = type.GetConstructor(Type.EmptyTypes) ?? throw new EntityGraphQLCompilerException("Could not create dynamic type"); + var constructor = type.GetConstructor(Type.EmptyTypes) ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "Could not create dynamic type"); var newExp = Expression.New(constructor); var mi = Expression.MemberInit(newExp, bindings); return mi; @@ -596,10 +627,10 @@ private static bool CheckFieldType(ParameterExpression currentContextParam, Comp dynamicType = LinqRuntimeTypeBuilder.GetDynamicType(fieldExpressionsByName.ToDictionary(f => f.Key, f => f.Value.Type), fieldDescription, parentType: parentType); if (dynamicType == null) - throw new EntityGraphQLCompilerException("Could not create dynamic type"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "Could not create dynamic type"); var bindings = dynamicType.GetFields().Select(p => Expression.Bind(p, fieldExpressionsByName[p.Name])).OfType(); - var constructor = dynamicType.GetConstructor(Type.EmptyTypes) ?? throw new EntityGraphQLCompilerException("Could not create dynamic type"); + var constructor = dynamicType.GetConstructor(Type.EmptyTypes) ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "Could not create dynamic type"); var newExp = Expression.New(constructor); var mi = Expression.MemberInit(newExp, bindings); return mi; @@ -616,7 +647,7 @@ private static MemberInitExpression CreateNewExpression(string fieldDescription, var dynamicType = LinqRuntimeTypeBuilder.GetDynamicType(fieldExpressionsByName.ToDictionary(f => f.Key, f => f.Value.Type), fieldDescription); var bindings = dynamicType.GetFields().Select(p => Expression.Bind(p, fieldExpressionsByName[p.Name])).OfType(); - var constructor = dynamicType.GetConstructor(Type.EmptyTypes) ?? throw new EntityGraphQLCompilerException("Could not create dynamic type"); + var constructor = dynamicType.GetConstructor(Type.EmptyTypes) ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "Could not create dynamic type"); var newExp = Expression.New(constructor); var mi = Expression.MemberInit(newExp, bindings); return mi; @@ -664,7 +695,7 @@ List validTypes { anonType = LinqRuntimeTypeBuilder.GetDynamicType(fieldsOnBaseType.ToDictionary(x => x.Key, x => x.Value.Type), name + "baseDynamicType") - ?? throw new EntityGraphQLCompilerException("Could not create dynamic type"); + ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "Could not create dynamic type"); // loop through possible types and create the TypeIs check var previous = CreateNewExpression(fieldsOnBaseType, anonType) ?? Expression.Constant(null, anonType); var allNonBaseDynamicTypes = new List(); @@ -685,95 +716,18 @@ List validTypes return (previous, allNonBaseDynamicTypes); } - /// - /// Wrap a field expression in a method that does a null check for us and avoid calling the field multiple times. - /// E.g. if the field is (item) => CallSomeService(item) and the result is an object (not IEnumerable) we do not want to generate - /// CallSomeService(item) == null ? null : new { - /// field1 = CallSomeService(item).field1, - /// field2 = CallSomeService(item).field2 - // } - /// As that will call the function 3 times (or 1 + number of fields selected) - /// - /// This wraps the field expression that does the call once - /// - internal static Expression WrapObjectProjectionFieldForNullCheck( - string fieldName, - Expression nullCheckExpression, - IEnumerable paramsForFieldExpressions, - Dictionary fieldExpressions, - IEnumerable fieldSelectParamValues, - ParameterExpression nullWrapParam, - Expression schemaContext - ) + public static Expression BuildEntityQueryExpression(ISchemaProvider schemaProvider, Type queryType, string query, EqlCompileContext compileContext, ParameterExpression? contextParam) { - var arguments = new Expression[] - { - Expression.Constant(fieldName), - nullCheckExpression, - Expression.Constant(nullWrapParam, typeof(ParameterExpression)), - Expression.Constant(paramsForFieldExpressions.ToList()), - Expression.Constant(fieldExpressions), - Expression.Constant(fieldSelectParamValues), - schemaContext == null ? Expression.Constant(null, typeof(ParameterExpression)) : Expression.Constant(schemaContext), - schemaContext ?? Expression.Constant(null), - }; - var call = Expression.Call(typeof(ExpressionUtil), nameof(WrapObjectProjectionFieldForNullCheckExec), null, arguments); - return call; - } - - /// - /// Used at runtime. - /// Actually implements the null check code. This is executed at execution time of the whole query not at compile time - /// - /// Object that we build the select on. Check if it is null first - /// The ParameterExpression for the null check - /// Parameters needed for the expression - /// Selection fields - /// Values (arguments) for the paramsForFieldExpressions - /// - /// - /// - public static object? WrapObjectProjectionFieldForNullCheckExec( - string fieldDescription, - object? nullCheck, - ParameterExpression nullWrapParam, - List paramsForFieldExpressions, - Dictionary fieldExpressions, - IEnumerable fieldSelectParamValues, - ParameterExpression schemaContextParam, - object schemaContextValue - ) - { - if (nullCheck == null) - return null; - - var newExp = CreateNewExpression(fieldDescription, fieldExpressions); - var args = new List(fieldSelectParamValues.Count()); - args.AddRange(fieldSelectParamValues); - if (schemaContextParam != null) - { - args.Add(schemaContextValue); - if (!paramsForFieldExpressions.Contains(schemaContextParam)) - paramsForFieldExpressions.Add(schemaContextParam); - } - if (nullWrapParam != null) - { - if (!paramsForFieldExpressions.Contains(nullWrapParam)) - paramsForFieldExpressions.Add(nullWrapParam); - args.Add(nullCheck); - } - var result = Expression.Lambda(newExp, paramsForFieldExpressions).Compile().DynamicInvoke(args.ToArray()); - return result; - } - - public static Expression BuildEntityQueryExpression(ISchemaProvider schemaProvider, Type queryType, string query) - { - var contextParam = Expression.Parameter(queryType, $"q_{queryType.Name}"); - // TODO we should have the execution options here + contextParam ??= Expression.Parameter(queryType, $"q_{queryType.Name}"); Expression expression = EntityQueryCompiler - .CompileWith(query, contextParam, schemaProvider, new QueryRequestContext(null, null), new ExecutionOptions(), schemaProvider.MethodProvider) + .CompileWith(query, contextParam, schemaProvider, new QueryRequestContext(null, null), compileContext, schemaProvider.MethodProvider) .ExpressionResult; expression = Expression.Lambda(expression, contextParam); return expression; } + +#if NET8_0_OR_GREATER + [GeneratedRegex(@"^[0-9A-F]{8}[-]?([0-9A-F]{4}[-]?){3}[0-9A-F]{12}$", RegexOptions.IgnoreCase, "en-AU")] + private static partial Regex GuidRegexImpl(); +#endif } diff --git a/src/EntityGraphQL/Compiler/Util/LinqRuntimeTypeBuilder.cs b/src/EntityGraphQL/Compiler/Util/LinqRuntimeTypeBuilder.cs index 23cb6ac9..e9515129 100644 --- a/src/EntityGraphQL/Compiler/Util/LinqRuntimeTypeBuilder.cs +++ b/src/EntityGraphQL/Compiler/Util/LinqRuntimeTypeBuilder.cs @@ -4,7 +4,9 @@ using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; +#if NET9_0_OR_GREATER using System.Threading; +#endif namespace EntityGraphQL.Compiler.Util; @@ -17,7 +19,6 @@ public static class LinqRuntimeTypeBuilder public static readonly string DynamicTypePrefix = "Dynamic_"; private static readonly AssemblyName assemblyName = new() { Name = DynamicAssemblyName }; private static readonly ModuleBuilder moduleBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run).DefineDynamicModule(assemblyName.Name); - private static readonly Dictionary builtTypes = []; #if NET9_0_OR_GREATER private static readonly Lock lockObj = new(); @@ -25,12 +26,25 @@ public static class LinqRuntimeTypeBuilder private static readonly object lockObj = new(); #endif - // We build a class name based on all the selected fields so we can cache the anonymous types we built - // Names can't be > 1024 length, so we store them against a shorter Guid string - private static readonly Dictionary typesByFullName = []; + // We build a key based on all the selected fields so we can cache the anonymous types we built + // Type names can't be > 1024 length, so we store them against a shorter Guid string + // Key: concatenated field names + field types + // Value: (ClassName, Type) + private static readonly Dictionary typesByFullName = []; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string GetTypeKey(Dictionary fields) => fields.OrderBy(f => f.Key).Aggregate(DynamicTypePrefix, (current, field) => current + field.Key + field.Value.GetHashCode()); + private static int GetTypeKey(IReadOnlyDictionary fields, Type? parentType) + { + var hash = new HashCode(); + foreach (var field in fields.OrderBy(f => f.Key)) + { + hash.Add(field.Key); + hash.Add(field.Value); + } + if (parentType != null) + hash.Add(parentType.Name); + return hash.ToHashCode(); + } /// /// Build a dynamic type based on the fields. Types are cached so they only are created once @@ -41,7 +55,7 @@ public static class LinqRuntimeTypeBuilder /// /// /// - public static Type GetDynamicType(Dictionary fields, string description, Type? parentType = null) + public static Type GetDynamicType(IReadOnlyDictionary fields, string description, Type? parentType = null) { #if NET8_0_OR_GREATER ArgumentNullException.ThrowIfNull(fields, nameof(fields)); @@ -50,19 +64,16 @@ public static Type GetDynamicType(Dictionary fields, string descri throw new ArgumentNullException(nameof(fields)); #endif - string classFullName = GetTypeKey(fields) + parentType?.Name.GetHashCode(); + var typeHashCode = GetTypeKey(fields, parentType); lock (lockObj) { - if (!typesByFullName.TryGetValue(classFullName, out var classId)) + if (typesByFullName.TryGetValue(typeHashCode, out var typeInfo)) { - classId = $"{DynamicTypePrefix}{(description != null ? $"{description}_" : "")}{Guid.NewGuid()}"; - typesByFullName[classFullName] = classId; + return typeInfo.Type; } - if (builtTypes.TryGetValue(classId, out var builtType)) - return builtType; - - var typeBuilder = moduleBuilder.DefineType(classId.ToString(), TypeAttributes.Public | TypeAttributes.Class, parentType); + var className = $"{DynamicTypePrefix}{description}_{Guid.NewGuid()}"; + var typeBuilder = moduleBuilder.DefineType(className, TypeAttributes.Public | TypeAttributes.Class, parentType); foreach (var field in fields) { @@ -72,8 +83,8 @@ public static Type GetDynamicType(Dictionary fields, string descri typeBuilder.DefineField(field.Key, field.Value, FieldAttributes.Public); } - builtTypes[classId] = typeBuilder.CreateTypeInfo()!.AsType(); - return builtTypes[classId]; + typesByFullName[typeHashCode] = (className, typeBuilder.CreateTypeInfo()!.AsType()); + return typesByFullName[typeHashCode].Type; } } } diff --git a/src/EntityGraphQL/Compiler/Util/ParameterReplacer.cs b/src/EntityGraphQL/Compiler/Util/ParameterReplacer.cs index a7789d9d..b8ac7082 100644 --- a/src/EntityGraphQL/Compiler/Util/ParameterReplacer.cs +++ b/src/EntityGraphQL/Compiler/Util/ParameterReplacer.cs @@ -184,7 +184,7 @@ protected override Expression VisitMember(MemberExpression node) { if (nodeExp == null) { - throw new EntityGraphQLCompilerException($"Could not find field {node.Member.Name} on type {node.Type.Name}"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Could not find field {node.Member.Name} on type {node.Type.Name}"); } } } @@ -305,7 +305,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node) } } if (oldTypeArgs.Length != newTypeArgs.Count) - throw new EntityGraphQLCompilerException($"Post service object selection contains a method call with mismatched generic type arguments."); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Post service object selection contains a method call with mismatched generic type arguments."); var newCall = Expression.Call(node.Method.DeclaringType!, node.Method.Name, newTypeArgs.ToArray(), newArgs.ToArray()); return newCall; } diff --git a/src/EntityGraphQL/Compiler/Util/ServiceExpressionMarker.cs b/src/EntityGraphQL/Compiler/Util/ServiceExpressionMarker.cs new file mode 100644 index 00000000..e62f3b17 --- /dev/null +++ b/src/EntityGraphQL/Compiler/Util/ServiceExpressionMarker.cs @@ -0,0 +1,11 @@ +namespace EntityGraphQL.Compiler.Util; + +/// +/// Helper used only inside expression trees. We wrap service-backed field expressions +/// with a call to this method so later visitors can reliably detect service usage. +/// The method is a no-op at runtime and just returns the value. +/// +public static class ServiceExpressionMarker +{ + public static T MarkService(T value) => value; +} diff --git a/src/EntityGraphQL/Compiler/VariableReference.cs b/src/EntityGraphQL/Compiler/VariableReference.cs new file mode 100644 index 00000000..101d2e41 --- /dev/null +++ b/src/EntityGraphQL/Compiler/VariableReference.cs @@ -0,0 +1,16 @@ +namespace EntityGraphQL.Compiler; + +/// +/// Represents an unresolved variable reference in a fragment definition. +/// Variables in fragments cannot be resolved until the fragment is used within an operation, +/// since the fragment doesn't have access to the operation's variable context during parsing. +/// +internal sealed class VariableReference +{ + public string VariableName { get; } + + public VariableReference(string variableName) + { + VariableName = variableName; + } +} diff --git a/src/EntityGraphQL/Directives/DirectiveLocation.cs b/src/EntityGraphQL/Directives/DirectiveLocation.cs deleted file mode 100644 index 1389760b..00000000 --- a/src/EntityGraphQL/Directives/DirectiveLocation.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace EntityGraphQL.Directives; - -public enum ExecutableDirectiveLocation -{ - QUERY, - MUTATION, - SUBSCRIPTION, - FIELD, -#pragma warning disable CA1707 - FRAGMENT_DEFINITION, - FRAGMENT_SPREAD, - INLINE_FRAGMENT, - VARIABLE_DEFINITION, -#pragma warning restore CA1707 -} diff --git a/src/EntityGraphQL/Directives/ExecutableDirectiveLocation.cs b/src/EntityGraphQL/Directives/ExecutableDirectiveLocation.cs new file mode 100644 index 00000000..4388d474 --- /dev/null +++ b/src/EntityGraphQL/Directives/ExecutableDirectiveLocation.cs @@ -0,0 +1,30 @@ +using System.ComponentModel; + +namespace EntityGraphQL.Directives; + +public enum ExecutableDirectiveLocation +{ + [Description("QUERY")] + Query, + + [Description("MUTATION")] + Mutation, + + [Description("SUBSCRIPTION")] + Subscription, + + [Description("FIELD")] + Field, + + [Description("FRAGMENT_DEFINITION")] + FragmentDefinition, + + [Description("FRAGMENT_SPREAD")] + FragmentSpread, + + [Description("INLINE_FRAGMENT")] + InlineFragment, + + [Description("VARIABLE_DEFINITION")] + VariableDefinition, +} diff --git a/src/EntityGraphQL/Directives/IncludeDirectiveProcessor.cs b/src/EntityGraphQL/Directives/IncludeDirectiveProcessor.cs index 646792ce..7dd24b7d 100644 --- a/src/EntityGraphQL/Directives/IncludeDirectiveProcessor.cs +++ b/src/EntityGraphQL/Directives/IncludeDirectiveProcessor.cs @@ -9,12 +9,12 @@ public class IncludeDirectiveProcessor : DirectiveProcessor public override string Name => "include"; public override string Description => "Directs the executor to include this field or fragment only when the `if` argument is true."; - public override List Location => [ExecutableDirectiveLocation.FIELD, ExecutableDirectiveLocation.FRAGMENT_SPREAD, ExecutableDirectiveLocation.INLINE_FRAGMENT]; + public override List Location => [ExecutableDirectiveLocation.Field, ExecutableDirectiveLocation.FragmentSpread, ExecutableDirectiveLocation.InlineFragment]; public override IGraphQLNode? VisitNode(ExecutableDirectiveLocation location, IGraphQLNode? node, object? arguments) { if (arguments is null) - throw new EntityGraphQLException("Argument 'if' is required for @include directive"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "Argument 'if' is required for @include directive"); return ((IncludeArguments)arguments).If ? node : null; } } diff --git a/src/EntityGraphQL/Directives/SkipDirectiveProcessor.cs b/src/EntityGraphQL/Directives/SkipDirectiveProcessor.cs index 5e566cd4..0ee8a543 100644 --- a/src/EntityGraphQL/Directives/SkipDirectiveProcessor.cs +++ b/src/EntityGraphQL/Directives/SkipDirectiveProcessor.cs @@ -8,12 +8,12 @@ public class SkipDirectiveProcessor : DirectiveProcessor { public override string Name => "skip"; public override string Description => "Directs the executor to skip this field or fragment when the `if` argument is true."; - public override List Location => [ExecutableDirectiveLocation.FIELD, ExecutableDirectiveLocation.FRAGMENT_SPREAD, ExecutableDirectiveLocation.INLINE_FRAGMENT]; + public override List Location => [ExecutableDirectiveLocation.Field, ExecutableDirectiveLocation.FragmentSpread, ExecutableDirectiveLocation.InlineFragment]; public override IGraphQLNode? VisitNode(ExecutableDirectiveLocation location, IGraphQLNode? node, object? arguments) { if (arguments is null) - throw new EntityGraphQLException("Argument 'if' is required for @skip directive"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "Argument 'if' is required for @skip directive"); return !((SkipArguments)arguments).If ? node : null; } } diff --git a/src/EntityGraphQL/EntityGraphQL.csproj b/src/EntityGraphQL/EntityGraphQL.csproj index ce09e0e8..1df9dcf5 100644 --- a/src/EntityGraphQL/EntityGraphQL.csproj +++ b/src/EntityGraphQL/EntityGraphQL.csproj @@ -1,11 +1,11 @@ - netstandard2.1;net6.0;net8.0;net9.0 + netstandard2.1;net8.0;net9.0;net10.0 13.0 EntityGraphQL EntityGraphQL - 5.8.0 - A GraphQL library for .NET Core. Compiles queries into .NET Expressions (LinqProvider) for runtime execution against object graphs. E.g. against an ORM data model (EntityFramework or others) or just an in-memory object. + 6.0.0-beta3 + A GraphQL library for .NET. Compiles queries into .NET Expressions (LinqProvider) for runtime execution against object graphs. E.g. against an ORM data model (EntityFramework or others) or just an in-memory object. Luke Murray https://github.com/lukemurray/EntityGraphQL https://github.com/lukemurray/EntityGraphQL @@ -21,15 +21,16 @@ true - + + + + - - - - + + - + diff --git a/src/EntityGraphQL/EntityGraphQLArgumentException.cs b/src/EntityGraphQL/EntityGraphQLArgumentException.cs deleted file mode 100644 index 314ca5a6..00000000 --- a/src/EntityGraphQL/EntityGraphQLArgumentException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace EntityGraphQL; - -public class EntityGraphQLArgumentException : Exception -{ - public EntityGraphQLArgumentException(string message) - : base(message) { } - - public EntityGraphQLArgumentException(string parameterName, string message) - : base($"{message} (Parameter '{parameterName}')") { } -} diff --git a/src/EntityGraphQL/EntityGraphQLException.cs b/src/EntityGraphQL/EntityGraphQLException.cs index 8d845d34..9d6622da 100644 --- a/src/EntityGraphQL/EntityGraphQLException.cs +++ b/src/EntityGraphQL/EntityGraphQLException.cs @@ -1,22 +1,53 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Linq; namespace EntityGraphQL; +/// +/// Represents errors that occur during the execution of a GraphQL query. +/// Available for users to throw and add additional metadata. +/// public class EntityGraphQLException : Exception { - public IReadOnlyDictionary Extensions { get; } + public HashSet Messages { get; } + public Dictionary Extensions { get; } = new(); + public GraphQLErrorCategory Category { get; } + public List Path { get; set; } = new(); - public EntityGraphQLException(string message) - : base(message) - { - Extensions = new Dictionary(); - } + public EntityGraphQLException(string message, IDictionary? extensions = null, Exception? innerException = null) + : this(GraphQLErrorCategory.ExecutionError, [message], extensions, null, innerException) { } - public EntityGraphQLException(string message, IDictionary extensions) - : base(message) + public EntityGraphQLException(GraphQLErrorCategory category, string message, IDictionary? extensions = null, IEnumerable? path = null, Exception? innerException = null) + : this(category, [message], extensions, path, innerException) { } + + public EntityGraphQLException( + GraphQLErrorCategory category, + IEnumerable messages, + IDictionary? extensions = null, + IEnumerable? path = null, + Exception? innerException = null + ) + : base(messages.First(), innerException) { - Extensions = new ReadOnlyDictionary(extensions); + Category = category; + Messages = messages.ToHashSet(); + if (path != null) + Path = path.ToList(); + if (extensions != null) + Extensions = new Dictionary(extensions.ToDictionary(kv => kv.Key, kv => kv.Value)); } } + +public enum GraphQLErrorCategory +{ + /// + /// Document parsing/validation - should return 200 + /// + DocumentError, + + /// + /// Field execution errors - should return 200 + /// + ExecutionError, +} diff --git a/src/EntityGraphQL/EntityGraphQLFieldException.cs b/src/EntityGraphQL/EntityGraphQLFieldException.cs index 17ae789d..c10e255e 100644 --- a/src/EntityGraphQL/EntityGraphQLFieldException.cs +++ b/src/EntityGraphQL/EntityGraphQLFieldException.cs @@ -1,14 +1,13 @@ -using System; +using System; namespace EntityGraphQL; -internal sealed class EntityGraphQLFieldException : Exception +/// +/// Used to indicate errors that occur during the processing of a GraphQL field so we can catch it and add +/// information about the field to the error. +/// +public sealed class EntityGraphQLFieldException : Exception { - public readonly string FieldName; - - public EntityGraphQLFieldException(string fieldName, Exception innerException) - : base(null, innerException) - { - FieldName = fieldName; - } + public EntityGraphQLFieldException(string message, Exception? innerException = null) + : base(message, innerException) { } } diff --git a/src/EntityGraphQL/EntityGraphQLSchemaException.cs b/src/EntityGraphQL/EntityGraphQLSchemaException.cs new file mode 100644 index 00000000..ac6df83c --- /dev/null +++ b/src/EntityGraphQL/EntityGraphQLSchemaException.cs @@ -0,0 +1,12 @@ +using System; + +namespace EntityGraphQL; + +/// +/// Represents errors that occur during the building of a GraphQL schema. +/// +public class EntityGraphQLSchemaException : Exception +{ + public EntityGraphQLSchemaException(string message, Exception? innerException = null) + : base(message, innerException) { } +} diff --git a/src/EntityGraphQL/Extensions/EnumExtensions.cs b/src/EntityGraphQL/Extensions/EnumExtensions.cs new file mode 100644 index 00000000..af09f474 --- /dev/null +++ b/src/EntityGraphQL/Extensions/EnumExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace EntityGraphQL.Extensions; + +public static class EnumExtensions +{ + public static string GetDescription(this Enum value) + { + FieldInfo fieldInfo = value.GetType().GetField(value.ToString())!; + + if (fieldInfo == null) + { + return value.ToString(); // Fallback to enum name if field not found + } + + DescriptionAttribute[] attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + + if (attributes != null && attributes.Length != 0) + { + return attributes.First().Description; + } + + return value.ToString(); // Fallback to enum name if no DescriptionAttribute + } +} diff --git a/src/EntityGraphQL/Extensions/EnumerableExtensions.cs b/src/EntityGraphQL/Extensions/EnumerableExtensions.cs index f0b4e372..b8a7ebf5 100644 --- a/src/EntityGraphQL/Extensions/EnumerableExtensions.cs +++ b/src/EntityGraphQL/Extensions/EnumerableExtensions.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using EntityGraphQL.Schema; +using System.Threading.Tasks; namespace EntityGraphQL.Extensions; @@ -35,13 +35,6 @@ public static IEnumerable WhereWhen(this IEnumerable return source; } - public static IEnumerable WhereWhen(this IEnumerable source, EntityQueryType filter, bool applyPredicate) - { - if (applyPredicate) - return Queryable.Where(source.AsQueryable(), filter.Query!); - return source; - } - /// /// Does a null check on source. Returns null if source is null, otherwise returns source.ToList() /// If returnEmptyList is true, returns an empty list if source is null @@ -64,6 +57,14 @@ public static IEnumerable WhereWhen(this IEnumerable return source.Select(selector); } + public static async Task?> SelectWithNullCheck(this Task> source, Func selector) + { + var awaitedSource = await source; + if (awaitedSource == null) + return null; + return awaitedSource.Select(selector); + } + public static IEnumerable? SelectWithNullCheck(this IEnumerable? source, Func selector, bool returnEmptyList) { if (source == null) @@ -77,4 +78,27 @@ public static IEnumerable WhereWhen(this IEnumerable return returnEmptyList ? [] : null; return source.SelectMany(selector); } + + public static TResult? ProjectWithNullCheck(this TSource? source, Func selector) + { + if (source == null) + return default; + return selector(source); + } + + // public static async Task ProjectWithNullCheck(this Task source, Func selector) + // { + // var result = await source; + // if (result == null) + // return default; + // return selector(result); + // } + + public static async Task ProjectWithNullCheck(this Task source, Func selector) + { + var result = await source; + if (result == null) + return default; + return selector(result); + } } diff --git a/src/EntityGraphQL/Extensions/Nullability/NullabilityInfoExtensions.cs b/src/EntityGraphQL/Extensions/Nullability/NullabilityInfoExtensions.cs index 5038a008..1b21aa3b 100644 --- a/src/EntityGraphQL/Extensions/Nullability/NullabilityInfoExtensions.cs +++ b/src/EntityGraphQL/Extensions/Nullability/NullabilityInfoExtensions.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Linq.Expressions; using System.Reflection; -using EntityGraphQL.Compiler; namespace Nullability; @@ -66,17 +65,6 @@ public static NullabilityInfo GetNullabilityInfo(this MethodInfo info) return info.ReturnParameter.GetNullabilityInfo(); } - public static NullabilityState GetNullability(this MemberInfo info) - { - return GetReadOrWriteState(info.GetNullabilityInfo()); - } - - public static bool IsNullable(this MemberInfo info) - { - var nullability = info.GetNullabilityInfo(); - return IsNullable(info.Name, nullability); - } - public static NullabilityInfo GetNullabilityInfo(this FieldInfo info) { return fieldCache.GetOrAdd( @@ -89,17 +77,6 @@ public static NullabilityInfo GetNullabilityInfo(this FieldInfo info) ); } - public static NullabilityState GetNullability(this FieldInfo info) - { - return GetReadOrWriteState(info.GetNullabilityInfo()); - } - - public static bool IsNullable(this FieldInfo info) - { - var nullability = info.GetNullabilityInfo(); - return IsNullable(info.Name, nullability); - } - public static NullabilityInfo GetNullabilityInfo(this EventInfo info) { return eventCache.GetOrAdd( @@ -112,17 +89,6 @@ public static NullabilityInfo GetNullabilityInfo(this EventInfo info) ); } - public static NullabilityState GetNullability(this EventInfo info) - { - return GetReadOrWriteState(info.GetNullabilityInfo()); - } - - public static bool IsNullable(this EventInfo info) - { - var nullability = info.GetNullabilityInfo(); - return IsNullable(info.Name, nullability); - } - public static NullabilityInfo GetNullabilityInfo(this PropertyInfo info) { return propertyCache.GetOrAdd( @@ -135,17 +101,6 @@ public static NullabilityInfo GetNullabilityInfo(this PropertyInfo info) ); } - public static NullabilityState GetNullability(this PropertyInfo info) - { - return GetReadOrWriteState(info.GetNullabilityInfo()); - } - - public static bool IsNullable(this PropertyInfo info) - { - var nullability = info.GetNullabilityInfo(); - return IsNullable(info.Name, nullability); - } - public static NullabilityInfo GetNullabilityInfo(this ParameterInfo info) { return parameterCache.GetOrAdd( @@ -158,49 +113,6 @@ public static NullabilityInfo GetNullabilityInfo(this ParameterInfo info) ); } - public static NullabilityState GetNullability(this ParameterInfo info) - { - return GetReadOrWriteState(info.GetNullabilityInfo()); - } - - public static bool IsNullable(this ParameterInfo info) - { - var nullability = info.GetNullabilityInfo(); - return IsNullable(info.Name!, nullability); - } - - private static NullabilityState GetReadOrWriteState(NullabilityInfo nullability) - { - if (nullability.ReadState != NullabilityState.Unknown) - { - return nullability.ReadState; - } - - return nullability.WriteState; - } - - private static NullabilityState GetKnownState(string name, NullabilityInfo nullability) - { - var readState = nullability.ReadState; - if (readState != NullabilityState.Unknown) - { - return readState; - } - - var writeState = nullability.WriteState; - if (writeState != NullabilityState.Unknown) - { - return writeState; - } - - throw new EntityGraphQLCompilerException($"The nullability of '{nullability.Type.FullName}.{name}' is unknown. Assembly: {nullability.Type.Assembly.FullName}."); - } - - private static bool IsNullable(string name, NullabilityInfo nullability) - { - return GetKnownState(name, nullability) == NullabilityState.Nullable; - } - //Patching public static MemberInfo GetMemberWithSameMetadataDefinitionAs(Type type, MemberInfo member) { diff --git a/src/EntityGraphQL/Extensions/QueryableExtensions.cs b/src/EntityGraphQL/Extensions/QueryableExtensions.cs index 24bcc97b..ade89c16 100644 --- a/src/EntityGraphQL/Extensions/QueryableExtensions.cs +++ b/src/EntityGraphQL/Extensions/QueryableExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using System.Linq.Expressions; -using EntityGraphQL.Schema; +using System.Threading.Tasks; namespace EntityGraphQL.Extensions; @@ -34,17 +34,18 @@ public static IQueryable WhereWhen(this IQueryable so return source; } - public static IQueryable WhereWhen(this IQueryable source, EntityQueryType filter, bool applyPredicate) - { - if (applyPredicate) - return Queryable.Where(source, filter.Query!); - return source; - } - public static IQueryable? SelectWithNullCheck(this IQueryable source, Expression> selector) { if (source == null) return null; return source.Select(selector); } + + public static async Task?> SelectWithNullCheck(this Task> source, Expression> selector) + { + var awaitedSource = await source; + if (awaitedSource == null) + return null; + return awaitedSource.Select(selector); + } } diff --git a/src/EntityGraphQL/Extensions/TypeExtensions.cs b/src/EntityGraphQL/Extensions/TypeExtensions.cs index c19c8604..f63daf14 100644 --- a/src/EntityGraphQL/Extensions/TypeExtensions.cs +++ b/src/EntityGraphQL/Extensions/TypeExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace EntityGraphQL.Extensions; @@ -95,20 +96,38 @@ public static bool IsGenericTypeEnumerable(this Type source) return isEnumerable; } + public static bool IsAsyncGenericType(this Type source) + { + return source.IsGenericType + && (source.GetGenericTypeDefinition() == typeof(Task<>) || source.GetGenericTypeDefinition() == typeof(ValueTask<>) || source.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)); + } + public static bool IsGenericTypeQueryable(this Type source) { - bool isQueryable = source.IsGenericType && source.GetGenericTypeDefinition() == typeof(IQueryable<>); - if (isQueryable) - return isQueryable; + // Handle Task<> or other generic wrappers potentially containing IQueryable<> + if (source.IsGenericType) + { + var genericDef = source.GetGenericTypeDefinition(); + + if (genericDef.IsAsyncGenericType()) + { + var innerType = source.GetGenericArguments()[0]; + return IsGenericTypeQueryable(innerType); + } + } + + // Check if source is directly IQueryable<> + if (source.IsGenericType && source.GetGenericTypeDefinition() == typeof(IQueryable<>)) + return true; + // Recursively check all interfaces to find IQueryable<> foreach (var intType in source.GetInterfaces()) { - isQueryable = IsGenericTypeQueryable(intType); - if (isQueryable) - break; + if (IsGenericTypeQueryable(intType)) + return true; } - return isQueryable; + return false; } /// diff --git a/src/EntityGraphQL/GraphQLError.cs b/src/EntityGraphQL/GraphQLError.cs new file mode 100644 index 00000000..859ce085 --- /dev/null +++ b/src/EntityGraphQL/GraphQLError.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; + +namespace EntityGraphQL; + +/// +/// Describes any errors that might happen while resolving the query request +/// +public class GraphQLError : Dictionary +{ + private static readonly string MessageKey = "message"; + private static readonly string PathKey = "path"; + + public string Message => (string)this[MessageKey]; + + public List? Path + { + get => (TryGetValue(PathKey, out var value) && value is List list) ? list : null; + set + { + if (value == null) + Remove(PathKey); + else + this[PathKey] = value; + } + } + + public Dictionary? Extensions => (Dictionary?)this.GetValueOrDefault(QueryResult.ExtensionsKey); + + public GraphQLError(string message, IDictionary? extensions) + { + this[MessageKey] = message; + if (extensions != null) + this[QueryResult.ExtensionsKey] = new Dictionary(extensions); + } + + public bool IsExecutionError => Path != null; + + public GraphQLError(string message, IEnumerable? path, IDictionary? extensions) + { + this[MessageKey] = message; + if (path != null) + this[PathKey] = path.ToList(); + if (extensions != null) + this[QueryResult.ExtensionsKey] = new Dictionary(extensions); + } + + public override bool Equals(object? obj) + { + if (obj is not GraphQLError other) + return false; + + bool extensionsEqual = + (Extensions == null && other.Extensions == null) + || (Extensions != null && other.Extensions != null && Extensions.Count == other.Extensions.Count && !Extensions.Except(other.Extensions).Any()); + + return Message == other.Message && ((Path == null && other.Path == null) || (Path != null && other.Path != null && Path.SequenceEqual(other.Path))) && extensionsEqual; + } + + public override int GetHashCode() + { + int hash = Message?.GetHashCode() ?? 0; + if (Path != null) + { + foreach (var p in Path) + hash = hash * 31 + (p?.GetHashCode() ?? 0); + } + if (Extensions != null) + { + foreach (var kv in Extensions.OrderBy(kv => kv.Key)) + { + hash = hash * 31 + kv.Key.GetHashCode(); + hash = hash * 31 + (kv.Value?.GetHashCode() ?? 0); + } + } + return hash; + } +} diff --git a/src/EntityGraphQL/QueryRequest.cs b/src/EntityGraphQL/QueryRequest.cs index e592f2d3..633a631a 100644 --- a/src/EntityGraphQL/QueryRequest.cs +++ b/src/EntityGraphQL/QueryRequest.cs @@ -64,22 +64,3 @@ public class QueryVariables : Dictionary return ContainsKey(varKey) ? this[varKey] : null; } } - -/// -/// Describes any errors that might happen while resolving the query request -/// -public class GraphQLError : Dictionary -{ - private static readonly string MessageKey = "message"; - - public string Message => (string)this[MessageKey]; - - public Dictionary? Extensions => (Dictionary?)this.GetValueOrDefault(QueryResult.ExtensionsKey); - - public GraphQLError(string message, IDictionary? extensions) - { - this[MessageKey] = message; - if (extensions != null) - this[QueryResult.ExtensionsKey] = new Dictionary(extensions); - } -} diff --git a/src/EntityGraphQL/QueryResult.cs b/src/EntityGraphQL/QueryResult.cs index 591c01c0..9719f022 100644 --- a/src/EntityGraphQL/QueryResult.cs +++ b/src/EntityGraphQL/QueryResult.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; using System.Linq; -using EntityGraphQL.Schema; namespace EntityGraphQL; -public class QueryResult : Dictionary +public class QueryResult : Dictionary { private static readonly string DataKey = "data"; private static readonly string ErrorsKey = "errors"; @@ -43,7 +42,7 @@ public void AddError(GraphQLError error) this[ErrorsKey] = new List(); } - ((List)this[ErrorsKey]).Add(error); + ((List)this[ErrorsKey]!).Add(error); } public void AddErrors(IEnumerable errors) @@ -52,14 +51,16 @@ public void AddErrors(IEnumerable errors) { this[ErrorsKey] = new List(); } - ((List)this[ErrorsKey]).AddRange(errors); + ((List)this[ErrorsKey]!).AddRange(errors); } - public void SetData(IDictionary data) + public void SetData(IDictionary? data) { - this[DataKey] = data.ToDictionary(d => d.Key, d => d.Value); + this[DataKey] = data?.ToDictionary(d => d.Key, d => d.Value) ?? null; } + public bool HasDataKey => ContainsKey(DataKey); + public void RemoveDataKey() { Remove(DataKey); @@ -80,7 +81,7 @@ internal void SetQueryInfo(QueryInfo queryInfo) this[ExtensionsKey] = new Dictionary(); } - var extensions = (Dictionary)this[ExtensionsKey]; + var extensions = (Dictionary)this[ExtensionsKey]!; extensions["queryInfo"] = queryInfo; } } diff --git a/src/EntityGraphQL/Schema/ArgType.cs b/src/EntityGraphQL/Schema/ArgType.cs index 17f134d7..18a47959 100644 --- a/src/EntityGraphQL/Schema/ArgType.cs +++ b/src/EntityGraphQL/Schema/ArgType.cs @@ -4,7 +4,9 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using EntityGraphQL.Extensions; +using EntityGraphQL.Schema.Validators; using Nullability; namespace EntityGraphQL.Schema; @@ -37,19 +39,16 @@ public class ArgType public string Description { get; set; } public GqlTypeInfo Type { get; private set; } public DefaultArgValue DefaultValue { get; set; } - public MemberInfo? MemberInfo { get; internal set; } - private RequiredAttribute? requiredAttribute; public bool IsRequired { get; set; } public Type RawType { get; private set; } - public ArgType(string name, string dotnetName, GqlTypeInfo type, MemberInfo? memberInfo, Type rawType) + public ArgType(string name, string dotnetName, GqlTypeInfo type, Type rawType) { Name = name; DotnetName = dotnetName; Description = string.Empty; Type = type; - MemberInfo = memberInfo; RawType = rawType; DefaultValue = new DefaultArgValue(false, null); IsRequired = false; @@ -99,7 +98,7 @@ NullabilityInfo nullability defaultValue.Value = null; defaultValue.IsSet = false; } - if (gqlLookupType.IsConstructedGenericType && gqlLookupType.GetGenericTypeDefinition() == typeof(EntityQueryType<>)) + if (gqlLookupType == typeof(EntityQueryType)) { gqlLookupType = typeof(string); } @@ -113,7 +112,7 @@ NullabilityInfo nullability } var gqlTypeInfo = new GqlTypeInfo(() => schema.GetSchemaType(gqlLookupType, true, null), argType, nullability); - var arg = new ArgType(schema.SchemaFieldNamer(name), name, gqlTypeInfo, memberInfo, type) + var arg = new ArgType(schema.SchemaFieldNamer(name), name, gqlTypeInfo, type) { DefaultValue = defaultValue, IsRequired = markedRequired, @@ -150,17 +149,28 @@ NullabilityInfo nullability /// Validate that the value for the argument meets the requirements of the argument /// /// - /// + /// /// - public void Validate(object? val, string fieldName, IList validationErrors) + public async Task ValidateAsync(object? val, IField field, HashSet validationErrors) { var valType = val?.GetType(); if (valType != null && valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(RequiredField<>)) val = valType.GetProperty("Value")!.GetValue(val); if (requiredAttribute != null && !requiredAttribute.IsValid(val)) - validationErrors.Add(requiredAttribute.ErrorMessage != null ? $"Field '{fieldName}' - {requiredAttribute.ErrorMessage}" : $"Field '{fieldName}' - missing required argument '{Name}'"); + validationErrors.Add(requiredAttribute.ErrorMessage != null ? $"Field '{field.Name}' - {requiredAttribute.ErrorMessage}" : $"Field '{field.Name}' - missing required argument '{Name}'"); else if (IsRequired && val == null && !DefaultValue.IsSet) - validationErrors.Add($"Field '{fieldName}' - missing required argument '{Name}'"); + validationErrors.Add($"Field '{field.Name}' - missing required argument '{Name}'"); + + // Validate using all DataAnnotations validation attributes on the member + if (Type.SchemaType.IsInput) + { + // For input types, validate the entire object and its properties recursively + var validator = new DataAnnotationsValidator(); + var context = new ArgumentValidatorContext(field, val); + + await validator.ValidateAsync(context); + validationErrors.UnionWith(context.Errors); + } Type.SchemaType.Validate(val); } diff --git a/src/EntityGraphQL/Schema/ArgumentHelper.cs b/src/EntityGraphQL/Schema/ArgumentHelper.cs index 19e450a5..60f9a161 100644 --- a/src/EntityGraphQL/Schema/ArgumentHelper.cs +++ b/src/EntityGraphQL/Schema/ArgumentHelper.cs @@ -1,6 +1,4 @@ using System; -using System.Linq.Expressions; -using EntityGraphQL.Compiler; namespace EntityGraphQL.Schema; @@ -19,17 +17,6 @@ public static RequiredField Required() { return new RequiredField(); } - - /// - /// Creates a field argument that takes a String value which will be compiled into an expression and used to filter the collection - /// The argument will not be null if not supplied. Has .HasValue on this argument to test if it have a filter expression. - /// - /// - /// - public static EntityQueryType EntityQuery() - { - return new EntityQueryType(); - } } /// @@ -56,7 +43,10 @@ public RequiredField(TType value) public static implicit operator TType(RequiredField field) { if (field.Value == null) - throw new EntityGraphQLExecutionException($"Required field argument being used without a value being set. Are you trying to use RequiredField outside a of field expression?"); + throw new EntityGraphQLException( + GraphQLErrorCategory.ExecutionError, + $"Required field argument being used without a value being set. Are you trying to use RequiredField outside a of field expression?" + ); return field.Value; } @@ -70,38 +60,3 @@ public override string ToString() return Value?.ToString() ?? "null"; } } - -public class EntityQueryType : BaseEntityQueryType -{ - /// - /// The compiler will end up setting this to the compiled lambda that can be used in LINQ functions - /// - /// - public Expression>? Query { get; set; } - public override bool HasValue => Query != null; - - public EntityQueryType() - : base(typeof(TType)) { } - - public static implicit operator Expression>(EntityQueryType q) - { - if (q.Query == null) - throw new InvalidOperationException("Query is null"); - return q.Query; - } -} - -public abstract class BaseEntityQueryType -{ - public BaseEntityQueryType(Type type) - { - QueryType = type; - } - - /// - /// Use this in your expression to make a choice - /// - /// - public abstract bool HasValue { get; } - public Type QueryType { get; private set; } -} diff --git a/src/EntityGraphQL/Schema/Attributes/AllowedExceptionAttribute.cs b/src/EntityGraphQL/Schema/Attributes/AllowedExceptionAttribute.cs index e0f041b7..662c5d78 100644 --- a/src/EntityGraphQL/Schema/Attributes/AllowedExceptionAttribute.cs +++ b/src/EntityGraphQL/Schema/Attributes/AllowedExceptionAttribute.cs @@ -3,7 +3,7 @@ namespace EntityGraphQL.Schema; /// -/// Exceptions markwed with this attribute will be allowed to have their details included in the response results. +/// Exceptions marked with this attribute will be allowed to have their details included in the response results. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class AllowedExceptionAttribute : Attribute diff --git a/src/EntityGraphQL/Schema/BaseField.cs b/src/EntityGraphQL/Schema/BaseField.cs index 29c8ffbf..24ec9091 100644 --- a/src/EntityGraphQL/Schema/BaseField.cs +++ b/src/EntityGraphQL/Schema/BaseField.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Reflection; using System.Threading.Tasks; using EntityGraphQL.Compiler; using EntityGraphQL.Compiler.Util; @@ -36,12 +35,8 @@ public abstract class BaseField : IField /// /// Indicates if this field returns a Task and requires async resolution /// - public bool IsAsync { get; protected set; } + public bool IsAsync { get; internal set; } - [Obsolete( - "Avoid using this method, it creates issues if the field's type is used on multiple fields with different arguments. It will be removed in future versions. See updated OffsetPagingExtension for a better way using GetExpressionAndArguments" - )] - public IField? UseArgumentsFromField { get; set; } public Expression? ResolveExpression { get; protected set; } #endregion IField properties @@ -53,6 +48,7 @@ public abstract class BaseField : IField /// Expressions used to resolve the field in a bulk fashion. This is used for optimising the number of calls to the underlying data source. /// public IBulkFieldResolver? BulkResolver { get; protected set; } + public bool ExecuteAsService { get; private set; } protected BaseField(ISchemaProvider schema, ISchemaType fromType, string name, string? description, GqlTypeInfo returnType) { @@ -107,7 +103,7 @@ public ArgType GetArgumentType(string argName) public abstract (Expression? expression, ParameterExpression? argumentParam) GetExpression( Expression fieldExpression, Expression? fieldContext, - IGraphQLNode? parentNode, + BaseGraphQLField? fieldNode, ParameterExpression? schemaContext, CompileContext compileContext, IReadOnlyDictionary args, @@ -143,11 +139,6 @@ public void AddArguments(object args) // Update the values - we don't read new values from this as the type has now lost any default values etc but we have them in allArguments newArgs.ToList().ForEach(k => Arguments.Add(k.Key, k.Value)); - // now we need to update the MemberInfo - foreach (var item in Arguments) - { - item.Value.MemberInfo = (MemberInfo?)newArgType.GetProperty(item.Value.DotnetName) ?? newArgType.GetField(item.Value.DotnetName); - } var parameterReplacer = new ParameterReplacer(); var argParam = Expression.Parameter(newArgType, $"arg_{newArgType.Name}"); @@ -164,64 +155,6 @@ public IField Returns(GqlTypeInfo gqlTypeInfo) return this; } - [Obsolete( - "Avoid using this method, it creates issues if the field's type is used on multiple fields with different arguments. It will be removed in future versions. See updated OffsetPagingExtension for a better way using GetExpressionAndArguments" - )] - public void UseArgumentsFrom(IField field) - { - // Move the arguments definition to the new field as it needs them for processing - // don't push field.FieldParam over - ExpressionArgumentType = field.ExpressionArgumentType; - ArgumentsParameter = field.ArgumentsParameter; - Arguments = field.Arguments; - ArgumentsAreInternal = true; - UseArgumentsFromField = field; - } - - /// - /// To access this field all roles listed here are required - /// - /// - public IField RequiresAllRoles(params string[] roles) - { - RequiredAuthorization ??= new RequiredAuthorization(); - RequiredAuthorization.RequiresAllRoles(roles); - return this; - } - - /// - /// To access this field any role listed is required - /// - /// - public IField RequiresAnyRole(params string[] roles) - { - RequiredAuthorization ??= new RequiredAuthorization(); - RequiredAuthorization.RequiresAnyRole(roles); - return this; - } - - /// - /// To access this field all policies listed here are required - /// - /// - public IField RequiresAllPolicies(params string[] policies) - { - RequiredAuthorization ??= new RequiredAuthorization(); - RequiredAuthorization.RequiresAllPolicies(policies); - return this; - } - - /// - /// To access this field any policy listed is required - /// - /// - public IField RequiresAnyPolicy(params string[] policies) - { - RequiredAuthorization ??= new RequiredAuthorization(); - RequiredAuthorization.RequiresAnyPolicy(policies); - return this; - } - /// /// Clears any authorization requirements for this field /// @@ -272,4 +205,10 @@ public IField IsNullable(bool nullable) return this; } + + public IField AsService() + { + ExecuteAsService = true; + return this; + } } diff --git a/src/EntityGraphQL/Schema/BaseSchemaTypeWithFields.cs b/src/EntityGraphQL/Schema/BaseSchemaTypeWithFields.cs index 487cca16..d5d60327 100644 --- a/src/EntityGraphQL/Schema/BaseSchemaTypeWithFields.cs +++ b/src/EntityGraphQL/Schema/BaseSchemaTypeWithFields.cs @@ -2,12 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using EntityGraphQL.Compiler; using EntityGraphQL.Schema.Directives; namespace EntityGraphQL.Schema; -public abstract class BaseSchemaTypeWithFields : ISchemaType +public abstract partial class BaseSchemaTypeWithFields : ISchemaType where TFieldType : IField { public ISchemaProvider Schema { get; } @@ -31,7 +30,13 @@ public abstract class BaseSchemaTypeWithFields : ISchemaType public bool RequiresSelection => GqlType != GqlTypes.Scalar && GqlType != GqlTypes.Enum; public RequiredAuthorization? RequiredAuthorization { get; set; } + +#if NET8_0_OR_GREATER + private readonly Regex nameRegex = GqlNameRegex(); +#endif +#if NETSTANDARD2_1 private readonly Regex nameRegex = new("^[_a-zA-Z0-9]+$"); +#endif public event Action OnAddField = delegate { }; public event Action OnValidate = delegate { }; @@ -39,7 +44,7 @@ public abstract class BaseSchemaTypeWithFields : ISchemaType protected BaseSchemaTypeWithFields(ISchemaProvider schema, string name, string? description, RequiredAuthorization? requiredAuthorization) { if (!nameRegex.IsMatch(name)) - throw new EntityGraphQLCompilerException($"Names must only contain [_a-zA-Z0-9] but '{name}' does not."); + throw new EntityGraphQLSchemaException($"Names must only contain [_a-zA-Z0-9] but '{name}' does not."); this.Schema = schema; Name = name; Description = description; @@ -71,21 +76,19 @@ public void ApplyAttributes(IEnumerable attributes) /// Field name. Case sensitive /// Current request context. Used by EntityGraphQL when compiling queries. If are calling this during schema configure, you can pass null /// The field object for further configuration - /// - /// If field if not found public IField GetField(string identifier, QueryRequestContext? requestContext) { if (FieldsByName.TryGetValue(identifier, out var field)) { if (requestContext != null && !requestContext.AuthorizationService.IsAuthorized(requestContext.User, field.RequiredAuthorization)) - throw new EntityGraphQLAccessException($"You are not authorized to access the '{identifier}' field on type '{Name}'."); + throw new EntityGraphQLFieldException($"You are not authorized to access the '{identifier}' field on type '{Name}'."); if (requestContext != null && !requestContext.AuthorizationService.IsAuthorized(requestContext.User, field.ReturnType.SchemaType.RequiredAuthorization)) - throw new EntityGraphQLAccessException($"You are not authorized to access the '{field.ReturnType.SchemaType.Name}' type returned by field '{identifier}'."); + throw new EntityGraphQLFieldException($"You are not authorized to access the '{field.ReturnType.SchemaType.Name}' type returned by field '{identifier}'."); return FieldsByName[identifier]; } - throw new EntityGraphQLCompilerException($"Field '{identifier}' not found on type '{Name}'"); + throw new EntityGraphQLFieldException($"Field '{identifier}' not found on type '{Name}'"); } public bool GetField(string identifier, QueryRequestContext? requestContext, out IField? field) @@ -139,7 +142,7 @@ public void AddFields(IEnumerable fields) public IField AddField(IField field) { if (FieldsByName.ContainsKey(field.Name)) - throw new EntityQuerySchemaException($"Field '{field.Name}' already exists on type '{this.Name}'. Use ReplaceField() if this is intended."); + throw new EntityGraphQLSchemaException($"Field '{field.Name}' already exists on type '{Name}'. Use ReplaceField() if this is intended."); OnAddField(field); @@ -167,7 +170,7 @@ public ISchemaType AddDirective(ISchemaDirective directive) || (GqlType == GqlTypes.Union && !directive.Location.Contains(TypeSystemDirectiveLocation.Union)) ) { - throw new EntityQuerySchemaException($"{TypeDotnet.Name} marked with {directive.GetType().Name} directive which is not valid on a {GqlType}"); + throw new EntityGraphQLSchemaException($"{TypeDotnet.Name} marked with {directive.GetType().Name} directive which is not valid on a {GqlType}"); } directives.Add(directive); @@ -185,4 +188,9 @@ public void Validate(object? value) public abstract ISchemaType Implements(bool addTypeIfNotInSchema = true, bool addAllFieldsOnAddedType = true); public abstract ISchemaType Implements(string typeName); #pragma warning restore CA1716 + +#if NET8_0_OR_GREATER + [GeneratedRegex("^[_a-zA-Z0-9]+$")] + private static partial Regex GqlNameRegex(); +#endif } diff --git a/src/EntityGraphQL/Schema/ControllerType.cs b/src/EntityGraphQL/Schema/ControllerType.cs index 0ce541bc..af267482 100644 --- a/src/EntityGraphQL/Schema/ControllerType.cs +++ b/src/EntityGraphQL/Schema/ControllerType.cs @@ -86,11 +86,16 @@ public BaseField Add(string fieldName, string description, Delegate @delegate, S private BaseField AddMethodAsField(string name, RequiredAuthorization? classLevelRequiredAuth, MethodInfo method, string? description, SchemaBuilderOptions? options) { options ??= new SchemaBuilderOptions(); - var isAsync = method.GetCustomAttribute() != null || (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)); + var isAsync = method.GetCustomAttribute() != null || (method.ReturnType.IsGenericType && method.ReturnType.IsAsyncGenericType()); var methodAuth = SchemaType.Schema.AuthorizationService.GetRequiredAuthFromMember(method); var requiredClaims = methodAuth; if (classLevelRequiredAuth != null) - requiredClaims = requiredClaims.Concat(classLevelRequiredAuth); + { + if (requiredClaims != null) + requiredClaims = requiredClaims.Concat(classLevelRequiredAuth); + else + requiredClaims = classLevelRequiredAuth; + } var actualReturnType = GetTypeFromMethodReturn(method.ReturnType, isAsync); var nonListReturnType = actualReturnType.IsEnumerableOrArray() ? actualReturnType.GetNonNullableOrEnumerableType() : actualReturnType; if (!SchemaType.Schema.HasType(nonListReturnType) && options.AutoCreateNewComplexTypes) @@ -119,7 +124,7 @@ protected abstract BaseField MakeField( string? description, SchemaBuilderOptions? options, bool isAsync, - RequiredAuthorization requiredClaims, + RequiredAuthorization? requiredClaims, GqlTypeInfo returnType ); diff --git a/src/EntityGraphQL/Schema/Directives/OneOfDirective.cs b/src/EntityGraphQL/Schema/Directives/OneOfDirective.cs index 863b6921..3fdca5cc 100644 --- a/src/EntityGraphQL/Schema/Directives/OneOfDirective.cs +++ b/src/EntityGraphQL/Schema/Directives/OneOfDirective.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using EntityGraphQL.Compiler; using EntityGraphQL.Schema.Directives; namespace EntityGraphQL.Schema @@ -16,7 +15,7 @@ public static void OneOf(this ISchemaType type) { if (field.ReturnType.TypeNotNullable) { - throw new EntityQuerySchemaException($"{type.TypeDotnet.Name} is a OneOf type but all its fields are not nullable. OneOf input types require all the field to be nullable."); + throw new EntityGraphQLSchemaException($"{type.TypeDotnet.Name} is a OneOf type but all its fields are not nullable. OneOf input types require all the field to be nullable."); } }; @@ -27,7 +26,7 @@ public static void OneOf(this ISchemaType type) var singleField = value.GetType().GetProperties().Count(x => x.GetValue(value) != null); if (singleField != 1) // we got multiple set - throw new EntityGraphQLValidationException($"Exactly one field must be specified for argument of type {type.Name}."); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Exactly one field must be specified for argument of type {type.Name}."); } }; } diff --git a/src/EntityGraphQL/Schema/Directives/SpecifiedByDirective.cs b/src/EntityGraphQL/Schema/Directives/SpecifiedByDirective.cs index 7642e5e6..3787ba31 100644 --- a/src/EntityGraphQL/Schema/Directives/SpecifiedByDirective.cs +++ b/src/EntityGraphQL/Schema/Directives/SpecifiedByDirective.cs @@ -10,7 +10,7 @@ public static void SpecifiedBy(this ISchemaType type, string url) { if (!type.IsScalar) { - throw new EntityQuerySchemaException($"@specifiedBy can only be used on scalars"); + throw new EntityGraphQLSchemaException($"@specifiedBy can only be used on scalars"); } type.AddDirective(new SpecifiedByDirective(url)); diff --git a/src/EntityGraphQL/Schema/EntityQuerySchemaException.cs b/src/EntityGraphQL/Schema/EntityQuerySchemaException.cs deleted file mode 100644 index 0f9c58d6..00000000 --- a/src/EntityGraphQL/Schema/EntityQuerySchemaException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace EntityGraphQL.Schema; - -public class EntityQuerySchemaException : Exception -{ - public EntityQuerySchemaException(string message) - : base(message) { } - - public EntityQuerySchemaException(string message, Exception innerException) - : base(message, innerException) { } -} diff --git a/src/EntityGraphQL/Schema/EntityQueryType.cs b/src/EntityGraphQL/Schema/EntityQueryType.cs new file mode 100644 index 00000000..c5e87dff --- /dev/null +++ b/src/EntityGraphQL/Schema/EntityQueryType.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace EntityGraphQL.Schema; + +public class EntityQueryType +{ + /// + /// The compiler will end up setting this to the compiled lambda that can be used in LINQ functions + /// + /// + public LambdaExpression? Query { get; set; } + + /// + /// Stores the raw filter text when compiled later (e.g. inside field extension with an active CompileContext) + /// + public string? Text { get; set; } + public bool HasValue => Query != null || !string.IsNullOrWhiteSpace(Text); + public List ServiceFieldDependencies { get; set; } = new(); + public Expression? OriginalContext { get; set; } +} diff --git a/src/EntityGraphQL/Schema/Field.cs b/src/EntityGraphQL/Schema/Field.cs index b13d7e5d..68f839f7 100644 --- a/src/EntityGraphQL/Schema/Field.cs +++ b/src/EntityGraphQL/Schema/Field.cs @@ -1,7 +1,7 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Threading.Tasks; using EntityGraphQL.Compiler; using EntityGraphQL.Compiler.Util; using EntityGraphQL.Extensions; @@ -164,7 +164,7 @@ public Field Returns(string schemaTypeName) public override (Expression? expression, ParameterExpression? argumentParam) GetExpression( Expression fieldExpression, Expression? fieldContext, - IGraphQLNode? parentNode, + BaseGraphQLField? fieldNode, ParameterExpression? schemaContext, CompileContext compileContext, IReadOnlyDictionary args, @@ -177,7 +177,7 @@ ParameterReplacer replacer { Expression? expression = fieldExpression; // don't store parameterReplacer as a class field as GetExpression is called in compiling - i.e. across threads - (var result, var argumentParam) = PrepareFieldExpression(args, expression!, replacer, expression, parentNode, docParam, docVariables, contextChanged, compileContext); + (var result, var argumentParam) = PrepareFieldExpression(args, expression!, replacer, expression, fieldNode, docParam, docVariables, contextChanged, compileContext); if (result == null) return (null, null); // the expressions we collect have a different starting parameter. We need to change that @@ -186,8 +186,8 @@ ParameterReplacer replacer { if (fieldContext != null) result = replacer.Replace(result, FieldParam, fieldContext); - else if (parentNode?.NextFieldContext != null && parentNode.NextFieldContext.Type != FieldParam.Type) - result = replacer.Replace(result, FieldParam, parentNode.NextFieldContext); + else if (fieldNode?.ParentNode?.NextFieldContext != null && fieldNode?.ParentNode.NextFieldContext.Type != FieldParam.Type) + result = replacer.Replace(result, FieldParam, fieldNode!.ParentNode.NextFieldContext); } // need to make sure the schema context param is correct if (schemaContext != null && !contextChanged) @@ -201,7 +201,7 @@ ParameterReplacer replacer Expression fieldExpression, ParameterReplacer replacer, Expression context, - IGraphQLNode? parentNode, + BaseGraphQLField? fieldNode, ParameterExpression? docParam, IArgumentsTracker? docVariables, bool servicesPass, @@ -210,7 +210,7 @@ CompileContext compileContext { object? argumentValue = null; Expression? result = fieldExpression; - var validationErrors = new List(); + var validationErrors = new HashSet(); var originalArgParam = ArgumentsParameter; ParameterExpression? newArgParam = null; @@ -220,41 +220,27 @@ CompileContext compileContext newArgParam = Expression.Parameter(originalArgParam.Type, $"{originalArgParam.Name}_exec"); compileContext.AddArgsToCompileContext(this, args, docParam, docVariables, ref argumentValue, validationErrors, newArgParam); } - - // check if we are taking args from elsewhere (extensions do this) -#pragma warning disable CS0618 // Type or member is obsolete - // TODO remove in 6.0 - if (UseArgumentsFromField != null) - { - newArgParam = - compileContext.GetConstantParameterForField(UseArgumentsFromField) - ?? throw new EntityGraphQLCompilerException($"Could not find arguments for field '{UseArgumentsFromField.Name}' in compile context."); - argumentValue = compileContext.ConstantParameters[newArgParam]; - } if (Extensions.Count > 0) { foreach (var extension in Extensions) { - // TODO merge with GetExpression below in 6.0 if (result != null) { (result, originalArgParam, newArgParam, argumentValue) = extension.GetExpressionAndArguments( this, + fieldNode!, result!, newArgParam, argumentValue, context, - parentNode, servicesPass, replacer, originalArgParam, compileContext ); - result = extension.GetExpression(this, result!, newArgParam, argumentValue, context, parentNode, servicesPass, replacer); } } } -#pragma warning restore CS0618 // Type or member is obsolete GraphQLHelper.ValidateAndReplaceFieldArgs(this, originalArgParam, replacer, ref argumentValue, ref result!, validationErrors, newArgParam); @@ -264,8 +250,9 @@ CompileContext compileContext protected void SetUpField(LambdaExpression fieldExpression, bool withServices, bool hasArguments, bool isAsync) { ProcessResolveExpression(fieldExpression, withServices, hasArguments); - // Because we use the return type as object to make the compile time interface nicer we need to get the real return type var returnType = fieldExpression.Body.Type; + + // Because we use the return type as object to make the compile time interface nicer we need to get the real return type if (fieldExpression.Body.NodeType == ExpressionType.Convert) { returnType = ((UnaryExpression)fieldExpression.Body).Operand.Type; @@ -275,27 +262,20 @@ protected void SetUpField(LambdaExpression fieldExpression, bool withServices, b if (fieldExpression.Body.NodeType == ExpressionType.Call) returnType = ((MethodCallExpression)fieldExpression.Body).Type; - // For async-returning shapes, extract underlying T for schema building and mark as async - if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) - { - IsAsync = true; - returnType = returnType.GetGenericArguments()[0]; - } - else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) - { + // do the above before unwrapping Task<> + returnType = SchemaBuilder.GetReturnType(returnType, out bool returnsAsync); + if (returnsAsync) IsAsync = true; - returnType = returnType.GetGenericArguments()[0]; - } - else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) - { - IsAsync = true; - var t = returnType.GetGenericArguments()[0]; - returnType = typeof(IEnumerable<>).MakeGenericType(t); - } if (!isAsync && IsAsync) - throw new EntityGraphQLCompilerException("Field is synchronous but returns an async type. Use ResolveAsync() or resolve the field expression with .GetAwaiter().GetResult()"); + throw new EntityGraphQLSchemaException("Field is synchronous but returns an async type. Use ResolveAsync() or resolve the field expression with .GetAwaiter().GetResult()"); ReturnType = SchemaBuilder.MakeGraphQlType(Schema, false, returnType, null, Name, FromType); } + + public new Field AsService() + { + base.AsService(); + return this; + } } diff --git a/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimitFieldExtension.cs b/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimitFieldExtension.cs index 02a58722..247fd34a 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimitFieldExtension.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimitFieldExtension.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using EntityGraphQL.Compiler; using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Extensions; namespace EntityGraphQL.Schema.FieldExtensions; @@ -31,11 +32,11 @@ public ConcurrencyLimitFieldExtension(IEnumerable? serviceTypes, int? maxC public override (Expression? expression, ParameterExpression? originalArgParam, ParameterExpression? newArgParam, object? argumentValue) GetExpressionAndArguments( IField field, + BaseGraphQLField fieldNode, Expression expression, ParameterExpression? argumentParam, dynamic? arguments, Expression context, - IGraphQLNode? parentNode, bool servicesPass, ParameterReplacer parameterReplacer, ParameterExpression? originalArgParam, @@ -62,12 +63,7 @@ CompileContext compileContext return (newExp, originalArgParam, argumentParam, arguments); } - private static bool IsAsyncExpression(Expression expression) - { - var type = expression.Type; - return type.IsGenericType - && (type.GetGenericTypeDefinition() == typeof(Task<>) || type.GetGenericTypeDefinition() == typeof(ValueTask<>) || type.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)); - } + private static bool IsAsyncExpression(Expression expression) => expression.Type.IsAsyncGenericType(); /// /// Wraps an async field expression with hierarchical concurrency limiting diff --git a/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimiterRegistry.cs b/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimiterRegistry.cs index 6ace1dcc..98c0dde9 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimiterRegistry.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimiterRegistry.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Linq; using System.Threading; diff --git a/src/EntityGraphQL/Schema/FieldExtensions/BaseFieldExtension.cs b/src/EntityGraphQL/Schema/FieldExtensions/BaseFieldExtension.cs index f0a99ca0..757ec55e 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/BaseFieldExtension.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/BaseFieldExtension.cs @@ -15,20 +15,6 @@ public abstract class BaseFieldExtension : IFieldExtension /// public virtual void Configure(ISchemaProvider schema, IField field) { } - public virtual Expression? GetExpression( - IField field, - Expression expression, - ParameterExpression? argumentParam, - dynamic? arguments, - Expression context, - IGraphQLNode? parentNode, - bool servicesPass, - ParameterReplacer parameterReplacer - ) - { - return expression; - } - public virtual (Expression, ParameterExpression?) ProcessExpressionPreSelection(Expression baseExpression, ParameterExpression? listTypeParam, ParameterReplacer parameterReplacer) { return (baseExpression, listTypeParam); @@ -68,11 +54,11 @@ public virtual Expression ProcessScalarExpression(Expression expression, Paramet public virtual (Expression? expression, ParameterExpression? originalArgParam, ParameterExpression? newArgParam, object? argumentValue) GetExpressionAndArguments( IField field, + BaseGraphQLField fieldNode, Expression expression, ParameterExpression? argumentParam, dynamic? arguments, Expression context, - IGraphQLNode? parentNode, bool servicesPass, ParameterReplacer parameterReplacer, ParameterExpression? originalArgParam, diff --git a/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/Connection.cs b/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/Connection.cs index 8785c6c1..964c1697 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/Connection.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/Connection.cs @@ -3,24 +3,34 @@ namespace EntityGraphQL.Schema.FieldExtensions; -public class Connection +public class Connection(dynamic arguments) { - public Connection(int totalCount, dynamic arguments) - { - TotalCount = totalCount; - PageInfo = new ConnectionPageInfo(totalCount, arguments); - arguments.TotalCount = totalCount; - } + private readonly dynamic arguments = arguments; - [GraphQLNotNull] [Description("Edge information about each node in the collection")] public IEnumerable> Edges { get; set; } = new List>(); - [GraphQLNotNull] + private int totalCount; + [Description("Total count of items in the collection")] - public int TotalCount { get; set; } + public int TotalCount + { + get => totalCount; + set + { + totalCount = value; + // Store in arguments for cursor/skip calculations when using 'last' argument + arguments.TotalCount = value; + } + } + + // Lazy PageInfo - only create when accessed/needed + private ConnectionPageInfo? pageInfo; - [GraphQLNotNull] [Description("Information about this page of data")] - public ConnectionPageInfo PageInfo { get; set; } + public ConnectionPageInfo PageInfo + { + get => pageInfo ??= new ConnectionPageInfo(TotalCount, arguments); + set => pageInfo = value; + } } diff --git a/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionEdgeExtension.cs b/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionEdgeExtension.cs index 847cdf69..89b3aa15 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionEdgeExtension.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionEdgeExtension.cs @@ -23,11 +23,11 @@ public ConnectionEdgeExtension(Type listType, bool isQueryable) public override (Expression? expression, ParameterExpression? originalArgParam, ParameterExpression? newArgParam, object? argumentValue) GetExpressionAndArguments( IField field, + BaseGraphQLField fieldNode, Expression expression, ParameterExpression? argumentParam, dynamic? arguments, Expression context, - IGraphQLNode? parentNode, bool servicesPass, ParameterReplacer parameterReplacer, ParameterExpression? originalArgParam, @@ -35,32 +35,31 @@ CompileContext compileContext ) { // We know we need the arguments from the parent field as that is where they are defined - if (parentNode != null) + if (fieldNode.ParentNode != null) { argumentParam = - compileContext.GetConstantParameterForField(parentNode.Field!) - ?? throw new EntityGraphQLCompilerException($"Could not find arguments for field '{parentNode.Field!.Name}' in compile context."); + compileContext.GetConstantParameterForField(fieldNode.ParentNode.Field!) + ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Could not find arguments for field '{fieldNode.ParentNode.Field!.Name}' in compile context."); arguments = compileContext.ConstantParameters[argumentParam]; - originalArgParam = parentNode.Field!.ArgumentsParameter; + originalArgParam = fieldNode.ParentNode.Field!.ArgumentsParameter; } if (argumentParam == null) - throw new EntityGraphQLCompilerException("ConnectionEdgeExtension requires an argument parameter to be passed in"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "ConnectionEdgeExtension requires an argument parameter to be passed in"); // field.Resolve will be built with the original field context and needs to be updated // we use the resolveExpression & extensions from our parent extension. We need to figure this out at runtime as the type this Edges field // is on may be used in multiple places and have different arguments etc // See OffsetConnectionPagingTests.TestMultiUseWithArgs - var pagingExtension = (ConnectionPagingExtension)parentNode!.Field!.Extensions.Find(e => e is ConnectionPagingExtension)!; - expression = servicesPass ? expression : parameterReplacer.Replace(pagingExtension.OriginalFieldExpression!, parentNode!.Field!.FieldParam!, parentNode!.ParentNode!.NextFieldContext!); + var pagingExtension = (ConnectionPagingExtension)fieldNode.ParentNode!.Field!.Extensions.Find(e => e is ConnectionPagingExtension)!; + expression = servicesPass + ? expression + : parameterReplacer.Replace(pagingExtension.OriginalFieldExpression!, fieldNode.ParentNode!.Field!.FieldParam!, fieldNode.ParentNode!.ParentNode!.NextFieldContext!); // expression here is the adjusted Connection(). This field (edges) is where we deal with the list again - field.Resolve foreach (var extension in pagingExtension.ExtensionsBeforePaging) { - var res = extension.GetExpressionAndArguments(field, expression, argumentParam, arguments, context, parentNode, servicesPass, parameterReplacer, originalArgParam, compileContext); + var res = extension.GetExpressionAndArguments(field, fieldNode, expression, argumentParam, arguments, context, servicesPass, parameterReplacer, originalArgParam, compileContext); (expression, originalArgParam, argumentParam, arguments) = (res.Item1!, res.Item2, res.Item3!, res.Item4); -#pragma warning disable CS0618 // Type or member is obsolete - expression = extension.GetExpression(field, expression, argumentParam, arguments, context, parentNode, servicesPass, parameterReplacer)!; -#pragma warning restore CS0618 // Type or member is obsolete } if (servicesPass) @@ -70,11 +69,11 @@ CompileContext compileContext // check and set up arguments if (arguments.Before != null && arguments.After != null) - throw new EntityGraphQLArgumentException($"Field only supports either before or after being supplied, not both."); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Field '{fieldNode.ParentNode.Name}' - Field only supports either before or after being supplied, not both."); if (arguments.First != null && arguments.First < 0) - throw new EntityGraphQLArgumentException($"first argument can not be less than 0."); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Field '{fieldNode.ParentNode.Name}' - first argument can not be less than 0."); if (arguments.Last != null && arguments.Last < 0) - throw new EntityGraphQLArgumentException($"last argument can not be less than 0."); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Field '{fieldNode.ParentNode.Name}' - last argument can not be less than 0."); // deserialize cursors here once (not many times in the fields) arguments.AfterNum = ConnectionHelper.DeserializeCursor(arguments.After); @@ -83,9 +82,15 @@ CompileContext compileContext if (pagingExtension.MaxPageSize.HasValue) { if (arguments.First != null && arguments.First > pagingExtension.MaxPageSize.Value) - throw new EntityGraphQLArgumentException($"first argument can not be greater than {pagingExtension.MaxPageSize.Value}."); + throw new EntityGraphQLException( + GraphQLErrorCategory.DocumentError, + $"Field '{fieldNode.ParentNode.Name}' - first argument can not be greater than {pagingExtension.MaxPageSize.Value}." + ); if (arguments.Last != null && arguments.Last > pagingExtension.MaxPageSize.Value) - throw new EntityGraphQLArgumentException($"last argument can not be greater than {pagingExtension.MaxPageSize.Value}."); + throw new EntityGraphQLException( + GraphQLErrorCategory.DocumentError, + $"Field '{fieldNode.ParentNode.Name}' - last argument can not be greater than {pagingExtension.MaxPageSize.Value}." + ); } if (arguments.First == null && arguments.Last == null && pagingExtension.DefaultPageSize != null) @@ -112,13 +117,13 @@ CompileContext compileContext ); // we have moved the expression from the parent node to here. We need to call the before callback - if (parentNode?.IsRootField == true) + if (fieldNode.ParentNode?.IsRootField == true) BaseGraphQLField.HandleBeforeRootFieldExpressionBuild( compileContext, - BaseGraphQLField.GetOperationName((BaseGraphQLField)parentNode), - parentNode.Name!, + BaseGraphQLField.GetOperationName((BaseGraphQLField)fieldNode.ParentNode), + fieldNode.ParentNode.Name!, servicesPass, - parentNode.IsRootField, + fieldNode.ParentNode.IsRootField, ref edgeExpression ); @@ -153,7 +158,7 @@ ParameterReplacer parameterReplacer ) { if (argumentParam == null) - throw new EntityGraphQLCompilerException("ConnectionEdgeExtension requires an argument parameter to be passed in"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "ConnectionEdgeExtension requires an argument parameter to be passed in"); if (servicesPass) return (baseExpression, selectionExpressions, selectContextParam); @@ -174,7 +179,7 @@ ParameterReplacer parameterReplacer baseExpression = Expression.Call( isQueryable ? typeof(Queryable) : typeof(Enumerable), nameof(Enumerable.Select), - new Type[] { listType, edgeType }, + [listType, edgeType], baseExpression, // we have the node selection from ConnectionEdgeNodeExtension we can insert into here for a nice EF compatible query Expression.Lambda(Expression.MemberInit(Expression.New(edgeType), new List { Expression.Bind(edgeType.GetProperty("Node")!, newNodeExpression) }), firstSelectParam) @@ -186,7 +191,7 @@ ParameterReplacer parameterReplacer baseExpression = Expression.Call( typeof(Enumerable), "Select", - new Type[] { edgeType, edgeType }, + [edgeType, edgeType], baseExpression, Expression.Lambda( Expression.MemberInit( @@ -194,7 +199,7 @@ ParameterReplacer parameterReplacer new List { Expression.Bind(edgeType.GetProperty("Node")!, Expression.PropertyOrField(edgeParam, "Node")), - Expression.Bind(edgeType.GetProperty("Cursor")!, Expression.Call(typeof(ConnectionHelper), "GetCursor", null, argumentParam, idxParam, offsetParam)), + Expression.Bind(edgeType.GetProperty("Cursor")!, Expression.Call(typeof(ConnectionHelper), nameof(ConnectionHelper.GetCursor), null, argumentParam, idxParam, offsetParam)), } ), edgeParam, diff --git a/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionHelper.cs b/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionHelper.cs index 2419edc1..afa21258 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionHelper.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionHelper.cs @@ -13,7 +13,7 @@ public static class ConnectionHelper /// public static unsafe string SerializeCursor(int index) { - // resuts in less allocations + // results in less allocations const int totalUtf8Bytes = 4 * (20 / 3); Span resultSpan = stackalloc byte[totalUtf8Bytes]; if (!Utf8Formatter.TryFormat(index, resultSpan, out int writtenBytes)) diff --git a/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionPageInfo.cs b/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionPageInfo.cs index 7e9c46d0..bbf22920 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionPageInfo.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionPageInfo.cs @@ -14,7 +14,6 @@ public ConnectionPageInfo(int totalCount, dynamic arguments) this.arguments = arguments; } - [GraphQLNotNull] [Description("Last cursor in the page. Use this as the next from argument")] public string EndCursor { @@ -32,13 +31,12 @@ public string EndCursor } } - [GraphQLNotNull] [Description("Start cursor in the page. Use this to go backwards with the before argument")] public string StartCursor { get { - var idx = 1; + int idx = 1; if (arguments.AfterNum != null) idx = arguments.AfterNum + 1; else if (arguments.Last != null) diff --git a/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionPagingExtension.cs b/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionPagingExtension.cs index 46f169a4..b6624672 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionPagingExtension.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/ConnectionPaging/ConnectionPagingExtension.cs @@ -45,16 +45,16 @@ public ConnectionPagingExtension(int? defaultPageSize, int? maxPageSize) public override void Configure(ISchemaProvider schema, IField field) { if (field.ResolveExpression == null) - throw new EntityGraphQLCompilerException($"ConnectionPagingExtension requires a Resolve function set on the field"); + throw new EntityGraphQLSchemaException($"ConnectionPagingExtension requires a Resolve function set on the field"); if (!field.ResolveExpression.Type.IsEnumerableOrArray()) - throw new ArgumentException($"Expression for field {field.Name} must be a collection to use ConnectionPagingExtension. Found type {field.ReturnType.TypeDotnet}"); + throw new EntityGraphQLSchemaException($"Expression for field {field.Name} must be a collection to use ConnectionPagingExtension. Found type {field.ReturnType.TypeDotnet}"); // Make sure required types are in the schema if (!schema.HasType(typeof(ConnectionPageInfo))) schema.AddType("PageInfo", "Metadata about a page of data").AddAllFields(); listType = field.ReturnType.TypeDotnet.GetEnumerableOrArrayType()!; - isQueryable = typeof(IQueryable).IsAssignableFrom(field.ReturnType.TypeDotnet); + isQueryable = field.ReturnType.TypeDotnet.IsGenericTypeQueryable(); var edgeType = typeof(ConnectionEdge<>).MakeGenericType(listType); if (!schema.HasType(edgeType)) @@ -76,7 +76,7 @@ public override void Configure(ISchemaProvider schema, IField field) } returnType = returnSchemaType.TypeDotnet; - field.Returns(SchemaBuilder.MakeGraphQlType(schema, false, returnType, connectionName, field.Name, field.FromType)); + field.Returns(SchemaBuilder.MakeGraphQlType(schema, false, returnType, returnSchemaType, field.Name, field.FromType)); // Update field arguments field.AddArguments(new ConnectionArgs()); @@ -102,10 +102,9 @@ public override void Configure(ISchemaProvider schema, IField field) // conceptually it does similar to below (using Demo context) // See Connection for implementation details of TotalCount and PageInfo // (ctx, arguments) => { - // var connection = new Connection(ctx.Actors.Select(a => a.Person) - // -- other extensions might do things here (e.g. filter / sort) - // .Count(), arguments) + // var connection = new Connection(arguments) // { + // TotalCount = ctx.Actors.Select(a => a.Person).Count(), // only if needed // Edges = ctx.Actors.Select(a => a.Person) // -- other extensions might do things here (e.g. filter / sort) // .Skip(GetSkipNumber(arguments)) @@ -131,28 +130,48 @@ public override void Configure(ISchemaProvider schema, IField field) // return .... // does the select of only the Connection fields asked for // need to set this up here as the types are needed as we visiting the query tree // we build the real one below in GetExpression() - var totalCountExp = Expression.Call(isQueryable ? typeof(Queryable) : typeof(Enumerable), "Count", [listType], OriginalFieldExpression!); - var argTypes = new List { totalCountExp.Type, field.ArgumentsParameter!.Type }; - var paramsArgs = new List { totalCountExp, field.ArgumentsParameter }; - var fieldExpression = Expression.MemberInit(Expression.New(returnType.GetConstructor(argTypes.ToArray())!, paramsArgs)); - + var fieldExpression = BuildConnectionExpression(null, null, OriginalFieldExpression!, field.ArgumentsParameter!); field.UpdateExpression(fieldExpression); } - public override Expression? GetExpression( + private MemberInitExpression BuildConnectionExpression(BaseGraphQLField? fieldNode, dynamic? arguments, Expression resolve, ParameterExpression argumentParam) + { + // Check if we need to compute totalCount: + // 1. totalCount field is selected + // 2. pageInfo field is selected (all pageInfo fields depend on totalCount) + // 3. 'last' argument is used (skip calculation needs totalCount when last is used without before) + var needsCount = fieldNode?.QueryFields?.Any(f => f.Field?.Name == "totalCount" || f.Field?.Name == "pageInfo") ?? true; + + // Also need count if 'last' argument is provided (for skip/cursor calculations) + if (!needsCount && arguments?.Last != null) + needsCount = true; + + var bindings = new List(); + if (needsCount) + { + var totalCountExp = Expression.Call(isQueryable ? typeof(Queryable) : typeof(Enumerable), nameof(Enumerable.Count), [listType!], resolve); + bindings.Add(Expression.Bind(returnType!.GetProperty("TotalCount")!, totalCountExp)); + } + + return Expression.MemberInit(Expression.New(returnType!.GetConstructor([argumentParam.Type])!, argumentParam), bindings); + } + + public override (Expression? expression, ParameterExpression? originalArgParam, ParameterExpression? newArgParam, object? argumentValue) GetExpressionAndArguments( IField field, + BaseGraphQLField fieldNode, Expression expression, ParameterExpression? argumentParam, dynamic? arguments, Expression context, - IGraphQLNode? parentNode, bool servicesPass, - ParameterReplacer parameterReplacer + ParameterReplacer parameterReplacer, + ParameterExpression? originalArgParam, + CompileContext compileContext ) { // second pass with services we have the new edges shape. We need to handle things on the EdgeExtension if (servicesPass) - return expression; + return (expression, originalArgParam, argumentParam, arguments); #if NET8_0_OR_GREATER ArgumentNullException.ThrowIfNull(argumentParam, nameof(argumentParam)); @@ -161,7 +180,6 @@ ParameterReplacer parameterReplacer throw new ArgumentNullException(nameof(argumentParam)); #endif - // totalCountExp gets executed once in the new Connection() {} and we can reuse it var edgeExpression = OriginalFieldExpression!; if (ExtensionsBeforePaging.Count > 0) @@ -169,12 +187,12 @@ ParameterReplacer parameterReplacer // if we have other extensions (filter etc) we need to apply them to the totalCount foreach (var extension in ExtensionsBeforePaging) { - edgeExpression = extension.GetExpression(field, edgeExpression, argumentParam, arguments, context, parentNode, servicesPass, parameterReplacer)!; + var res = extension.GetExpressionAndArguments(field, fieldNode, edgeExpression, argumentParam, arguments, context, servicesPass, parameterReplacer, originalArgParam, compileContext); + (edgeExpression, originalArgParam, argumentParam, arguments) = (res.Item1!, res.Item2, res.Item3!, res.Item4); } } - var totalCountExp = Expression.Call(isQueryable ? typeof(Queryable) : typeof(Enumerable), nameof(Enumerable.Count), [listType!], edgeExpression!); - expression = Expression.MemberInit(Expression.New(returnType!.GetConstructor([totalCountExp.Type, argumentParam.Type])!, totalCountExp, argumentParam)); - return expression; + expression = BuildConnectionExpression(fieldNode, arguments, edgeExpression, argumentParam); + return (expression, originalArgParam, argumentParam, arguments); } } diff --git a/src/EntityGraphQL/Schema/FieldExtensions/Filter/FilterArgs.cs b/src/EntityGraphQL/Schema/FieldExtensions/Filter/FilterArgs.cs index 568e3f09..f7f2752f 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/Filter/FilterArgs.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/Filter/FilterArgs.cs @@ -1,6 +1,6 @@ namespace EntityGraphQL.Schema.FieldExtensions; -public class FilterArgs +public class FilterArgs { - public EntityQueryType? Filter { get; set; } = ArgumentHelper.EntityQuery(); + public EntityQueryType? Filter { get; set; } } diff --git a/src/EntityGraphQL/Schema/FieldExtensions/Filter/FilterExpressionExtension.cs b/src/EntityGraphQL/Schema/FieldExtensions/Filter/FilterExpressionExtension.cs index a730618d..7a08f2a8 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/Filter/FilterExpressionExtension.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/Filter/FilterExpressionExtension.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Linq.Expressions; using EntityGraphQL.Compiler; +using EntityGraphQL.Compiler.EntityQuery; using EntityGraphQL.Compiler.Util; using EntityGraphQL.Extensions; @@ -20,41 +21,90 @@ public class FilterExpressionExtension : BaseFieldExtension public override void Configure(ISchemaProvider schema, IField field) { if (field.ResolveExpression == null) - throw new EntityGraphQLCompilerException($"FilterExpressionExtension requires a Resolve function set on the field"); + throw new EntityGraphQLSchemaException($"FilterExpressionExtension requires a Resolve function set on the field"); if (!field.ResolveExpression.Type.IsEnumerableOrArray()) - throw new ArgumentException($"Expression for field {field.Name} must be a collection to use FilterExpressionExtension. Found type {field.ReturnType.TypeDotnet}"); + throw new EntityGraphQLSchemaException($"Expression for field {field.Name} must be a collection to use FilterExpressionExtension. Found type {field.ReturnType.TypeDotnet}"); listType = field.ReturnType.TypeDotnet.GetEnumerableOrArrayType()!; // Update field arguments - var args = Activator.CreateInstance(typeof(FilterArgs<>).MakeGenericType(listType))!; + var args = Activator.CreateInstance()!; field.AddArguments(args); - isQueryable = typeof(IQueryable).IsAssignableFrom(field.ResolveExpression.Type); + isQueryable = field.ResolveExpression.Type.IsGenericTypeQueryable(); } - public override Expression? GetExpression( + public override (Expression? expression, ParameterExpression? originalArgParam, ParameterExpression? newArgParam, object? argumentValue) GetExpressionAndArguments( IField field, + BaseGraphQLField fieldNode, Expression expression, ParameterExpression? argumentParam, dynamic? arguments, Expression context, - IGraphQLNode? parentNode, bool servicesPass, - ParameterReplacer parameterReplacer + ParameterReplacer parameterReplacer, + ParameterExpression? originalArgParam, + CompileContext compileContext ) { - // data is already filtered - if (servicesPass) - return expression; - - // we have current context update Items field - if (arguments != null && arguments?.Filter != null && arguments?.Filter.HasValue) + var filter = arguments?.Filter as EntityQueryType; + if (arguments != null && filter != null && filter?.HasValue) { - expression = Expression.Call(isQueryable ? typeof(Queryable) : typeof(Enumerable), "Where", new Type[] { listType! }, expression, arguments!.Filter.Query); + // Ensure the filter Expression is compiled at this point if only raw text was provided earlier + if (filter!.Query == null && !string.IsNullOrWhiteSpace(filter.Text)) + { + try + { + var eqlContext = new EqlCompileContext(compileContext); + var compiled = ExpressionUtil.BuildEntityQueryExpression(field.Schema, listType!, filter.Text!, eqlContext, fieldNode.NextFieldContext as ParameterExpression); + // Set back the compiled lambda to the arguments.Filter.Query property + filter.Query = (LambdaExpression)compiled; + filter.ServiceFieldDependencies = eqlContext.ServiceFieldDependencies; + filter.OriginalContext = eqlContext.OriginalContext; + } + catch (EntityGraphQLException ex) + { + throw new EntityGraphQLException(ex.Category, $"Field '{fieldNode.Name}' - {ex.Message}"); + } + } + + var filterExpression = filter.Query!; + + if (compileContext.ExecutionOptions.ExecuteServiceFieldsSeparately) + { + // Split filter into EF-safe and service-dependent parts + var splitter = new FilterSplitter(listType!); + var split = splitter.SplitFilter(filterExpression); + + if (!servicesPass && split.NonServiceFilter != null) + { + expression = Expression.Call(isQueryable ? typeof(Queryable) : typeof(Enumerable), "Where", [expression.Type.GetEnumerableOrArrayType()!], expression, split.NonServiceFilter); + } + else if (servicesPass && split.ServiceFilter != null) + { + var newListType = expression.Type.GetGenericArguments()[0]; + Expression filterExpressionServices = split.ServiceFilter.Body; + var filterContext = Expression.Parameter(newListType, "ctx_filter_services"); + foreach (var item in filter.ServiceFieldDependencies) + { + var extractedFields = item.ExtractedFieldsFromServices ?? []; + var expReplacer = new ExpressionReplacer(extractedFields, filterContext, false, false, [newListType]); + filterExpressionServices = expReplacer.Replace(filterExpressionServices); + } + // filter might have non service fields in it that need the parameter replaced + filterExpressionServices = parameterReplacer.Replace(filterExpressionServices, filter.OriginalContext!, filterContext); + filterExpressionServices = Expression.Lambda(filterExpressionServices, filterContext); + expression = Expression.Call(isQueryable ? typeof(Queryable) : typeof(Enumerable), "Where", [newListType], expression, filterExpressionServices); + } + } + else + { + // Single pass execution - apply full filter + expression = Expression.Call(isQueryable ? typeof(Queryable) : typeof(Enumerable), "Where", [expression.Type.GetEnumerableOrArrayType()!], expression, filterExpression); + } } - return expression; + return (expression, originalArgParam, argumentParam, arguments); } } diff --git a/src/EntityGraphQL/Schema/FieldExtensions/Filter/FilterSplitter.cs b/src/EntityGraphQL/Schema/FieldExtensions/Filter/FilterSplitter.cs new file mode 100644 index 00000000..d5275ebd --- /dev/null +++ b/src/EntityGraphQL/Schema/FieldExtensions/Filter/FilterSplitter.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using EntityGraphQL.Compiler.Util; + +namespace EntityGraphQL.Schema.FieldExtensions; + +public sealed class FilterSplitter +{ + private readonly Type listType; + + public FilterSplitter(Type listType) + { + this.listType = listType; + } + + public FilterSplitResult SplitFilter(LambdaExpression filterExpression) + { + var splitVisitor = new ExpressionSplitVisitor(); + splitVisitor.Visit(filterExpression.Body); + + // If there are no service markers, treat whole filter as non-service + if (!splitVisitor.ContainsServiceMarker) + return new FilterSplitResult(filterExpression, null); + + LambdaExpression? nonServiceFilter = null; + LambdaExpression? serviceFilter = null; + + if (splitVisitor.NonServiceParts?.Count > 0) + { + var nonServiceBody = splitVisitor.NonServiceParts.Aggregate(Expression.AndAlso); + nonServiceFilter = Expression.Lambda(typeof(Func<,>).MakeGenericType(listType, typeof(bool)), nonServiceBody, filterExpression.Parameters[0]); + } + + if (splitVisitor.ServiceParts?.Count > 0) + { + var serviceBody = splitVisitor.ServiceParts.Aggregate(Expression.AndAlso); + serviceFilter = Expression.Lambda(typeof(Func<,>).MakeGenericType(listType, typeof(bool)), serviceBody, filterExpression.Parameters[0]); + } + + return new FilterSplitResult(nonServiceFilter, serviceFilter); + } +} + +// Internal helpers for splitting filter expressions +public sealed class FilterSplitResult +{ + public LambdaExpression? NonServiceFilter { get; } + public LambdaExpression? ServiceFilter { get; } + + public FilterSplitResult(LambdaExpression? nonServiceFilter, LambdaExpression? serviceFilter) + { + NonServiceFilter = nonServiceFilter; + ServiceFilter = serviceFilter; + } +} + +public sealed class ServiceMarkerCheckVisitor : ExpressionVisitor +{ + public bool ContainsServiceMarker { get; private set; } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.DeclaringType == typeof(ServiceExpressionMarker)) + { + ContainsServiceMarker = true; + return node; + } + return ContainsServiceMarker ? node : base.VisitMethodCall(node); + } + + public override Expression? Visit(Expression? node) + { + // Early termination if we already found a service marker + return ContainsServiceMarker ? node : base.Visit(node); + } +} + +internal sealed class ExpressionSplitVisitor : ExpressionVisitor +{ + private readonly List nonServiceParts = []; + private readonly List serviceParts = []; + + public List? NonServiceParts => nonServiceParts; + public List? ServiceParts => serviceParts; + public bool ContainsServiceMarker { get; private set; } + + public override Expression? Visit(Expression? node) + { + if (node == null) + return null; + + if (node is BinaryExpression binary) + { + if (binary.NodeType == ExpressionType.AndAlso) + { + Visit(binary.Left); + Visit(binary.Right); + return node; + } + if (binary.NodeType == ExpressionType.OrElse) + { + if (ContainsServiceField(node)) + { + serviceParts.Add(node); + ContainsServiceMarker = true; + } + else + nonServiceParts.Add(node); + return node; + } + } + + if (node.NodeType == ExpressionType.Not) + { + if (ContainsServiceField(node)) + { + serviceParts.Add(node); + ContainsServiceMarker = true; + } + else + nonServiceParts.Add(node); + return node; + } + + if (ContainsServiceField(node)) + { + serviceParts.Add(node); + ContainsServiceMarker = true; + } + else + nonServiceParts.Add(node); + + return node; + } + + private bool ContainsServiceField(Expression expression) + { + var visitor = new ServiceMarkerCheckVisitor(); + visitor.Visit(expression); + if (visitor.ContainsServiceMarker) + ContainsServiceMarker = true; + return visitor.ContainsServiceMarker; + } +} diff --git a/src/EntityGraphQL/Schema/FieldExtensions/IFieldExtension.cs b/src/EntityGraphQL/Schema/FieldExtensions/IFieldExtension.cs index 2380267e..3580207d 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/IFieldExtension.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/IFieldExtension.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq.Expressions; using EntityGraphQL.Compiler; @@ -16,32 +15,6 @@ public interface IFieldExtension /// void Configure(ISchemaProvider schema, IField field); - /// - /// Called when the field is used in a query. This is at the compiling of the query stage, it is before the - /// field expression is joined with a Select() or built into a new {}. - /// Use this as a chance to make any expression changes based on arguments or do rules/error checks on arguments. - /// - /// This should be thread safe - /// - /// - /// The current expression for the field - /// The ParameterExpression used for accessing the arguments. Null if the field has no augments - /// The value of the arguments. Null if field have no arguments - /// The context of the schema - /// True if this is the second visit. This means the object graph is built and we are now bringing in fields that use services - /// - [Obsolete("Use GetExpressionAndArguments")] - Expression? GetExpression( - IField field, - Expression expression, - ParameterExpression? argumentParam, - dynamic? arguments, - Expression context, - IGraphQLNode? parentNode, - bool servicesPass, - ParameterReplacer parameterReplacer - ); - /// /// Get the list expression for the bulk resolve. Useful if your extension rebuilt the the graph like ConnectionPaging /// @@ -62,7 +35,6 @@ ParameterReplacer parameterReplacer /// The ParameterExpression used for accessing the arguments. Null if the field has no augments /// The value of the arguments. Null if field have no arguments /// The context of the schema - /// /// True if this is the second visit. This means the object graph is built and we are now bringing in fields that use services /// /// @@ -70,11 +42,11 @@ ParameterReplacer parameterReplacer /// (Expression? expression, ParameterExpression? originalArgParam, ParameterExpression? newArgParam, object? argumentValue) GetExpressionAndArguments( IField field, + BaseGraphQLField fieldNode, Expression expression, ParameterExpression? argumentParam, dynamic? arguments, Expression context, - IGraphQLNode? parentNode, bool servicesPass, ParameterReplacer parameterReplacer, ParameterExpression? originalArgParam, diff --git a/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPage.cs b/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPage.cs index 1399e296..5cd63eb2 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPage.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPage.cs @@ -3,7 +3,7 @@ namespace EntityGraphQL.Schema.FieldExtensions; -public class OffsetPage(int totalItems, int? skip, int? take) +public class OffsetPage(int? skip, int? take) { private readonly int? skip = skip; private readonly int? take = take; @@ -18,5 +18,5 @@ public class OffsetPage(int totalItems, int? skip, int? take) public bool HasNextPage => take != null && ((skip ?? 0) + (take ?? 0)) < TotalItems; [Description("Count of the total items in the collection")] - public int TotalItems { get; set; } = totalItems; + public int TotalItems { get; set; } } diff --git a/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPagingExtension.cs b/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPagingExtension.cs index e2ca5404..5fa57bd2 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPagingExtension.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPagingExtension.cs @@ -35,17 +35,17 @@ public OffsetPagingExtension(int? defaultPageSize, int? maxPageSize) public override void Configure(ISchemaProvider schema, IField field) { if (field.ResolveExpression == null) - throw new EntityGraphQLCompilerException($"OffsetPagingExtension requires a Resolve function set on the field"); + throw new EntityGraphQLSchemaException($"{nameof(OffsetPagingExtension)} requires a Resolve function set on the field"); if (!field.ResolveExpression.Type.IsEnumerableOrArray()) - throw new ArgumentException($"Expression for field {field.Name} must be a collection to use OffsetPagingExtension. Found type {field.ReturnType.TypeDotnet}"); + throw new EntityGraphQLSchemaException($"Expression for field {field.Name} must be a collection to use {nameof(OffsetPagingExtension)}. Found type {field.ReturnType.TypeDotnet}"); if (field.FieldType == GraphQLQueryFieldType.Mutation) - throw new EntityGraphQLCompilerException($"OffsetPagingExtension cannot be used on a mutation field {field.Name}"); + throw new EntityGraphQLSchemaException($"{nameof(OffsetPagingExtension)} cannot be used on a mutation field {field.Name}"); listType = field.ReturnType.TypeDotnet.GetEnumerableOrArrayType() - ?? throw new ArgumentException($"Expression for field {field.Name} must be a collection to use OffsetPagingExtension. Found type {field.ReturnType.TypeDotnet}"); + ?? throw new EntityGraphQLSchemaException($"Expression for field {field.Name} must be a collection to use {nameof(OffsetPagingExtension)}. Found type {field.ReturnType.TypeDotnet}"); ISchemaType returnSchemaType; var page = $"{field.ReturnType.SchemaType.Name}Page"; @@ -60,14 +60,14 @@ public override void Configure(ISchemaProvider schema, IField field) } returnType = returnSchemaType.TypeDotnet; - field.Returns(SchemaBuilder.MakeGraphQlType(schema, false, returnType, page, field.Name, field.FromType)); + field.Returns(SchemaBuilder.MakeGraphQlType(schema, false, returnType, returnSchemaType, field.Name, field.FromType)); // Update field arguments field.AddArguments(new OffsetArgs()); if (defaultPageSize.HasValue) field.Arguments["take"].DefaultValue = new DefaultArgValue(true, defaultPageSize.Value); - isQueryable = typeof(IQueryable).IsAssignableFrom(field.ResolveExpression.Type); + isQueryable = field.ResolveExpression.Type.IsGenericTypeQueryable(); // We steal any previous extensions as they were expected to work on the original Resolve which we moved to Edges Extensions = field.Extensions.Take(field.Extensions.FindIndex(e => e is OffsetPagingExtension)).ToList(); @@ -82,55 +82,56 @@ public override void Configure(ISchemaProvider schema, IField field) itemsField.AddExtension(new OffsetPagingItemsExtension(isQueryable, listType!)); // set up the field's expression so the types are all good - var fieldExpression = BuildTotalCountExpression(returnType, field.ResolveExpression, field.ArgumentsParameter!); + var fieldExpression = BuildTotalCountExpression(null, returnType, field.ResolveExpression, field.ArgumentsParameter!); field.UpdateExpression(fieldExpression); } - private MemberInitExpression BuildTotalCountExpression(Type returnType, Expression resolve, ParameterExpression argumentParam) + private MemberInitExpression BuildTotalCountExpression(BaseGraphQLField? fieldNode, Type returnType, Expression resolve, ParameterExpression argumentParam) { - var totalCountExp = Expression.Call(isQueryable ? typeof(Queryable) : typeof(Enumerable), "Count", [listType!], resolve); + var needsCount = fieldNode?.QueryFields?.Any(f => f.Field?.Name == "totalItems" || f.Field?.Name == "hasNextPage") ?? true; + + var totalCountExp = Expression.Call(isQueryable ? typeof(Queryable) : typeof(Enumerable), nameof(Enumerable.Count), [listType!], resolve); var expression = Expression.MemberInit( - Expression.New( - returnType.GetConstructor([typeof(int), typeof(int?), typeof(int?)])!, - totalCountExp, - Expression.PropertyOrField(argumentParam!, "skip"), - Expression.PropertyOrField(argumentParam!, "take") - ) + Expression.New(returnType.GetConstructor([typeof(int?), typeof(int?)])!, Expression.PropertyOrField(argumentParam!, "skip"), Expression.PropertyOrField(argumentParam!, "take")), + needsCount ? [Expression.Bind(returnType.GetProperty("TotalItems")!, totalCountExp)] : [] ); return expression; } - public override Expression? GetExpression( + public override (Expression? expression, ParameterExpression? originalArgParam, ParameterExpression? newArgParam, object? argumentValue) GetExpressionAndArguments( IField field, + BaseGraphQLField fieldNode, Expression expression, ParameterExpression? argumentParam, dynamic? arguments, Expression context, - IGraphQLNode? parentNode, bool servicesPass, - ParameterReplacer parameterReplacer + ParameterReplacer parameterReplacer, + ParameterExpression? originalArgParam, + CompileContext compileContext ) { if (servicesPass) - return expression; // we don't need to do anything. items field is there to handle it now + return (expression, originalArgParam, argumentParam, arguments); if (argumentParam == null) - throw new EntityGraphQLCompilerException($"OffsetPagingExtension requires argumentParams to be set"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"{nameof(OffsetPagingExtension)} requires argumentParams to be set"); if (maxPageSize != null && arguments?.Take > maxPageSize.Value) - throw new EntityGraphQLArgumentException($"Argument take can not be greater than {maxPageSize}."); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Field '{fieldNode.Name}' - Argument take can not be greater than {maxPageSize}."); // other extensions expect to run on the collection not our new shape var newItemsExp = OriginalFieldExpression!; // update the context foreach (var extension in Extensions) { - newItemsExp = extension.GetExpression(field, newItemsExp, argumentParam, arguments, context, parentNode, servicesPass, parameterReplacer); + var res = extension.GetExpressionAndArguments(field, fieldNode, newItemsExp, argumentParam, arguments, context, servicesPass, parameterReplacer, originalArgParam, compileContext); + (newItemsExp, originalArgParam, argumentParam, arguments) = (res.Item1!, res.Item2, res.Item3!, res.Item4); } // Build our field expression and hold it for use in the next step - var fieldExpression = BuildTotalCountExpression(returnType!, newItemsExp, argumentParam); - return fieldExpression; + Expression fieldExpression = BuildTotalCountExpression(fieldNode, returnType!, newItemsExp, argumentParam); + return (fieldExpression, originalArgParam, argumentParam, arguments); } } diff --git a/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPagingItemsExtension.cs b/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPagingItemsExtension.cs index f52e448f..dd1d6f29 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPagingItemsExtension.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/OffsetPaging/OffsetPagingItemsExtension.cs @@ -19,11 +19,11 @@ public OffsetPagingItemsExtension(bool isQueryable, Type listType) public override (Expression? expression, ParameterExpression? originalArgParam, ParameterExpression? newArgParam, object? argumentValue) GetExpressionAndArguments( IField field, + BaseGraphQLField fieldNode, Expression expression, ParameterExpression? argumentParam, dynamic? arguments, Expression context, - IGraphQLNode? parentNode, bool servicesPass, ParameterReplacer parameterReplacer, ParameterExpression? originalArgParam, @@ -31,39 +31,36 @@ CompileContext compileContext ) { // We know we need the arguments from the parent field as that is where they are defined - if (parentNode != null) + if (fieldNode.ParentNode != null) { argumentParam = - compileContext.GetConstantParameterForField(parentNode.Field!) - ?? throw new EntityGraphQLCompilerException($"Could not find arguments for field '{parentNode.Field!.Name}' in compile context."); + compileContext.GetConstantParameterForField(fieldNode.ParentNode.Field!) + ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Could not find arguments for field '{fieldNode.ParentNode.Field!.Name}' in compile context."); arguments = compileContext.ConstantParameters[argumentParam]; - originalArgParam = parentNode.Field!.ArgumentsParameter; + originalArgParam = fieldNode.ParentNode.Field!.ArgumentsParameter; } // we use the resolveExpression & extensions from our parent extension. We need to figure this out at runtime as the type this Items field // is on may be used in multiple places and have different arguments etc // See OffsetPagingTests.TestMultiUseWithArgs - var offsetPagingExtension = (OffsetPagingExtension)parentNode!.Field!.Extensions.Find(e => e is OffsetPagingExtension)!; + var offsetPagingExtension = (OffsetPagingExtension)fieldNode.ParentNode!.Field!.Extensions.Find(e => e is OffsetPagingExtension)!; var resolveExpression = offsetPagingExtension.OriginalFieldExpression!; - var originalFieldParam = parentNode.Field!.FieldParam!; - Expression newItemsExp = servicesPass ? expression : parameterReplacer.Replace(resolveExpression, originalFieldParam, parentNode!.ParentNode!.NextFieldContext!); + var originalFieldParam = fieldNode.ParentNode.Field!.FieldParam!; + Expression newItemsExp = servicesPass ? expression : parameterReplacer.Replace(resolveExpression, originalFieldParam, fieldNode.ParentNode!.ParentNode!.NextFieldContext!); // other extensions defined on the original field need to run on the collection foreach (var extension in offsetPagingExtension.Extensions) { - var res = extension.GetExpressionAndArguments(field, newItemsExp, argumentParam, arguments, context, parentNode, servicesPass, parameterReplacer, originalArgParam, compileContext); + var res = extension.GetExpressionAndArguments(field, fieldNode, newItemsExp, argumentParam, arguments, context, servicesPass, parameterReplacer, originalArgParam, compileContext); (newItemsExp, originalArgParam, argumentParam, arguments) = (res.Item1!, res.Item2, res.Item3!, res.Item4); -#pragma warning disable CS0618 // Type or member is obsolete - newItemsExp = extension.GetExpression(field, newItemsExp, argumentParam, arguments, context, parentNode, servicesPass, parameterReplacer)!; -#pragma warning restore CS0618 // Type or member is obsolete } if (servicesPass) return (newItemsExp, originalArgParam, argumentParam, arguments); // paging is done already if (argumentParam == null) - throw new EntityGraphQLCompilerException("OffsetPagingItemsExtension requires an argument parameter to be passed in"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "OffsetPagingItemsExtension requires an argument parameter to be passed in"); // Build our items expression with the paging newItemsExp = Expression.Call( @@ -81,13 +78,13 @@ CompileContext compileContext ); // we have moved the expression from the parent node to here. We need to call the before callback - if (parentNode?.IsRootField == true) + if (fieldNode.ParentNode?.IsRootField == true) BaseGraphQLField.HandleBeforeRootFieldExpressionBuild( compileContext, - BaseGraphQLField.GetOperationName((BaseGraphQLField)parentNode), - parentNode.Name!, + BaseGraphQLField.GetOperationName((BaseGraphQLField)fieldNode.ParentNode), + fieldNode.ParentNode.Name!, servicesPass, - parentNode.IsRootField, + fieldNode.ParentNode.IsRootField, ref newItemsExp ); diff --git a/src/EntityGraphQL/Schema/FieldExtensions/Sorting/SortExtension.cs b/src/EntityGraphQL/Schema/FieldExtensions/Sorting/SortExtension.cs index c24dfa7b..d8289d7a 100644 --- a/src/EntityGraphQL/Schema/FieldExtensions/Sorting/SortExtension.cs +++ b/src/EntityGraphQL/Schema/FieldExtensions/Sorting/SortExtension.cs @@ -34,16 +34,16 @@ public SortExtension(LambdaExpression? fieldSelection, bool useSchemaFields, par public override void Configure(ISchemaProvider schema, IField field) { if (field.ResolveExpression == null) - throw new EntityGraphQLCompilerException($"SortExtension requires a Resolve function set on the field"); + throw new EntityGraphQLSchemaException($"SortExtension requires a Resolve function set on the field"); if (!field.ResolveExpression.Type.IsEnumerableOrArray()) - throw new ArgumentException($"Expression for field {field.Name} must be a collection to use SortExtension. Found type {field.ReturnType.TypeDotnet}"); + throw new EntityGraphQLSchemaException($"Expression for field {field.Name} must be a collection to use SortExtension. Found type {field.ReturnType.TypeDotnet}"); if (!schema.HasType(typeof(SortDirection))) schema.AddEnum("SortDirectionEnum", "Sort direction enum"); schemaReturnType = field.ReturnType.SchemaType; listType = field.ReturnType.TypeDotnet.GetEnumerableOrArrayType()!; - methodType = typeof(IQueryable).IsAssignableFrom(field.ReturnType.TypeDotnet) ? typeof(Queryable) : typeof(Enumerable); + methodType = field.ReturnType.TypeDotnet.IsGenericTypeQueryable() ? typeof(Queryable) : typeof(Enumerable); fieldNamer = schema.SchemaFieldNamer; var sortInputName = $"{field.FromType.Name}{field.Name.FirstCharToUpper()}SortInput".FirstCharToUpper(); @@ -125,20 +125,22 @@ private static bool IsNotInputType(Type type) return type.IsEnumerableOrArray() || (type.IsClass && type != typeof(string)); } - public override Expression? GetExpression( + public override (Expression? expression, ParameterExpression? originalArgParam, ParameterExpression? newArgParam, object? argumentValue) GetExpressionAndArguments( IField field, + BaseGraphQLField fieldNode, Expression expression, ParameterExpression? argumentParam, dynamic? arguments, Expression context, - IGraphQLNode? parentNode, bool servicesPass, - ParameterReplacer parameterReplacer + ParameterReplacer parameterReplacer, + ParameterExpression? originalArgParam, + CompileContext compileContext ) { // things are sorted already and the field shape has changed if (servicesPass) - return expression; + return (expression, originalArgParam, argumentParam, arguments); // default sort gets put in arguments if (arguments != null && arguments!.Sort != null && arguments!.Sort.Count > 0) @@ -196,6 +198,6 @@ ParameterReplacer parameterReplacer thenBy = true; } } - return expression; + return (expression, originalArgParam, argumentParam, arguments); } } diff --git a/src/EntityGraphQL/Schema/FieldToResolve.cs b/src/EntityGraphQL/Schema/FieldToResolve.cs index 33e2310d..1f273bba 100644 --- a/src/EntityGraphQL/Schema/FieldToResolve.cs +++ b/src/EntityGraphQL/Schema/FieldToResolve.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Threading; using System.Threading.Tasks; using EntityGraphQL.Compiler; using EntityGraphQL.Compiler.Util; @@ -107,47 +106,35 @@ public class FieldToResolveWithArgs : FieldWithContextAndArgs public FieldToResolveWithArgs(ISchemaProvider schema, ISchemaType fromType, string name, string? description, TParams argTypes) : base(schema, fromType, name, description, argTypes) { } - public Field Resolve(Expression> fieldExpression) + public Field Resolve(Expression> fieldExpression) { SetUpField(fieldExpression, true, true, false); return this; } - public FieldWithContextAndArgs Resolve(Expression> fieldExpression) + public FieldWithContextAndArgs Resolve(Expression> fieldExpression) { SetUpField(fieldExpression, true, true, false); Services = [fieldExpression.Parameters[2]]; return this; } - [Obsolete("Use Resolve")] - public FieldWithContextAndArgs ResolveWithService(Expression> fieldExpression) => Resolve(fieldExpression); - - public FieldWithContextAndArgs Resolve(Expression> fieldExpression) + public FieldWithContextAndArgs Resolve(Expression> fieldExpression) { SetUpField(fieldExpression, true, true, false); Services = [fieldExpression.Parameters[2], fieldExpression.Parameters[3]]; return this; } - [Obsolete("Use Resolve")] - public FieldWithContextAndArgs ResolveWithServices(Expression> fieldExpression) => - Resolve(fieldExpression); - - public FieldWithContextAndArgs Resolve(Expression> fieldExpression) + public FieldWithContextAndArgs Resolve(Expression> fieldExpression) { SetUpField(fieldExpression, true, true, false); Services = [fieldExpression.Parameters[2], fieldExpression.Parameters[3], fieldExpression.Parameters[4]]; return this; } - [Obsolete("Use Resolve")] - public FieldWithContextAndArgs ResolveWithServices( - Expression> fieldExpression - ) => Resolve(fieldExpression); - public FieldWithContextAndArgs Resolve( - Expression> fieldExpression + Expression> fieldExpression ) { SetUpField(fieldExpression, true, true, false); @@ -155,13 +142,8 @@ Expression ResolveWithServices( - Expression> fieldExpression - ) => Resolve(fieldExpression); - public FieldWithContextAndArgs Resolve( - Expression> fieldExpression + Expression> fieldExpression ) { SetUpField(fieldExpression, true, true, false); @@ -169,11 +151,6 @@ Expression ResolveWithServices( - Expression> fieldExpression - ) => Resolve(fieldExpression); - public FieldWithContextAndArgs ResolveAsync(Expression> fieldExpression, int? maxConcurrency = null) => ResolveAsyncImpl(fieldExpression, maxConcurrency); @@ -245,14 +222,14 @@ public FieldWithContextAndArgs ResolveAsync ResolveAsyncImpl(LambdaExpression fieldExpression, int? maxConcurrency) { - SetUpField(fieldExpression, true, false, true); - Services = [.. fieldExpression.Parameters.Skip(1)]; + SetUpField(fieldExpression, true, true, true); + Services = [.. fieldExpression.Parameters.Skip(2)]; var serviceTypes = Services.Select(s => s.Type).ToArray(); // if return type is Task check that it is Task<> otherwise throw if (typeof(Task).IsAssignableFrom(fieldExpression.Body.Type) && !fieldExpression.Body.Type.IsGenericType) { - throw new EntityGraphQLCompilerException("Async field expression must return Task not Task as the field needs a result."); + throw new EntityGraphQLSchemaException("Async field expression must return Task not Task as the field needs a result."); } // Add concurrency limiting @@ -284,9 +261,6 @@ public FieldWithContext Resolve(Expression ResolveWithService(Expression> fieldExpression) => Resolve(fieldExpression); - public FieldWithContext Resolve(Expression> fieldExpression) { SetUpField(fieldExpression, true, false, false); @@ -294,9 +268,6 @@ public FieldWithContext Resolve(Expression ResolveWithServices(Expression> fieldExpression) => Resolve(fieldExpression); - public FieldWithContext Resolve(Expression> fieldExpression) { SetUpField(fieldExpression, true, false, false); @@ -304,10 +275,6 @@ public FieldWithContext Resolve(Expre return this; } - [Obsolete("Use Resolve")] - public FieldWithContext ResolveWithServices(Expression> fieldExpression) => - Resolve(fieldExpression); - public FieldWithContext Resolve(Expression> fieldExpression) { SetUpField(fieldExpression, true, false, false); @@ -315,11 +282,6 @@ public FieldWithContext Resolve ResolveWithServices( - Expression> fieldExpression - ) => Resolve(fieldExpression); - public FieldWithContext Resolve( Expression> fieldExpression ) @@ -329,11 +291,6 @@ public FieldWithContext Resolve ResolveWithServices( - Expression> fieldExpression - ) => Resolve(fieldExpression); - /// /// Resolve an async field with optional concurrency limiting /// @@ -412,7 +369,7 @@ private FieldWithContext ResolveAsyncImpl(LambdaExpression fieldExpres // if return type is Task check that it is Task<> otherwise throw if (typeof(Task).IsAssignableFrom(fieldExpression.Body.Type) && !fieldExpression.Body.Type.IsGenericType) { - throw new EntityGraphQLCompilerException("Async field expression must return Task not Task as the field needs a result."); + throw new EntityGraphQLSchemaException("Async field expression must return Task not Task as the field needs a result."); } // Add concurrency limiting diff --git a/src/EntityGraphQL/Schema/GqlTypeInfo.cs b/src/EntityGraphQL/Schema/GqlTypeInfo.cs index 52718f9f..5f8c34cd 100644 --- a/src/EntityGraphQL/Schema/GqlTypeInfo.cs +++ b/src/EntityGraphQL/Schema/GqlTypeInfo.cs @@ -35,14 +35,14 @@ public GqlTypeInfo(Func schemaTypeGetter, Type typeDotnet) /// Func to get the ISchemaType. Lookup is func as the type might be added later. It is cached after first look up /// The dotnet type as it is. E.g. the List etc. /// Nullability information about the property - public GqlTypeInfo(Func schemaTypeGetter, Type typeDotnet, NullabilityInfo nullability) + public GqlTypeInfo(Func schemaTypeGetter, Type typeDotnet, NullabilityInfo? nullability) { SchemaTypeGetter = schemaTypeGetter; TypeDotnet = typeDotnet; IsList = TypeDotnet.IsEnumerableOrArray(); - TypeNotNullable = nullability.ReadState == NullabilityState.NotNull; - ElementTypeNullable = nullability.GenericTypeArguments.Length > 0 && nullability.GenericTypeArguments[0].ReadState == NullabilityState.Nullable; + TypeNotNullable = nullability != null ? nullability.ReadState == NullabilityState.NotNull : TypeDotnet.IsValueType && !TypeDotnet.IsNullableType(); + ElementTypeNullable = nullability != null && nullability.GenericTypeArguments.Length > 0 && nullability.GenericTypeArguments[0].ReadState == NullabilityState.Nullable; } /// diff --git a/src/EntityGraphQL/Schema/ICustomTypeConverter.cs b/src/EntityGraphQL/Schema/ICustomTypeConverter.cs deleted file mode 100644 index 09f483f9..00000000 --- a/src/EntityGraphQL/Schema/ICustomTypeConverter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace EntityGraphQL.Schema; - -public interface ICustomTypeConverter -{ - Type Type { get; } - - /// - /// Change a non-null value of fromType to toType using you're custom method - /// - /// The new object as toType - object? ChangeType(object value, Type toType, ISchemaProvider schema); -} diff --git a/src/EntityGraphQL/Schema/IField.cs b/src/EntityGraphQL/Schema/IField.cs index 17c32f35..54983c4e 100644 --- a/src/EntityGraphQL/Schema/IField.cs +++ b/src/EntityGraphQL/Schema/IField.cs @@ -84,11 +84,6 @@ public interface IField IReadOnlyCollection> Validators { get; } - [Obsolete( - "Avoid using this method, it creates issues if the field's type is used on multiple fields with different arguments. It will be removed in future versions. See updated OffsetPagingExtension for a better way using GetExpressionAndArguments" - )] - IField? UseArgumentsFromField { get; } - /// /// Given the current context, a type and a field name, it returns the expression for that field. Allows the provider to have a complex expression for a simple field. /// Note this will change fieldExpression if the expression references arguments @@ -96,7 +91,7 @@ public interface IField (Expression? expression, ParameterExpression? argumentParam) GetExpression( Expression fieldExpression, Expression? fieldContext, - IGraphQLNode? parentNode, + BaseGraphQLField? fieldNode, ParameterExpression? schemaContext, CompileContext compileContext, IReadOnlyDictionary args, @@ -118,37 +113,12 @@ ParameterReplacer replacer void AddArguments(object args); IField Returns(GqlTypeInfo gqlTypeInfo); - [Obsolete( - "Avoid using this method, it creates issues if the field's type is used on multiple fields with different arguments. It will be removed in future versions. See updated OffsetPagingExtension for a better way using GetExpressionAndArguments" - )] - void UseArgumentsFrom(IField field); IField AddValidator() where TValidator : IArgumentValidator; IField AddValidator(Action callback); - /// - /// To access this field all roles listed here are required - /// - /// - IField RequiresAllRoles(params string[] roles); - - /// - /// To access this field any role listed is required - /// - /// - IField RequiresAnyRole(params string[] roles); - - /// - /// To access this field all policies listed here are required - /// - /// - IField RequiresAllPolicies(params string[] policies); - - /// - /// To access this field any policy listed is required - /// - /// - IField RequiresAnyPolicy(params string[] policies); - IField IsNullable(bool nullable); + + IField AsService(); + bool ExecuteAsService { get; } } diff --git a/src/EntityGraphQL/Schema/IGqlAuthorizationService.cs b/src/EntityGraphQL/Schema/IGqlAuthorizationService.cs index 038a49ed..672174fb 100644 --- a/src/EntityGraphQL/Schema/IGqlAuthorizationService.cs +++ b/src/EntityGraphQL/Schema/IGqlAuthorizationService.cs @@ -11,7 +11,7 @@ namespace EntityGraphQL.Schema; public interface IGqlAuthorizationService { RequiredAuthorization? GetRequiredAuthFromExpression(LambdaExpression fieldSelection); - RequiredAuthorization GetRequiredAuthFromMember(MemberInfo field); - RequiredAuthorization GetRequiredAuthFromType(Type type); + RequiredAuthorization? GetRequiredAuthFromMember(MemberInfo field); + RequiredAuthorization? GetRequiredAuthFromType(Type type); bool IsAuthorized(ClaimsPrincipal? user, RequiredAuthorization? requiredAuthorization); } diff --git a/src/EntityGraphQL/Schema/ISchemaProvider.cs b/src/EntityGraphQL/Schema/ISchemaProvider.cs index 1e0339da..dd3234fd 100644 --- a/src/EntityGraphQL/Schema/ISchemaProvider.cs +++ b/src/EntityGraphQL/Schema/ISchemaProvider.cs @@ -5,6 +5,27 @@ namespace EntityGraphQL.Schema; +// Generic TryConvert delegate for custom converters + +/// +/// A delegate for trying to convert from one type to another with an out parameter for the result +/// +/// type to convert from +/// type to convert to +public delegate bool TypeConverterTryFromTo(TFrom value, ISchemaProvider schema, out TTo result); + +/// +/// A delegate for trying to convert from object to a specific type with an out parameter for the result +/// +/// type to convert to +public delegate bool TypeConverterTryTo(object? value, Type toType, ISchemaProvider schema, out TTo result); + +/// +/// A delegate for trying to convert from a specific type to object with an out parameter for the result +/// +/// type to convert from +public delegate bool TypeConverterTryFrom(TFrom value, Type toType, ISchemaProvider schema, out object? result); + /// /// An interface that the Compiler uses to help understand the types it is building against. This abstraction lets us /// have a simple provider that maps directly to an object as well as other complex providers that read a schema from else where @@ -21,8 +42,18 @@ public interface ISchemaProvider Func SchemaFieldNamer { get; } IGqlAuthorizationService AuthorizationService { get; set; } string QueryContextName { get; } - IDictionary TypeConverters { get; } EqlMethodProvider MethodProvider { get; } + + ISchemaProvider AddCustomTypeConverter(Func convert); + ISchemaProvider AddCustomTypeConverter(TypeConverterTryFromTo tryConvert); + ISchemaProvider AddCustomTypeConverter(Func convert); + ISchemaProvider AddCustomTypeConverter(TypeConverterTryTo tryConvert); + ISchemaProvider AddCustomTypeConverter(Func convert, params Type[] supportedToTypes); + ISchemaProvider AddCustomTypeConverter(TypeConverterTryFrom tryConvert, params Type[] supportedToTypes); + + // Attempts to convert the value using custom converters (from-to first, then to-only, then from-only). + bool TryConvertCustom(object? value, Type toType, out object? result); + void AddDirective(IDirectiveProcessor directive); ISchemaType AddEnum(string name, Type type, string description); SchemaType AddEnum(string name, string description); @@ -50,11 +81,9 @@ void AddMutationsFrom(SchemaBuilderOptions? options = null) IExtensionAttributeHandler? GetAttributeHandlerFor(Type attributeType); ISchemaProvider AddAttributeHandler(IExtensionAttributeHandler handler); ISchemaType GetSchemaType(string typeName, QueryRequestContext? requestContext); - - [Obsolete("Use GetSchemaType(Type dotnetType, bool inputTypeScope, QueryRequestContext? requestContext) instead")] ISchemaType GetSchemaType(Type dotnetType, QueryRequestContext? requestContext); - ISchemaType GetSchemaType(Type dotnetType, bool inputTypeScope, QueryRequestContext? requestContext); - bool TryGetSchemaType(Type dotnetType, bool inputTypeScope, out ISchemaType? schemaType, QueryRequestContext? requestContext); + ISchemaType GetSchemaType(Type dotnetType, bool inputTypesOnly, QueryRequestContext? requestContext); + bool TryGetSchemaType(Type dotnetType, bool inputTypesOnly, out ISchemaType? schemaType, QueryRequestContext? requestContext); bool HasType(string typeName); bool HasType(Type type); void PopulateFromContext(SchemaBuilderOptions? options = null); @@ -81,4 +110,6 @@ void AddMutationsFrom(SchemaBuilderOptions? options = null) /// void Validate(); ISchemaType CheckTypeAccess(ISchemaType schemaType, QueryRequestContext? requestContext); + IEnumerable GenerateErrors(Exception exception, string? fieldName = null); + string AllowedExceptionMessage(Exception exception, string? fieldName = null); } diff --git a/src/EntityGraphQL/Schema/MethodField.cs b/src/EntityGraphQL/Schema/MethodField.cs index 4d72aab0..75f5cd45 100644 --- a/src/EntityGraphQL/Schema/MethodField.cs +++ b/src/EntityGraphQL/Schema/MethodField.cs @@ -30,7 +30,7 @@ public MethodField( GqlTypeInfo returnType, MethodInfo method, string description, - RequiredAuthorization requiredAuth, + RequiredAuthorization? requiredAuth, bool isAsync, SchemaBuilderOptions options ) @@ -63,27 +63,30 @@ SchemaBuilderOptions options ExpressionArgumentType = LinqRuntimeTypeBuilder.GetDynamicType(flattenedTypes, method.Name)!; } - public virtual async Task CallAsync( + public virtual async Task<(object? data, IGraphQLValidator? methodValidator)> CallAsync( object? context, IReadOnlyDictionary? gqlRequestArgs, IServiceProvider? serviceProvider, ParameterExpression? variableParameter, IArgumentsTracker? docVariables, - ExecutionOptions executionOptions + CompileContext compileContext ) { if (context == null) - return null; + return (null, null); // args in the mutation method - may be arguments in the graphql schema, services injected var allArgs = new List(); var argsToValidate = new Dictionary(); object? argInstance = null; - var validationErrors = new List(); + var validationErrors = new HashSet(); var setProperties = new List(); var graphQLArgumentsSet = new ArgumentsTracker(); + // we get the validator so we can get errors from it + IGraphQLValidator? validator = serviceProvider?.GetService(); + // add parameters and any DI services foreach (var p in Method.GetParameters()) { @@ -118,10 +121,10 @@ ExecutionOptions executionOptions // this could be int to RequiredField if (value != null && value.GetType() != argField.RawType) { - value = ExpressionUtil.ConvertObjectType(value, argField.RawType, Schema, executionOptions); + value = ExpressionUtil.ConvertObjectType(value, argField.RawType, Schema); } - argField.Validate(value, Name, validationErrors); + await argField.ValidateAsync(value, this, validationErrors); allArgs.Add(value!); argsToValidate.Add(p.Name!, value!); @@ -134,10 +137,15 @@ ExecutionOptions executionOptions } else if (serviceProvider != null) { - var service = - serviceProvider.GetService(p.ParameterType) - ?? throw new EntityGraphQLExecutionException($"Service {p.ParameterType.Name} not found for dependency injection for mutation {Method.Name}"); - allArgs.Add(service); + if (p.ParameterType == typeof(IGraphQLValidator) && validator != null) + allArgs.Add(validator); + else + { + var service = + serviceProvider.GetService(p.ParameterType) + ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Service {p.ParameterType.Name} not found for dependency injection for mutation {Method.Name}"); + allArgs.Add(service); + } } else if (typeof(IArgumentsTracker) == p.ParameterType) { @@ -162,13 +170,13 @@ ExecutionOptions executionOptions } if (validatorContext.Errors != null && validatorContext.Errors.Count > 0) { - validationErrors.AddRange(validatorContext.Errors); + validationErrors.UnionWith(validatorContext.Errors); } } if (validationErrors.Count > 0) { - throw new EntityGraphQLValidationException(validationErrors); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, validationErrors); } // we create an instance _per request_ injecting any parameters to the constructor @@ -178,30 +186,31 @@ ExecutionOptions executionOptions object? instance = serviceProvider != null ? ActivatorUtilities.CreateInstance(serviceProvider, Method.DeclaringType!) : Activator.CreateInstance(Method.DeclaringType!); object? result; - if (IsAsync) + try { - result = await (dynamic?)Method.Invoke(instance, allArgs.Count > 0 ? allArgs.ToArray() : null); - } - else - { - try + if (IsAsync) { - result = Method.Invoke(instance, allArgs.ToArray()); + result = await (dynamic?)Method.Invoke(instance, allArgs.Count > 0 ? allArgs.ToArray() : null); } - catch (TargetInvocationException ex) + else { - if (ex.InnerException != null) - throw ex.InnerException; - throw; + result = Method.Invoke(instance, allArgs.ToArray()); } + + return (result, validator); + } + catch (TargetInvocationException ex) + { + if (ex.InnerException != null) + throw ex.InnerException; + throw; } - return result; } public override (Expression? expression, ParameterExpression? argumentParam) GetExpression( Expression fieldExpression, Expression? fieldContext, - IGraphQLNode? parentNode, + BaseGraphQLField? fieldNode, ParameterExpression? schemaContext, CompileContext? compileContext, IReadOnlyDictionary args, diff --git a/src/EntityGraphQL/Schema/Models/Introspection.cs b/src/EntityGraphQL/Schema/Models/Introspection.cs index 4e8974d6..f15e5cd4 100644 --- a/src/EntityGraphQL/Schema/Models/Introspection.cs +++ b/src/EntityGraphQL/Schema/Models/Introspection.cs @@ -43,7 +43,7 @@ public TypeElement(string? kind, string? name) // Fields is added dynamically so it is lazily loaded - public InputValue[] InputFields { get; set; } = []; + public InputValue[]? InputFields { get; set; } = []; public TypeElement[] Interfaces { get; set; } = []; diff --git a/src/EntityGraphQL/Schema/MutationField.cs b/src/EntityGraphQL/Schema/MutationField.cs index a4f70a23..4f9d705a 100644 --- a/src/EntityGraphQL/Schema/MutationField.cs +++ b/src/EntityGraphQL/Schema/MutationField.cs @@ -13,7 +13,7 @@ public MutationField( GqlTypeInfo returnType, MethodInfo method, string description, - RequiredAuthorization requiredAuth, + RequiredAuthorization? requiredAuth, bool isAsync, SchemaBuilderOptions options ) diff --git a/src/EntityGraphQL/Schema/MutationSchemaType.cs b/src/EntityGraphQL/Schema/MutationSchemaType.cs index 9cfe822a..f4ff0f59 100644 --- a/src/EntityGraphQL/Schema/MutationSchemaType.cs +++ b/src/EntityGraphQL/Schema/MutationSchemaType.cs @@ -22,16 +22,16 @@ public override ISchemaType AddAllFields(SchemaBuilderOptions? options = null) public override ISchemaType ImplementAllBaseTypes(bool addTypeIfNotInSchema = true, bool addAllFieldsOnAddedType = true) { - throw new EntityQuerySchemaException("Cannot add base types to a mutation"); + throw new EntityGraphQLSchemaException("Cannot add base types to a mutation"); } public override ISchemaType Implements(bool addTypeIfNotInSchema = true, bool addAllFieldsOnAddedType = true) { - throw new EntityQuerySchemaException("Cannot add base types to a mutation"); + throw new EntityGraphQLSchemaException("Cannot add base types to a mutation"); } public override ISchemaType Implements(string typeName) { - throw new EntityQuerySchemaException("Cannot add base types to a mutation"); + throw new EntityGraphQLSchemaException("Cannot add base types to a mutation"); } } diff --git a/src/EntityGraphQL/Schema/MutationType.cs b/src/EntityGraphQL/Schema/MutationType.cs index d056a6cd..3bddb6ba 100644 --- a/src/EntityGraphQL/Schema/MutationType.cs +++ b/src/EntityGraphQL/Schema/MutationType.cs @@ -1,7 +1,7 @@ using System; using System.Linq.Expressions; using System.Reflection; -using System.Threading.Tasks; +using EntityGraphQL.Extensions; namespace EntityGraphQL.Schema; @@ -15,7 +15,7 @@ public MutationType(ISchemaProvider schema, string name, string? description, Re protected override Type GetTypeFromMethodReturn(Type type, bool isAsync) { - if (isAsync || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>))) + if (isAsync || type.IsAsyncGenericType()) { type = type.GetGenericArguments()[0]; } @@ -33,7 +33,7 @@ protected override BaseField MakeField( string? description, SchemaBuilderOptions? options, bool isAsync, - RequiredAuthorization requiredClaims, + RequiredAuthorization? requiredClaims, GqlTypeInfo returnType ) { diff --git a/src/EntityGraphQL/Schema/RequiredAuthorization.cs b/src/EntityGraphQL/Schema/RequiredAuthorization.cs index 4dee87de..e2d02363 100644 --- a/src/EntityGraphQL/Schema/RequiredAuthorization.cs +++ b/src/EntityGraphQL/Schema/RequiredAuthorization.cs @@ -4,84 +4,86 @@ namespace EntityGraphQL.Schema; /// -/// Details on the authorisation required by a field or type +/// Details on the authorization required by a field or type. +/// Uses a keyed data structure to allow different authorization implementations to store their requirements. +/// +/// Data is keyed by your authorization implementation and the value is a list of list of strings to allow for AND/OR combinations +/// For example: +/// To require any of roles A or B and also any of roles C or D you would have: +/// [ [ "A", "B" ], [ "C", "D" ] ] +/// +/// Core EntityGraphQL provides a role-based authorization implementation via extension methods /// public class RequiredAuthorization { /// - /// Each item in the "first" list is AND claims and each in the inner list is OR claims - /// This means [Authorize(Roles = "Blah,Blah2")] is either of those roles - /// and - /// [Authorize(Roles = "Blah")] - /// [Authorize(Roles = "Blah2")] is both of those roles + /// Keyed authorization data that can be used by authorization implementations /// - private readonly List> requiredPolicies; - public IEnumerable> Policies => requiredPolicies; - private readonly List> requiredRoles; - public IEnumerable> Roles => requiredRoles; + private readonly Dictionary>> authData = []; + public IReadOnlyDictionary>> AuthData => authData; - public RequiredAuthorization() - { - requiredPolicies = []; - requiredRoles = []; - } + public bool Any() => authData.Count > 0; /// - /// Create a new RequiredAuthorization object from a list of roles and/or policies + /// Set keyed authorization data /// - /// Roles required - /// ASP.NET policies requried - public RequiredAuthorization(IEnumerable>? roles, IEnumerable>? policies) - { - requiredRoles = roles?.ToList() ?? []; - requiredPolicies = policies?.ToList() ?? []; - } - - public bool Any() => requiredPolicies.Count > 0 || requiredRoles.Count > 0; - - public void RequiresAnyRole(params string[] roles) - { - requiredRoles.Add(roles.ToList()); - } - - public void RequiresAllRoles(params string[] roles) - { - requiredRoles.AddRange(roles.Select(s => new List { s })); - } - - public void RequiresAnyPolicy(params string[] policies) + public void SetData(string key, List> value) { - requiredPolicies.Add(policies.ToList()); + authData[key] = value; } - public void RequiresAllPolicies(params string[] policies) - { - requiredPolicies.AddRange(policies.Select(s => new List { s })); - } - - public void ClearRoles() + /// + /// Get keyed authorization data + /// + public bool TryGetData(string key, out List>? value) { - requiredRoles.Clear(); + if (authData.TryGetValue(key, out var obj)) + { + value = obj; + return true; + } + value = default; + return false; } - public void ClearPolicies() + /// + /// Remove keyed authorization data + /// + public bool RemoveData(string key) { - requiredPolicies.Clear(); + return authData.Remove(key); } + /// + /// Clear all authorization data + /// public void Clear() { - ClearRoles(); - ClearPolicies(); + authData.Clear(); } public RequiredAuthorization Concat(RequiredAuthorization requiredAuthorization) { var newRequiredAuthorization = new RequiredAuthorization(); - newRequiredAuthorization.requiredPolicies.AddRange(requiredPolicies); - newRequiredAuthorization.requiredPolicies.AddRange(requiredAuthorization.requiredPolicies); - newRequiredAuthorization.requiredRoles.AddRange(requiredRoles); - newRequiredAuthorization.requiredRoles.AddRange(requiredAuthorization.requiredRoles); + + // Merge keyed data - need to handle list merging specially + foreach (var kvp in authData) + { + newRequiredAuthorization.authData[kvp.Key] = kvp.Value.Select(group => group.ToList()).ToList(); + } + + foreach (var kvp in requiredAuthorization.authData) + { + if (newRequiredAuthorization.authData.TryGetValue(kvp.Key, out var existing)) + { + existing.AddRange(kvp.Value.Select(group => group.ToList())); + } + else + { + newRequiredAuthorization.authData[kvp.Key] = kvp.Value.Select(group => group.ToList()).ToList(); + } + } + return newRequiredAuthorization; } } diff --git a/src/EntityGraphQL/Schema/RoleAuthorizationExtensions.cs b/src/EntityGraphQL/Schema/RoleAuthorizationExtensions.cs new file mode 100644 index 00000000..6434ec5f --- /dev/null +++ b/src/EntityGraphQL/Schema/RoleAuthorizationExtensions.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Linq; + +namespace EntityGraphQL.Schema; + +/// +/// Extension methods to add role-based authorization to GraphQL fields and types +/// +public static class RoleAuthorizationExtensions +{ + internal const string RolesKey = "egql:core:roles"; + + /// + /// Get the roles from a RequiredAuthorization object + /// + public static IEnumerable>? GetRoles(this RequiredAuthorization requiredAuthorization) + { + if (requiredAuthorization.TryGetData(RolesKey, out var roles)) + { + return roles; + } + return null; + } + + /// + /// To access this field all roles listed here are required + /// + public static IField RequiresAllRoles(this IField field, params string[] roles) + { + field.RequiredAuthorization ??= new RequiredAuthorization(); + AddAllRoles(field.RequiredAuthorization, roles); + return field; + } + + /// + /// To access this field any role listed is required + /// + public static IField RequiresAnyRole(this IField field, params string[] roles) + { + field.RequiredAuthorization ??= new RequiredAuthorization(); + AddAnyRole(field.RequiredAuthorization, roles); + return field; + } + + /// + /// To access this type all roles listed here are required + /// + public static SchemaType RequiresAllRoles(this SchemaType schemaType, params string[] roles) + { + schemaType.RequiredAuthorization ??= new RequiredAuthorization(); + AddAllRoles(schemaType.RequiredAuthorization, roles); + return schemaType; + } + + /// + /// To access this type any of the roles listed is required + /// + public static SchemaType RequiresAnyRole(this SchemaType schemaType, params string[] roles) + { + schemaType.RequiredAuthorization ??= new RequiredAuthorization(); + AddAnyRole(schemaType.RequiredAuthorization, roles); + return schemaType; + } + + /// + /// Clear role requirements + /// + public static void ClearRoles(this RequiredAuthorization requiredAuthorization) + { + requiredAuthorization.RemoveData(RolesKey); + } + + /// + /// Add roles to a RequiredAuthorization object where any role in the list satisfies (OR) + /// + public static void RequiresAnyRole(this RequiredAuthorization auth, params string[] roles) + { + AddAnyRole(auth, roles); + } + + /// + /// Add roles to a RequiredAuthorization object where all roles are required (AND) + /// + public static void RequiresAllRoles(this RequiredAuthorization auth, params string[] roles) + { + AddAllRoles(auth, roles); + } + + private static void AddAnyRole(RequiredAuthorization auth, params string[] roles) + { + var roleList = GetOrCreateRoleList(auth); + roleList.Add(roles.ToList()); + } + + private static void AddAllRoles(RequiredAuthorization auth, params string[] roles) + { + var roleList = GetOrCreateRoleList(auth); + roleList.AddRange(roles.Select(r => new List { r })); + } + + private static List> GetOrCreateRoleList(RequiredAuthorization auth) + { + if (!auth.TryGetData(RolesKey, out var roleList) || roleList == null) + { + roleList = []; + auth.SetData(RolesKey, roleList); + } + return roleList; + } +} diff --git a/src/EntityGraphQL/Schema/RoleBasedAuthorization.cs b/src/EntityGraphQL/Schema/RoleBasedAuthorization.cs index 499c34e3..d8fef744 100644 --- a/src/EntityGraphQL/Schema/RoleBasedAuthorization.cs +++ b/src/EntityGraphQL/Schema/RoleBasedAuthorization.cs @@ -24,18 +24,22 @@ public virtual bool IsAuthorized(ClaimsPrincipal? user, RequiredAuthorization? r // if the list is empty it means identity.IsAuthenticated needs to be true, if full it requires certain authorization if (requiredAuthorization != null && requiredAuthorization.Any()) { - // check roles - var allRolesValid = true; - foreach (var role in requiredAuthorization.Roles) + // check roles if any are defined + var roles = requiredAuthorization.GetRoles(); + if (roles != null) { - // each role now is an OR - var hasValidRole = role.Any(r => user?.IsInRole(r) == true); - allRolesValid = allRolesValid && hasValidRole; + var allRolesValid = true; + foreach (var role in roles) + { + // each role now is an OR + var hasValidRole = role.Any(r => user?.IsInRole(r) == true); + allRolesValid = allRolesValid && hasValidRole; + if (!allRolesValid) + break; + } if (!allRolesValid) - break; + return false; } - if (!allRolesValid) - return false; return true; } @@ -49,25 +53,48 @@ public virtual bool IsAuthorized(ClaimsPrincipal? user, RequiredAuthorization? r { var attributes = ((MemberExpression)fieldSelection.Body).Member.GetCustomAttributes(typeof(GraphQLAuthorizeAttribute), true).Cast(); var requiredRoles = attributes.Select(c => c.Roles).Where(r => r != null).ToList(); - requiredAuth = new RequiredAuthorization(requiredRoles!, null); + if (requiredRoles.Count > 0) + { + requiredAuth = new RequiredAuthorization(); + foreach (var roles in requiredRoles) + { + requiredAuth.RequiresAnyRole(roles!.ToArray()); + } + } } return requiredAuth; } - public virtual RequiredAuthorization GetRequiredAuthFromMember(MemberInfo field) + public virtual RequiredAuthorization? GetRequiredAuthFromMember(MemberInfo field) { var attributes = field.GetCustomAttributes(typeof(GraphQLAuthorizeAttribute), true).Cast(); var requiredRoles = attributes.Select(c => c.Roles).Where(r => r != null).ToList(); - var requiredAuth = new RequiredAuthorization(requiredRoles!, null); - return requiredAuth; + if (requiredRoles.Count > 0) + { + var auth = new RequiredAuthorization(); + foreach (var roles in requiredRoles) + { + auth.RequiresAnyRole(roles!.ToArray()); + } + return auth; + } + return null; } - public virtual RequiredAuthorization GetRequiredAuthFromType(Type type) + public virtual RequiredAuthorization? GetRequiredAuthFromType(Type type) { var attributes = type.GetCustomAttributes(typeof(GraphQLAuthorizeAttribute), true).Cast(); var requiredRoles = attributes.Select(c => c.Roles).Where(r => r != null).ToList(); - var requiredAuth = new RequiredAuthorization(requiredRoles!, null); - return requiredAuth; + if (requiredRoles.Count > 0) + { + var auth = new RequiredAuthorization(); + foreach (var roles in requiredRoles) + { + auth.RequiresAnyRole(roles!.ToArray()); + } + return auth; + } + return null; } } diff --git a/src/EntityGraphQL/Schema/SchemaBuilder.cs b/src/EntityGraphQL/Schema/SchemaBuilder.cs index 1c251ded..f377b509 100644 --- a/src/EntityGraphQL/Schema/SchemaBuilder.cs +++ b/src/EntityGraphQL/Schema/SchemaBuilder.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; -using EntityGraphQL.Compiler; using EntityGraphQL.Compiler.Util; using EntityGraphQL.Extensions; using Humanizer; @@ -24,23 +23,23 @@ public static class SchemaBuilder /// /// Apply any options not passed via the constructor /// - private static SchemaProvider ApplyOptions(SchemaProvider schema, SchemaBuilderSchemaOptions options) + private static SchemaProvider ApplyOptions(SchemaProvider schema, SchemaProviderOptions options) { schema.AllowedExceptions.AddRange(options.AllowedExceptions); return schema; } /// - /// Create a new SchemaProvider with the query context of type TContext and using the SchemaBuilderSchemaOptions supplied or the default if null. + /// Create a new SchemaProvider<TContext> with the query context of type TContext and using the SchemaProviderOptions supplied or the default if null. /// Note the schema is empty, you need to add types and fields. /// /// Query context type - /// SchemaBuilderSchemaOptions to configure the options of the schema provider created + /// SchemaProviderOptions to configure the options of the schema provider created /// A logger to use in the schema /// - public static SchemaProvider Create(SchemaBuilderSchemaOptions? options = null, ILogger>? logger = null) + public static SchemaProvider Create(SchemaProviderOptions? options = null, ILogger>? logger = null) { - options ??= new SchemaBuilderSchemaOptions(); + options ??= new SchemaProviderOptions(); var schema = new SchemaProvider(options.AuthorizationService, options.FieldNamer, logger, options.IntrospectionEnabled, options.IsDevelopment); return ApplyOptions(schema, options); } @@ -48,48 +47,48 @@ public static SchemaProvider Create(SchemaBuilderSchemaOptio /// /// Given the type TContextType recursively create a query schema based on the public properties of the object. /// - /// SchemaBuilderOptions to use to create the SchemaProvider and configure the rules for auto creating the schema types and fields + /// SchemaBuilderOptions to configure the rules for auto creating the schema types and fields /// A logger to use in the schema /// /// - public static SchemaProvider FromObject(SchemaBuilderOptions? buildOptions = null, ILogger>? logger = null) + public static SchemaProvider FromObject(SchemaBuilderOptions? reflectionOptions = null, ILogger>? logger = null) { - buildOptions ??= new SchemaBuilderOptions(); - var schemaOptions = new SchemaBuilderSchemaOptions(); + reflectionOptions ??= new SchemaBuilderOptions(); + var schemaOptions = new SchemaProviderOptions(); var schema = new SchemaProvider(schemaOptions.AuthorizationService, schemaOptions.FieldNamer, logger, schemaOptions.IntrospectionEnabled, schemaOptions.IsDevelopment); schema = ApplyOptions(schema, schemaOptions); - return FromObject(schema, buildOptions); + return FromObject(schema, reflectionOptions); } /// /// Given the type TContextType recursively create a query schema based on the public properties of the object. /// /// Options to create the SchemaProvider. - /// SchemaBuilderOptions to use to create the SchemaProvider and configure the rules for auto creating the schema types and fields + /// SchemaBuilderOptions to configure the rules for auto creating the schema types and fields /// A logger to use in the schema /// /// public static SchemaProvider FromObject( - SchemaBuilderSchemaOptions? schemaOptions, - SchemaBuilderOptions? buildOptions = null, + SchemaProviderOptions? schemaOptions, + SchemaBuilderOptions? builderOptions = null, ILogger>? logger = null ) { - buildOptions ??= new SchemaBuilderOptions(); - schemaOptions ??= new SchemaBuilderSchemaOptions(); + builderOptions ??= new SchemaBuilderOptions(); + schemaOptions ??= new SchemaProviderOptions(); var schema = new SchemaProvider(schemaOptions.AuthorizationService, schemaOptions.FieldNamer, logger, schemaOptions.IntrospectionEnabled, schemaOptions.IsDevelopment); - schemaOptions.PreBuildSchemaFromContext?.Invoke(schema); + builderOptions.PreBuildSchemaFromContext?.Invoke(schema); schema = ApplyOptions(schema, schemaOptions); - return FromObject(schema, buildOptions); + return FromObject(schema, builderOptions); } /// /// Given the type TContextType recursively create a query schema based on the public properties of the object. Schema is added into the provider schema /// /// Schema to add types to. - /// SchemaBuilderOptions to use to create the SchemaProvider and configure the rules for auto creating the schema types and fields + /// SchemaBuilderOptions to configure the rules for auto creating the schema types and fields /// /// internal static SchemaProvider FromObject(SchemaProvider schema, SchemaBuilderOptions options) @@ -201,7 +200,6 @@ public static List GetFieldsFromObject(Type type, ISchemaType fromTyp (string name, string description) = GetNameAndDescription(method, schema); options ??= new SchemaBuilderOptions(); - var isAsync = method.GetCustomAttribute() != null || (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)); var requiredClaims = schema.AuthorizationService.GetRequiredAuthFromMember(method); LambdaExpression? le = null; @@ -260,7 +258,7 @@ public static List GetFieldsFromObject(Type type, ISchemaType fromTyp } var newExpArg = ExpressionUtil.CreateNewExpression(propExpressions, item.FlattenType!, true) - ?? throw new EntityQuerySchemaException($"Could not create expression for argument {item.ArgName} of type {item.ArgType!.RawType.Name}"); + ?? throw new EntityGraphQLSchemaException($"Could not create expression for argument {item.ArgName} of type {item.ArgType!.RawType.Name}"); argsForCallExpression.Add(item.ArgName, newExpArg); } else @@ -279,16 +277,23 @@ public static List GetFieldsFromObject(Type type, ISchemaType fromTyp le = Expression.Lambda(call, param); } - var baseReturnType = GetBaseReturnType(schema, le.ReturnType, options); - + var returnType = GetReturnType(le.ReturnType, out bool isAsyncFromReturn); + var baseReturnType = GetBaseReturnType(schema, returnType, options); if (options.IgnoreTypes.Contains(baseReturnType)) return null; var schemaType = CacheType(baseReturnType, schema, options, false); - var nullabilityInfo = method.GetNullabilityInfo(); - var returnTypeInfo = schema.GetCustomTypeMapping(le.ReturnType) ?? new GqlTypeInfo(() => schemaType ?? schema.GetSchemaType(baseReturnType, isInputType, null), le.Body.Type, nullabilityInfo); + var returnTypeInfo = MakeGraphQlType(schema, isInputType, returnType, schemaType, name, fromType, nullabilityInfo); + var field = new Field(schema, fromType, name, le, description, fieldSchemaArgs, returnTypeInfo, requiredClaims); + + // Set IsAsync for async methods so the field execution handles the Task<> properly + if (isAsyncFromReturn) + { + field.IsAsync = true; + } + options.OnFieldCreated?.Invoke(field); if (fieldServices.Count > 0) @@ -379,8 +384,8 @@ bool isInputType private static Type GetBaseReturnType(ISchemaProvider schema, Type returnType, SchemaBuilderOptions options) { // get the object type returned (ignoring list etc) so we know the context to find fields etc - var returnsTask = returnType.GetCustomAttribute() != null || (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)); - if (returnsTask || (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))) + var returnsTask = returnType.GetCustomAttribute() != null || returnType.IsAsyncGenericType(); + if (returnsTask || returnType.IsAsyncGenericType()) { returnType = returnType.GetGenericArguments()[0]; } @@ -462,7 +467,7 @@ internal static (string name, string description) GetNameAndDescription(MemberIn _ => nameof(ISchemaProvider.AddType), }; - var method = schema.GetType().GetMethod(addMethod, [typeof(string), typeof(string)]) ?? throw new EntityQuerySchemaException($"Could not find {addMethod} method on schema"); + var method = schema.GetType().GetMethod(addMethod, [typeof(string), typeof(string)]) ?? throw new EntityGraphQLSchemaException($"Could not find {addMethod} method on schema"); method = method.MakeGenericMethod(propType); var typeAdded = (ISchemaType)method.Invoke(schema, new object[] { typeName, description })!; typeAdded.RequiredAuthorization = schema.AuthorizationService.GetRequiredAuthFromType(propType); @@ -498,21 +503,30 @@ internal static string BuildTypeName(Type propType) return propType.IsGenericType ? $"{propType.Name[..propType.Name.IndexOf('`')]}{string.Join("", propType.GetGenericArguments().Select(BuildTypeName))}" : propType.Name; } - public static GqlTypeInfo MakeGraphQlType(ISchemaProvider schema, bool isInputType, Type returnType, string? returnSchemaType, string fieldName, ISchemaType fromType) + public static GqlTypeInfo MakeGraphQlType( + ISchemaProvider schema, + bool isInputType, + Type returnType, + ISchemaType? returnSchemaType, + string fieldName, + ISchemaType fromType, + NullabilityInfo? nullabilityInfo = null + ) { - Func typeGetter = !string.IsNullOrEmpty(returnSchemaType) - ? () => schema.Type(returnSchemaType) // We can look the type up by it's unique schema name - // we need to look it up by the dotnet type - : () => - { - var getType = returnType.IsEnumerableOrArray() || returnType.IsNullableType() ? returnType.GetNonNullableOrEnumerableType() : returnType; - if (schema.TryGetSchemaType(getType, isInputType, out var schemaType, null)) - return schemaType!; - throw new EntityGraphQLCompilerException( - $"No schema type found for dotnet type '{getType.Name}'. Make sure you add it or add a type mapping. Lookup failed for field '{fieldName}' on type '{fromType.Name}'" - ); - }; - return new GqlTypeInfo(typeGetter, returnType); + Func typeGetter = + returnSchemaType != null + ? () => returnSchemaType // We can look the type up by it's unique schema name + // we need to look it up by the dotnet type + : () => + { + var getType = returnType.IsEnumerableOrArray() || returnType.IsNullableType() ? returnType.GetNonNullableOrEnumerableType() : returnType; + if (schema.TryGetSchemaType(getType, isInputType, out var schemaType, null)) + return schemaType!; + throw new EntityGraphQLSchemaException( + $"No schema type found for dotnet type '{getType.Name}'. Make sure you add it or add a type mapping. Lookup failed for field '{fieldName}' on type '{fromType.Name}'" + ); + }; + return new GqlTypeInfo(typeGetter, returnType, nullabilityInfo); } public static IEnumerable GetGraphQlSchemaArgumentsFromMethod( @@ -608,6 +622,30 @@ private static IEnumerable FlattenArguments(Type argType, ISchemaP } } } + + public static Type GetReturnType(Type returnType, out bool isAsync) + { + isAsync = false; + // For async-returning shapes, extract underlying T for schema building and mark as async + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) + { + isAsync = true; + returnType = returnType.GetGenericArguments()[0]; + } + else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + isAsync = true; + returnType = returnType.GetGenericArguments()[0]; + } + else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) + { + isAsync = true; + var t = returnType.GetGenericArguments()[0]; + returnType = typeof(IEnumerable<>).MakeGenericType(t); + } + + return returnType; + } } public class FieldArgInfo diff --git a/src/EntityGraphQL/Schema/SchemaBuilderOptions.cs b/src/EntityGraphQL/Schema/SchemaBuilderOptions.cs index 28ed27ad..856a365f 100644 --- a/src/EntityGraphQL/Schema/SchemaBuilderOptions.cs +++ b/src/EntityGraphQL/Schema/SchemaBuilderOptions.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; -using EntityGraphQL.Compiler; namespace EntityGraphQL.Schema; public delegate void OnFieldCreated(IField field); /// -/// Options used by SchemaBuilder when reflection the object graph to auto create schema types & fields +/// Options that control how SchemaBuilder reflects the object graph to auto-create schema types and fields. /// public class SchemaBuilderOptions { @@ -24,7 +23,7 @@ public class SchemaBuilderOptions /// /// If true when SchemaBuilder encounters a field that returns a list of entities and the entity has a property /// or field named Id SchemaBuilder will also create a schema field with a singular name and an argument of id for that entity. - /// e.g. if it sees IEnumerable People; It will create the schema fields + /// e.g. if it sees IEnumerable<Person> People; It will create the schema fields /// { /// people: [Person] /// person(id: ID!): Person @@ -41,7 +40,7 @@ public class SchemaBuilderOptions /// /// If true (default) and an object type is encountered during reflection of the query object graph it will be added to the schema /// as a Type including it's fields. If that type is an interface it will be added as an interface. This includes return - /// types form mutations + /// types from mutations /// public bool AutoCreateNewComplexTypes { get; set; } = true; @@ -70,13 +69,22 @@ public class SchemaBuilderOptions /// public HashSet IgnoreAttributes { get; set; } = []; + /// + /// Callback invoked when a field is created during schema reflection + /// public OnFieldCreated? OnFieldCreated { get; set; } + + /// + /// Called after the schema object is created but before the context is reflected into it. Use for set up of type mappings or + /// anything that may be needed for the schema to be built correctly. + /// + public Action? PreBuildSchemaFromContext { get; set; } } /// -/// Options used by the SchemaBuilder factory with creating the SchemaProvider +/// Options for configuring a SchemaProvider instance. /// -public class SchemaBuilderSchemaOptions +public class SchemaProviderOptions { public static readonly Func DefaultFieldNamer = name => { @@ -94,16 +102,10 @@ public class SchemaBuilderSchemaOptions public bool IntrospectionEnabled { get; set; } = true; /// - /// + /// Authorization service implementation for the schema /// public IGqlAuthorizationService AuthorizationService { get; set; } = new RoleBasedAuthorization(); - /// - /// Called after the schema object is created but before the context is reflected into it. Use for set up of type mappings or - /// anything that may be needed for the schema to be built correctly. - /// - public Action? PreBuildSchemaFromContext { get; set; } - /// /// If true (default), exceptions will have their messages rendered in the 'errors' object /// If false, exceptions not in AllowedExceptions will have their message replaced with 'Error occurred' @@ -113,12 +115,5 @@ public class SchemaBuilderSchemaOptions /// /// List of allowed exceptions that will be rendered in the 'errors' object when IsDevelopment is false /// - public List AllowedExceptions { get; set; } = - [ - new(typeof(EntityGraphQLArgumentException)), - new(typeof(EntityGraphQLException)), - new(typeof(EntityGraphQLFieldException)), - new(typeof(EntityGraphQLAccessException)), - new(typeof(EntityGraphQLValidationException)), - ]; + public List AllowedExceptions { get; set; } = [new(typeof(EntityGraphQLException)), new(typeof(EntityGraphQLFieldException)), new(typeof(EntityGraphQLSchemaException))]; } diff --git a/src/EntityGraphQL/Schema/SchemaGenerator.cs b/src/EntityGraphQL/Schema/SchemaGenerator.cs index 15c91603..541f0552 100644 --- a/src/EntityGraphQL/Schema/SchemaGenerator.cs +++ b/src/EntityGraphQL/Schema/SchemaGenerator.cs @@ -5,12 +5,14 @@ using System.Linq; using System.Text; using EntityGraphQL.Directives; +using EntityGraphQL.Extensions; using EntityGraphQL.Schema.Directives; -// can remove this when we drop netstandard2.1 -#pragma warning disable CA1305 namespace EntityGraphQL.Schema; +// can remove this when/if we drop netstandard2.1 +#pragma warning disable CA1305 + public class SchemaGenerator { internal static string EscapeString(string? input) @@ -53,13 +55,8 @@ internal static string Make(ISchemaProvider schema) { if (!string.IsNullOrEmpty(directive.Description)) schemaBuilder.AppendLine($"\"\"\"{EscapeString(directive.Description)}\"\"\""); - schemaBuilder.AppendLine( -#if NETSTANDARD2_1 - $"directive @{directive.Name}{GetDirectiveArgs(schema, directive)} on {string.Join(" | ", directive.Location.Select(i => Enum.GetName(typeof(ExecutableDirectiveLocation), i)))}" -#else - $"directive @{directive.Name}{GetDirectiveArgs(schema, directive)} on {string.Join(" | ", directive.Location.Select(i => Enum.GetName(i)))}" -#endif - ); + + schemaBuilder.AppendLine($"directive @{directive.Name}{GetDirectiveArgs(schema, directive)} on {string.Join(" | ", directive.Location.Select(i => i.GetDescription()))}"); } schemaBuilder.AppendLine(); @@ -187,15 +184,6 @@ private static string GetGqlArgs(ISchemaProvider schema, IField field, string no { return $"[{string.Join(", ", e.Cast().Select(item => GetArgDefaultValue(new DefaultArgValue(true, item), fieldNamer)).Where(item => item != null))}]"; } - else if (valueType.IsConstructedGenericType && valueType.GetGenericTypeDefinition() == typeof(EntityQueryType<>)) - { - if (((BaseEntityQueryType)defaultArgValue.Value).HasValue) - { - var property = valueType.GetProperty("Query"); - return $"\"{property!.GetValue(defaultArgValue.Value)}\""; - } - return string.Empty; - } else if (defaultArgValue.Value is object o) { ret += "{ "; diff --git a/src/EntityGraphQL/Schema/SchemaIntrospection.cs b/src/EntityGraphQL/Schema/SchemaIntrospection.cs index 409744b3..dc7bd841 100644 --- a/src/EntityGraphQL/Schema/SchemaIntrospection.cs +++ b/src/EntityGraphQL/Schema/SchemaIntrospection.cs @@ -28,9 +28,9 @@ public static Models.Schema Make(ISchemaProvider schema) types.AddRange(BuildScalarTypes(schema)); var schemaDescription = new Models.Schema( - new TypeElement(null, schema.QueryContextName), - schema.HasType(schema.Mutation().SchemaType.TypeDotnet) ? new TypeElement(null, schema.Mutation().SchemaType.Name) : null, - schema.HasType(schema.Subscription().SchemaType.TypeDotnet) ? new TypeElement(null, schema.Subscription().SchemaType.Name) : null, + new TypeElement("OBJECT", schema.QueryContextName), + schema.HasType(schema.Mutation().SchemaType.TypeDotnet) ? new TypeElement("OBJECT", schema.Mutation().SchemaType.Name) : null, + schema.HasType(schema.Subscription().SchemaType.TypeDotnet) ? new TypeElement("OBJECT", schema.Subscription().SchemaType.Name) : null, types.OrderBy(x => x.Name).ToList(), BuildDirectives(schema) ); @@ -203,44 +203,61 @@ private static TypeElement BuildType(ISchemaProvider schema, GqlTypeInfo typeInf } /// - /// This is used in a lazy evaluated field as a graph can have circular dependencies + /// This is used in a lazy evaluated field as a graph can have circular dependencies. + /// Per GraphQL spec, fields should only be returned for OBJECT and INTERFACE types. + /// Returns null for INPUT_OBJECT, ENUM, SCALAR, and UNION types. /// /// /// + /// The GraphQL type kind (OBJECT, INTERFACE, INPUT_OBJECT, etc.) + /// Whether to include deprecated fields /// - public static Models.Field[] BuildFieldsForType(ISchemaProvider schema, string typeName) + public static IEnumerable? BuildFieldsForType(ISchemaProvider schema, string typeName, string? typeKind, bool includeDeprecated) { - if (typeName == schema.QueryContextName) + // Per GraphQL spec, fields should only be returned for OBJECT and INTERFACE types + if (typeKind != "OBJECT" && typeKind != "INTERFACE") { - return BuildRootQueryFields(schema); + return null; } - if (typeName == schema.Mutation().SchemaType.Name) + + Models.Field[] fields; + if (typeName == schema.QueryContextName) { - return BuildMutationFields(schema); + fields = BuildRootQueryFields(schema); } - - var fieldDescs = new List(); - if (!schema.HasType(typeName)) + else if (typeName == schema.Mutation().SchemaType.Name) { - return fieldDescs.ToArray(); + fields = BuildMutationFields(schema); } - var type = schema.Type(typeName); - foreach (var field in type.GetFields()) + else { - if (field.Name.StartsWith("__", StringComparison.InvariantCulture)) - continue; - - var f = new Models.Field(schema.SchemaFieldNamer(field.Name), BuildType(schema, field.ReturnType, field.ReturnType.TypeDotnet)) + var fieldDescs = new List(); + if (!schema.HasType(typeName)) { - Args = BuildArgs(schema, field).ToArray(), - Description = field.Description, - }; + return fieldDescs; + } + var type = schema.Type(typeName); + foreach (var field in type.GetFields()) + { + if (field.Name.StartsWith("__", StringComparison.InvariantCulture)) + continue; - field.DirectivesReadOnly.ProcessField(f); + var f = new Models.Field(schema.SchemaFieldNamer(field.Name), BuildType(schema, field.ReturnType, field.ReturnType.TypeDotnet)) + { + Args = BuildArgs(schema, field).ToArray(), + Description = field.Description, + }; + + field.DirectivesReadOnly.ProcessField(f); - fieldDescs.Add(f); + fieldDescs.Add(f); + } + fields = fieldDescs.ToArray(); } - return fieldDescs.ToArray(); + + if (includeDeprecated) + return fields; + return fields.Where(f => !f.IsDeprecated); } private static Models.Field[] BuildRootQueryFields(ISchemaProvider schema) diff --git a/src/EntityGraphQL/Schema/SchemaProvider.cs b/src/EntityGraphQL/Schema/SchemaProvider.cs index faace770..7ca9c1fa 100644 --- a/src/EntityGraphQL/Schema/SchemaProvider.cs +++ b/src/EntityGraphQL/Schema/SchemaProvider.cs @@ -39,14 +39,15 @@ public class SchemaProvider : ISchemaProvider, IDisposable private readonly SchemaType queryType; private readonly ILogger>? logger; - private readonly GraphQLCompiler graphQLCompiler; private readonly bool introspectionEnabled; private readonly bool isDevelopment; private readonly MutationType mutationType; private readonly SubscriptionType subscriptionType; private readonly Dictionary attributeHandlers = []; - public IDictionary TypeConverters { get; } = new Dictionary(); + private readonly Dictionary<(Type from, Type to), TryConvertShim> fromToConverters = new(); + private readonly Dictionary toConverters = new(); + private readonly Dictionary fromConverters = new(); public List AllowedExceptions { get; } = []; @@ -70,10 +71,9 @@ public SchemaProvider( ) { AuthorizationService = authorizationService ?? new RoleBasedAuthorization(); - SchemaFieldNamer = fieldNamer ?? SchemaBuilderSchemaOptions.DefaultFieldNamer; + SchemaFieldNamer = fieldNamer ?? SchemaProviderOptions.DefaultFieldNamer; MethodProvider = new EqlMethodProvider(); this.logger = logger; - this.graphQLCompiler = new GraphQLCompiler(this); this.introspectionEnabled = introspectionEnabled; this.isDevelopment = isDevelopment; queryCache = new QueryCache(); @@ -87,10 +87,12 @@ public SchemaProvider( AddScalarType("Char", "Char scalar"); // default custom scalar for DateTime - // TODO 6.0 - Rename Date to DateTime? - AddScalarType("Date", "Date with time scalar"); + AddScalarType("DateTime", "Date with time scalar"); AddScalarType("DateTimeOffset", "DateTimeOffset scalar"); -#if NET6_0_OR_GREATER + + // default custom scalar for TimeSpan + AddScalarType("TimeSpan", "TimeSpan scalar"); +#if NET8_0_OR_GREATER AddScalarType("DateOnly", "Date value only scalar"); AddScalarType("TimeOnly", "Time value only scalar"); #endif @@ -160,14 +162,214 @@ public SchemaProvider( /// the request which may be strings or JSON into the dotnet types on the argument classes. /// For example a string to DateTime converter. /// + /// Uses a from-to converter, when both the source and target types are known. + /// /// EntityGraphQL already handles Guid, DateTime, InputTypes from the schema, arrays/lists, System.Text.Json elements, float/double/decimal/int/short/uint/long/etc /// - /// - public void AddCustomTypeConverter(ICustomTypeConverter typeConverter) + /// The source type to convert from + /// The target type to convert to + /// A function that does the conversion + /// The schema provider for chaining + public ISchemaProvider AddCustomTypeConverter(Func convert) { - TypeConverters.Add(typeConverter.Type, typeConverter); + fromToConverters[(typeof(TFrom), typeof(TTo))] = (object? obj, Type to, ISchemaProvider schema, out object? result) => + { + if (obj == null) + { + result = default(TTo); + return false; + } + result = convert((TFrom)obj, schema); + return true; + }; + MethodProvider.ExtendIsAnySupportedTypes(typeof(TTo)); + return this; } + /// + /// Add a custom type converter to convert query variables into the expected dotnet types. I.e. the incoming variables from + /// the request which may be strings or JSON into the dotnet types on the argument classes. + /// For example a string to DateTime converter. + /// + /// Uses a from-to converter, when both the source and target types are known. + /// + /// EntityGraphQL already handles Guid, DateTime, InputTypes from the schema, arrays/lists, System.Text.Json elements, float/double/decimal/int/short/uint/long/etc + /// + /// The source type to convert from + /// The target type to convert to + /// A function that does the conversion, returning true if it worked + /// The schema provider for chaining + public ISchemaProvider AddCustomTypeConverter(TypeConverterTryFromTo tryConvert) + { + fromToConverters[(typeof(TFrom), typeof(TTo))] = (object? obj, Type to, ISchemaProvider schema, out object? result) => + { + var ok = tryConvert((TFrom)obj!, schema, out var r); + result = r; + return ok; + }; + MethodProvider.ExtendIsAnySupportedTypes(typeof(TTo)); + return this; + } + + /// + /// Add a custom type converter to convert query variables into the expected dotnet types. I.e. the incoming variables from + /// the request which may be strings or JSON into the dotnet types on the argument classes. + /// For example a string to DateTime converter. + /// + /// Uses a to-only converter, when only the target type is known. + /// + /// EntityGraphQL already handles Guid, DateTime, InputTypes from the schema, arrays/lists, System.Text.Json elements, float/double/decimal/int/short/uint/long/etc + /// + /// The target type to convert to + /// A function that does the conversion + /// The schema provider for chaining + public ISchemaProvider AddCustomTypeConverter(Func convert) + { + toConverters[typeof(TTo)] = (object? obj, Type to, ISchemaProvider schema, out object? result) => + { + result = convert(obj, schema); + return true; + }; + MethodProvider.ExtendIsAnySupportedTypes(typeof(TTo)); + return this; + } + + /// + /// Add a custom type converter to convert query variables into the expected dotnet types. I.e. the incoming variables from + /// the request which may be strings or JSON into the dotnet types on the argument classes. + /// For example a string to DateTime converter. + /// + /// Uses a to-only converter, when only the target type is known. + /// + /// EntityGraphQL already handles Guid, DateTime, InputTypes from the schema, arrays/lists, System.Text.Json elements, float/double/decimal/int/short/uint/long/etc + /// + /// The target type to convert to + /// A function that does the conversion, returning true if it worked + /// The schema provider for chaining + public ISchemaProvider AddCustomTypeConverter(TypeConverterTryTo tryConvert) + { + toConverters[typeof(TTo)] = (object? obj, Type to, ISchemaProvider schema, out object? result) => + { + if (tryConvert(obj, to, schema, out var r)) + { + result = r; + return true; + } + result = null; + return false; + }; + MethodProvider.ExtendIsAnySupportedTypes(typeof(TTo)); + return this; + } + + /// + /// Add a custom type converter to convert query variables into the expected dotnet types. I.e. the incoming variables from + /// the request which may be strings or JSON into the dotnet types on the argument classes. + /// For example a string to DateTime converter. + /// + /// Uses a from-only converter, when only the source type is known. + /// + /// EntityGraphQL already handles Guid, DateTime, InputTypes from the schema, arrays/lists, System.Text.Json elements, float/double/decimal/int/short/uint/long/etc + /// + /// The source type to convert from + /// A function that does the conversion + /// The target types this converter supports + /// The schema provider for chaining + public ISchemaProvider AddCustomTypeConverter(Func convert, params Type[] supportedToTypes) + { + fromConverters[typeof(TFrom)] = (object? obj, Type to, ISchemaProvider schema, out object? result) => + { + result = convert((TFrom)obj!, to, schema); + return true; + }; + + foreach (var toType in supportedToTypes) + { + MethodProvider.ExtendIsAnySupportedTypes(toType); + } + return this; + } + + /// + /// Add a custom type converter to convert query variables into the expected dotnet types. I.e. the incoming variables from + /// the request which may be strings or JSON into the dotnet types on the argument classes. + /// For example a string to DateTime converter. + /// + /// Uses a from-only converter, when only the source type is known. + /// + /// EntityGraphQL already handles Guid, DateTime, InputTypes from the schema, arrays/lists, System.Text.Json elements, float/double/decimal/int/short/uint/long/etc + /// + /// The source type to convert from + /// A function that does the conversion, returning true if it worked + /// The target types this converter supports + /// The schema provider for chaining + public ISchemaProvider AddCustomTypeConverter(TypeConverterTryFrom tryConvert, params Type[] supportedToTypes) + { + fromConverters[typeof(TFrom)] = (object? obj, Type to, ISchemaProvider schema, out object? result) => + { + return tryConvert((TFrom)obj!, to, schema, out result); + }; + + foreach (var toType in supportedToTypes) + { + MethodProvider.ExtendIsAnySupportedTypes(toType); + } + return this; + } + + public bool TryConvertCustom(object? value, Type toType, out object? result) + { + var fromType = value?.GetType(); + + if (TryConvertFromTo(value, fromType, toType, out result)) + { + return true; + } + if (TryConvertToOnly(value, toType, out result)) + { + return true; + } + if (TryConvertFromOnly(value, fromType, toType, out result)) + { + return true; + } + + result = null; + return false; + } + + private bool TryConvertFromTo(object? value, Type? fromType, Type toType, out object? result) + { + if (fromType != null && fromToConverters.TryGetValue((fromType, toType), out var shim)) + { + return shim(value, toType, this, out result); + } + result = null; + return false; + } + + private bool TryConvertToOnly(object? value, Type toType, out object? result) + { + if (toConverters.TryGetValue(toType, out var shim)) + { + return shim(value, toType, this, out result); + } + result = null; + return false; + } + + private bool TryConvertFromOnly(object? value, Type? fromType, Type toType, out object? result) + { + if (fromType != null && fromConverters.TryGetValue(fromType, out var shim)) + { + return shim(value, toType, this, out result); + } + result = null; + return false; + } + + private delegate bool TryConvertShim(object? value, Type toType, ISchemaProvider schema, out object? result); + private void SetupIntrospectionTypesAndField() { if (!introspectionEnabled) @@ -175,16 +377,14 @@ private void SetupIntrospectionTypesAndField() // evaluate Fields lazily so we don't end up in endless loop Type("__Type") - .ReplaceField( - "fields", - new { includeDeprecated = false }, - (t, p) => SchemaIntrospection.BuildFieldsForType(this, t.Name!).Where(f => p.includeDeprecated ? f.IsDeprecated || !f.IsDeprecated : !f.IsDeprecated).ToList(), - "Fields available on type" - ); + .ReplaceField("fields", new { includeDeprecated = false }, "Fields available on type") + .Resolve((t, p) => SchemaIntrospection.BuildFieldsForType(this, t.Name!, t.Kind, p.includeDeprecated)) + .AsService(); - Query().ReplaceField("__schema", db => SchemaIntrospection.Make(this), "Introspection of the schema").Returns("__Schema"); + Query().ReplaceField("__schema", db => SchemaIntrospection.Make(this), "Introspection of the schema").Returns("__Schema").AsService(); Query() .ReplaceField("__type", new { name = ArgumentHelper.Required() }, (db, p) => SchemaIntrospection.Make(this).Types.Where(s => s.Name == p.name).First(), "Query a type by name") + .AsService() .Returns("__Type"); } @@ -272,23 +472,23 @@ private async Task DoExecuteRequestAsync( GraphQLDocument? compiledQuery = null; if (options.EnablePersistedQueries) { - var persistedQuery = (PersistedQueryExtension?)ExpressionUtil.ConvertObjectType(gql.Extensions.GetValueOrDefault("persistedQuery"), typeof(PersistedQueryExtension), null, options); + var persistedQuery = (PersistedQueryExtension?)ExpressionUtil.ConvertObjectType(gql.Extensions.GetValueOrDefault("persistedQuery"), typeof(PersistedQueryExtension), null); if (persistedQuery != null && persistedQuery.Version != 1) - throw new EntityGraphQLExecutionException("PersistedQueryNotSupported"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "PersistedQueryNotSupported"); string? hash = persistedQuery?.Sha256Hash; if (hash == null && gql.Query == null) - throw new EntityGraphQLExecutionException("Please provide a persisted query hash or a query string"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "Please provide a persisted query hash or a query string"); if (hash != null) { compiledQuery = queryCache.GetCompiledQueryWithHash(hash); if (compiledQuery == null && gql.Query == null) - throw new EntityGraphQLExecutionException("PersistedQueryNotFound"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "PersistedQueryNotFound"); else if (compiledQuery == null) { - compiledQuery = graphQLCompiler.Compile(gql); + compiledQuery = GraphQLParser.Parse(gql, this); queryCache.AddCompiledQuery(hash, compiledQuery); } } @@ -299,7 +499,7 @@ private async Task DoExecuteRequestAsync( if (options.EnableQueryCache) compiledQuery = CompileQueryWithCache(gql, options); else - compiledQuery = graphQLCompiler.Compile(gql); + compiledQuery = GraphQLParser.Parse(gql, this); } } else if (options.EnableQueryCache) @@ -311,16 +511,14 @@ private async Task DoExecuteRequestAsync( // no cache if (gql.Query == null) { - string? hash = ( - (PersistedQueryExtension?)ExpressionUtil.ConvertObjectType(gql.Extensions.GetValueOrDefault("persistedQuery"), typeof(PersistedQueryExtension), null, options) - )?.Sha256Hash; + string? hash = ((PersistedQueryExtension?)ExpressionUtil.ConvertObjectType(gql.Extensions.GetValueOrDefault("persistedQuery"), typeof(PersistedQueryExtension), null))?.Sha256Hash; if (hash != null) - throw new EntityGraphQLExecutionException("PersistedQueryNotSupported"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "PersistedQueryNotSupported"); - throw new EntityGraphQLException("Query field must be set unless you are using persisted queries"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "Query field must be set unless you are using persisted queries"); } - compiledQuery = graphQLCompiler.Compile(gql); + compiledQuery = GraphQLParser.Parse(gql, this); } result = await compiledQuery.ExecuteQueryAsync( @@ -335,7 +533,7 @@ private async Task DoExecuteRequestAsync( } catch (Exception ex) { - result = HandleException(ex); + return HandleException(ex); } return result; @@ -347,32 +545,34 @@ private QueryResult HandleException(Exception exception) logErrorMessage(logger, exception); var result = new QueryResult(); - foreach (var (errorMessage, extensions) in GenerateMessage(exception).Distinct()) - result.AddError(errorMessage, extensions); + result.AddErrors(GenerateErrors(exception).Distinct()); + return result; } - private IEnumerable<(string errorMessage, IDictionary? extensions)> GenerateMessage(Exception exception) + public IEnumerable GenerateErrors(Exception exception, string? fieldName = null) { - switch (exception) + return exception switch { - case EntityGraphQLValidationException validationException: - return validationException.ValidationErrors.Select(v => (v, (IDictionary?)null)); - case AggregateException aggregateException: - return aggregateException.InnerExceptions.SelectMany(GenerateMessage); - case EntityGraphQLException graphqlException: - return new[] { (graphqlException.Message, (IDictionary?)graphqlException.Extensions) }; - case TargetInvocationException targetInvocationException: - return GenerateMessage(targetInvocationException.InnerException!); - case EntityGraphQLFieldException fieldException: - return GenerateMessage(fieldException.InnerException!).Select(f => ($"Field '{fieldException.FieldName}' - {f.errorMessage}", f.extensions)); - default: - if (isDevelopment || AllowedExceptions.Any(e => e.IsAllowed(exception)) || exception.GetType().GetCustomAttribute() != null) - return new[] { (exception.Message, (IDictionary?)null) }; - return new[] { ("Error occurred", (IDictionary?)null) }; - } + AggregateException aggregateException => aggregateException.InnerExceptions.SelectMany(ie => GenerateErrors(ie, fieldName)), + EntityGraphQLFieldException fieldException => [new GraphQLError(AllowedExceptionMessage(fieldException, fieldName), null, null)], + EntityGraphQLException graphqlException => graphqlException.Messages.Select(m => new GraphQLError(m, graphqlException.Path, (IDictionary?)graphqlException.Extensions)), + TargetInvocationException targetInvocationException => GenerateErrors(targetInvocationException.InnerException!), + _ => [new GraphQLError(AllowedExceptionMessage(exception, fieldName), null, null)], + }; } + public string AllowedExceptionMessage(Exception exception, string? fieldName = null) + { + var message = IsAllowedException(exception) ? exception.Message : "Error occurred"; + if (fieldName != null) + message = $"Field '{fieldName}' - {message}"; + return message; + } + + private bool IsAllowedException(Exception exception) => + isDevelopment || AllowedExceptions.Any(e => e.IsAllowed(exception)) || exception.GetType().GetCustomAttribute() != null; + private GraphQLDocument CompileQueryWithCache(QueryRequest gql, ExecutionOptions executionOptions) { GraphQLDocument? compiledQuery; @@ -380,18 +580,16 @@ private GraphQLDocument CompileQueryWithCache(QueryRequest gql, ExecutionOptions // cache the result if (gql.Query == null) { - string? phash = ( - (PersistedQueryExtension?)ExpressionUtil.ConvertObjectType(gql.Extensions.GetValueOrDefault("persistedQuery"), typeof(PersistedQueryExtension), null, executionOptions) - )?.Sha256Hash; + string? phash = ((PersistedQueryExtension?)ExpressionUtil.ConvertObjectType(gql.Extensions.GetValueOrDefault("persistedQuery"), typeof(PersistedQueryExtension), null))?.Sha256Hash; if (phash != null) - throw new EntityGraphQLExecutionException("PersistedQueryNotSupported"); - throw new EntityGraphQLException("Query field must be set unless you are using persisted queries"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "PersistedQueryNotSupported"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "Query field must be set unless you are using persisted queries"); } (compiledQuery, var hash) = queryCache.GetCompiledQuery(gql.Query, null); if (compiledQuery == null) { - compiledQuery = graphQLCompiler.Compile(gql); + compiledQuery = GraphQLParser.Parse(gql, this); queryCache.AddCompiledQuery(hash, compiledQuery); } @@ -567,19 +765,16 @@ public void AddMutationsFrom(SchemaBuilderOptions? options = null) /// /// /// An ISchemaType if the type is found in the schema - /// If type name not found + /// If type name not found public ISchemaType GetSchemaType(string typeName, QueryRequestContext? requestContext) { if (schemaTypes.TryGetValue(typeName, out var schemaType)) return CheckTypeAccess(schemaType, requestContext); - throw new EntityGraphQLCompilerException($"Type {typeName} not found in schema"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Type {typeName} not found in schema"); } - public ISchemaType GetSchemaType(Type dotnetType, QueryRequestContext? requestContext) - { - return GetSchemaType(dotnetType, false, requestContext); - } + public ISchemaType GetSchemaType(Type dotnetType, QueryRequestContext? requestContext) => GetSchemaType(dotnetType, false, requestContext); /// /// Search for a GraphQL type with the given name. Lookup is done by DotNet type first. @@ -587,26 +782,21 @@ public ISchemaType GetSchemaType(Type dotnetType, QueryRequestContext? requestCo /// /// Use the Type() methods for returning typed SchemaType /// - /// - /// Used the filter the search as dotnet type may be shared across Input type and query - /// type with different names in the schema - /// - /// - public ISchemaType GetSchemaType(Type dotnetType, bool inputTypeScope, QueryRequestContext? requestContext) + public ISchemaType GetSchemaType(Type dotnetType, bool inputTypesOnly, QueryRequestContext? requestContext) { - if (TryGetSchemaType(dotnetType, inputTypeScope, out var schemaType, requestContext)) + if (TryGetSchemaType(dotnetType, inputTypesOnly, out var schemaType, requestContext)) return schemaType!; - throw new EntityGraphQLCompilerException($"No schema type found for dotnet type '{dotnetType.Name}'. Make sure you add it or add a type mapping."); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"No schema type found for dotnet type '{dotnetType.Name}'. Make sure you add it or add a type mapping."); } - public bool TryGetSchemaType(Type dotnetType, bool inputTypeScope, out ISchemaType? schemaType, QueryRequestContext? requestContext) + public bool TryGetSchemaType(Type dotnetType, bool inputTypesOnly, out ISchemaType? schemaType, QueryRequestContext? requestContext) { // look up by the actual type not the name schemaType = schemaTypes - .Values.WhereWhen(t => t.GqlType == GqlTypes.Scalar || t.GqlType == GqlTypes.Enum || t.GqlType == GqlTypes.InputObject, inputTypeScope) + .Values.WhereWhen(t => t.GqlType == GqlTypes.Scalar || t.GqlType == GqlTypes.Enum || t.GqlType == GqlTypes.InputObject, inputTypesOnly) .FirstOrDefault(t => t.TypeDotnet == dotnetType) ?? schemaTypes.GetValueOrDefault(dotnetType.Name); - if (inputTypeScope && schemaType?.GqlType.IsNotValidForInput() == true) + if (inputTypesOnly && schemaType?.GqlType.IsNotValidForInput() == true) { // chance the Type has an input type that inherits from it schemaType = schemaTypes.Values.Where(t => dotnetType.IsAssignableFrom(t.TypeDotnet) && t.GqlType == GqlTypes.InputObject).FirstOrDefault(); @@ -628,7 +818,7 @@ public ISchemaType CheckTypeAccess(ISchemaType schemaType, QueryRequestContext? return schemaType; if (!requestContext.AuthorizationService.IsAuthorized(requestContext.User, schemaType.RequiredAuthorization)) - throw new EntityGraphQLAccessException($"You are not authorized to access the '{schemaType.Name}' type."); + throw new EntityGraphQLFieldException($"You are not authorized to access the '{schemaType.Name}' type."); return schemaType; } @@ -663,7 +853,6 @@ public SchemaType Type(string typeName) /// /// /// An ISchemaType if the type is found in the schema - /// If type name not found public ISchemaType Type(string typeName) { return GetSchemaType(typeName, null); @@ -924,7 +1113,7 @@ private static void RemoveFieldsOfType(string schemaType, ISchemaType contextTyp contextType.RemoveField(field.Name); } } - catch (EntityGraphQLCompilerException) + catch (EntityGraphQLException) { // SchemaType looks up the type in the schema. And there is a chance that type is not in there // either not added or removed previously @@ -937,12 +1126,11 @@ private static void RemoveFieldsOfType(string schemaType, ISchemaType contextTyp /// /// /// - /// public IDirectiveProcessor GetDirective(string name) { if (directives.TryGetValue(name, out var directive)) return directive; - throw new EntityGraphQLCompilerException($"Directive {name} not defined in schema"); + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Directive {name} not defined in schema"); } /// @@ -958,11 +1146,11 @@ public IEnumerable GetDirectives() /// Add a directive to the schema /// /// - /// + /// public void AddDirective(IDirectiveProcessor directive) { if (directives.ContainsKey(directive.Name)) - throw new EntityGraphQLCompilerException($"Directive {directive.Name} already exists on schema"); + throw new EntityGraphQLSchemaException($"Directive {directive.Name} already exists on schema"); directives.Add(directive.Name, directive); } @@ -1016,9 +1204,9 @@ public void Validate() // schema type will throw if if can't look it up var _ = field.ReturnType.SchemaType; } - catch (EntityGraphQLCompilerException) + catch (EntityGraphQLSchemaException) { - throw new EntityGraphQLCompilerException( + throw new EntityGraphQLSchemaException( $"Field '{field.Name}' on type '{schemaType.Name}' returns type '{(field.ReturnType.TypeDotnet.IsEnumerableOrArray() ? field.ReturnType.TypeDotnet.GetEnumerableOrArrayType() : field.ReturnType.TypeDotnet)}' that is not in the schema" ); } diff --git a/src/EntityGraphQL/Schema/SchemaType.cs b/src/EntityGraphQL/Schema/SchemaType.cs index 76ec6564..ac81ccb2 100644 --- a/src/EntityGraphQL/Schema/SchemaType.cs +++ b/src/EntityGraphQL/Schema/SchemaType.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using EntityGraphQL.Compiler; using EntityGraphQL.Compiler.Util; using Nullability; @@ -91,13 +90,13 @@ public Field AddField(Field field) throw new InvalidOperationException("Unions cannot contain fields"); if (FieldsByName.ContainsKey(field.Name)) - throw new EntityQuerySchemaException($"Field '{field.Name}' already exists on type '{Name}'. Use ReplaceField() if this is intended."); + throw new EntityGraphQLSchemaException($"Field '{field.Name}' already exists on type '{Name}'. Use ReplaceField() if this is intended."); if (field.Arguments.Any() && (GqlType == GqlTypes.InputObject || GqlType == GqlTypes.Enum)) - throw new EntityQuerySchemaException($"Field '{field.Name}' on type '{Name}' has arguments but is a GraphQL '{GqlType}' type and can not have arguments."); + throw new EntityGraphQLSchemaException($"Field '{field.Name}' on type '{Name}' has arguments but is a GraphQL '{GqlType}' type and can not have arguments."); if (GqlType == GqlTypes.Scalar) - throw new EntityQuerySchemaException($"Cannot add field '{field.Name}' to type '{Name}', as '{Name}' is a scalar type and can not have fields."); + throw new EntityGraphQLSchemaException($"Cannot add field '{field.Name}' to type '{Name}', as '{Name}' is a scalar type and can not have fields."); FieldsByName.Add(field.Name, field); return field; @@ -308,50 +307,6 @@ public void RemoveField(Expression> fieldSelection) RemoveField(Schema.SchemaFieldNamer(exp.Member.Name)); } - /// - /// To access this type all roles listed here are required - /// - /// - public SchemaType RequiresAllRoles(params string[] roles) - { - RequiredAuthorization ??= new RequiredAuthorization(); - RequiredAuthorization.RequiresAllRoles(roles); - return this; - } - - /// - /// To access this type any of the roles listed is required - /// - /// - public SchemaType RequiresAnyRole(params string[] roles) - { - RequiredAuthorization ??= new RequiredAuthorization(); - RequiredAuthorization.RequiresAnyRole(roles); - return this; - } - - /// - /// To access this type all policies listed here are required - /// - /// - public SchemaType RequiresAllPolicies(params string[] policies) - { - RequiredAuthorization ??= new RequiredAuthorization(); - RequiredAuthorization.RequiresAllPolicies(policies); - return this; - } - - /// - /// To access this type any of the policies listed is required - /// - /// - public SchemaType RequiresAnyPolicy(params string[] policies) - { - RequiredAuthorization ??= new RequiredAuthorization(); - RequiredAuthorization.RequiresAnyPolicy(policies); - return this; - } - public override ISchemaType ImplementAllBaseTypes(bool addTypeIfNotInSchema = true, bool addAllFieldsOnAddedType = true) { if (TypeDotnet.BaseType != null) @@ -382,7 +337,7 @@ private SchemaType Implements(Type type, bool addTypeIfNotInSchema = interfaceType = Schema.GetSchemaType(type, false, null); if (!interfaceType.IsInterface) - throw new EntityGraphQLCompilerException($"Schema type {type.Name} can not be implemented as it is not an interface. You can only implement interfaces"); + throw new EntityGraphQLSchemaException($"Schema type {type.Name} can not be implemented as it is not an interface. You can only implement interfaces"); } else if (!hasInterface && addTypeIfNotInSchema) { @@ -392,7 +347,7 @@ private SchemaType Implements(Type type, bool addTypeIfNotInSchema = interfaceType.AddAllFields(); } if (interfaceType == null) - throw new EntityGraphQLCompilerException( + throw new EntityGraphQLSchemaException( $"No schema interface found for dotnet type {type.Name}. Make sure you add the interface to the schema. Or use parameter addTypeIfNotInSchema = true" ); @@ -404,7 +359,7 @@ public override ISchemaType Implements(string typeName) { var interfaceType = Schema.GetSchemaType(typeName, null); if (!interfaceType.IsInterface) - throw new EntityGraphQLCompilerException($"Schema type {typeName} can not be implemented as it is not an interface. You can only implement interfaces"); + throw new EntityGraphQLSchemaException($"Schema type {typeName} can not be implemented as it is not an interface. You can only implement interfaces"); BaseTypes.Add(interfaceType); return this; @@ -413,7 +368,7 @@ public override ISchemaType Implements(string typeName) public ISchemaType AddAllPossibleTypes(bool addTypeIfNotInSchema = true, bool addAllFieldsOnAddedType = true) { if (GqlType != GqlTypes.Union) - throw new EntityGraphQLCompilerException($"Schema type {TypeDotnet} is not a union type"); + throw new EntityGraphQLSchemaException($"Schema type {TypeDotnet} is not a union type"); var types = AppDomain .CurrentDomain.GetAssemblies() @@ -443,7 +398,7 @@ public ISchemaType AddAllPossibleTypes(bool addTypeIfNotInSchema = true, bool ad public ISchemaType AddPossibleType(bool addTypeIfNotInSchema = true, bool addAllFieldsOnAddedType = true) { if (GqlType != GqlTypes.Union) - throw new EntityGraphQLCompilerException($"Schema type {TypeDotnet} is not a union type"); + throw new EntityGraphQLSchemaException($"Schema type {TypeDotnet} is not a union type"); var type = typeof(TClrType); return AddPossibleType(type, addTypeIfNotInSchema, addAllFieldsOnAddedType); @@ -452,7 +407,7 @@ public ISchemaType AddPossibleType(bool addTypeIfNotInSchema = true, b private SchemaType AddPossibleType(Type type, bool addTypeIfNotInSchema = true, bool addAllFieldsOnAddedType = true) { if (GqlType != GqlTypes.Union) - throw new EntityGraphQLCompilerException($"Schema type {TypeDotnet} is not a union type"); + throw new EntityGraphQLSchemaException($"Schema type {TypeDotnet} is not a union type"); var hasType = Schema.HasType(type); ISchemaType? schemaType = null; @@ -469,12 +424,12 @@ private SchemaType AddPossibleType(Type type, bool addTypeIfNotInSche schemaType = Schema.GetSchemaType(type, false, null); } if (schemaType == null) - throw new EntityGraphQLCompilerException( + throw new EntityGraphQLSchemaException( $"No schema type found for dotnet type '{type.Name}' while adding it as a possible type for schema type '{Name}'. Make sure you add the type to the schema before calling AddPossibleType. Or use parameter addTypeIfNotInSchema = true" ); if (schemaType.GqlType != GqlTypes.QueryObject) - throw new EntityGraphQLCompilerException($"The member types of a Union type must all be Object base types"); + throw new EntityGraphQLSchemaException($"The member types of a Union type must all be Object base types"); PossibleTypes.Add(schemaType); diff --git a/src/EntityGraphQL/Schema/SubscriptionField.cs b/src/EntityGraphQL/Schema/SubscriptionField.cs index 2ef06589..6a576792 100644 --- a/src/EntityGraphQL/Schema/SubscriptionField.cs +++ b/src/EntityGraphQL/Schema/SubscriptionField.cs @@ -1,6 +1,5 @@ using System; using System.Reflection; -using EntityGraphQL.Compiler; using EntityGraphQL.Extensions; namespace EntityGraphQL.Schema; @@ -16,13 +15,13 @@ public SubscriptionField( GqlTypeInfo returnType, MethodInfo method, string description, - RequiredAuthorization requiredAuth, + RequiredAuthorization? requiredAuth, bool isAsync, SchemaBuilderOptions options ) : base(schema, fromType, methodName, returnType, method, description, requiredAuth, isAsync, options) { if (!method.ReturnType.ImplementsGenericInterface(typeof(IObservable<>))) - throw new EntityGraphQLCompilerException($"Subscription {methodName} should return an IObservable<>"); + throw new EntityGraphQLSchemaException($"Subscription {methodName} should return an IObservable<>"); } } diff --git a/src/EntityGraphQL/Schema/SubscriptionType.cs b/src/EntityGraphQL/Schema/SubscriptionType.cs index 36b6c990..1a781420 100644 --- a/src/EntityGraphQL/Schema/SubscriptionType.cs +++ b/src/EntityGraphQL/Schema/SubscriptionType.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Threading.Tasks; using EntityGraphQL.Extensions; namespace EntityGraphQL.Schema; @@ -14,7 +13,7 @@ public SubscriptionType(ISchemaProvider schema, string name) protected override Type GetTypeFromMethodReturn(Type type, bool isAsync) { - if (isAsync || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>))) + if (isAsync || type.IsAsyncGenericType()) { type = type.GetGenericArguments()[0]; } @@ -36,7 +35,7 @@ protected override BaseField MakeField( string? description, SchemaBuilderOptions? options, bool isAsync, - RequiredAuthorization requiredClaims, + RequiredAuthorization? requiredClaims, GqlTypeInfo returnType ) { diff --git a/src/EntityGraphQL/Schema/Validators/DataAnnotationsValidator.cs b/src/EntityGraphQL/Schema/Validators/DataAnnotationsValidator.cs index a2ad2fc4..44c9cc48 100644 --- a/src/EntityGraphQL/Schema/Validators/DataAnnotationsValidator.cs +++ b/src/EntityGraphQL/Schema/Validators/DataAnnotationsValidator.cs @@ -110,7 +110,7 @@ private static void ValidateObjectRecursive(ArgumentValidatorContext context, ob continue; } - if (property.PropertyType.GetGenericArguments().Length > 0 && property.PropertyType.GetGenericTypeDefinition() == typeof(EntityQueryType<>)) + if (property.PropertyType == typeof(EntityQueryType)) { continue; } diff --git a/src/examples/AzureFunctionApp/GraphQLSchema.cs b/src/examples/AzureFunctionApp/GraphQLSchema.cs index 447dac49..8e76c5b9 100644 --- a/src/examples/AzureFunctionApp/GraphQLSchema.cs +++ b/src/examples/AzureFunctionApp/GraphQLSchema.cs @@ -1,7 +1,6 @@ using System.IO; using System.Linq; using demo.Mutations; -using EntityGraphQL.Extensions; using EntityGraphQL.Schema; using EntityGraphQL.Schema.FieldExtensions; @@ -31,12 +30,7 @@ public static void ConfigureSchema(SchemaProvider demoSchema) // type.AddField("age", l => (int)((DateTime.Now - l.Dob).TotalDays / 365), "Show the person's age"); // AgeService needs to be added to the ServiceProvider type.AddField("age", "Show the person's age").Resolve((person, ageService) => ageService.Calc(person.Dob)); - type.AddField( - "filteredDirectorOf", - new { filter = ArgumentHelper.EntityQuery() }, - (person, args) => person.DirectorOf.WhereWhen(args.filter, args.filter.HasValue).OrderBy(a => a.Name), - "Get Director of based on filter" - ); + type.AddField("filteredDirectorOf", (person) => person.DirectorOf.OrderBy(a => a.Name), "Get Director of based on filter").UseFilter(); type.ReplaceField("writerOf", m => m.WriterOf.Select(a => a.Movie), "Movies they wrote"); type.ReplaceField("actorIn", m => m.ActorIn.Select(a => a.Movie), "Movies they acted in"); }); diff --git a/src/examples/AzureFunctionApp/Startup.cs b/src/examples/AzureFunctionApp/Startup.cs index 5fbedfc2..0f9f12f4 100644 --- a/src/examples/AzureFunctionApp/Startup.cs +++ b/src/examples/AzureFunctionApp/Startup.cs @@ -21,7 +21,7 @@ public override void Configure(IFunctionsHostBuilder builder) builder .Services.AddGraphQLSchema(options => { - options.PreBuildSchemaFromContext = schema => + options.Builder.PreBuildSchemaFromContext = schema => { // add in needed mappings for our context schema.AddScalarType>("StringKeyValuePair", "Represents a pair of strings"); diff --git a/src/examples/demo/GraphQLSchema.cs b/src/examples/demo/GraphQLSchema.cs index c6a0641f..a18a9068 100644 --- a/src/examples/demo/GraphQLSchema.cs +++ b/src/examples/demo/GraphQLSchema.cs @@ -1,12 +1,19 @@ using System.IO; using System.Linq; using demo.Mutations; -using EntityGraphQL.Extensions; using EntityGraphQL.Schema; using EntityGraphQL.Schema.FieldExtensions; namespace demo; +/// +/// Configures the GraphQL schema showcasing EntityGraphQL features: +/// - Custom fields and computed properties +/// - Service field resolution +/// - Filtering, sorting, and pagination +/// - Mutations with validation +/// - Type relationships and field replacements +/// public class GraphQLSchema { public static void ConfigureSchema(SchemaProvider demoSchema) @@ -31,12 +38,7 @@ public static void ConfigureSchema(SchemaProvider demoSchema) // type.AddField("age", l => (int)((DateTime.Now - l.Dob).TotalDays / 365), "Show the person's age"); // AgeService needs to be added to the ServiceProvider type.AddField("age", "Show the person's age").Resolve((person, ageService) => ageService.Calc(person.Dob)); - type.AddField( - "filteredDirectorOf", - new { filter = ArgumentHelper.EntityQuery() }, - (person, args) => person.DirectorOf.WhereWhen(args.filter, args.filter.HasValue).OrderBy(a => a.Name), - "Get Director of based on filter" - ); + type.AddField("filteredDirectorOf", (person) => person.DirectorOf.OrderBy(a => a.Name), "Get Director of based on filter").UseFilter(); type.ReplaceField("writerOf", m => m.WriterOf.Select(a => a.Movie), "Movies they wrote"); type.ReplaceField("actorIn", m => m.ActorIn.Select(a => a.Movie), "Movies they acted in"); }); diff --git a/src/examples/demo/Program.cs b/src/examples/demo/Program.cs index 54581c25..07a8da52 100755 --- a/src/examples/demo/Program.cs +++ b/src/examples/demo/Program.cs @@ -1,15 +1,87 @@ -using System.IO; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text.Json; +using System.Text.Json.Serialization; +using demo; +using EntityGraphQL.AspNet; +using EntityGraphQL.Extensions; +using EntityGraphQL.Schema; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; -namespace demo; +var builder = WebApplication.CreateBuilder(args); -public class Program -{ - public static void Main() +// Configure database +builder.Services.AddDbContext(opt => opt.UseSqlite("Filename=demo.db")); + +// Add services that will be injected into GraphQL fields +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Configure JSON serialization for the API +builder + .Services.AddControllers() + .AddJsonOptions(opts => { - var host = new WebHostBuilder().UseKestrel().UseContentRoot(Directory.GetCurrentDirectory()).UseIISIntegration().UseStartup().Build(); + opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + opts.JsonSerializerOptions.IncludeFields = true; + opts.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + }); - host.Run(); - } +// Add GraphQL schema - showcasing EntityGraphQL.AspNet features +builder + .Services.AddGraphQLSchema(options => + { + // Configure builder options - controls how schema is built from reflection + options.Builder.IgnoreProps.Add("IsDeleted"); // Don't expose soft-deleted flag + options.Builder.PreBuildSchemaFromContext = schema => + { + // Add custom scalar types before reflection + schema.AddScalarType>("StringKeyValuePair", "Represents a pair of strings"); + }; + + options.ConfigureSchema = GraphQLSchema.ConfigureSchema; + }) + .AddGraphQLValidator(); // Add validation support for mutations + +var app = builder.Build(); + +// Initialize database with sample data +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + SeedData.Initialize(db); } + +// Serve static files (GraphiQL UI) +app.UseStaticFiles(); + +// Map GraphQL endpoint with advanced features +app.MapGraphQL( + options: new ExecutionOptions + { + // Add EF Core query tags for debugging + BeforeRootFieldExpressionBuild = (exp, op, field) => + { + if (exp.Type.IsGenericTypeQueryable()) + return Expression.Call( + typeof(EntityFrameworkQueryableExtensions), + nameof(EntityFrameworkQueryableExtensions.TagWith), + [exp.Type.GetGenericArguments()[0]], + exp, + Expression.Constant($"GQL op: {op ?? "n/a"}, field: {field}") + ); + return exp; + }, +#if DEBUG + IncludeDebugInfo = true, +#endif + IncludeQueryInfo = true, // Include query execution metadata + } +); + +// Fallback to index.html for SPA routing +app.MapFallbackToFile("index.html"); + +app.Run(); diff --git a/src/examples/demo/SeedData.cs b/src/examples/demo/SeedData.cs new file mode 100644 index 00000000..84d813f5 --- /dev/null +++ b/src/examples/demo/SeedData.cs @@ -0,0 +1,214 @@ +using System; + +namespace demo; + +/// +/// Seeds the database with sample movie and people data +/// +public static class SeedData +{ + public static void Initialize(DemoContext db) + { + db.Database.EnsureDeleted(); + db.Database.EnsureCreated(); + + // The Shawshank Redemption + var shawshank = new Movie + { + Name = "The Shawshank Redemption", + Genre = Genre.Drama, + Released = new DateTime(1994, 10, 14), + Rating = 9.3, + CreatedBy = 1, + Director = new Person + { + FirstName = "Frank", + LastName = "Darabont", + Dob = new DateTime(1959, 1, 28), + }, + }; + shawshank.Actors = + [ + new Actor + { + Person = new Person + { + Dob = new DateTime(1958, 10, 16), + FirstName = "Tim", + LastName = "Robbins", + }, + Movie = shawshank, + }, + new Actor + { + Person = new Person + { + Dob = new DateTime(1937, 9, 21), + FirstName = "Morgan", + LastName = "Freeman", + }, + Movie = shawshank, + }, + ]; + + db.Movies.Add(shawshank); + + // The Godfather + var francis = new Person + { + Dob = new DateTime(1939, 4, 7), + FirstName = "Francis", + LastName = "Coppola", + }; + var godfather = new Movie + { + Name = "The Godfather", + Genre = Genre.Drama, + Released = new DateTime(1972, 3, 24), + Rating = 9.2, + Director = francis, + CreatedBy = 1, + }; + godfather.Actors = + [ + new Actor + { + Person = new Person + { + Dob = new DateTime(1924, 4, 3), + Died = new DateTime(2004, 7, 1), + FirstName = "Marlon", + LastName = "Brando", + }, + Movie = godfather, + }, + new Actor + { + Person = new Person + { + Dob = new DateTime(1940, 4, 25), + FirstName = "Al", + LastName = "Pacino", + }, + Movie = godfather, + }, + ]; + godfather.Writers = [new Writer { Person = francis, Movie = godfather }]; + + db.Movies.Add(godfather); + + // The Dark Knight + var nolan = new Person + { + Dob = new DateTime(1970, 7, 30), + FirstName = "Christopher", + LastName = "Nolan", + }; + var darkKnight = new Movie + { + Name = "The Dark Knight", + Genre = Genre.Action, + Released = new DateTime(2008, 7, 18), + Rating = 9.0, + Director = nolan, + CreatedBy = 2, + }; + darkKnight.Actors = + [ + new Actor + { + Person = new Person + { + Dob = new DateTime(1974, 1, 30), + FirstName = "Christian", + LastName = "Bale", + }, + Movie = darkKnight, + }, + new Actor + { + Person = new Person + { + Dob = new DateTime(1979, 4, 4), + Died = new DateTime(2008, 1, 22), + FirstName = "Heath", + LastName = "Ledger", + }, + Movie = darkKnight, + }, + ]; + + db.Movies.Add(darkKnight); + + // Inception + var inception = new Movie + { + Name = "Inception", + Genre = Genre.Scifi, + Released = new DateTime(2010, 7, 16), + Rating = 8.8, + Director = nolan, + CreatedBy = 2, + }; + inception.Actors = + [ + new Actor + { + Person = new Person + { + Dob = new DateTime(1974, 11, 11), + FirstName = "Leonardo", + LastName = "DiCaprio", + }, + Movie = inception, + }, + ]; + + db.Movies.Add(inception); + + // Pulp Fiction + var tarantino = new Person + { + Dob = new DateTime(1963, 3, 27), + FirstName = "Quentin", + LastName = "Tarantino", + }; + var pulpFiction = new Movie + { + Name = "Pulp Fiction", + Genre = Genre.Drama, + Released = new DateTime(1994, 10, 14), + Rating = 8.9, + Director = tarantino, + CreatedBy = 1, + }; + pulpFiction.Actors = + [ + new Actor + { + Person = new Person + { + Dob = new DateTime(1954, 2, 18), + FirstName = "John", + LastName = "Travolta", + }, + Movie = pulpFiction, + }, + new Actor + { + Person = new Person + { + Dob = new DateTime(1948, 12, 21), + FirstName = "Samuel", + LastName = "Jackson", + }, + Movie = pulpFiction, + }, + ]; + pulpFiction.Writers = [new Writer { Person = tarantino, Movie = pulpFiction }]; + + db.Movies.Add(pulpFiction); + + db.SaveChanges(); + } +} diff --git a/src/examples/demo/Startup.cs b/src/examples/demo/Startup.cs deleted file mode 100755 index 05d7f11e..00000000 --- a/src/examples/demo/Startup.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Text.Json; -using System.Text.Json.Serialization; -using EntityGraphQL.AspNet; -using EntityGraphQL.Extensions; -using EntityGraphQL.Schema; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace demo; - -public class Startup -{ - public Startup(IWebHostEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddEnvironmentVariables(); - Configuration = builder.Build(); - } - - public IConfigurationRoot Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddDbContext(opt => - opt.UseSqlite("Filename=demo.db") - // .UseProjectables() - ); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddLogging(logging => - { - logging.AddConsole(configure => Configuration.GetSection("Logging")); - logging.AddDebug(); - }); - // add schema provider so we don't need to create it every time - // if you want to override json serialization - say PascalCase response - // You will also need to override the default fieldNamer in SchemaProvider - // var jsonOptions = new JsonSerializerOptions - // { - // // the generated types use fields - // IncludeFields = true, - // }; - // services.AddSingleton(new DefaultGraphQLRequestDeserializer(jsonOptions)); - // services.AddSingleton(new DefaultGraphQLResponseSerializer(jsonOptions)); - // Or you could override the whole interface and do something other than JSON - - services - .AddGraphQLSchema(options => - { - options.PreBuildSchemaFromContext = schema => - { - // add in needed mappings for our context - schema.AddScalarType>("StringKeyValuePair", "Represents a pair of strings"); - }; - options.ConfigureSchema = GraphQLSchema.ConfigureSchema; - // below this will generate the field names as they are from the reflected dotnet types - i.e matching the case - // builder.FieldNamer = name => name; - }) - .AddGraphQLValidator(); - - services.AddRouting(); - services - .AddControllers() - .AddJsonOptions(opts => - { - // configure JSON serializer like this if you are return GraphQL execution results in your own controller - // assuming you want the default behavior of serializing GraphQL execution results to JSON - opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - opts.JsonSerializerOptions.IncludeFields = true; - opts.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, DemoContext db) - { - CreateData(db); - - app.UseFileServer(); - - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapGraphQL( - options: new ExecutionOptions - { - BeforeRootFieldExpressionBuild = (exp, op, field) => - { - if (exp.Type.IsGenericTypeQueryable()) - return Expression.Call( - typeof(EntityFrameworkQueryableExtensions), - nameof(EntityFrameworkQueryableExtensions.TagWith), - [exp.Type.GetGenericArguments()[0]], - exp, - Expression.Constant($"GQL op: {op ?? "n/a"}, field: {field}") - ); - return exp; - }, -#if DEBUG - IncludeDebugInfo = true -#endif - } - ); - }); - } - - private static void CreateData(DemoContext db) - { - db.Database.EnsureDeleted(); - db.Database.EnsureCreated(); - - // add test data - var shawshank = new Movie - { - Name = "The Shawshank Redemption", - Genre = Genre.Drama, - Released = new DateTime(1994, 10, 14), - Rating = 9.2, - CreatedBy = 1, - Director = new Person - { - FirstName = "Frank", - LastName = "Darabont", - Dob = new DateTime(1959, 1, 28), - }, - }; - shawshank.Actors = - [ - new Actor - { - Person = new Person - { - Dob = new DateTime(1958, 10, 16), - FirstName = "Tim", - LastName = "Robbins", - }, - Movie = shawshank, - }, - ]; - - db.Movies.Add(shawshank); - var francis = new Person - { - Dob = new DateTime(1939, 4, 7), - FirstName = "Francis", - LastName = "Coppola", - }; - var godfather = new Movie - { - Name = "The Godfather", - Genre = Genre.Drama, - Released = new DateTime(1972, 3, 24), - Rating = 9.2, - Director = francis, - }; - godfather.Actors = - [ - new Actor - { - Person = new Person - { - Dob = new DateTime(1924, 4, 3), - Died = new DateTime(2004, 7, 1), - FirstName = "Marlon", - LastName = "Brando", - }, - Movie = godfather, - }, - new Actor - { - Person = new Person - { - Dob = new DateTime(1940, 4, 25), - FirstName = "Al", - LastName = "Pacino", - }, - Movie = godfather, - }, - ]; - godfather.Writers = [new Writer { Person = francis, Movie = godfather }]; - - db.Movies.Add(godfather); - - db.SaveChanges(); - } -} diff --git a/src/examples/demo/demo.csproj b/src/examples/demo/demo.csproj index 9e51b645..c819762a 100755 --- a/src/examples/demo/demo.csproj +++ b/src/examples/demo/demo.csproj @@ -1,23 +1,17 @@ - - net9.0 + net10.0 false true enable - - - - + - - + - diff --git a/src/examples/demo/schema.graphql b/src/examples/demo/schema.graphql index a1806484..6b9a0647 100644 --- a/src/examples/demo/schema.graphql +++ b/src/examples/demo/schema.graphql @@ -25,6 +25,8 @@ scalar String scalar StringKeyValuePair """Time value only scalar""" scalar TimeOnly +"""TimeSpan scalar""" +scalar TimeSpan """Directs the executor to include this field or fragment only when the `if` argument is true.""" directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT @@ -154,7 +156,6 @@ type Person { filteredDirectorOf(filter: String): [Movie!] firstName: String! id: Int! - isDeleted: Boolean! lastName: String! """Person's name""" name: String diff --git a/src/examples/demo/wwwroot/index.html b/src/examples/demo/wwwroot/index.html index 2fb64c2d..463bd1bd 100644 --- a/src/examples/demo/wwwroot/index.html +++ b/src/examples/demo/wwwroot/index.html @@ -1,43 +1,201 @@ - + + + EntityGraphQL Demo - Movie Database API - - + + - - + + -
Loading...
+
Loading GraphiQL...
diff --git a/src/examples/subscriptions/wwwroot/package-lock.json b/src/examples/subscriptions/wwwroot/package-lock.json index a38ffe74..326fbb7b 100644 --- a/src/examples/subscriptions/wwwroot/package-lock.json +++ b/src/examples/subscriptions/wwwroot/package-lock.json @@ -17,7 +17,7 @@ "devDependencies": { "@babel/preset-react": "^7.24.7", "babel-loader": "^9.1.3", - "webpack": "^5.94.0", + "webpack": "^5.105.0", "webpack-cli": "^5.1.4" } }, @@ -537,9 +537,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -562,170 +562,190 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/node": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.2.0.tgz", - "integrity": "sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==", + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz", + "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", "dev": true, "dependencies": { - "undici-types": "~6.13.0" + "undici-types": "~7.16.0" } }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -830,9 +850,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -841,25 +861,28 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, + "engines": { + "node": ">=10.13.0" + }, "peerDependencies": { - "acorn": "^8" + "acorn": "^8.14.0" } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -883,35 +906,16 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, "peerDependencies": { - "ajv": "^6.9.1" + "ajv": "^8.8.2" } }, "node_modules/ansi-styles": { @@ -943,63 +947,19 @@ "webpack": ">=5" } }, - "node_modules/babel-loader/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/babel-loader/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/babel-loader/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/babel-loader/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "bin": { + "baseline-browser-mapping": "dist/cli.js" } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -1016,10 +976,11 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -1035,9 +996,9 @@ "dev": true }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "dev": true, "funding": [ { @@ -1173,19 +1134,19 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz", - "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -1204,15 +1165,15 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -1285,11 +1246,21 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] }, "node_modules/fastest-levenshtein": { "version": "1.0.16", @@ -1646,9 +1617,9 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, "node_modules/json5": { @@ -1674,12 +1645,16 @@ } }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/locate-path": { @@ -1745,9 +1720,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true }, "node_modules/object-assign": { @@ -1841,9 +1816,9 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/pkg-dir": { @@ -1868,15 +1843,6 @@ "react-is": "^16.13.1" } }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2027,14 +1993,15 @@ } }, "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 10.13.0" @@ -2148,22 +2115,26 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser": { - "version": "5.31.5", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.5.tgz", - "integrity": "sha512-YPmas0L0rE1UyLL/llTWA0SiDOqIcAQYLeUj7cJYzXHlRTAnMSg9pPe4VJ5PlKvTrPQsdVFuiRiwyeNlYgwh2Q==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -2175,16 +2146,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -2234,15 +2205,15 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "node_modules/undici-types": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", - "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -2259,8 +2230,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -2269,19 +2240,10 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -2292,34 +2254,36 @@ } }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "dependencies": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -2405,9 +2369,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "engines": { "node": ">=10.13.0" @@ -2829,9 +2793,9 @@ "dev": true }, "@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2854,170 +2818,190 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "@types/node": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.2.0.tgz", - "integrity": "sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==", + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz", + "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", "dev": true, "requires": { - "undici-types": "~6.13.0" + "undici-types": "~7.16.0" } }, "@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "requires": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true }, "@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true }, "@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true }, "@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "requires": { "@xtuc/long": "4.2.2" } }, "@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true }, "@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -3087,28 +3071,28 @@ "dev": true }, "acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true }, - "acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "requires": {} }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" } }, "ajv-formats": { @@ -3118,34 +3102,16 @@ "dev": true, "requires": { "ajv": "^8.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } } }, "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "requires": {} + "requires": { + "fast-deep-equal": "^3.1.3" + } }, "ansi-styles": { "version": "3.2.1", @@ -3164,59 +3130,25 @@ "requires": { "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - } - } } }, + "baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true + }, "browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" } }, "buffer-from": { @@ -3226,9 +3158,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "dev": true }, "chalk": { @@ -3332,19 +3264,19 @@ } }, "electron-to-chromium": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz", - "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "dev": true }, "enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "requires": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" } }, "envinfo": { @@ -3354,15 +3286,15 @@ "dev": true }, "es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true }, "escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, "escape-string-regexp": { @@ -3416,10 +3348,10 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true }, "fastest-levenshtein": { @@ -3668,9 +3600,9 @@ "dev": true }, "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, "json5": { @@ -3687,9 +3619,9 @@ "dev": true }, "loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true }, "locate-path": { @@ -3743,9 +3675,9 @@ "dev": true }, "node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true }, "object-assign": { @@ -3817,9 +3749,9 @@ "dev": true }, "picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "pkg-dir": { @@ -3841,12 +3773,6 @@ "react-is": "^16.13.1" } }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3945,14 +3871,15 @@ } }, "schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" } }, "semver": { @@ -4032,34 +3959,34 @@ "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==" }, "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true }, "terser": { - "version": "5.31.5", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.5.tgz", - "integrity": "sha512-YPmas0L0rE1UyLL/llTWA0SiDOqIcAQYLeUj7cJYzXHlRTAnMSg9pPe4VJ5PlKvTrPQsdVFuiRiwyeNlYgwh2Q==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "requires": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" } }, "terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" } }, "to-fast-properties": { @@ -4082,34 +4009,25 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "undici-types": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", - "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true }, "update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", - "dev": true, - "requires": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "requires": { - "punycode": "^2.1.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" } }, "watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "requires": { "glob-to-regexp": "^0.4.1", @@ -4117,34 +4035,36 @@ } }, "webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "requires": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" } }, "webpack-cli": { @@ -4187,9 +4107,9 @@ } }, "webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true }, "which": { diff --git a/src/examples/subscriptions/wwwroot/package.json b/src/examples/subscriptions/wwwroot/package.json index eb58a5c4..cd191423 100644 --- a/src/examples/subscriptions/wwwroot/package.json +++ b/src/examples/subscriptions/wwwroot/package.json @@ -12,7 +12,7 @@ "devDependencies": { "@babel/preset-react": "^7.24.7", "babel-loader": "^9.1.3", - "webpack": "^5.94.0", + "webpack": "^5.105.0", "webpack-cli": "^5.1.4" }, "dependencies": { diff --git a/src/tests/EntityGraphQL.AspNet.Tests/EntityGraphQL.AspNet.Tests.csproj b/src/tests/EntityGraphQL.AspNet.Tests/EntityGraphQL.AspNet.Tests.csproj index 0a9616d2..d4822903 100644 --- a/src/tests/EntityGraphQL.AspNet.Tests/EntityGraphQL.AspNet.Tests.csproj +++ b/src/tests/EntityGraphQL.AspNet.Tests/EntityGraphQL.AspNet.Tests.csproj @@ -1,26 +1,29 @@ - - net9.0 + net8.0;net9.0;net10.0 enable enable true false true - - - + + + + + + + + + - - - + + + - - diff --git a/src/tests/EntityGraphQL.AspNet.Tests/EntityGraphQLEndpointRouteExtensionsTests.cs b/src/tests/EntityGraphQL.AspNet.Tests/EntityGraphQLEndpointRouteExtensionsTests.cs index 9a04a9b3..44fb5ada 100644 --- a/src/tests/EntityGraphQL.AspNet.Tests/EntityGraphQLEndpointRouteExtensionsTests.cs +++ b/src/tests/EntityGraphQL.AspNet.Tests/EntityGraphQLEndpointRouteExtensionsTests.cs @@ -183,7 +183,8 @@ public async Task GraphQL_Endpoint_200_No_Data_Mutation_Error() var json = JsonNode.Parse(responseString); Assert.NotNull(json); - Assert.False(json.AsObject().ContainsKey("data"), "Expected 'data' field to be absent, but it exists in the JSON response."); + Assert.True(json.AsObject().ContainsKey("data"), "Expected 'data' field to be absent, but it exists in the JSON response."); + Assert.Null(json["data"]); Assert.NotNull(json["errors"]); Assert.Equal("This is a test error", json["errors"]!.AsArray()[0]!["message"]!.GetValue()); } @@ -359,7 +360,7 @@ protected override IHost CreateHost(IHostBuilder builder) WebApplication realApp = realBuilder.Build(); // Same mapping as Program.cs - realApp.MapGraphQL(followSpec: true); + realApp.MapGraphQL(); // Force ephemeral port realApp.Urls.Add("http://127.0.0.1:0"); @@ -551,12 +552,12 @@ public async Task GraphQL_Endpoint_200_On_Chunked_Data_Query() // if PostAsJsonAsync is used with WebApplicationFactory, // i.e. the in-memory test server, with no network involved, // it works fine and the ContentLength is set correctly. - // hence, the CustomWebApplicationFactory is used to ensure - // network communication is used, and the ContentLength is not set. + // hence, the CustomWebApplicationFactory is used to ensure + // network communication is used, and the ContentLength is not set. HttpResponseMessage resp = await client.PostAsJsonAsync("/graphql", new { query = "{ hello }" }); resp.EnsureSuccessStatusCode(); string json = await resp.Content.ReadAsStringAsync(); Assert.Contains("\"hello\":\"world\"", json); } -} \ No newline at end of file +} diff --git a/src/tests/EntityGraphQL.AspNet.Tests/PoliciesTests.cs b/src/tests/EntityGraphQL.AspNet.Tests/PoliciesTests.cs index 5803b0b9..09d7afc9 100644 --- a/src/tests/EntityGraphQL.AspNet.Tests/PoliciesTests.cs +++ b/src/tests/EntityGraphQL.AspNet.Tests/PoliciesTests.cs @@ -15,17 +15,19 @@ public void TestAttributeOnTypeFromObject() serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(); var schema = SchemaBuilder.FromObject( - new SchemaBuilderSchemaOptions + new SchemaProviderOptions { AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!) }, + new SchemaBuilderOptions { - AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!), PreBuildSchemaFromContext = (context) => { context.AddAttributeHandler(new AuthorizeAttributeHandler()); }, } ); - Assert.Single(schema.Type().RequiredAuthorization!.Policies); - Assert.Equal("admin", schema.Type().RequiredAuthorization!.Policies.ElementAt(0).ElementAt(0)); + var projectAuth = schema.Type().RequiredAuthorization!; + var policies = projectAuth.GetPolicies()!; + Assert.Single(policies); + Assert.Equal("admin", policies.ElementAt(0).ElementAt(0)); var sdl = schema.ToGraphQLSchemaString(); Assert.Contains("type Project @authorize(roles: \"\", policies: \"admin\") {", sdl); @@ -42,8 +44,10 @@ public void TestAttributeOnTypeAddType() var schema = new SchemaProvider(new PolicyOrRoleBasedAuthorization(services.GetService()!)); schema.AddType("Project"); - Assert.Single(schema.Type().RequiredAuthorization!.Policies); - Assert.Equal("admin", schema.Type().RequiredAuthorization!.Policies.ElementAt(0).ElementAt(0)); + var projectAuth = schema.Type().RequiredAuthorization!; + var policies = projectAuth.GetPolicies()!; + Assert.Single(policies); + Assert.Equal("admin", policies.ElementAt(0).ElementAt(0)); } [Fact] @@ -54,15 +58,17 @@ public void TestMethodOnType() var services = serviceCollection.BuildServiceProvider(); var schema = SchemaBuilder.FromObject( - new SchemaBuilderSchemaOptions { AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!) } + new SchemaProviderOptions { AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!) } ); - Assert.Empty(schema.Type().RequiredAuthorization!.Policies); + Assert.Null(schema.Type().RequiredAuthorization); schema.Type().RequiresAnyPolicy("admin"); - Assert.Single(schema.Type().RequiredAuthorization!.Policies); - Assert.Equal("admin", schema.Type().RequiredAuthorization!.Policies.ElementAt(0).ElementAt(0)); + var taskAuth = schema.Type().RequiredAuthorization!; + var policies = taskAuth.GetPolicies()!; + Assert.Single(policies); + Assert.Equal("admin", policies.ElementAt(0).ElementAt(0)); } [Fact] @@ -73,11 +79,13 @@ public void TestAttributeOnField() var services = serviceCollection.BuildServiceProvider(); var schema = SchemaBuilder.FromObject( - new SchemaBuilderSchemaOptions { AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!) } + new SchemaProviderOptions { AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!) } ); - Assert.Single(schema.Type().GetField("type", null).RequiredAuthorization!.Policies); - Assert.Equal("can-type", schema.Type().GetField("type", null).RequiredAuthorization!.Policies.ElementAt(0).ElementAt(0)); + var fieldAuth = schema.Type().GetField("type", null).RequiredAuthorization!; + var policies = fieldAuth.GetPolicies()!; + Assert.Single(policies); + Assert.Equal("can-type", policies.ElementAt(0).ElementAt(0)); } [Fact] @@ -91,8 +99,10 @@ public void TestAttributeOnFieldAddField() schema.AddType("Project", "All about the project").AddField(p => p.Type, "The type info"); - Assert.Single(schema.Type().GetField("type", null).RequiredAuthorization!.Policies); - Assert.Equal("can-type", schema.Type().GetField("type", null).RequiredAuthorization!.Policies.ElementAt(0).ElementAt(0)); + var fieldAuth = schema.Type().GetField("type", null).RequiredAuthorization!; + var policies = fieldAuth.GetPolicies()!; + Assert.Single(policies); + Assert.Equal("can-type", policies.ElementAt(0).ElementAt(0)); } [Fact] @@ -106,8 +116,10 @@ public void TestMethodOnField() schema.AddType("Task", "All about tasks").AddField(p => p.IsActive, "Is it active").RequiresAllPolicies("admin"); - Assert.Single(schema.Type().GetField("isActive", null).RequiredAuthorization!.Policies); - Assert.Equal("admin", schema.Type().GetField("isActive", null).RequiredAuthorization!.Policies.ElementAt(0).ElementAt(0)); + var fieldAuth = schema.Type().GetField("isActive", null).RequiredAuthorization!; + var policies = fieldAuth.GetPolicies()!; + Assert.Single(policies); + Assert.Equal("admin", policies.ElementAt(0).ElementAt(0)); } [Fact] @@ -121,8 +133,10 @@ public void TestMethodAllOnField() schema.AddType("Task", "All about tasks").AddField(p => p.IsActive, "Is it active").RequiresAllPolicies("admin", "can-type"); - Assert.Equal(2, schema.Type().GetField("isActive", null).RequiredAuthorization!.Policies.Count()); - Assert.Equal("admin", schema.Type().GetField("isActive", null).RequiredAuthorization!.Policies.ElementAt(0).ElementAt(0)); + var fieldAuth = schema.Type().GetField("isActive", null).RequiredAuthorization!; + var policies = fieldAuth.GetPolicies()!; + Assert.Equal(2, policies.Count()); + Assert.Equal("admin", policies.ElementAt(0).ElementAt(0)); } [Fact] @@ -135,7 +149,7 @@ public void TestFieldIsSecured() var services = serviceCollection.BuildServiceProvider(); var schema = SchemaBuilder.FromObject( - new SchemaBuilderSchemaOptions { AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!) } + new SchemaProviderOptions { AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!) } ); var claims = new ClaimsIdentity([new Claim(ClaimTypes.Role, "admin")], "authed"); @@ -167,7 +181,7 @@ public void TestTypeIsSecured() var services = serviceCollection.BuildServiceProvider(); var schema = SchemaBuilder.FromObject( - new SchemaBuilderSchemaOptions { AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!) } + new SchemaProviderOptions { AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!) } ); var claims = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, "not-admin") }, "authed"); @@ -198,7 +212,7 @@ public void TestNonTopLevelTypeIsSecured() var services = serviceCollection.BuildServiceProvider(); var schema = SchemaBuilder.FromObject( - new SchemaBuilderSchemaOptions { AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!) } + new SchemaProviderOptions { AuthorizationService = new PolicyOrRoleBasedAuthorization(services.GetService()!) } ); var claims = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, "not-admin") }, "authed"); diff --git a/src/tests/EntityGraphQL.AspNet.Tests/RuntimeTypeJsonConverterTests.cs b/src/tests/EntityGraphQL.AspNet.Tests/RuntimeTypeJsonConverterTests.cs index aed4ee03..22ffe05a 100644 --- a/src/tests/EntityGraphQL.AspNet.Tests/RuntimeTypeJsonConverterTests.cs +++ b/src/tests/EntityGraphQL.AspNet.Tests/RuntimeTypeJsonConverterTests.cs @@ -62,7 +62,7 @@ public void SerializeSubTypes() [Fact] public void SerializeOffsetPage() { - var item = new OffsetPage(0, 0, 10) { Items = [] }; + var item = new OffsetPage(0, 10) { Items = [] }; var graphqlResponseSerializer = new DefaultGraphQLResponseSerializer(); var memoryStream = new MemoryStream(); diff --git a/src/tests/EntityGraphQL.EF.Tests/EFCacheTests.cs b/src/tests/EntityGraphQL.EF.Tests/EFCacheTests.cs index 2e304178..8e8853c2 100644 --- a/src/tests/EntityGraphQL.EF.Tests/EFCacheTests.cs +++ b/src/tests/EntityGraphQL.EF.Tests/EFCacheTests.cs @@ -216,8 +216,7 @@ public class TestLogger : ILogger public int QueryCompilationCount { get; private set; } public int QueryExecutionCount { get; private set; } - public IDisposable? BeginScope(TState state) - where TState : notnull => null!; + IDisposable ILogger.BeginScope(TState state) => null!; public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Debug; diff --git a/src/tests/EntityGraphQL.EF.Tests/EntityGraphQL.EF.Tests.csproj b/src/tests/EntityGraphQL.EF.Tests/EntityGraphQL.EF.Tests.csproj index 255b7a39..250613f9 100644 --- a/src/tests/EntityGraphQL.EF.Tests/EntityGraphQL.EF.Tests.csproj +++ b/src/tests/EntityGraphQL.EF.Tests/EntityGraphQL.EF.Tests.csproj @@ -1,29 +1,16 @@  - - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 enable true enable false 13.0 - - - - - - - - - - - - @@ -32,10 +19,13 @@ - + + + + - - - + + + diff --git a/src/tests/EntityGraphQL.EF.Tests/EqlMethodProviderEFMethodsTests.cs b/src/tests/EntityGraphQL.EF.Tests/EqlMethodProviderEFMethodsTests.cs index 1137d814..7df352b8 100644 --- a/src/tests/EntityGraphQL.EF.Tests/EqlMethodProviderEFMethodsTests.cs +++ b/src/tests/EntityGraphQL.EF.Tests/EqlMethodProviderEFMethodsTests.cs @@ -1,5 +1,4 @@ using System.Linq.Expressions; -using EntityGraphQL.Compiler; using EntityGraphQL.Compiler.EntityQuery; using EntityGraphQL.Schema; using EntityGraphQL.Schema.FieldExtensions; @@ -207,7 +206,7 @@ private static void RegisterEFDatePart(EqlMethodProvider provider) makeCallFunc: (context, argContext, methodName, args) => { if (args.Length != 1) - throw new EntityGraphQLCompilerException($"Method '{methodName}' expects 1 argument but {args.Length} were supplied"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' expects 1 argument but {args.Length} were supplied"); return Expression.PropertyOrField(context, Expression.Lambda(args[0]).Compile().DynamicInvoke()!.ToString()!); } diff --git a/src/tests/EntityGraphQL.Tests.Util/EntityGraphQL.Tests.Util.csproj b/src/tests/EntityGraphQL.Tests.Util/EntityGraphQL.Tests.Util.csproj index f5649fb5..fd83688a 100644 --- a/src/tests/EntityGraphQL.Tests.Util/EntityGraphQL.Tests.Util.csproj +++ b/src/tests/EntityGraphQL.Tests.Util/EntityGraphQL.Tests.Util.csproj @@ -1,11 +1,9 @@  - - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0 enable enable false 13.0 - diff --git a/src/tests/EntityGraphQL.Tests/BinaryComparisonTests.cs b/src/tests/EntityGraphQL.Tests/BinaryComparisonTests.cs new file mode 100644 index 00000000..631c07d3 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/BinaryComparisonTests.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EntityGraphQL.Compiler; +using EntityGraphQL.Compiler.EntityQuery; +using EntityGraphQL.Schema; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class BinaryComparisonTests +{ + private readonly EqlCompileContext compileContext = new(new CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null)); + + private class GuidHolder + { + public GuidHolder(Guid id, Guid? idN, string name) + { + Id = id; + IdN = idN; + Name = name; + } + + public Guid Id { get; set; } + public Guid? IdN { get; set; } + public string Name { get; set; } + } + + [Fact] + public void Guid_Equals_StringLiteral_LeftGuid_RightString() + { + var schema = SchemaBuilder.FromObject(); + var target = Guid.NewGuid(); + var compiled = EntityQueryCompiler.Compile($"id == \"{target}\"", schema, compileContext); + var data = new List { new(Guid.NewGuid(), null, "A"), new(target, null, "B") }; + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Single(res); + Assert.Equal("B", res[0].Name); + } + + [Fact] + public void Guid_Equals_StringLiteral_LeftString_RightGuid() + { + var schema = SchemaBuilder.FromObject(); + var target = Guid.NewGuid(); + var compiled = EntityQueryCompiler.Compile($"\"{target}\" == id", schema, compileContext); + var data = new List { new(Guid.NewGuid(), null, "A"), new(target, null, "B") }; + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Single(res); + Assert.Equal("B", res[0].Name); + } + + [Fact] + public void NullableGuid_Equals_StringLiteral() + { + var schema = SchemaBuilder.FromObject(); + var target = Guid.NewGuid(); + var compiled = EntityQueryCompiler.Compile($"idN == \"{target}\"", schema, compileContext); + var data = new List { new(Guid.NewGuid(), null, "A"), new(Guid.NewGuid(), target, "B") }; + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Single(res); + Assert.Equal("B", res[0].Name); + } + + private enum EColor + { + Red, + Green, + Blue, + } + + private class EnumHolder + { + public EnumHolder(EColor color, string name) + { + Color = color; + Name = name; + } + + public EColor Color { get; set; } + public string Name { get; set; } + } + + [Fact] + public void Enum_Equals_StringLiteral() + { + var schema = SchemaBuilder.FromObject(); + var compiled = EntityQueryCompiler.Compile("color == \"Green\"", schema, compileContext); + var data = new List { new(EColor.Red, "A"), new(EColor.Green, "B") }; + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Single(res); + Assert.Equal("B", res[0].Name); + } + + private class NumericHolder + { + public NumericHolder(int priceI, uint priceU, decimal priceD, double priceF, int? n, string name) + { + PriceI = priceI; + PriceU = priceU; + PriceD = priceD; + PriceF = priceF; + N = n; + Name = name; + } + + public int PriceI { get; set; } + public uint PriceU { get; set; } + public decimal PriceD { get; set; } + public double PriceF { get; set; } + public int? N { get; set; } + public string Name { get; set; } + } + + [Fact] + public void Numeric_Int_Vs_Decimal_Promotion() + { + var schema = SchemaBuilder.FromObject(); + var compiled = EntityQueryCompiler.Compile("priceI > 3.5", schema, compileContext); + var data = new List { new(3, 3, 3m, 3.0, 1, "A"), new(4, 4, 4m, 4.0, 2, "B") }; + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Single(res); + Assert.Equal("B", res[0].Name); + } + + [Fact] + public void Numeric_Int_Vs_UInt_Alignment() + { + var schema = SchemaBuilder.FromObject(); + var compiled = EntityQueryCompiler.Compile("priceU == 4", schema, compileContext); + var data = new List { new(1, 3u, 0m, 0.0, null, "A"), new(2, 4u, 0m, 0.0, null, "B") }; + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Single(res); + Assert.Equal("B", res[0].Name); + } + + [Fact] + public void Nullable_Int_Equals_Constant() + { + var schema = SchemaBuilder.FromObject(); + var compiled = EntityQueryCompiler.Compile("n == 2", schema, compileContext); + var data = new List { new(0, 0, 0m, 0.0, 1, "A"), new(0, 0, 0m, 0.0, 2, "B") }; + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Single(res); + Assert.Equal("B", res[0].Name); + } + + [Fact] + public void Binary_Version_All_Operators_Work_With_CustomType() + { + var schema = MakeSchemaWithVersionCustomConverter(); + var data = new List { new(new Version(1, 2, 2), "A"), new(new Version(1, 2, 3), "B"), new(new Version(2, 0, 0), "C") }; + + // == + var eq = EntityQueryCompiler.Compile("v == \"1.2.3\"", schema, compileContext); + var eqRes = data.Where((Func)eq.LambdaExpression.Compile()).Select(d => d.Name).ToArray(); + Assert.Equal(new[] { "B" }, eqRes); + + // != + var ne = EntityQueryCompiler.Compile("v != \"1.2.3\"", schema, compileContext); + var neRes = data.Where((Func)ne.LambdaExpression.Compile()).Select(d => d.Name).ToArray(); + Assert.Equal(new[] { "A", "C" }, neRes); + + // < + var lt = EntityQueryCompiler.Compile("v < \"1.2.3\"", schema, compileContext); + var ltRes = data.Where((Func)lt.LambdaExpression.Compile()).Select(d => d.Name).ToArray(); + Assert.Equal(new[] { "A" }, ltRes); + + // <= + var lte = EntityQueryCompiler.Compile("v <= \"1.2.3\"", schema, compileContext); + var lteRes = data.Where((Func)lte.LambdaExpression.Compile()).Select(d => d.Name).ToArray(); + Assert.Equal(new[] { "A", "B" }, lteRes); + + // > + var gt = EntityQueryCompiler.Compile("v > \"1.2.3\"", schema, compileContext); + var gtRes = data.Where((Func)gt.LambdaExpression.Compile()).Select(d => d.Name).ToArray(); + Assert.Equal(new[] { "C" }, gtRes); + + // >= + var gte = EntityQueryCompiler.Compile("v >= \"1.2.3\"", schema, compileContext); + var gteRes = data.Where((Func)gte.LambdaExpression.Compile()).Select(d => d.Name).ToArray(); + Assert.Equal(new[] { "B", "C" }, gteRes); + } + + [Fact] + public void Binary_Version_Literal_On_Left_Uses_CustomConverter() + { + var schema = MakeSchemaWithVersionCustomConverter(); + var data = new[] { new WithVersion(new Version(1, 2, 3), "B"), new WithVersion(new Version(2, 0, 0), "C") }; + var compiled = EntityQueryCompiler.Compile("\"1.2.3\" <= v", schema, compileContext); + var res = data.Where((Func)compiled.LambdaExpression.Compile()).Select(d => d.Name).ToArray(); + Assert.Equal(new[] { "B", "C" }, res); + } + + [Fact] + public void Binary_NullableVersion_Handles_Nulls_Correctly() + { + var schema = SchemaBuilder.FromObject(); + schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + + var compiled = EntityQueryCompiler.Compile("v >= \"1.2.3\"", schema, compileContext); + var data = new[] { new WithNullableVersion(null, "N"), new WithNullableVersion(new Version(1, 2, 3), "B"), new WithNullableVersion(new Version(2, 0, 0), "C") }; + var res = data.Where((Func)compiled.LambdaExpression.Compile()).Select(x => x.Name).ToArray(); + Assert.Equal(new[] { "B", "C" }, res); + } + + [Fact] + public void Binary_Version_Invalid_Literal_Shows_Parser_Error() + { + var schema = MakeSchemaWithVersionCustomConverter(); + // Evaluate on a single row to trigger Version.Parse of the literal + var ex = Assert.ThrowsAny(() => + { + var compiled = EntityQueryCompiler.Compile("v >= \"not-a-version\"", schema, compileContext); + var pred = (Func)compiled.LambdaExpression.Compile(); + pred(new WithVersion(new Version(0, 0), "X")); + }); + Assert.Contains("Version", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + private static SchemaProvider MakeSchemaWithVersionCustomConverter() + { + var schema = SchemaBuilder.FromObject(); + schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + return schema; + } + + private class WithVersion + { + public WithVersion(Version v, string name) + { + V = v; + Name = name; + } + + public Version V { get; set; } + public string Name { get; set; } + } + + private class WithNullableVersion + { + public WithNullableVersion(Version? v, string name) + { + V = v; + Name = name; + } + + public Version? V { get; set; } + public string Name { get; set; } + } +} diff --git a/src/tests/EntityGraphQL.Tests/CustomConvertersWithBinaryAndIsAnyTests.cs b/src/tests/EntityGraphQL.Tests/CustomConvertersWithBinaryAndIsAnyTests.cs new file mode 100644 index 00000000..a8456eba --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/CustomConvertersWithBinaryAndIsAnyTests.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using EntityGraphQL.Compiler; +using EntityGraphQL.Compiler.EntityQuery; +using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Schema; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class CustomConvertersWithBinaryAndIsAnyTests +{ + private readonly EqlCompileContext compileContext = new(new CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null)); + + private class WithVersion + { + public WithVersion(Version v, string name) + { + V = v; + Name = name; + } + + public Version V { get; set; } + public string Name { get; set; } + } + + [Fact] + public void Binary_Version_Uses_CustomConverter_For_String_Literals() + { + var schema = SchemaBuilder.FromObject(); + schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + + var compiled = EntityQueryCompiler.Compile("v >= \"1.2.3\"", schema, compileContext); + var data = new List { new(new Version(1, 2, 2), "A"), new(new Version(1, 2, 3), "B"), new(new Version(2, 0, 0), "C") }; + + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Equal(2, res.Count); + Assert.Equal("B", res[0].Name); + Assert.Equal("C", res[1].Name); + } + + [Fact] + public void IsAny_On_String_And_Binary_On_Version_With_CustomConverter() + { + var schema = SchemaBuilder.FromObject(); + schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + + var compiled = EntityQueryCompiler.Compile("name.isAny([\"B\", \"C\"]) && v >= \"1.2.3\"", schema, compileContext, schema.MethodProvider); + var data = new List { new(new Version(1, 2, 2), "A"), new(new Version(1, 2, 3), "B"), new(new Version(2, 0, 0), "C"), new(new Version(3, 0, 0), "D") }; + + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Equal(2, res.Count); + Assert.Equal("B", res[0].Name); + Assert.Equal("C", res[1].Name); + } + + [Fact] + public void IsAny_On_String_And_Binary_On_Version_With_ToOnly_Converter() + { + var schema = SchemaBuilder.FromObject(); + // String isAny supported by default; add custom type converter for Version and also a to-only converter + schema.AddCustomTypeConverter( + (obj, _) => + { + return obj switch + { + Version v => v, + string s => Version.Parse(s), + _ => Version.Parse(obj!.ToString()!), + }; + } + ); + + var compiled = EntityQueryCompiler.Compile("name.isAny([\"Hit\"]) && v >= \"1.2.3\" ", schema, compileContext, schema.MethodProvider); + var data = new List { new(new Version(1, 2, 3), "Hit"), new(new Version(1, 2, 4), "Miss") }; + + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Single(res); + Assert.Equal("Hit", res[0].Name); + } + + [Fact] + public void Binary_Version_With_Variable_Converted_By_FromTo_Converter() + { + var schema = SchemaBuilder.FromObject(); + // Register a precise from-to converter for string -> Version (used for variables/JSON path) + schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + + // Build lambda that compares V >= $min using ExpressionUtil on the variable + var param = Expression.Parameter(typeof(WithVersion), "w"); + var left = Expression.Property(param, nameof(WithVersion.V)); + // simulate variable coming as string and converted by custom converters + object? variable = "1.2.3"; + var converted = ExpressionUtil.ConvertObjectType(variable, typeof(Version), schema); + var right = Expression.Constant((Version)converted!, typeof(Version)); + var body = Expression.GreaterThanOrEqual(left, right); + var lambda = Expression.Lambda>(body, param).Compile(); + + var data = new List { new(new Version(1, 2, 2), "A"), new(new Version(1, 2, 3), "B"), new(new Version(2, 0, 0), "C") }; + var res = data.Where(lambda).ToList(); + Assert.Equal(2, res.Count); + Assert.Equal("B", res[0].Name); + Assert.Equal("C", res[1].Name); + } + + [Fact] + public void Null_Variable_Is_Ignored_By_Custom_Converters_And_Returns_Null() + { + var schema = SchemaBuilder.FromObject(); + schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + + object? variable = null; + var converted = ExpressionUtil.ConvertObjectType(variable, typeof(Version), schema); + Assert.Null(converted); // Null bypasses custom converters and remains null + } + + [Fact] + public void Combining_Extensions_In_Single_Query_And_Execution_Path() + { + // Arrange + var schema = SchemaBuilder.FromObject(); + // Register custom converters for Version (from-to and to-only). This automatically enables isAny for Version + schema.AddCustomTypeConverter((obj, _) => obj is Version v ? v : Version.Parse(obj!.ToString()!)); + Assert.True(schema.MethodProvider.EntityTypeHasMethod(typeof(Version), "isAny")); + + // Query combines: string isAny + binary (uses literal parser) + var compiled = EntityQueryCompiler.Compile("name.isAny([\"B\", \"C\"]) && v >= \"1.2.3\"", schema, compileContext, schema.MethodProvider); + + var data = new List { new(new Version(1, 2, 2), "A"), new(new Version(1, 2, 3), "B"), new(new Version(2, 0, 0), "C"), new(new Version(3, 0, 0), "D") }; + + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Equal(new[] { "B", "C" }, res.Select(r => r.Name).ToArray()); + + // Additionally, validate custom converters on a Version list used similarly to isAny semantics + var versionSources = new[] { "1.2.3", "2.0.0" }; + var convertedList = (IEnumerable)ExpressionUtil.ConvertObjectType(versionSources, typeof(List), schema)!; + var versionSet = convertedList.Cast().ToHashSet(); + var resContains = data.Where(d => versionSet.Contains(d.V)).Select(d => d.Name).ToList(); + Assert.Equal(new[] { "B", "C" }, resContains); + } +} diff --git a/src/tests/EntityGraphQL.Tests/CustomTypeConvertersGenericTests.cs b/src/tests/EntityGraphQL.Tests/CustomTypeConvertersGenericTests.cs new file mode 100644 index 00000000..36357d2e --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/CustomTypeConvertersGenericTests.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Schema; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class CustomTypeConvertersGenericTests +{ + [Fact] + public void FromToConverter_Func_StringToUri() + { + var schema = new SchemaProvider(); + schema.AddCustomTypeConverter((s, _) => new Uri(s, UriKind.RelativeOrAbsolute)); + + var input = "https://example.com/a"; + var result = ExpressionUtil.ConvertObjectType(input, typeof(Uri), schema); + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(new Uri(input), (Uri)result!); + } + + [Fact] + public void FromToConverter_Try_StringToGuid() + { + var schema = new SchemaProvider(); + schema.AddCustomTypeConverter((string s, ISchemaProvider _, out Guid g) => Guid.TryParse(s, out g)); + + var input = Guid.NewGuid().ToString(); + var result = ExpressionUtil.ConvertObjectType(input, typeof(Guid), schema); + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(Guid.Parse(input), (Guid)result!); + } + + [Fact] + public void ToTypeConverter_Func_ObjectToUri() + { + var schema = new SchemaProvider(); + schema.AddCustomTypeConverter( + (obj, _) => + { + return obj switch + { + string s => new Uri(s, UriKind.RelativeOrAbsolute), + Uri u => u, + _ => new Uri(obj!.ToString()!, UriKind.RelativeOrAbsolute), + }; + } + ); + + var input = "https://example.com/b"; + var result = ExpressionUtil.ConvertObjectType(input, typeof(Uri), schema); + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(new Uri(input), (Uri)result!); + } + + [Fact] + public void FromConverter_Func_StringSources() + { + var schema = new SchemaProvider(); + // from-only: string => toType can be Uri or Version + schema.AddCustomTypeConverter( + (s, to, _) => + { + if (to == typeof(Uri)) + return new Uri(s, UriKind.RelativeOrAbsolute); + if (to == typeof(Version)) + return Version.Parse(s); + return s; // no-op for other targets + } + ); + + var input1 = "https://example.com/c"; + var r1 = ExpressionUtil.ConvertObjectType(input1, typeof(Uri), schema); + Assert.IsType(r1); + + var input2 = "1.2.3.4"; + var r2 = ExpressionUtil.ConvertObjectType(input2, typeof(Version), schema); + Assert.IsType(r2); + } + + [Fact] + public void FromConverter_Try_StringSources() + { + var schema = new SchemaProvider(); + schema.AddCustomTypeConverter( + (string s, Type to, ISchemaProvider _, out object? result) => + { + if (to == typeof(int) && int.TryParse(s, out var i)) + { + result = i; + return true; + } + else if (to == typeof(Guid) && Guid.TryParse(s, out var g)) + { + result = g; + return true; + } + result = null; + return false; + } + ); + + var guidStr = Guid.NewGuid().ToString(); + var r1 = ExpressionUtil.ConvertObjectType(guidStr, typeof(Guid), schema); + Assert.IsType(r1); + + var r2 = ExpressionUtil.ConvertObjectType("123", typeof(int), schema); + Assert.Equal(123, r2); + } + + [Fact] + public void Converters_Array_To_List_And_Vice_Versa_Are_Converted() + { + var schema = new SchemaProvider(); + // Register conversions for string -> Version + schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + // Also support to-only for Version passthrough to help nested conversion + schema.AddCustomTypeConverter((obj, _) => obj is Version v ? v : Version.Parse(obj!.ToString()!)); + + // string[] -> List + var arr = new[] { "1.2.3", "2.0.0" }; + var listResult = ExpressionUtil.ConvertObjectType(arr, typeof(List), schema); + Assert.NotNull(listResult); + var versionList = Assert.IsType>(listResult); + Assert.Equal(new Version(1, 2, 3), versionList[0]); + Assert.Equal(new Version(2, 0, 0), versionList[1]); + + // List -> Version[] + var strList = new List { "3.0.0", "3.1.0" }; + var arrayResult = ExpressionUtil.ConvertObjectType(strList, typeof(Version[]), schema); + Assert.NotNull(arrayResult); + var versionArray = Assert.IsType(arrayResult); + Assert.Equal(new Version(3, 0, 0), versionArray[0]); + Assert.Equal(new Version(3, 1, 0), versionArray[1]); + } + + [Fact] + public void Converters_String_To_NullableInt_And_Null_Input_Remains_Null() + { + var schema = new SchemaProvider(); + schema.AddCustomTypeConverter( + (string s, Type to, ISchemaProvider _, out object? result) => + { + if ((to == typeof(int) || to == typeof(int?)) && int.TryParse(s, out var i)) + { + result = i; + return true; + } + result = null; + return false; + } + ); + + var r1 = ExpressionUtil.ConvertObjectType("123", typeof(int?), schema); + Assert.NotNull(r1); + Assert.Equal(123, Assert.IsType(r1)); + + object? input = null; + var r2 = ExpressionUtil.ConvertObjectType(input, typeof(int?), schema); + Assert.Null(r2); + } + + private enum MyColor + { + Red = 1, + Green = 2, + Blue = 3, + } + + [Fact] + public void Converters_String_To_Enum_Succeeds_And_Invalid_Fails() + { + var schema = new SchemaProvider(); + schema.AddCustomTypeConverter( + (s, _) => + { + if (Enum.TryParse(s, ignoreCase: true, out var val)) + return val; + throw new ArgumentException($"Invalid enum value '{s}' for {typeof(MyColor).Name}"); + } + ); + + var ok = ExpressionUtil.ConvertObjectType("Green", typeof(MyColor), schema); + Assert.Equal(MyColor.Green, Assert.IsType(ok)); + + var ex = Assert.ThrowsAny(() => ExpressionUtil.ConvertObjectType("NotAColor", typeof(MyColor), schema)); + Assert.Contains("Invalid enum value", ex.Message); + } +} diff --git a/src/tests/EntityGraphQL.Tests/CustomTypeConvertersGenericTestsCombinations.cs b/src/tests/EntityGraphQL.Tests/CustomTypeConvertersGenericTestsCombinations.cs new file mode 100644 index 00000000..c8b6cbc1 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/CustomTypeConvertersGenericTestsCombinations.cs @@ -0,0 +1,108 @@ +using System; +using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Schema; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class CustomTypeConvertersGenericTestsCombinations +{ + [Fact] + public void Precedence_FromTo_Wins_Over_ToOnly() + { + var schema = new SchemaProvider(); + var toOnlyCalled = false; + + // to-only converter for Uri (should not be hit) + TypeConverterTryTo toOnlyUri = (object? obj, Type to, ISchemaProvider s, out Uri result) => + { + toOnlyCalled = true; + result = new Uri("toonly:" + (obj?.ToString() ?? "null"), UriKind.RelativeOrAbsolute); + return true; + }; + schema.AddCustomTypeConverter(toOnlyUri); + + // from-to converter for string->Uri (should win) + schema.AddCustomTypeConverter((s, _) => + new Uri("fromto:" + s, UriKind.RelativeOrAbsolute)); + + const string input = "abc"; + var output = ExpressionUtil.ConvertObjectType(input, typeof(Uri), schema); + + Assert.IsType(output); + Assert.Equal(new Uri("fromto:abc", UriKind.RelativeOrAbsolute), (Uri)output); + Assert.False(toOnlyCalled); + } + + [Fact] + public void Precedence_ToOnly_Wins_Over_FromOnly() + { + var schema = new SchemaProvider(); + var toOnlyCalled = false; + var fromOnlyCalled = false; + + // from-only for string (should not be hit if to-only matches) + schema.AddCustomTypeConverter((string s, Type to, ISchemaProvider _, out object? result) => + { + fromOnlyCalled = true; + if (to == typeof(Uri)) + { + result = new Uri("fromonly:" + s, UriKind.RelativeOrAbsolute); + return true; + } + result = null; + return false; + }); + + // to-only for Uri (should win) + schema.AddCustomTypeConverter((object? obj, Type to, ISchemaProvider _, out Uri result) => + { + toOnlyCalled = true; + result = obj as Uri ?? new Uri("toonly:" + (obj?.ToString() ?? "null"), UriKind.RelativeOrAbsolute); + return true; + }); + + var output = ExpressionUtil.ConvertObjectType("zzz", typeof(Uri), schema); + Assert.IsType(output); + Assert.True(toOnlyCalled); + Assert.False(fromOnlyCalled); + Assert.Equal(new Uri("toonly:zzz", UriKind.RelativeOrAbsolute), (Uri)output); + } + + [Fact] + public void Null_Input_With_Multiple_Converters_Returns_Null_And_Does_Not_Invoke_Converters() + { + var schema = new SchemaProvider(); + var fromOnlyCalled = false; + var toOnlyCalled = false; + var fromToCalled = false; + + // Register various kinds + schema.AddCustomTypeConverter((s, _) => + { + fromToCalled = true; + return 1; + }); + + TypeConverterTryTo toOnlyInt = (object? obj, Type to, ISchemaProvider _, out int result) => + { + toOnlyCalled = true; result = 2; + return true; + }; + schema.AddCustomTypeConverter(toOnlyInt); + + schema.AddCustomTypeConverter((string s, Type to, ISchemaProvider _, out object? result) => + { + fromOnlyCalled = true; + result = 3; + return true; + }); + + object? input = null; + var output = ExpressionUtil.ConvertObjectType(input, typeof(int), schema); + Assert.Null(output); + Assert.False(fromToCalled); + Assert.False(toOnlyCalled); + Assert.False(fromOnlyCalled); + } +} diff --git a/src/tests/EntityGraphQL.Tests/CustomTypeConvertersGenericTestsNullCases.cs b/src/tests/EntityGraphQL.Tests/CustomTypeConvertersGenericTestsNullCases.cs new file mode 100644 index 00000000..fa3a1260 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/CustomTypeConvertersGenericTestsNullCases.cs @@ -0,0 +1,22 @@ +using System; +using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Schema; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class CustomTypeConvertersGenericTestsNullCases +{ + [Fact] + public void NullValue_BypassesCustomConverters_AndReturnsNull() + { + var schema = new SchemaProvider(); + bool fromToCalled = false; + schema.AddCustomTypeConverter((s, _) => { fromToCalled = true; return new Uri(s, UriKind.RelativeOrAbsolute); }); + + object? input = null; + var result = ExpressionUtil.ConvertObjectType(input, typeof(Uri), schema); + Assert.Null(result); + Assert.False(fromToCalled); + } +} diff --git a/src/tests/EntityGraphQL.Tests/DateAndTimeScalarsTests.cs b/src/tests/EntityGraphQL.Tests/DateAndTimeScalarsTests.cs new file mode 100644 index 00000000..81fe3b61 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/DateAndTimeScalarsTests.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EntityGraphQL.Compiler; +using EntityGraphQL.Compiler.EntityQuery; +using EntityGraphQL.Schema; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class DateAndTimeScalarsTests +{ + private readonly EqlCompileContext compileContext = new(new CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null)); + +#if NET8_0_OR_GREATER + private class WithDateOnly + { + public WithDateOnly(DateOnly d, string name) + { + D = d; + Name = name; + } + + public DateOnly D { get; set; } + public string Name { get; set; } + } + + [Theory] + [InlineData("\"2020-08-11\"")] + public void EntityQuery_WorksWithDateOnly(string dateValue) + { + var schemaProvider = SchemaBuilder.FromObject(); + var compiled = EntityQueryCompiler.Compile($"d >= {dateValue}", schemaProvider, compileContext); + var list = new List { new(new DateOnly(2020, 08, 10), "First"), new(new DateOnly(2020, 08, 11), "Second"), new(new DateOnly(2020, 08, 12), "Third") }; + var res = list.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Equal(2, res.Count); + Assert.Equal("Second", res[0].Name); + Assert.Equal("Third", res[1].Name); + } + + private class WithTimeOnly + { + public WithTimeOnly(TimeOnly t, string name) + { + T = t; + Name = name; + } + + public TimeOnly T { get; set; } + public string Name { get; set; } + } + + [Theory] + [InlineData("\"13:22:11\"", 2)] + [InlineData("\"13:22:11.3000003\"", 1)] + public void EntityQuery_WorksWithTimeOnly(string timeValue, int expectedCount) + { + var schemaProvider = SchemaBuilder.FromObject(); + var compiled = EntityQueryCompiler.Compile($"t >= {timeValue}", schemaProvider, compileContext); + var list = new List { new(new TimeOnly(13, 21, 11), "First"), new(new TimeOnly(13, 22, 11), "Second"), new(new TimeOnly(13, 23, 11), "Third") }; + var res = list.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Equal(expectedCount, res.Count); + Assert.Equal(res.Last().Name, expectedCount == 2 ? "Third" : "Third"); + } +#endif + + private class WithTimeSpan + { + public WithTimeSpan(TimeSpan span, string name) + { + Span = span; + Name = name; + } + + public TimeSpan Span { get; set; } + public string Name { get; set; } + } + + [Theory] + [InlineData("\"01:02:03\"", 2)] + [InlineData("\"00:00:00\"", 3)] + public void EntityQuery_WorksWithTimeSpan(string spanValue, int expectedCount) + { + var schemaProvider = SchemaBuilder.FromObject(); + var compiled = EntityQueryCompiler.Compile($"span >= {spanValue}", schemaProvider, compileContext); + var list = new List { new(TimeSpan.FromHours(1), "First"), new(new TimeSpan(1, 2, 3), "Second"), new(new TimeSpan(3, 0, 0), "Third") }; + var res = list.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Equal(expectedCount, res.Count); + } + +#if NET8_0_OR_GREATER + private class MutationContext { } + + private class EchoTypes + { + [GraphQLMutation] + public EchoResult Echo(DateOnly d, TimeOnly t, TimeSpan span) + { + return new EchoResult + { + D = d, + T = t, + Span = span, + }; + } + } + + private class EchoResult + { + public DateOnly D { get; set; } + public TimeOnly T { get; set; } + public TimeSpan Span { get; set; } + } + + [Fact] + public void Mutation_Deserializes_DateOnly_TimeOnly_TimeSpan() + { + var schema = new SchemaProvider(); + schema.AddType(nameof(EchoResult), null).AddAllFields(); + schema.AddMutationsFrom(new SchemaBuilderOptions { AutoCreateInputTypes = true }); + + var req = new QueryRequest { Query = @"mutation m { echo(d: ""2020-08-11"", t: ""13:22:11"", span: ""01:02:03"") { d t span } }" }; + + var result = schema.ExecuteRequestWithContext(req, new MutationContext(), null, null); + Assert.Null(result.Errors); + } +#endif +} diff --git a/src/tests/EntityGraphQL.Tests/EntityGraphQL.Tests.csproj b/src/tests/EntityGraphQL.Tests/EntityGraphQL.Tests.csproj index 4bde308f..1ee1f828 100755 --- a/src/tests/EntityGraphQL.Tests/EntityGraphQL.Tests.csproj +++ b/src/tests/EntityGraphQL.Tests/EntityGraphQL.Tests.csproj @@ -1,24 +1,20 @@  - Tests for EntityGraphQL - net9.0 + net8.0;net9.0;net10.0 true false enable - - - - - - - + + + + + - diff --git a/src/tests/EntityGraphQL.Tests/EntityQuery/EntityQueryCompilerTests.cs b/src/tests/EntityGraphQL.Tests/EntityQuery/EntityQueryCompilerTests.cs index b6a8aef6..8101c008 100644 --- a/src/tests/EntityGraphQL.Tests/EntityQuery/EntityQueryCompilerTests.cs +++ b/src/tests/EntityGraphQL.Tests/EntityQuery/EntityQueryCompilerTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Linq.Expressions; using EntityGraphQL.Compiler.EntityQuery.Grammar; @@ -13,75 +14,75 @@ namespace EntityGraphQL.Compiler.EntityQuery.Tests; /// public class EntityQueryCompilerTests { - private readonly ExecutionOptions executionOptions = new(); + private readonly EqlCompileContext compileContext = new(new CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null)); [Fact] public void CompilesNumberConstant() { - var exp = EntityQueryCompiler.Compile("3", executionOptions); + var exp = EntityQueryCompiler.Compile("3", compileContext); Assert.Equal((long)3, exp.Execute()); } [Fact] public void CompilesNegativeNumberConstant() { - var exp = EntityQueryCompiler.Compile("-43", executionOptions); + var exp = EntityQueryCompiler.Compile("-43", compileContext); Assert.Equal((long)-43, exp.Execute()); } [Fact] public void CompilesNumberDecimalConstant() { - var exp = EntityQueryCompiler.Compile("23.3", executionOptions); + var exp = EntityQueryCompiler.Compile("23.3", compileContext); Assert.Equal(23.3m, exp.Execute()); } [Fact] public void CompilesNullConstant() { - var exp = EntityQueryCompiler.Compile("null", executionOptions); + var exp = EntityQueryCompiler.Compile("null", compileContext); Assert.Null(exp.Execute()); } [Fact] public void CompilesTrueConstant() { - var exp = EntityQueryCompiler.Compile("true", executionOptions); + var exp = EntityQueryCompiler.Compile("true", compileContext); Assert.True((bool)exp.Execute()!); } [Fact] public void CompilesFalseConstant() { - var exp = EntityQueryCompiler.Compile("false", executionOptions); + var exp = EntityQueryCompiler.Compile("false", compileContext); Assert.False((bool)exp.Execute()!); } [Fact] public void CompilesStringConstant() { - var exp = EntityQueryCompiler.Compile("\"Hello there_987-%#& ;;s\"", executionOptions); + var exp = EntityQueryCompiler.Compile("\"Hello there_987-%#& ;;s\"", compileContext); Assert.Equal("Hello there_987-%#& ;;s", exp.Execute()); } [Fact] public void CompilesStringConstant2() { - var exp = EntityQueryCompiler.Compile("\"\\\"Hello\\\" there\"", executionOptions); + var exp = EntityQueryCompiler.Compile("\"\\\"Hello\\\" there\"", compileContext); Assert.Equal("\"Hello\" there", exp.Execute()); } [Fact] public void CompilesStringConstant3() { - var exp = EntityQueryCompiler.Compile("\" \\\"\\n\\r\\0\\a\\b\\f\\t\\v \"", executionOptions); + var exp = EntityQueryCompiler.Compile("\" \\\"\\n\\r\\0\\a\\b\\f\\t\\v \"", compileContext); Assert.Equal(" \"\n\r\0\a\b\f\t\v ", exp.Execute()); } [Fact] public void CompilesIdentityCall() { - var exp = EntityQueryCompiler.Compile("hello", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("hello", SchemaBuilder.FromObject(), compileContext); Assert.Equal("returned value", exp.Execute(new TestSchema())); } @@ -89,63 +90,63 @@ public void CompilesIdentityCall() public void CompilesIdentityCallWithKeyWord() { // null is the keyword - var exp = EntityQueryCompiler.Compile("nullableInt", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("nullableInt", SchemaBuilder.FromObject(), compileContext); Assert.Equal(55, exp.Execute(new TestSchema())); } [Fact] public void FailsIdentityNotThere() { - var ex = Assert.Throws(() => EntityQueryCompiler.Compile("wrongField", SchemaBuilder.FromObject(), executionOptions)); + var ex = Assert.Throws(() => EntityQueryCompiler.Compile("wrongField", SchemaBuilder.FromObject(), compileContext)); Assert.Equal("Field 'wrongField' not found on type 'Query'", ex.Message); } [Fact] public void CompilesIdentityCallFullPath() { - var exp = EntityQueryCompiler.Compile("someRelation.field1", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("someRelation.field1", SchemaBuilder.FromObject(), compileContext); Assert.Equal(2, exp.Execute(new TestSchema())); } [Fact] public void CompilesIdentityCallFullPathDeep() { - var exp = EntityQueryCompiler.Compile("someRelation.relation.id", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("someRelation.relation.id", SchemaBuilder.FromObject(), compileContext); Assert.Equal(99, exp.Execute(new TestSchema())); } [Fact] public void CompilesBinaryExpressionEquals() { - var exp = EntityQueryCompiler.Compile("someRelation.relation.id == 99", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("someRelation.relation.id == 99", SchemaBuilder.FromObject(), compileContext); Assert.True((bool)exp.Execute(new TestSchema())!); } [Fact] public void CompilesBinaryExpressionPlus() { - var exp = EntityQueryCompiler.Compile("someRelation.relation.id + 99", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("someRelation.relation.id + 99", SchemaBuilder.FromObject(), compileContext); Assert.Equal(198, exp.Execute(new TestSchema())); } [Fact] public void CompilesBinaryExpressionEqualsRoot() { - var exp = EntityQueryCompiler.Compile("num == 34", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("num == 34", SchemaBuilder.FromObject(), compileContext); Assert.False((bool)exp.Execute(new TestSchema())!); } [Fact] public void CompilesBinaryExpressionEqualsAndAddRoot() { - var exp = EntityQueryCompiler.Compile("num == (90 - 57)", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("num == (90 - 57)", SchemaBuilder.FromObject(), compileContext); Assert.True((bool)exp.Execute(new TestSchema())!); } [Fact] public void CompilesBinaryExpressionEqualsAndAdd() { - var exp = EntityQueryCompiler.Compile("someRelation.relation.id == (99 - 32)", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("someRelation.relation.id == (99 - 32)", SchemaBuilder.FromObject(), compileContext); Assert.False((bool)exp.Execute(new TestSchema())!); } @@ -153,9 +154,7 @@ public void CompilesBinaryExpressionEqualsAndAdd() public void FailsIfThenElseInlineNoBrackets() { // no brackets so it reads it as someRelation.relation.id == (99 ? 'wooh' : 66) and fails as 99 is not a bool - var ex = Assert.Throws(() => - EntityQueryCompiler.Compile("someRelation.relation.id == 99 ? \"wooh\" : 66", SchemaBuilder.FromObject(), executionOptions) - ); + var ex = Assert.Throws(() => EntityQueryCompiler.Compile("someRelation.relation.id == 99 ? \"wooh\" : 66", SchemaBuilder.FromObject(), compileContext)); Assert.Equal("Conditional result types mismatch. Types 'String' and 'Int64' must be the same.", ex.Message); } @@ -163,7 +162,7 @@ public void FailsIfThenElseInlineNoBrackets() public void CompilesIfThenElseInlineTrueBrackets() { // tells it how to read it - var exp = EntityQueryCompiler.Compile("(someRelation.relation.id == 99) ? 100 : 66", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("(someRelation.relation.id == 99) ? 100 : 66", SchemaBuilder.FromObject(), compileContext); Assert.Equal((long)100, exp.Execute(new TestSchema())); } @@ -171,49 +170,49 @@ public void CompilesIfThenElseInlineTrueBrackets() public void CompilesIfThenElseInlineFalseBrackets() { // tells it how to read it - var exp = EntityQueryCompiler.Compile("(someRelation.relation.id == 98) ? 100 : 66", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("(someRelation.relation.id == 98) ? 100 : 66", SchemaBuilder.FromObject(), compileContext); Assert.Equal((long)66, exp.Execute(new TestSchema())); } [Fact] public void CompilesIfThenElseTrue() { - var exp = EntityQueryCompiler.Compile("if someRelation.relation.id == 99 then 100 else 66", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("if someRelation.relation.id == 99 then 100 else 66", SchemaBuilder.FromObject(), compileContext); Assert.Equal((long)100, exp.Execute(new TestSchema())); } [Fact] public void CompilesIfThenElseFalse() { - var exp = EntityQueryCompiler.Compile("if someRelation.relation.id == 33 then 100 else 66", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("if someRelation.relation.id == 33 then 100 else 66", SchemaBuilder.FromObject(), compileContext); Assert.Equal((long)66, exp.Execute(new TestSchema())); } [Fact] public void CompilesBinaryWithIntAndUint() { - var exp = EntityQueryCompiler.Compile("if someRelation.unisgnedInt == 33 then 100 else 66", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("if someRelation.unisgnedInt == 33 then 100 else 66", SchemaBuilder.FromObject(), compileContext); Assert.Equal((long)66, exp.Execute(new TestSchema())); } [Fact] public void CompilesBinaryWithNullableAndNonNullable() { - var exp = EntityQueryCompiler.Compile("if someRelation.nullableInt == 8 then 100 else 66", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("if someRelation.nullableInt == 8 then 100 else 66", SchemaBuilder.FromObject(), compileContext); Assert.Equal((long)100, exp.Execute(new TestSchema())); } [Fact] public void CompilesBinaryAnd() { - var exp = EntityQueryCompiler.Compile("(someRelation.nullableInt == 9) && (hello == \"Hi\")", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("(someRelation.nullableInt == 9) && (hello == \"Hi\")", SchemaBuilder.FromObject(), compileContext); Assert.Equal(false, exp.Execute(new TestSchema())); } [Fact] public void CanUseCompiledExpressionInWhereMethod() { - var exp = EntityQueryCompiler.Compile("name == \"Bob\"", SchemaBuilder.FromObject(), executionOptions); + var exp = EntityQueryCompiler.Compile("name == \"Bob\"", SchemaBuilder.FromObject(), compileContext); var objects = new List { new TestEntity("Sally"), new TestEntity("Bob") }; Assert.Equal(2, objects.Count); var results = objects.Where((Func)exp.LambdaExpression.Compile()); @@ -225,7 +224,7 @@ public void CanUseCompiledExpressionInWhereMethod() public void TestEntityQueryWorks() { var schemaProvider = SchemaBuilder.FromObject(); - var compiledResult = EntityQueryCompiler.Compile("(relation.id == 1) || (relation.id == 2)", schemaProvider, executionOptions); + var compiledResult = EntityQueryCompiler.Compile("(relation.id == 1) || (relation.id == 2)", schemaProvider, compileContext); var list = new List { new TestEntity("bob") { Relation = new Person { Id = 1 } }, @@ -247,8 +246,7 @@ public void TestEntityQueryWorks() public void TestEntityQueryWorksWithDates(string dateValue) { var schemaProvider = SchemaBuilder.FromObject(); - schemaProvider.AddType("DateTime"); //<-- Tried with and without - var compiledResult = EntityQueryCompiler.Compile($"when >= {dateValue}", schemaProvider, executionOptions); + var compiledResult = EntityQueryCompiler.Compile($"when >= {dateValue}", schemaProvider, compileContext); var list = new List { new("First") { When = new DateTime(2020, 08, 10) }, @@ -271,8 +269,7 @@ public void TestEntityQueryWorksWithDates(string dateValue) public void TestEntityQueryFailsOnInvalidDate(string dateValue) { var schemaProvider = SchemaBuilder.FromObject(); - schemaProvider.AddType("DateTime"); //<-- Tried with and without - var compiledResult = EntityQueryCompiler.Compile($"when >= {dateValue}", schemaProvider, executionOptions); + var compiledResult = EntityQueryCompiler.Compile($"when >= {dateValue}", schemaProvider, compileContext); var list = new List { new Entry("First") { When = new DateTime(2020, 08, 10) } }; Assert.Single(list); var results = list.Where((Func)compiledResult.LambdaExpression.Compile()); @@ -286,16 +283,15 @@ public void TestEntityQueryFailsOnInvalidDate(string dateValue) [InlineData("\"2020-08-11 13:22:11\"")] [InlineData("\"2020-08-11 13:22:11.1\"")] [InlineData("\"2020-08-11 13:22:11.3000003\"")] - [InlineData("\"2020-08-11 13:22:11.3000003+000\"")] public void TestEntityQueryWorksWithDateTimes(string dateValue) { var schemaProvider = SchemaBuilder.FromObject(); - var compiledResult = EntityQueryCompiler.Compile($"when >= {dateValue}", schemaProvider, executionOptions); + var compiledResult = EntityQueryCompiler.Compile($"when >= {dateValue}", schemaProvider, compileContext); var list = new List { - new("First") { When = new DateTime(2020, 08, 10, 0, 0, 0) }, - new("Second") { When = new DateTime(2020, 08, 11, 13, 21, 11) }, - new("Third") { When = new DateTime(2020, 08, 12, 13, 22, 11) }, + new("First") { When = new DateTime(2020, 08, 10, 0, 0, 0, DateTimeKind.Unspecified) }, + new("Second") { When = new DateTime(2020, 08, 11, 13, 21, 11, DateTimeKind.Unspecified) }, + new("Third") { When = new DateTime(2020, 08, 12, 13, 22, 11, DateTimeKind.Unspecified) }, }; Assert.Equal(3, list.Count); var results = list.Where((Func)compiledResult.LambdaExpression.Compile()); @@ -304,6 +300,40 @@ public void TestEntityQueryWorksWithDateTimes(string dateValue) Assert.Equal("Third", results.ElementAt(0).Message); } + [Theory] + [InlineData("\"2020-08-11 13:22:11.3000003+0000\"")] + [InlineData("\"2020-08-11T13:22:11+0000\"")] + public void TestEntityQueryWorksWithDateTimesWithOffset(string dateValue) + { + var schemaProvider = SchemaBuilder.FromObject(); + var compiledResult = EntityQueryCompiler.Compile($"when >= {dateValue}", schemaProvider, compileContext); + + // When parsing a string with an offset, DateTime.Parse converts it to local time + // The parsed value will be: UTC time from the string, converted to local timezone + var parsedUtcTime = DateTimeOffset.Parse(dateValue.Trim('"'), CultureInfo.InvariantCulture); + var parsedAsLocalTime = parsedUtcTime.LocalDateTime; + + var list = new List + { + new("First") { When = new DateTime(2020, 08, 10, 0, 0, 0, DateTimeKind.Unspecified) }, + new("Second") { When = new DateTime(2020, 08, 11, 13, 21, 11, DateTimeKind.Unspecified) }, + new("Third") { When = new DateTime(2020, 08, 12, 13, 22, 11, DateTimeKind.Unspecified) }, + }; + + var results = list.Where((Func)compiledResult.LambdaExpression.Compile()).ToList(); + + // The expected count depends on what the parsed local time is compared to the test data + // In UTC timezone: "2020-08-11 13:22:11+0000" stays as 13:22:11, matches only Third + // In NY (UTC-4/5): "2020-08-11 13:22:11+0000" becomes 09:22:11 or 08:22:11, matches both Second and Third + var expectedMatches = list.Where(e => e.When >= parsedAsLocalTime).ToList(); + + Assert.Equal(expectedMatches.Count, results.Count); + for (int i = 0; i < expectedMatches.Count; i++) + { + Assert.Equal(expectedMatches[i].Message, results[i].Message); + } + } + [Theory] // If is missing, its default value is the offset of the local time zone. so running them locally will fail [InlineData("\"2020-08-11T13:22:11+0000\"", 1)] @@ -311,7 +341,7 @@ public void TestEntityQueryWorksWithDateTimes(string dateValue) public void TestEntityQueryWorksWithDateTimeOffsets(string dateValue, int count) { var schemaProvider = SchemaBuilder.FromObject(); - var compiledResult = EntityQueryCompiler.Compile($"whenOffset >= {dateValue}", schemaProvider, executionOptions); + var compiledResult = EntityQueryCompiler.Compile($"whenOffset >= {dateValue}", schemaProvider, compileContext); var list = new List { new("First") { WhenOffset = new DateTimeOffset(2020, 08, 10, 0, 0, 0, TimeSpan.FromTicks(0)) }, @@ -332,14 +362,7 @@ public void CompilesEnumSimple() var schema = SchemaBuilder.FromObject(); var param = Expression.Parameter(typeof(Person)); - var exp = EntityQueryParser.Instance.Parse( - "gender == Female", - param, - schema, - new QueryRequestContext(null, null), - new EqlMethodProvider(), - new CompileContext(executionOptions, null, new QueryRequestContext(null, null)) - ); + var exp = EntityQueryParser.Instance.Parse("gender == Female", param, schema, new QueryRequestContext(null, null), new EqlMethodProvider(), compileContext); var res = (bool?)Expression.Lambda(exp, param).Compile().DynamicInvoke(new Person { Gender = Gender.Female }); Assert.NotNull(res); @@ -350,7 +373,7 @@ public void CompilesEnumSimple() public void CompilesEnum() { var schema = SchemaBuilder.FromObject(); - var exp = EntityQueryCompiler.Compile("people.where(gender == Female)", schema, executionOptions); + var exp = EntityQueryCompiler.Compile("people.where(gender == Female)", schema, compileContext); var res = (IEnumerable?)exp.Execute(new TestSchema()); Assert.NotNull(res); Assert.Empty(res); @@ -360,7 +383,7 @@ public void CompilesEnum() public void CompilesEnum2() { var schema = SchemaBuilder.FromObject(); - var exp = EntityQueryCompiler.Compile("people.where(gender == Other)", schema, executionOptions); + var exp = EntityQueryCompiler.Compile("people.where(gender == Other)", schema, compileContext); var res = (IEnumerable?) exp.Execute( new TestSchema @@ -381,7 +404,7 @@ public void CompilesEnum2() public void CompilesEnum3() { var schema = SchemaBuilder.FromObject(); - var exp = EntityQueryCompiler.Compile("people.where(gender == Gender.Other)", schema, executionOptions); + var exp = EntityQueryCompiler.Compile("people.where(gender == Gender.Other)", schema, compileContext); var res = (IEnumerable?) exp.Execute( new TestSchema @@ -404,9 +427,9 @@ public void CompilesEnum4() var schema = SchemaBuilder.FromObject(); schema.AddEnum("Size", typeof(Size), ""); - Assert.Throws(() => + Assert.Throws(() => { - EntityQueryCompiler.Compile("people.where(gender == Size.Other)", schema, executionOptions); + EntityQueryCompiler.Compile("people.where(gender == Size.Other)", schema, compileContext); }); } @@ -415,7 +438,7 @@ public void CompilesConstantArray() { var schema = SchemaBuilder.FromObject(); - var res = EntityQueryCompiler.Compile("[1, 4,5]", schema, executionOptions).Execute(new TestSchema()); + var res = EntityQueryCompiler.Compile("[1, 4,5]", schema, compileContext).Execute(new TestSchema()); Assert.Collection((IEnumerable)res!, i => Assert.Equal(1, i), i => Assert.Equal(4, i), i => Assert.Equal(5, i)); } @@ -424,7 +447,7 @@ public void CompilesConstantArrayString() { var schema = SchemaBuilder.FromObject(); - var res = EntityQueryCompiler.Compile("[\"Hi\", \"World\"]", schema, executionOptions).Execute(new TestSchema()); + var res = EntityQueryCompiler.Compile("[\"Hi\", \"World\"]", schema, compileContext).Execute(new TestSchema()); Assert.Collection((IEnumerable)res!, i => Assert.Equal("Hi", i), i => Assert.Equal("World", i)); } diff --git a/src/tests/EntityGraphQL.Tests/EntityQuery/EntityQueryCompilerWithMappedSchemaTests.cs b/src/tests/EntityGraphQL.Tests/EntityQuery/EntityQueryCompilerWithMappedSchemaTests.cs index 183a1336..65d1880a 100644 --- a/src/tests/EntityGraphQL.Tests/EntityQuery/EntityQueryCompilerWithMappedSchemaTests.cs +++ b/src/tests/EntityGraphQL.Tests/EntityQuery/EntityQueryCompilerWithMappedSchemaTests.cs @@ -10,10 +10,12 @@ namespace EntityGraphQL.Compiler.EntityQuery.Tests; /// Tests that our compiler correctly compiles all the basic parts of our language against a given schema provider public class EntityQueryCompilerWithMappedSchemaTests { + private readonly CompileContext compileContext = new(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null); + [Fact] public void TestConversionToGuid() { - var exp = EntityQueryCompiler.Compile("people.where(guid == \"6492f5fe-0869-4279-88df-7f82f8e87a67\")", new TestObjectGraphSchema(), new ExecutionOptions()); + var exp = EntityQueryCompiler.Compile("people.where(guid == \"6492f5fe-0869-4279-88df-7f82f8e87a67\")", new TestObjectGraphSchema(), compileContext); dynamic result = exp.Execute(GetDataContext())!; Assert.Equal(1, Enumerable.Count(result)); } @@ -21,7 +23,7 @@ public void TestConversionToGuid() [Fact] public void CompilesIdentityCall() { - var exp = EntityQueryCompiler.Compile("people", new TestObjectGraphSchema(), new ExecutionOptions()); + var exp = EntityQueryCompiler.Compile("people", new TestObjectGraphSchema(), compileContext); dynamic result = exp.Execute(GetDataContext())!; Assert.Equal(1, Enumerable.Count(result)); } @@ -30,9 +32,9 @@ public void CompilesIdentityCall() public void CompilesIdentityCallFullPath() { var schema = new TestObjectGraphSchema(); - var exp = EntityQueryCompiler.Compile("privateProjects.where(id == 8).count()", schema, new ExecutionOptions()); + var exp = EntityQueryCompiler.Compile("privateProjects.where(id == 8).count()", schema, compileContext); Assert.Equal(0, exp.Execute(GetDataContext())); - var exp2 = EntityQueryCompiler.Compile("privateProjects.count()", schema, new ExecutionOptions()); + var exp2 = EntityQueryCompiler.Compile("privateProjects.count()", schema, compileContext); Assert.Equal(1, exp2.Execute(GetDataContext())); } @@ -40,28 +42,28 @@ public void CompilesIdentityCallFullPath() public void CompilesTypeBuiltFromObject() { // no brackets so it reads it as someRelation.relation.id = (99 ? 'wooh' : 66) and fails as 99 is not a bool - var exp = EntityQueryCompiler.Compile("defaultLocation.id == 10", new TestObjectGraphSchema(), new ExecutionOptions()); + var exp = EntityQueryCompiler.Compile("defaultLocation.id == 10", new TestObjectGraphSchema(), compileContext); Assert.True((bool)exp.Execute(GetDataContext())!); } [Fact] public void CompilesIfThenElseInlineFalseBrackets() { - var exp = EntityQueryCompiler.Compile("(publicProjects.Count(id == 90) == 1) ? \"Yes\" : \"No\"", new TestObjectGraphSchema(), new ExecutionOptions()); + var exp = EntityQueryCompiler.Compile("(publicProjects.Count(id == 90) == 1) ? \"Yes\" : \"No\"", new TestObjectGraphSchema(), compileContext); Assert.Equal("Yes", exp.Execute(GetDataContext())); } [Fact] public void CompilesIfThenElseTrue() { - var exp = EntityQueryCompiler.Compile("if publicProjects.Count() > 1 then \"Yes\" else \"No\"", new TestObjectGraphSchema(), new ExecutionOptions()); + var exp = EntityQueryCompiler.Compile("if publicProjects.Count() > 1 then \"Yes\" else \"No\"", new TestObjectGraphSchema(), compileContext); Assert.Equal("No", exp.Execute(GetDataContext())); } [Fact] public void CompilesAny() { - var exp = EntityQueryCompiler.Compile("people.any(id > 90)", new TestObjectGraphSchema(), new ExecutionOptions()); + var exp = EntityQueryCompiler.Compile("people.any(id > 90)", new TestObjectGraphSchema(), compileContext); dynamic data = exp.Execute(GetDataContext())!; Assert.Equal(false, data); } diff --git a/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderDefaultMethodTests.cs b/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderDefaultMethodTests.cs index b07e852d..29dff141 100644 --- a/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderDefaultMethodTests.cs +++ b/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderDefaultMethodTests.cs @@ -8,7 +8,7 @@ namespace EntityGraphQL.Compiler.EntityQuery.Tests; public class EqlMethodProviderDefaultMethodTests { - private readonly ExecutionOptions executionOptions = new(); + private readonly CompileContext compileContext = new(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null); [Fact] public void CompilesFirst() @@ -16,7 +16,7 @@ public void CompilesFirst() var exp = EntityQueryCompiler.Compile( @"people.first(guid == ""6492f5fe-0869-4279-88df-7f82f8e87a67"")", SchemaBuilder.FromObject(), - executionOptions, + compileContext, new EqlMethodProvider() ); var result = exp.Execute(new EqlMethodTestSchema()) as Person; @@ -27,7 +27,7 @@ public void CompilesFirst() [Fact] public void CompilesWhere() { - var exp = EntityQueryCompiler.Compile(@"people.where(name == ""bob"")", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile(@"people.where(name == ""bob"")", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(new EqlMethodTestSchema()) as IEnumerable; Assert.NotNull(result); Assert.Empty(result); @@ -36,7 +36,7 @@ public void CompilesWhere() [Fact] public void CompilesWhere2() { - var exp = EntityQueryCompiler.Compile(@"people.where(name == ""Luke"")", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile(@"people.where(name == ""Luke"")", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(new EqlMethodTestSchema()) as IEnumerable; Assert.NotNull(result); Assert.Single(result); @@ -45,8 +45,8 @@ public void CompilesWhere2() [Fact] public void FailsWhereNoParameter() { - var ex = Assert.Throws(() => - EntityQueryCompiler.Compile("people.where()", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()) + var ex = Assert.Throws(() => + EntityQueryCompiler.Compile("people.where()", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()) ); Assert.Equal("Method 'where' expects 1 argument(s) but 0 were supplied", ex.Message); } @@ -54,8 +54,8 @@ public void FailsWhereNoParameter() [Fact] public void FailsWhereWrongParameterType() { - var ex = Assert.Throws(() => - EntityQueryCompiler.Compile("people.where(name)", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()) + var ex = Assert.Throws(() => + EntityQueryCompiler.Compile("people.where(name)", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()) ); Assert.Equal("Method 'where' expects parameter that evaluates to a 'System.Boolean' result but found result type 'System.String'", ex.Message); } @@ -63,7 +63,7 @@ public void FailsWhereWrongParameterType() [Fact] public void CompilesFirstWithPredicate() { - var exp = EntityQueryCompiler.Compile(@"people.first(name == ""Luke"")", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile(@"people.first(name == ""Luke"")", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(new EqlMethodTestSchema()) as Person; Assert.NotNull(result); Assert.Equal("Luke", result.Name); @@ -72,7 +72,7 @@ public void CompilesFirstWithPredicate() [Fact] public void CompilesFirstNoPredicate() { - var exp = EntityQueryCompiler.Compile("people.first()", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile("people.first()", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(new EqlMethodTestSchema()) as Person; Assert.NotNull(result); Assert.Equal("Bob", result.Name); @@ -82,7 +82,7 @@ public void CompilesFirstNoPredicate() public void CompilesTake() { var context = new EqlMethodTestSchema(); - var exp = EntityQueryCompiler.Compile("people.take(1)", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile("people.take(1)", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(context) as IEnumerable; Assert.NotNull(result); Assert.Single(result); @@ -95,7 +95,7 @@ public void CompilesTake() public void CompilesSkip() { var context = new EqlMethodTestSchema(); - var exp = EntityQueryCompiler.Compile("people.Skip(1)", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile("people.Skip(1)", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(context) as IEnumerable; Assert.NotNull(result); Assert.Equal(3, result.Count()); @@ -107,7 +107,7 @@ public void CompilesSkip() [Fact] public void CompilesMethodsChained() { - var exp = EntityQueryCompiler.Compile("people.where(id == 9).take(2)", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile("people.where(id == 9).take(2)", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(new EqlMethodTestSchema()) as IEnumerable; Assert.NotNull(result); Assert.Equal(2, result.Count()); @@ -119,7 +119,7 @@ public void CompilesMethodsChained() [Fact] public void CompilesStringContains() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.contains(""ob""))", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile(@"people.where(name.contains(""ob""))", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(new EqlMethodTestSchema()) as IEnumerable; Assert.NotNull(result); Assert.Equal(3, result.Count()); @@ -131,7 +131,7 @@ public void CompilesStringContains() [Fact] public void CompilesStringStartsWith() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.startsWith(""Bo""))", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile(@"people.where(name.startsWith(""Bo""))", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(new EqlMethodTestSchema()) as IEnumerable; Assert.NotNull(result); Assert.Equal(2, result.Count()); @@ -142,7 +142,7 @@ public void CompilesStringStartsWith() [Fact] public void CompilesStringEndsWith() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.endsWith(""b""))", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile(@"people.where(name.endsWith(""b""))", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(new EqlMethodTestSchema()) as IEnumerable; Assert.NotNull(result); Assert.Single(result); @@ -152,7 +152,7 @@ public void CompilesStringEndsWith() [Fact] public void CompilesStringToLower() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.toLower() == ""bob"")", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile(@"people.where(name.toLower() == ""bob"")", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(new EqlMethodTestSchema()) as IEnumerable; Assert.NotNull(result); Assert.Single(result); @@ -162,7 +162,7 @@ public void CompilesStringToLower() [Fact] public void CompilesStringToUpper() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.toUpper() == ""BOB"")", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile(@"people.where(name.toUpper() == ""BOB"")", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var result = exp.Execute(new EqlMethodTestSchema()) as IEnumerable; Assert.NotNull(result); Assert.Single(result); @@ -175,7 +175,7 @@ public void CompilesAndConvertsStringToGuid() var exp = EntityQueryCompiler.Compile( @"people.where(guid == ""6492f5fe-0869-4279-88df-7f82f8e87a67"")", SchemaBuilder.FromObject(), - executionOptions, + compileContext, new EqlMethodProvider() ); var result = exp.Execute(new EqlMethodTestSchema()) as IEnumerable; @@ -187,7 +187,7 @@ public void CompilesAndConvertsStringToGuid() [Fact] public void SupportUseFilterIsAnyMethod() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.isAny([""Bob"", ""Robin""]))", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile(@"people.where(name.isAny([""Bob"", ""Robin""]))", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var data = new EqlMethodTestSchema(); var result = exp.Execute(data) as IEnumerable; Assert.True(data.People.Count() > 2); @@ -200,7 +200,7 @@ public void SupportUseFilterIsAnyMethod() [Fact] public void SupportUseFilterIsAnyMethodOnNullable() { - var exp = EntityQueryCompiler.Compile(@"people.where(age.isAny([99, 44]))", SchemaBuilder.FromObject(), executionOptions, new EqlMethodProvider()); + var exp = EntityQueryCompiler.Compile(@"people.where(age.isAny([99, 44]))", SchemaBuilder.FromObject(), compileContext, new EqlMethodProvider()); var data = new EqlMethodTestSchema(); Assert.Equal(4, data.People.Count()); var result = exp.Execute(data) as IEnumerable; diff --git a/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderTests.cs b/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderTests.cs index 4f9f1373..c2ca5a72 100644 --- a/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderTests.cs +++ b/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using EntityGraphQL.Compiler; using EntityGraphQL.Compiler.EntityQuery; using EntityGraphQL.Schema; using Xunit; @@ -10,6 +11,8 @@ namespace EntityGraphQL.Tests.EntityQuery; public class EqlMethodProviderTests { + private readonly CompileContext compileContext = new(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null); + [Fact] public void TestMethodRegistration() { @@ -155,7 +158,7 @@ public void EqlMethodProvider_Test_AddingStaticMethod() provider.RegisterMethod(typeof(string).GetMethod(nameof(string.Compare), [typeof(string), typeof(string)])!, typeof(string)); Assert.True(provider.EntityTypeHasMethod(typeof(string), "compare")); - var exp = EntityQueryCompiler.Compile(@"people.first(name.compare(""Bob"") == 0)", SchemaBuilder.FromObject(), new ExecutionOptions(), provider); + var exp = EntityQueryCompiler.Compile(@"people.first(name.compare(""Bob"") == 0)", SchemaBuilder.FromObject(), compileContext, provider); var result = exp.Execute(new EqlMethodTestSchema()) as Person; Assert.NotNull(result); Assert.Equal("Bob", result.Name); @@ -169,7 +172,7 @@ public void EqlMethodProvider_Test_AddingInstanceMethod() provider.RegisterMethod(typeof(Person).GetMethod(nameof(Person.GetFullName))!); Assert.True(provider.EntityTypeHasMethod(typeof(Person), "getFullName")); - var exp = EntityQueryCompiler.Compile(@"people.first(getFullName() == ""Robin Hood"")", SchemaBuilder.FromObject(), new ExecutionOptions(), provider); + var exp = EntityQueryCompiler.Compile(@"people.first(getFullName() == ""Robin Hood"")", SchemaBuilder.FromObject(), compileContext, provider); var result = exp.Execute(new EqlMethodTestSchema()) as Person; Assert.NotNull(result); Assert.Equal("Robin Hood", result.GetFullName()); @@ -192,14 +195,14 @@ public void EqlMethodProvider_Test_AddingCustomMakeCallFunc() } ); - var exp = EntityQueryCompiler.Compile(@"one.isOne()", SchemaBuilder.FromObject(), new ExecutionOptions(), provider); + var exp = EntityQueryCompiler.Compile(@"one.isOne()", SchemaBuilder.FromObject(), compileContext, provider); Assert.True(exp.Execute(new EqlMethodTestSchema()) as bool?); - exp = EntityQueryCompiler.Compile(@"notOne.isOne()", SchemaBuilder.FromObject(), new ExecutionOptions(), provider); + exp = EntityQueryCompiler.Compile(@"notOne.isOne()", SchemaBuilder.FromObject(), compileContext, provider); Assert.False(exp.Execute(new EqlMethodTestSchema()) as bool?); - exp = EntityQueryCompiler.Compile(@"oneStr.isOne()", SchemaBuilder.FromObject(), new ExecutionOptions(), provider); + exp = EntityQueryCompiler.Compile(@"oneStr.isOne()", SchemaBuilder.FromObject(), compileContext, provider); Assert.True(exp.Execute(new EqlMethodTestSchema()) as bool?); - exp = EntityQueryCompiler.Compile(@"notOneStr.isOne()", SchemaBuilder.FromObject(), new ExecutionOptions(), provider); + exp = EntityQueryCompiler.Compile(@"notOneStr.isOne()", SchemaBuilder.FromObject(), compileContext, provider); Assert.False(exp.Execute(new EqlMethodTestSchema()) as bool?); } diff --git a/src/tests/EntityGraphQL.Tests/EntityQuery/FilterExtensionTests.cs b/src/tests/EntityGraphQL.Tests/EntityQuery/FilterExtensionTests.cs index ef06dbb5..ab1d2137 100644 --- a/src/tests/EntityGraphQL.Tests/EntityQuery/FilterExtensionTests.cs +++ b/src/tests/EntityGraphQL.Tests/EntityQuery/FilterExtensionTests.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using EntityGraphQL.Extensions; using EntityGraphQL.Schema; using EntityGraphQL.Schema.FieldExtensions; +using Microsoft.Extensions.DependencyInjection; using Xunit; -using static EntityGraphQL.Schema.ArgumentHelper; namespace EntityGraphQL.Tests; @@ -15,7 +14,7 @@ public class FilterExtensionTests public void SupportEntityQuery() { var schemaProvider = SchemaBuilder.FromObject(); - schemaProvider.Query().ReplaceField("users", new { filter = EntityQuery() }, (ctx, p) => ctx.Users.WhereWhen(p.filter, p.filter.HasValue), "Return filtered users"); + schemaProvider.Query().GetField("users", null).UseFilter(); var gql = new QueryRequest { Query = @@ -37,7 +36,7 @@ public void SupportEntityQuery() public void SupportEntityQueryEmptyString() { var schemaProvider = SchemaBuilder.FromObject(); - schemaProvider.Query().ReplaceField("users", new { filter = EntityQuery() }, (ctx, p) => ctx.Users.WhereWhen(p.filter, p.filter.HasValue), "Return filtered users"); + schemaProvider.Query().GetField("users", null).UseFilter(); var gql = new QueryRequest { Query = @@ -57,7 +56,7 @@ public void SupportEntityQueryEmptyString() public void SupportEntityQueryStringWhitespace() { var schemaProvider = SchemaBuilder.FromObject(); - schemaProvider.Query().ReplaceField("users", new { filter = EntityQuery() }, (ctx, p) => ctx.Users.WhereWhen(p.filter, p.filter.HasValue), "Return filtered users"); + schemaProvider.Query().GetField("users", null).UseFilter(); var gql = new QueryRequest { Query = @@ -77,7 +76,7 @@ public void SupportEntityQueryStringWhitespace() public void SupportEntityQueryArgument() { var schemaProvider = SchemaBuilder.FromObject(); - schemaProvider.Query().ReplaceField("users", new { filter = EntityQuery() }, (ctx, p) => ctx.Users.WhereWhen(p.filter, p.filter.HasValue), "Return filtered users"); + schemaProvider.Query().GetField("users", null).UseFilter(); var gql = new QueryRequest { Query = @@ -100,7 +99,7 @@ public void SupportEntityQueryArgument() public void FilterExpressionWithNoValue() { var schemaProvider = SchemaBuilder.FromObject(); - schemaProvider.Query().ReplaceField("users", new { filter = EntityQuery() }, (ctx, p) => ctx.Users.WhereWhen(p.filter, p.filter.HasValue), "Return filtered users"); + schemaProvider.Query().GetField("users", null).UseFilter(); var gql = new QueryRequest { Query = @@ -126,7 +125,7 @@ public void FilterExpressionWithNoValue() public void FilterExpressionWithNoValueNoDocVar() { var schemaProvider = SchemaBuilder.FromObject(); - schemaProvider.Query().ReplaceField("users", new { filter = EntityQuery() }, (ctx, p) => ctx.Users.WhereWhen(p.filter, p.filter.HasValue), "Return filtered users"); + schemaProvider.Query().GetField("users", null).UseFilter(); var gql = new QueryRequest { Query = @@ -236,7 +235,7 @@ public void SupportUseFilterWithnotEqualStatement() public void SupportUseFilterWithIsAnyStatementInts() { var schemaProvider = SchemaBuilder.FromObject(); - schemaProvider.Query().ReplaceField("users", new { filter = EntityQuery() }, (ctx, p) => ctx.Users.WhereWhen(p.filter, p.filter.HasValue), "Return filtered users"); + schemaProvider.Query().GetField("users", null).UseFilter(); var gql = new QueryRequest { Query = @@ -566,6 +565,70 @@ public void SupportUseFilterEnumWithString() Assert.Equal(Gender.Male, person.gender); } + [Fact] + public void SupportUseFilterWithServiceAndNonServiceFields() + { + var schema = SchemaBuilder.FromObject(); + // Expose people with UseFilter + schema.Query().ReplaceField("people", ctx => ctx.People, "Return list of people").UseFilter(); + // Add a service-backed field on Person + schema.Type().AddField("age", "Person's age").Resolve((p, age) => age.GetAge(p.Birthday)); + + // Mixed filter: non-service (lastName, name) and service field (age) + var gql = new QueryRequest + { + Query = + @"{ + people(filter: ""lastName == \""Frank\"" and (age > 21 or name == \""Tom\"")"") { + id name lastName age + } + }", + }; + + // Test data: only Jill should match after both passes + var data = new TestDataContext(); + data.People.Add( + new Person + { + Id = 1, + Name = "Jill", + LastName = "Frank", + Birthday = DateTime.Now.AddYears(-22), + } + ); + data.People.Add( + new Person + { + Id = 2, + Name = "Cheryl", + LastName = "Frank", + Birthday = DateTime.Now.AddYears(-10), + } + ); + data.People.Add( + new Person + { + Id = 3, + Name = "Tom", + LastName = "Smith", + Birthday = DateTime.Now.AddYears(-30), + } + ); + + // Provide the service for the service-backed field + var services = new ServiceCollection(); + services.AddSingleton(new AgeService()); + + var result = schema.ExecuteRequestWithContext(gql, data, services.BuildServiceProvider(), null); + + Assert.Null(result.Errors); + dynamic people = ((IDictionary)result.Data!)["people"]; + Assert.Equal(1, Enumerable.Count(people)); + var person = Enumerable.First(people); + Assert.Equal("Jill", person.name); + Assert.Equal("Frank", person.lastName); + } + [Fact] public void SupportNullableDateTime() { @@ -594,6 +657,418 @@ public void SupportNullableDateTime() Assert.Equal(33, person.id); } + [Fact] + public void SupportGraphQLVariablesInFilter() + { + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().GetField("users", null).UseFilter(); + var gql = new QueryRequest + { + Query = + @" + query GetUsersByField($fieldValue: String) { + users(filter: ""field2 == $fieldValue"") { + field2 + } + }", + Variables = new QueryVariables { { "fieldValue", "2" } }, + }; + + var context = new TestDataContext().FillWithTestData(); + context.Users.Add(new User { Field2 = "99" }); + var tree = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + + Assert.Null(tree.Errors); + dynamic users = ((IDictionary)tree.Data!)["users"]; + Assert.Equal(1, Enumerable.Count(users)); + var user = Enumerable.First(users); + Assert.Equal("2", user.field2); + } + + [Fact] + public void SupportMultipleGraphQLVariablesInFilter() + { + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().GetField("users", null).UseFilter(); + var gql = new QueryRequest + { + Query = + @" + query GetUsersByFields($field1Value: Int, $field2Value: String) { + users(filter: ""field1 == $field1Value && field2 == $field2Value"") { + field1 + field2 + } + }", + Variables = new QueryVariables { { "field1Value", 2 }, { "field2Value", "2" } }, + }; + + var context = new TestDataContext().FillWithTestData(); + context.Users.Add(new User { Field1 = 1, Field2 = "1" }); + context.Users.Add(new User { Field1 = 3, Field2 = "3" }); + var tree = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(tree.Errors); + dynamic users = ((IDictionary)tree.Data!)["users"]; + Assert.Equal(1, Enumerable.Count(users)); + var user = Enumerable.First(users); + Assert.Equal(2, user.field1); + Assert.Equal("2", user.field2); + } + + [Fact] + public void ThrowsErrorForUndefinedVariableInFilter() + { + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().GetField("users", null).UseFilter(); + var gql = new QueryRequest + { + Query = + @" + query GetUsersByField { + users(filter: ""field2 == $undefinedVariable"") { + field2 + } + }", + Variables = new QueryVariables { { "fieldValue", "2" } }, + }; + + var context = new TestDataContext().FillWithTestData(); + var tree = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + Assert.NotNull(tree.Errors); + + Assert.Contains("Field 'users' - Variable $undefinedVariable not found in variables.", tree.Errors.First().Message); + } + + [Fact] + public void SupportUseFilterSelectManyMethod() + { + var schema = SchemaBuilder.FromObject(); + var gql = new QueryRequest + { + Query = + @"query Query($filter: String!) { + people(filter: $filter) { id name } + }", + Variables = new QueryVariables { { "filter", "projects.selectMany(tasks).any(name == \"Task 1\")" } }, + }; + var data = new TestDataContext2(); + var person1 = DataFiller.MakePerson(1, null, null); + person1.Name = "Alice"; + person1.Projects.Add(new Project { Name = "Project A", Tasks = [new Task { Name = "Task 1" }, new Task { Name = "Task 2" }] }); + person1.Projects.Add(new Project { Name = "Project B", Tasks = [new Task { Name = "Task 3" }] }); + data.People.Add(person1); + + var person2 = DataFiller.MakePerson(2, null, null); + person2.Name = "Bob"; + person2.Projects.Add(new Project { Name = "Project C", Tasks = [new Task { Name = "Task 4" }] }); + data.People.Add(person2); + + var tree = schema.ExecuteRequestWithContext(gql, data, null, null); + Assert.Null(tree.Errors); + dynamic people = ((IDictionary)tree.Data!)["people"]; + Assert.Equal(1, Enumerable.Count(people)); + var person = Enumerable.First(people); + Assert.Equal("Alice", person.name); + } + + [Fact] + public void SupportFilterWithNullableIntComparedToLiteral() + { + // Test for issue #484 - nullable int fields compared to numeric literals should not cause type misalignment + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().ReplaceField("users", (ctx) => ctx.Users, "Return filtered users").UseFilter(); + + var context = new TestDataContext(); + context.Users.Add(new User { Id = 1, RelationId = 4726 }); + context.Users.Add(new User { Id = 2, RelationId = 1000 }); + context.Users.Add(new User { Id = 3, RelationId = null }); + context.Users.Add(new User { Id = 4, RelationId = 4726 }); + + var gql = new QueryRequest + { + Query = + @"query { + users(filter: ""relationId == 4726"") { id relationId } +}", + }; + + var result = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + + dynamic users = ((IDictionary)result.Data!)["users"]; + Assert.Equal(2, Enumerable.Count(users)); + + var usersList = Enumerable.ToList(users); + Assert.Equal(1, usersList[0].id); + Assert.Equal(4726, usersList[0].relationId); + Assert.Equal(4, usersList[1].id); + Assert.Equal(4726, usersList[1].relationId); + } + + [Fact] + public void SupportFilterWithNullableIntComparedToLiteralReversed() + { + // Test for issue #484 - ensure reverse order (literal == field) also works correctly + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().ReplaceField("users", (ctx) => ctx.Users, "Return filtered users").UseFilter(); + + var context = new TestDataContext(); + context.Users.Add(new User { Id = 1, RelationId = 4726 }); + context.Users.Add(new User { Id = 2, RelationId = 1000 }); + context.Users.Add(new User { Id = 3, RelationId = null }); + context.Users.Add(new User { Id = 4, RelationId = 4726 }); + + var gql = new QueryRequest + { + Query = + @"query { + users(filter: ""4726 == relationId"") { id relationId } +}", + }; + + var result = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + + dynamic users = ((IDictionary)result.Data!)["users"]; + Assert.Equal(2, Enumerable.Count(users)); + + var usersList = Enumerable.ToList(users); + Assert.Equal(1, usersList[0].id); + Assert.Equal(4726, usersList[0].relationId); + Assert.Equal(4, usersList[1].id); + Assert.Equal(4726, usersList[1].relationId); + } + + [Fact] + public void SupportFilterWithNullableIntComparedToLiteralNotEqual() + { + // Additional test for issue #484 - testing != operator + // Note: In nullable comparisons, != will match null values + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().ReplaceField("users", (ctx) => ctx.Users, "Return filtered users").UseFilter(); + + var context = new TestDataContext(); + context.Users.Add(new User { Id = 1, RelationId = 4726 }); + context.Users.Add(new User { Id = 2, RelationId = 1000 }); + context.Users.Add(new User { Id = 3, RelationId = null }); + + var gql = new QueryRequest + { + Query = + @"query { + users(filter: ""relationId != 4726"") { id relationId } +}", + }; + + var result = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + + dynamic users = ((IDictionary)result.Data!)["users"]; + // Should match both the one with 1000 and the one with null (as null != 4726) + Assert.Equal(2, Enumerable.Count(users)); + + var usersList = Enumerable.ToList(users); + Assert.Equal(2, usersList[0].id); + Assert.Equal(1000, usersList[0].relationId); + Assert.Equal(3, usersList[1].id); + Assert.Null(usersList[1].relationId); + } + + [Fact] + public void SupportFilterWithNullableShortComparedToLiteral() + { + // Test for issue #484 - testing with short type to ensure it's handled + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().ReplaceField("items", (ctx) => ctx.Items, "Return filtered items").UseFilter(); + + var context = new TestContextWithNullableShort(); + context.Items.Add(new ItemWithNullableShort { Id = 1, Count = 100 }); + context.Items.Add(new ItemWithNullableShort { Id = 2, Count = 200 }); + context.Items.Add(new ItemWithNullableShort { Id = 3, Count = null }); + + var gql = new QueryRequest { Query = @"query { items(filter: ""count == 100"") { id count } }" }; + + var result = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + + dynamic items = ((IDictionary)result.Data!)["items"]; + Assert.Equal(1, Enumerable.Count(items)); + + var item = Enumerable.First(items); + Assert.Equal(1, item.id); + Assert.Equal((short)100, item.count); + } + + private class TestContextWithNullableShort + { + public List Items { get; set; } = []; + } + + private class ItemWithNullableShort + { + public int Id { get; set; } + public short? Count { get; set; } + } + + [Fact] + public void SupportFilterWithIntComparedToDouble() + { + // Test for integral to floating-point conversion + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().ReplaceField("users", (ctx) => ctx.Users, "Return filtered users").UseFilter(); + + var context = new TestDataContext(); + context.Users.Add(new User { Id = 1, Field1 = 100 }); + context.Users.Add(new User { Id = 2, Field1 = 200 }); + context.Users.Add(new User { Id = 3, Field1 = 150 }); + + var gql = new QueryRequest { Query = @"query { users(filter: ""field1 > 150.5"") { id field1 } }" }; + + var result = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + + dynamic users = ((IDictionary)result.Data!)["users"]; + Assert.Equal(1, Enumerable.Count(users)); + + var user = Enumerable.First(users); + Assert.Equal(2, user.id); + Assert.Equal(200, user.field1); + } + + [Fact] + public void SupportFilterWithDoubleFieldComparedToInt() + { + // Test for floating-point field with integral literal + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().ReplaceField("items", (ctx) => ctx.Items, "Return filtered items").UseFilter(); + + var context = new TestContextWithDouble(); + context.Items.Add(new ItemWithDouble { Id = 1, Value = 100.5 }); + context.Items.Add(new ItemWithDouble { Id = 2, Value = 200.5 }); + context.Items.Add(new ItemWithDouble { Id = 3, Value = 50.5 }); + + var gql = new QueryRequest { Query = @"query { items(filter: ""value > 100"") { id value } }" }; + + var result = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + + dynamic items = ((IDictionary)result.Data!)["items"]; + Assert.Equal(2, Enumerable.Count(items)); + } + + private class TestContextWithDouble + { + public List Items { get; set; } = []; + } + + private class ItemWithDouble + { + public int Id { get; set; } + public double Value { get; set; } + } + + [Fact] + public void SupportFilterWithFloatFieldComparedToDoubleLiteral() + { + // Test for float field with double literal (potential type mismatch) + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().ReplaceField("items", (ctx) => ctx.Items, "Return filtered items").UseFilter(); + + var context = new TestContextWithFloat(); + context.Items.Add(new ItemWithFloat { Id = 1, Value = 100.5f }); + context.Items.Add(new ItemWithFloat { Id = 2, Value = 200.5f }); + context.Items.Add(new ItemWithFloat { Id = 3, Value = 50.5f }); + + var gql = new QueryRequest { Query = @"query { items(filter: ""value > 100.5"") { id value } }" }; + + var result = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + + dynamic items = ((IDictionary)result.Data!)["items"]; + Assert.Equal(1, Enumerable.Count(items)); + var item = Enumerable.First(items); + Assert.Equal(2, item.id); + } + + private class TestContextWithFloat + { + public List Items { get; set; } = []; + } + + private class ItemWithFloat + { + public int Id { get; set; } + public float Value { get; set; } + } + + [Fact] + public void SupportFilterWithNullableFloatComparedToDoubleLiteral() + { + // Test for nullable float field with double literal + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().ReplaceField("items", (ctx) => ctx.Items, "Return filtered items").UseFilter(); + + var context = new TestContextWithNullableFloat(); + context.Items.Add(new ItemWithNullableFloat { Id = 1, Value = 100.5f }); + context.Items.Add(new ItemWithNullableFloat { Id = 2, Value = 200.5f }); + context.Items.Add(new ItemWithNullableFloat { Id = 3, Value = null }); + + var gql = new QueryRequest { Query = @"query { items(filter: ""value == 100.5"") { id value } }" }; + + var result = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + + dynamic items = ((IDictionary)result.Data!)["items"]; + Assert.Equal(1, Enumerable.Count(items)); + var item = Enumerable.First(items); + Assert.Equal(1, item.id); + } + + private class TestContextWithNullableFloat + { + public List Items { get; set; } = []; + } + + private class ItemWithNullableFloat + { + public int Id { get; set; } + public float? Value { get; set; } + } + + [Fact] + public void SupportFilterWithDecimalFieldComparedToDoubleLiteral() + { + // Test for decimal field with double literal + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.Query().ReplaceField("items", (ctx) => ctx.Items, "Return filtered items").UseFilter(); + + var context = new TestContextWithDecimal(); + context.Items.Add(new ItemWithDecimal { Id = 1, Value = 100.5m }); + context.Items.Add(new ItemWithDecimal { Id = 2, Value = 200.5m }); + context.Items.Add(new ItemWithDecimal { Id = 3, Value = 50.5m }); + + var gql = new QueryRequest { Query = @"query { items(filter: ""value > 100.5"") { id value } }" }; + + var result = schemaProvider.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + + dynamic items = ((IDictionary)result.Data!)["items"]; + Assert.Equal(1, Enumerable.Count(items)); + var item = Enumerable.First(items); + Assert.Equal(2, item.id); + } + + private class TestContextWithDecimal + { + public List Items { get; set; } = []; + } + + private class ItemWithDecimal + { + public int Id { get; set; } + public decimal Value { get; set; } + } + private class TestDataContext2 : TestDataContext { [UseFilter] diff --git a/src/tests/EntityGraphQL.Tests/EqlMethodProviderIsAnyTests.cs b/src/tests/EntityGraphQL.Tests/EqlMethodProviderIsAnyTests.cs new file mode 100644 index 00000000..6e530733 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/EqlMethodProviderIsAnyTests.cs @@ -0,0 +1,104 @@ +using System; +using EntityGraphQL.Compiler.EntityQuery; +using EntityGraphQL.Schema; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class EqlMethodProviderIsAnyTests +{ + [Fact] + public void IsAny_Default_Supports_Common_Scalars() + { + var provider = new EqlMethodProvider(); + // primitives + Assert.True(provider.EntityTypeHasMethod(typeof(string), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(int), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(int?), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(double), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(decimal), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(uint), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(long), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(ulong), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(byte), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(char), "isAny")); + // temporals & ids + Assert.True(provider.EntityTypeHasMethod(typeof(DateTime), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(Guid), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(DateTimeOffset), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(TimeSpan), "isAny")); +#if NET8_0_OR_GREATER + Assert.True(provider.EntityTypeHasMethod(typeof(DateOnly), "isAny")); + Assert.True(provider.EntityTypeHasMethod(typeof(TimeOnly), "isAny")); +#endif + } + + [Theory] + [InlineData(typeof(Version))] + [InlineData(typeof(Uri))] + public void IsAny_Can_Be_Extended_By_Type(Type type) + { + var provider = new EqlMethodProvider(); + Assert.False(provider.EntityTypeHasMethod(type, "isAny")); + provider.ExtendIsAnySupportedTypes(type); + Assert.True(provider.EntityTypeHasMethod(type, "isAny")); + } + + [Fact] + public void IsAny_Cant_Be_Extended_By_Type_Via_AddCustomTypeConverter_FromType() + { + var schema = new SchemaProvider(); + Assert.False(schema.MethodProvider.EntityTypeHasMethod(typeof(Version), "isAny")); + schema.AddCustomTypeConverter((v, t, sp) => t == typeof(Version) ? Version.Parse(v) : v); + Assert.False(schema.MethodProvider.EntityTypeHasMethod(typeof(Version), "isAny")); + } + + [Fact] + public void IsAny_Can_Be_Extended_By_SupportedToTypes_Via_AddCustomTypeConverter_FromType() + { + var schema = new SchemaProvider(); + Assert.False(schema.MethodProvider.EntityTypeHasMethod(typeof(Version), "isAny")); + schema.AddCustomTypeConverter((v, t, sp) => t == typeof(Version) ? Version.Parse(v) : v, typeof(Version)); + Assert.True(schema.MethodProvider.EntityTypeHasMethod(typeof(Version), "isAny")); + } + + [Fact] + public void IsAny_Can_Be_Extended_By_Type_Via_AddCustomTypeConverter_ToType() + { + var schema = new SchemaProvider(); + Assert.False(schema.MethodProvider.EntityTypeHasMethod(typeof(Version), "isAny")); + schema.AddCustomTypeConverter((o, sp) => Version.Parse(o!.ToString()!)); + Assert.True(schema.MethodProvider.EntityTypeHasMethod(typeof(Version), "isAny")); + } + + [Fact] + public void IsAny_Can_Be_Extended_By_Type_Via_AddCustomTypeConverter_FromToType() + { + var schema = new SchemaProvider(); + Assert.False(schema.MethodProvider.EntityTypeHasMethod(typeof(Version), "isAny")); + schema.AddCustomTypeConverter((o, sp) => Version.Parse(o.ToString()!)); + Assert.True(schema.MethodProvider.EntityTypeHasMethod(typeof(Version), "isAny")); + } + + private enum MyEnum + { + A = 1, + B = 2, + } + + [Fact] + public void IsAny_When_Extended_With_ValueTypeTarget_Adds_Nullable_Variant() + { + var schema = new SchemaProvider(); + // Precondition: enum and enum? are not supported by default + Assert.False(schema.MethodProvider.EntityTypeHasMethod(typeof(MyEnum), "isAny")); + Assert.False(schema.MethodProvider.EntityTypeHasMethod(typeof(MyEnum?), "isAny")); + + // Register converter targeting the enum type + schema.AddCustomTypeConverter((s, _) => (MyEnum)Enum.Parse(typeof(MyEnum), s, ignoreCase: true)); + + // Both the enum and its nullable form should be supported now + Assert.True(schema.MethodProvider.EntityTypeHasMethod(typeof(MyEnum), "isAny")); + Assert.True(schema.MethodProvider.EntityTypeHasMethod(typeof(MyEnum?), "isAny")); + } +} diff --git a/src/tests/EntityGraphQL.Tests/ErrorTests.cs b/src/tests/EntityGraphQL.Tests/ErrorTests.cs index c8036e32..ae9ad831 100644 --- a/src/tests/EntityGraphQL.Tests/ErrorTests.cs +++ b/src/tests/EntityGraphQL.Tests/ErrorTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using EntityGraphQL.Schema; using Xunit; @@ -26,7 +27,8 @@ public void MutationReportsError() Assert.NotNull(results.Errors); // error from execution that prevented a valid response, the data entry in the response should be null Assert.Null(results.Data); - Assert.Equal("Field 'addPersonError' - Name can not be null (Parameter 'name')", results.Errors[0].Message); + Assert.Equal("Argument name can not be null", results.Errors[0].Message); + Assert.Equal(["addPersonError"], results.Errors[0].Path); } [Fact] @@ -45,7 +47,8 @@ public void QueryReportsError() var testSchema = new TestDataContext().FillWithTestData(); var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, null, null); Assert.NotNull(results.Errors); - Assert.Equal("Field 'people' - Field failed to execute", results.Errors[0].Message); + Assert.Equal("Field failed to execute", results.Errors[0].Message); + Assert.Equal(1, results.Errors[0]?.Extensions?["code"]); } [Fact] @@ -85,18 +88,21 @@ public void TestExtensionException() var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, null, null); Assert.True(results.HasErrors()); Assert.NotNull(results.Errors); + // from spec errors "bubble up" to nullable field so we expect data to be null people is non-nullable + Assert.True(results.HasDataKey); + Assert.Null(results.Data); var error = results.Errors[0]; Assert.NotNull(error.Extensions); Assert.Equal(1, error.Extensions["code"]); var result = System.Text.Json.JsonSerializer.Serialize(results); Assert.Contains("errors", result); - Assert.DoesNotContain("data", result); + Assert.Contains("data", result); } [Fact] public void MutationReportsError_UnexposedException() { - var schemaProvider = SchemaBuilder.FromObject(new SchemaBuilderSchemaOptions { IsDevelopment = false }); + var schemaProvider = SchemaBuilder.FromObject(new SchemaProviderOptions { IsDevelopment = false }); schemaProvider.AddMutationsFrom(new SchemaBuilderOptions() { AutoCreateInputTypes = true }); // Add a argument field with a require parameter var gql = new QueryRequest @@ -114,7 +120,8 @@ public void MutationReportsError_UnexposedException() Assert.NotNull(results.Errors); // error from execution that prevented a valid response, the data entry in the response should be null Assert.Null(results.Data); - Assert.Equal("Field 'addPersonErrorUnexposedException' - Error occurred", results.Errors[0].Message); + Assert.Equal("Error occurred", results.Errors[0].Message); + Assert.Equal(["addPersonErrorUnexposedException"], results.Errors[0].Path); } [Fact] @@ -138,13 +145,14 @@ public void MutationReportsError_UnexposedException_Development() Assert.NotNull(results.Errors); // error from execution that prevented a valid response, the data entry in the response should be null Assert.Null(results.Data); - Assert.Equal("Field 'addPersonErrorUnexposedException' - You should not see this message outside of Development", results.Errors[0].Message); + Assert.Equal("You should not see this message outside of Development", results.Errors[0].Message); + Assert.Equal(["addPersonErrorUnexposedException"], results.Errors[0].Path); } [Fact] public void QueryReportsError_UnexposedException() { - var schemaProvider = SchemaBuilder.FromObject(new SchemaBuilderSchemaOptions { IsDevelopment = false }); + var schemaProvider = SchemaBuilder.FromObject(new SchemaProviderOptions { IsDevelopment = false }); // Add a argument field with a require parameter var gql = new QueryRequest { @@ -182,7 +190,7 @@ public void QueryReportsError_UnexposedException_Development() [Fact] public void QueryReportsError_UnexposedException_WithWhitelist() { - var schemaProvider = SchemaBuilder.FromObject(new SchemaBuilderSchemaOptions { IsDevelopment = false }); + var schemaProvider = SchemaBuilder.FromObject(new SchemaProviderOptions { IsDevelopment = false }); schemaProvider.AllowedExceptions.Add(new AllowedException(typeof(Exception))); // Add a argument field with a require parameter var gql = new QueryRequest @@ -222,7 +230,7 @@ public void QueryReportsError_UnexposedException_WithWhitelist_Development() [Fact] public void QueryReportsError_UnexposedException_Exact_WithWhitelist() { - var schemaProvider = SchemaBuilder.FromObject(new SchemaBuilderSchemaOptions { IsDevelopment = false }); + var schemaProvider = SchemaBuilder.FromObject(new SchemaProviderOptions { IsDevelopment = false }); schemaProvider.AllowedExceptions.Add(new AllowedException(typeof(ArgumentException), true)); // Add a argument field with a require parameter var gql = new QueryRequest @@ -262,7 +270,7 @@ public void QueryReportsError_UnexposedException_WithWhitelist_Exact_Development [Fact] public void QueryReportsError_UnexposedException_Exact_Mismatch_WithWhitelist() { - var schemaProvider = SchemaBuilder.FromObject(new SchemaBuilderSchemaOptions { IsDevelopment = false }); + var schemaProvider = SchemaBuilder.FromObject(new SchemaProviderOptions { IsDevelopment = false }); schemaProvider.AllowedExceptions.Add(new AllowedException(typeof(Exception), true)); // Add a argument field with a require parameter var gql = new QueryRequest @@ -302,7 +310,7 @@ public void QueryReportsError_UnexposedException_WithWhitelist_Exact_Mismatch_De [Fact] public void QueryReportsError_DistinctErrors() { - var schemaProvider = SchemaBuilder.FromObject(new SchemaBuilderSchemaOptions { IsDevelopment = false }); + var schemaProvider = SchemaBuilder.FromObject(new SchemaProviderOptions { IsDevelopment = false }); var gql = new QueryRequest { Query = @@ -314,14 +322,15 @@ public void QueryReportsError_DistinctErrors() var testSchema = new TestDataContext().FillWithTestData(); var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, null, null); Assert.NotNull(results.Errors); - Assert.Single(results.Errors); + Assert.Equal(2, results.Errors.Count); Assert.Equal("Field 'people' - Error occurred", results.Errors[0].Message); + Assert.Equal("Field 'people' - Error occurred", results.Errors[1].Message); } [Fact] public void QueryReportsError_AllowedExceptionAttribute() { - var schemaProvider = SchemaBuilder.FromObject(new SchemaBuilderSchemaOptions { IsDevelopment = false }); + var schemaProvider = SchemaBuilder.FromObject(new SchemaProviderOptions { IsDevelopment = false }); // Add a argument field with a require parameter var gql = new QueryRequest { @@ -336,4 +345,309 @@ public void QueryReportsError_AllowedExceptionAttribute() Assert.NotNull(results.Errors); Assert.Equal("Field 'people' - This error is allowed", results.Errors[0].Message); } + + [Fact] + public void MutationExecutionError_SingleField_NonNull() + { + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.AddMutationsFrom(new SchemaBuilderOptions() { AutoCreateInputTypes = true }); + // Add a argument field with a require parameter + var gql = new QueryRequest + { + Query = + @"mutation AddPerson($name: String) { + addPersonError(name: $name) + }", + Variables = new QueryVariables { { "name", "Bill" } }, + }; + + var testSchema = new TestDataContext(); + var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, null, null); + + // contains key 'data' as per spec + // as addPersonError result is not nullable it rolls up to data + Assert.True(results.HasDataKey); + Assert.Null(results.Data); + + Assert.NotNull(results.Errors); + Assert.Equal($"Argument name can not be null", results.Errors[0].Message); + Assert.Equal(["addPersonError"], results.Errors[0].Path); + } + + [Fact] + public void MutationExecutionError_MultipleFields_NonNull_AliasPath() + { + var aliasA = "a"; + var aliasB = "b"; + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.AddMutationsFrom(new SchemaBuilderOptions() { AutoCreateInputTypes = true }); + // Add a argument field with a require parameter + var gql = new QueryRequest + { + Query = + @"mutation AddPerson($name: String) { + a: addPersonError(name: $name) + b: addPersonError(name: $name) + }", + Variables = new QueryVariables { { "name", "Bill" } }, + }; + + var testSchema = new TestDataContext(); + var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, null, null); + + Assert.True(results.ContainsKey("data")); + var data = results.Data?.Values; + // from spec + // If an error was raised during the execution that prevented a valid response, the "data" entry + // in the response should be null. + // both fields are non nullable so no valid response can be returned, hence null + Assert.Null(data); + + Assert.NotNull(results.Errors); + Assert.Equal($"Argument name can not be null", results.Errors.First(e => e.Path != null && e.Path.Contains(aliasA)).Message); + Assert.Equal($"Argument name can not be null", results.Errors.First(e => e.Path != null && e.Path.Contains(aliasB)).Message); + var paths = results.Errors.Where(e => e.Path != null).SelectMany(e => e.Path!); + Assert.Equal(2, paths.Count()); + Assert.Contains(aliasA, paths); + Assert.Contains(aliasB, paths); + } + + [Fact] + public void MutationExecutionError_SingleField_Nullable() + { + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.AddMutationsFrom(new SchemaBuilderOptions() { AutoCreateInputTypes = true }); + // Add a argument field with a require parameter + var gql = new QueryRequest + { + Query = + @"mutation AddPerson($name: String) { + addPersonNullableError(name: $name) { id } + }", + Variables = new QueryVariables { { "name", "Bill" } }, + }; + + var testSchema = new TestDataContext(); + var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, null, null); + + Assert.True(results.HasDataKey); + Assert.NotNull(results.Data); + Assert.Single(results.Data); + Assert.All(results.Data.Values, Assert.Null); + + Assert.NotNull(results.Errors); + var error = results.Errors[0]; + Assert.Equal($"Argument name can not be null", error.Message); + Assert.Equal(["addPersonNullableError"], error.Path); + } + + [Fact] + public void MutationExecutionError_MultipleFields_Nullable_AliasPath() + { + var aliasA = "a"; + var aliasB = "b"; + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.AddMutationsFrom(new SchemaBuilderOptions() { AutoCreateInputTypes = true }); + // Add a argument field with a require parameter + var gql = new QueryRequest + { + Query = + @"mutation AddPerson($name: String) { + a: addPersonNullableError(name: $name) { id } + b: addPersonNullableError(name: $name) { id } + }", + Variables = new QueryVariables { { "name", "Bill" } }, + }; + + var testSchema = new TestDataContext(); + var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, null, null); + + Assert.True(results.ContainsKey("data")); + var data = results.Data?.Values; + Assert.NotNull(data); + Assert.Equal(2, data.Count); + Assert.All(data, Assert.Null); + + Assert.NotNull(results.Errors); + Assert.Equal($"Argument name can not be null", results.Errors.First(e => e.Path != null && e.Path.Contains(aliasA)).Message); + Assert.Equal($"Argument name can not be null", results.Errors.First(e => e.Path != null && e.Path.Contains(aliasB)).Message); + var paths = results.Errors.Where(e => e.Path != null).SelectMany(e => e.Path!); + Assert.Equal(2, paths.Count()); + Assert.Equal([aliasA], results.Errors[0].Path); + Assert.Equal([aliasB], results.Errors[1].Path); + } + + [Fact] + public void MutationExecutionError_MultipleFields_NonAliasPath() + { + var schemaProvider = SchemaBuilder.FromObject(); + schemaProvider.AddMutationsFrom(new SchemaBuilderOptions() { AutoCreateInputTypes = true }); + // Add a argument field with a require parameter + var gql = new QueryRequest + { + Query = + @"mutation AddPerson($name: String) { + addPersonError(name: $name) + addPersonNullableError(name: $name) { id } + }", + Variables = new QueryVariables { { "name", "Bill" } }, + }; + + var testSchema = new TestDataContext(); + var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, null, null); + + Assert.True(results.HasDataKey); + var data = results.Data?.Values; + Assert.NotNull(data); + Assert.Single(data); + Assert.Null(data.First()); + + Assert.NotNull(results.Errors); + Assert.Equal($"Argument name can not be null", results.Errors.First(e => e.Path != null && e.Path.Contains("addPersonError")).Message); + Assert.Equal($"Argument name can not be null", results.Errors.First(e => e.Path != null && e.Path.Contains("addPersonNullableError")).Message); + var paths = results.Errors.Where(e => e.Path != null).SelectMany(e => e.Path!); + Assert.Equal(2, paths.Count()); + Assert.Equal(["addPersonError"], results.Errors[0].Path); + Assert.Equal(["addPersonNullableError"], results.Errors[1].Path); + } + + private static string ThrowFieldError() => throw new Exception("This field failed"); + + private static string ThrowErrorOccurred() => throw new Exception("Error occurred"); + + private static string ThrowNonNullError() => throw new Exception("Non-null field failed"); + + [Fact] + public void QueryExecutionPartialResults_MultipleFields() + { + var schemaProvider = SchemaBuilder.FromObject(); + + // Add a field that will succeed + schemaProvider.Query().AddField("successField", ctx => "Success!", "A field that succeeds"); + + // Add a field that will fail + schemaProvider.Query().AddField("failField", ctx => ThrowFieldError(), "A field that fails"); + + // Add another field that will succeed + schemaProvider.Query().AddField("anotherSuccessField", ctx => 42, "Another field that succeeds"); + + var gql = new QueryRequest + { + Query = + @" + { + successField + failField + anotherSuccessField + }", + }; + + var testSchema = new TestDataContext(); + var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, null, null); + + // Should have partial data - the successful fields + Assert.NotNull(results.Data); + Assert.True(results.Data.ContainsKey("successField")); + Assert.Equal("Success!", results.Data["successField"]); + Assert.True(results.Data.ContainsKey("anotherSuccessField")); + Assert.Equal(42, results.Data["anotherSuccessField"]); + + // Failed field should be null + Assert.True(results.Data.ContainsKey("failField")); + Assert.Null(results.Data["failField"]); + + // Should have errors for the failed field + Assert.NotNull(results.Errors); + Assert.Single(results.Errors); + + var error = results.Errors[0]; + // The error message will be wrapped by EntityGraphQL + Assert.NotNull(error.Message); + Assert.NotNull(error.Path); + Assert.Single(error.Path); + Assert.Equal("failField", error.Path[0]); + } + + [Fact] + public void QueryExecutionPartialResults_WithAliases() + { + var schemaProvider = SchemaBuilder.FromObject(); + + // Add fields + schemaProvider.Query().AddField("dataField", ctx => "Some data", "A field that returns data"); + schemaProvider.Query().AddField("errorField", ctx => ThrowErrorOccurred(), "A field that throws an error"); + + var gql = new QueryRequest + { + Query = + @" + { + firstData: dataField + problemField: errorField + secondData: dataField + }", + }; + + var testSchema = new TestDataContext(); + var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, null, null); + + // Should have partial data with aliases + Assert.NotNull(results.Data); + Assert.Equal("Some data", results.Data["firstData"]); + Assert.Equal("Some data", results.Data["secondData"]); + Assert.Null(results.Data["problemField"]); + + // Error path should use the alias name + Assert.NotNull(results.Errors); + Assert.Single(results.Errors); + + var error = results.Errors[0]; + Assert.NotNull(error.Path); + Assert.Equal("problemField", error.Path[0]); // Uses alias, not original field name + } + + [Fact] + public void QueryExecutionPartialResults_NonNullableField() + { + var schemaProvider = SchemaBuilder.FromObject(); + + // Add a nullable field that succeeds + schemaProvider.Query().AddField("nullableSuccess", ctx => "Success", "A nullable field that succeeds"); + + // Add a non-nullable field that fails + schemaProvider.Query().AddField("nonNullableFail", ctx => ThrowNonNullError(), "A non-nullable field that fails").IsNullable(false); + + // Add another nullable field that succeeds + schemaProvider.Query().AddField("anotherNullableSuccess", ctx => "Also success", "Another nullable field that succeeds"); + + var gql = new QueryRequest + { + Query = + @" + { + nullableSuccess + nonNullableFail + anotherNullableSuccess + }", + }; + + var testSchema = new TestDataContext(); + var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, null, null); + + // The non-nullable field failure should bubble up and make data null + // as per GraphQL spec - non-null field errors bubble to the nearest nullable parent + // However, if other fields are nullable, they may still be included + // Let's check that we have errors for the non-nullable field + Assert.True(results.HasErrors()); + + // Should still have the error + Assert.NotNull(results.Errors); + Assert.Single(results.Errors); + + var error = results.Errors[0]; + // The error message will be wrapped by EntityGraphQL + Assert.NotNull(error.Message); + Assert.NotNull(error.Path); + Assert.Equal("nonNullableFail", error.Path[0]); + } } diff --git a/src/tests/EntityGraphQL.Tests/ExecutionOptionsTests.cs b/src/tests/EntityGraphQL.Tests/ExecutionOptionsTests.cs index a8aa5075..38134e29 100644 --- a/src/tests/EntityGraphQL.Tests/ExecutionOptionsTests.cs +++ b/src/tests/EntityGraphQL.Tests/ExecutionOptionsTests.cs @@ -104,7 +104,7 @@ public void TestBeforeExpressionBuildScalar() => ); [Fact] - public void TestBeforeExpressionBuildOffsetPaging() => + public void TestBeforeExpressionBuildOffsetPagingNoTotal() => TestBeforeExpressionBuildExpression( "query MyOp { projectItems { items { name } } }", AssertExpression.Conditional( @@ -132,6 +132,36 @@ public void TestBeforeExpressionBuildOffsetPaging() => "MyOp" ); + [Fact] + public void TestBeforeExpressionBuildOffsetPagingWithTotal() => + TestBeforeExpressionBuildExpression( + "query MyOp { projectItems { totalItems items { name } } }", + AssertExpression.Conditional( + AssertExpression.Any(), + AssertExpression.Any(), + AssertExpression.MemberInit( + [ + AssertExpression.MemberBinding("totalItems", AssertExpression.Any()), + AssertExpression.MemberBinding( + "items", + AssertExpression.Call( + null, + nameof(Enumerable.ToList), + AssertExpression.Call( + null, + "Select", + AssertExpression.Call(null, "TagWith", AssertExpression.Any(), AssertExpression.AnyOfType(typeof(Action))), + AssertExpression.Any() + ) + ) + ), + ] + ) + ), + "projectItems", + "MyOp" + ); + [Fact] public void TestBeforeExpressionBuildConnectionPaging() => TestBeforeExpressionBuildExpression( diff --git a/src/tests/EntityGraphQL.Tests/FilterExtensionBinaryCustomTypeTests.cs b/src/tests/EntityGraphQL.Tests/FilterExtensionBinaryCustomTypeTests.cs new file mode 100644 index 00000000..6e048246 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/FilterExtensionBinaryCustomTypeTests.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EntityGraphQL.Compiler; +using EntityGraphQL.Compiler.EntityQuery; +using EntityGraphQL.Schema; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class FilterExtensionBinaryCustomTypeTests +{ + private readonly EqlCompileContext compileContext = new(new CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null)); + + private class WithVersion + { + public WithVersion(Version v, string name) + { + V = v; + Name = name; + } + + public Version V { get; set; } + public string Name { get; set; } + } + + [Fact] + public void Binary_Uses_Custom_Custom_Converter_For_Target_Type() + { + var schema = SchemaBuilder.FromObject(); + schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + + var compiled = EntityQueryCompiler.Compile("v >= \"1.2.3\"", schema, compileContext); + var data = new List { new(new Version(1, 2, 2), "A"), new(new Version(1, 2, 3), "B"), new(new Version(2, 0, 0), "C") }; + + var res = data.Where((Func)compiled.LambdaExpression.Compile()).ToList(); + Assert.Equal(2, res.Count); + Assert.Equal("B", res[0].Name); + Assert.Equal("C", res[1].Name); + } +} diff --git a/src/tests/EntityGraphQL.Tests/FilterSplitterTests.cs b/src/tests/EntityGraphQL.Tests/FilterSplitterTests.cs new file mode 100644 index 00000000..57c9b322 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/FilterSplitterTests.cs @@ -0,0 +1,208 @@ +using System; +using System.Linq.Expressions; +using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Schema.FieldExtensions; +using Xunit; + +namespace EntityGraphQL.Tests; + +/// +/// Tests for the FilterSplitter functionality, which separates service-dependent +/// filter expressions from regular database-queryable expressions for two-pass execution. +/// +public class FilterSplitterTests +{ + private readonly FilterSplitter splitter = new(typeof(Person)); + + [Fact] + public void SplitFilter_NoServiceFields_ReturnsOnlyNonServiceFilter() + { + // Arrange: filter with only regular fields + Expression> filter = p => p.Name == "John" && p.Id > 5; + + // Act + var result = splitter.SplitFilter(filter); + + // Assert + Assert.NotNull(result.NonServiceFilter); + Assert.Null(result.ServiceFilter); + } + + [Fact] + public void SplitFilter_OrWithOnlyRegularFields_KeepsInNonService() + { + // Arrange: OR condition with only regular fields + Expression> filter = p => p.Name == "John" || p.Name == "Jane"; + + // Act + var result = splitter.SplitFilter(filter); + + // Assert + Assert.NotNull(result.NonServiceFilter); + Assert.Null(result.ServiceFilter); + } + + [Fact] + public void SplitFilter_NotExpressionWithRegularField_KeepsInNonService() + { + // Arrange: NOT expression with only regular fields + Expression> filter = p => !(p.Name == "John") && p.Id > 1; + + // Act + var result = splitter.SplitFilter(filter); + + // Assert + Assert.NotNull(result.NonServiceFilter); + Assert.Null(result.ServiceFilter); + } + + [Fact] + public void SplitFilter_ComplexAndConditions_WithRegularFieldsOnly() + { + // Arrange: complex AND conditions with only regular fields + Expression> filter = p => p.Id > 1 && p.Name == "John" && p.LastName != null && p.LastName.Length > 2; + + // Act + var result = splitter.SplitFilter(filter); + + // Assert + Assert.NotNull(result.NonServiceFilter); + Assert.Null(result.ServiceFilter); + } + + [Fact] + public void SplitFilter_VariousComplexLogicalExpressions_DoesNotThrow() + { + // Test various complex logical combinations to ensure robustness + var testCases = new[] + { + // Simple cases + p => p.Name == "John", + p => p.Id > 5 && p.Name == "John", + p => p.Name == "John" || p.Name == "Jane", + // Complex nested conditions + p => p.Name == "John" && (p.Id > 5 || p.LastName == "Doe"), + p => (p.Id > 1 && p.Name == "John") || (p.LastName == "Smith" && p.Name == "Jane"), + // NOT expressions + p => !(p.Name == "John") && p.Id > 1, + p => !(p.Id < 5 || p.Name == "John"), + // Deeply nested with service fields + (Expression>)(p => p.Id > 1 && (p.Name == "John" || p.Name == "Jane") && p.LastName != null && (p.LastName.Length > 2 || p.LastName == "Doe")), + }; + + foreach (var testCase in testCases) + { + // Act & Assert - should not throw and should return a result + var result = splitter.SplitFilter(testCase); + Assert.NotNull(result); + } + } + + [Fact] + public void SplitFilter_OnlyServiceFields_ReturnsOnlyServiceFilter() + { + // Arrange: Create a filter expression with only service fields + Expression> filter = p => ServiceExpressionMarker.MarkService(p.Id) > 21; + + // Act + var result = splitter.SplitFilter(filter); + + // Assert + Assert.Null(result.NonServiceFilter); + Assert.NotNull(result.ServiceFilter); + } + + [Fact] + public void SplitFilter_MixedAndFields_SplitsCorrectly() + { + // Arrange: Create a filter with both regular and service fields connected by AND + Expression> filter = p => p.Name == "John" && ServiceExpressionMarker.MarkService(p.Id) > 21; + + // Act + var result = splitter.SplitFilter(filter); + + // Assert - should split into both service and non-service parts + Assert.NotNull(result.NonServiceFilter); + Assert.NotNull(result.ServiceFilter); + + // Assert that NonServiceFilter contains only non-service fields + var nonServiceMarkerCheck = new ServiceMarkerCheckVisitor(); + nonServiceMarkerCheck.Visit(result.NonServiceFilter.Body); + Assert.False(nonServiceMarkerCheck.ContainsServiceMarker, "NonServiceFilter should not contain service markers"); + } + + [Fact] + public void SplitFilter_OrWithMixedFields_MovesToService() + { + // Arrange: Create an OR expression with mixed regular and service fields + Expression> filter = p => p.Name == "John" || ServiceExpressionMarker.MarkService(p.Id) > 21; + + // Act + var result = splitter.SplitFilter(filter); + + // Assert - OR expressions with service fields should move entirely to service filter + // because you can't safely split OR expressions + Assert.Null(result.NonServiceFilter); + Assert.NotNull(result.ServiceFilter); + } + + [Fact] + public void SplitFilter_ComplexNestedMixedFields_HandlesCorrectly() + { + // Arrange: Create a complex nested expression with mixed field types + // (regularField1 AND regularField2) AND (serviceField1 OR serviceField2) + Expression> filter = p => (p.Id > 1 && p.Name == "John") && (ServiceExpressionMarker.MarkService(p.Id) > 21 || ServiceExpressionMarker.MarkService(p.Name.Length) < 30); + + // Act + var result = splitter.SplitFilter(filter); + + // Assert - should have both parts since they're connected by AND + Assert.NotNull(result.NonServiceFilter); + Assert.NotNull(result.ServiceFilter); + + // Assert that NonServiceFilter contains only non-service fields + var nonServiceMarkerCheck = new ServiceMarkerCheckVisitor(); + nonServiceMarkerCheck.Visit(result.NonServiceFilter.Body); + Assert.False(nonServiceMarkerCheck.ContainsServiceMarker, "NonServiceFilter should not contain service markers"); + } + + [Fact] + public void SplitFilter_NotExpressionWithServiceField_MovesToService() + { + // Arrange: Create a NOT expression with a service field + Expression> filter = p => !(ServiceExpressionMarker.MarkService(p.Id) > 21) && p.Id > 1; + + // Act + var result = splitter.SplitFilter(filter); + + // Assert - should have both parts + Assert.NotNull(result.NonServiceFilter); + Assert.NotNull(result.ServiceFilter); + + // Assert that NonServiceFilter contains only non-service fields + var nonServiceMarkerCheck = new ServiceMarkerCheckVisitor(); + nonServiceMarkerCheck.Visit(result.NonServiceFilter.Body); + Assert.False(nonServiceMarkerCheck.ContainsServiceMarker, "NonServiceFilter should not contain service markers"); + } + + [Fact] + public void FilterSplitResult_Properties_WorkCorrectly() + { + // Test the FilterSplitResult class itself + var param = Expression.Parameter(typeof(Person), "p"); + var testExpr = Expression.Lambda>(Expression.Equal(Expression.Property(param, "Name"), Expression.Constant("test")), param); + + // Test constructor and properties + var result1 = new FilterSplitResult(testExpr, null); + Assert.Equal(testExpr, result1.NonServiceFilter); + Assert.Null(result1.ServiceFilter); + + var result2 = new FilterSplitResult(null, testExpr); + Assert.Null(result2.NonServiceFilter); + Assert.Equal(testExpr, result2.ServiceFilter); + + var result3 = new FilterSplitResult(testExpr, testExpr); + Assert.Equal(testExpr, result3.NonServiceFilter); + Assert.Equal(testExpr, result3.ServiceFilter); + } +} diff --git a/src/tests/EntityGraphQL.Tests/GraphQLParserTests.cs b/src/tests/EntityGraphQL.Tests/GraphQLParserTests.cs new file mode 100644 index 00000000..a74170c3 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/GraphQLParserTests.cs @@ -0,0 +1,328 @@ +using EntityGraphQL.Compiler; +using EntityGraphQL.Schema; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class GraphQLParserTests +{ + [Fact] + public void TestFourDigitUnicodeEscapeInString() + { + var schema = SchemaBuilder.FromObject(); + schema.Query().AddField("echo", new { text = "" }, (ctx, args) => args.text, "Echo text"); + + var query = + @" + query { + echo(text: ""Hello \u0041\u0042\u0043"") + } + "; + + var result = schema.ExecuteRequestWithContext(new QueryRequest { Query = query }, new TestDataContext(), null, null); + + Assert.Null(result.Errors); + Assert.Equal("Hello ABC", result.Data!["echo"]); + } + + [Fact] + public void TestVariableWidthUnicodeEscapeEmoji() + { + var schema = SchemaBuilder.FromObject(); + schema.Query().AddField("echo", new { text = "" }, (ctx, args) => args.text, "Echo text"); + + // Test the new \u{...} syntax for emoji (pile of poo emoji U+1F4A9) + var query = + @" + query { + echo(text: ""Hello \u{1F4A9}"") + } + "; + + var result = schema.ExecuteRequestWithContext(new QueryRequest { Query = query }, new TestDataContext(), null, null); + + // This test will currently fail as the feature is not yet implemented + Assert.Null(result.Errors); + Assert.Equal("Hello 💩", result.Data!["echo"]); + } + + [Fact] + public void TestVariableWidthUnicodeEscapeMultipleCharacters() + { + var schema = SchemaBuilder.FromObject(); + schema.Query().AddField("echo", new { text = "" }, (ctx, args) => args.text, "Echo text"); + + // Test multiple variable-width unicode escapes + var query = + @" + query { + echo(text: ""\u{1F600} \u{1F37A} \u{2764}"") + } + "; + + var result = schema.ExecuteRequestWithContext(new QueryRequest { Query = query }, new TestDataContext(), null, null); + + // This test will currently fail as the feature is not yet implemented + Assert.Null(result.Errors); + Assert.Equal("😀 🍺 ❤", result.Data!["echo"]); + } + + [Fact] + public void TestSurrogatePairEscapeSequence() + { + var schema = SchemaBuilder.FromObject(); + schema.Query().AddField("echo", new { text = "" }, (ctx, args) => args.text, "Echo text"); + + // Test legacy surrogate pair syntax for pile of poo emoji (U+1F4A9) + // High surrogate: 0xD83D, Low surrogate: 0xDCA9 + var query = + @" + query { + echo(text: ""Hello \uD83D\uDCA9"") + } + "; + + var result = schema.ExecuteRequestWithContext(new QueryRequest { Query = query }, new TestDataContext(), null, null); + + Assert.Null(result.Errors); + Assert.Equal("Hello 💩", result.Data!["echo"]); + } + + [Fact] + public void TestDescriptionOnQuery() + { + var schema = SchemaBuilder.FromObject(); + + // Test description on query operation (new in September 2025 spec) + var query = + @" + """""" + This query fetches all people. + It is used for testing purposes. + """""" + query GetAllPeople { + people { name } + } + "; + + var doc = GraphQLParser.Parse(query, schema); + + // This test validates that the parser doesn't fail when descriptions are present + // Full implementation would also store/expose the description + Assert.NotNull(doc); + Assert.Single(doc.Operations); + } + + [Fact] + public void TestDescriptionOnMutation() + { + var schema = SchemaBuilder.FromObject(); + schema.AddMutationsFrom(); + + var query = + @" + """""" + This mutation does nothing. + Used for testing description parsing. + """""" + mutation DoNothing { + noop + } + "; + + var doc = GraphQLParser.Parse(query, schema); + + Assert.NotNull(doc); + Assert.Single(doc.Operations); + } + + public class TestMutations + { + [GraphQLMutation] + public bool Noop() => true; + } + + [Fact] + public void TestDescriptionOnFragment() + { + var schema = SchemaBuilder.FromObject(); + + var query = + @" + query { + people { ...PersonFields } + } + + """""" + Fragment containing common person fields. + Reusable across multiple queries. + """""" + fragment PersonFields on Person { + name + } + "; + + var doc = GraphQLParser.Parse(query, schema); + + Assert.NotNull(doc); + Assert.Single(doc.Fragments); + } + + [Fact] + public void TestDescriptionOnField() + { + var schema = SchemaBuilder.FromObject(); + + var query = + @" + query { + ""this is a list of people"" + people { id } + } + "; + + var doc = GraphQLParser.Parse(query, schema); + + Assert.NotNull(doc); + } + + [Fact] + public void TestDescriptionOnAll() + { + var schema = SchemaBuilder.FromObject(); + schema.Mutation().Add("createPost", "Add new post", (string input) => true); + + var query = + @" + """""" + This is the main query operation. + It demonstrates descriptions on operations. + """""" + query GetUserData($userId: ID) { + """""" + Description on a field selection. + This fetches user information. + """""" + user(id: $userId) { + id + name + + """""" + Description on a nested field. + Gets the user's tasks. + """""" + tasks { + id + ""Another one"" + name + } + } + + ""Short description on another field"" + mainProject { + id + } + } + + """""" + This is a reusable fragment. + Contains common user fields. + """""" + fragment UserDetails on User { + id + + ""The user's display name"" + name + + """""" + The user's email address. + May be null if not shared. + """""" + field2 + } + + """""" + Mutation operation description. + Creates a new post for a user. + """""" + mutation CreatePost( + ""This variable does blah"" + $input: String + ) { + ""Creates and returns the new post"" + createPost(input: $input) + } + "; + + var doc = GraphQLParser.Parse(query, schema); + + Assert.NotNull(doc); + Assert.Single(doc.Fragments); + } + + [Fact] + public void TestMixedUnicodeEscapes() + { + var schema = SchemaBuilder.FromObject(); + schema.Query().AddField("echo", new { text = "" }, (ctx, args) => args.text, "Echo text"); + + // Mix old-style \uXXXX with new-style \u{...} + var query = + @" + query { + echo(text: ""\u0048ello \u{1F600}"") + } + "; + + var result = schema.ExecuteRequestWithContext(new QueryRequest { Query = query }, new TestDataContext(), null, null); + + Assert.Null(result.Errors); + Assert.Equal("Hello 😀", result.Data!["echo"]); + } + + [Fact] + public void TestBlockStringWithUnicodeEscapes() + { + var schema = SchemaBuilder.FromObject(); + schema.Query().AddField("echo", new { text = "" }, (ctx, args) => args.text, "Echo text"); + + var query = + @" + query { + echo(text: """""" + Multi-line text + with emoji \u{1F4A9} + and normal unicode \u0041 + """""") + } + "; + + var result = schema.ExecuteRequestWithContext(new QueryRequest { Query = query }, new TestDataContext(), null, null); + + Assert.Null(result.Errors); + Assert.Contains("emoji 💩", (string)result.Data!["echo"]!); + Assert.Contains("unicode A", (string)result.Data!["echo"]!); + } + + [Fact] + public void TestInvalidSurrogatePairShouldError() + { + var schema = SchemaBuilder.FromObject(); + schema.Query().AddField("echo", new { text = "" }, (ctx, args) => args.text, "Echo text"); + + // Invalid surrogate pair - high surrogate without low surrogate + var query = + @" + query { + echo(text: ""Invalid \uD83D not paired"") + } + "; + + // According to the spec, this should either produce an error or handle it gracefully + // Current implementation may pass it through as-is + var result = schema.ExecuteRequestWithContext(new QueryRequest { Query = query }, new TestDataContext(), null, null); + + // Test passes if either: error is produced OR string is handled + Assert.True(result.Errors != null || result.Data != null); + } +} diff --git a/src/tests/EntityGraphQL.Tests/IntrospectionTests/IntrospectionTests.cs b/src/tests/EntityGraphQL.Tests/IntrospectionTests/IntrospectionTests.cs index c287017a..132248f2 100644 --- a/src/tests/EntityGraphQL.Tests/IntrospectionTests/IntrospectionTests.cs +++ b/src/tests/EntityGraphQL.Tests/IntrospectionTests/IntrospectionTests.cs @@ -290,7 +290,7 @@ public void TestScalarDescription() { Query = @"query { - __type(name: ""Date"") { + __type(name: ""DateTime"") { name description } @@ -304,4 +304,93 @@ public void TestScalarDescription() var type = (dynamic)res.Data!["__type"]!; Assert.Equal("Date with time scalar", type.description); } + + [Fact] + public void TestInputTypeShouldNotHaveFields_OnlyInputFields() + { + // According to GraphQL spec, INPUT_OBJECT types should have inputFields, not fields + // fields should return null for input types + var schema = SchemaBuilder.FromObject(); + schema.AddInputType("TestInputType", "A test input type").AddAllFields(); + + var gql = new QueryRequest + { + Query = + @"query { + __type(name: ""TestInputType"") { + name + kind + fields { + name + } + inputFields { + name + } + } + }", + }; + + var context = new TestDataContext(); + var res = schema.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(res.Errors); + + var type = (dynamic)res.Data!["__type"]!; + Assert.Equal("TestInputType", type.name); + Assert.Equal("INPUT_OBJECT", type.kind); + + // fields should be null for INPUT_OBJECT types + Assert.Null(type.fields); + + // inputFields should have the fields + var inputFields = (IEnumerable)type.inputFields; + Assert.NotEmpty(inputFields); + Assert.Contains(inputFields, f => f.name == "name"); + Assert.Contains(inputFields, f => f.name == "value"); + } + + [Fact] + public void TestObjectTypeShouldHaveFields_NotInputFields() + { + // According to GraphQL spec, OBJECT types should have fields, not inputFields + var schema = SchemaBuilder.FromObject(); + + var gql = new QueryRequest + { + Query = + @"query { + __type(name: ""Person"") { + name + kind + fields { + name + } + inputFields { + name + } + } + }", + }; + + var context = new TestDataContext(); + var res = schema.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(res.Errors); + + var type = (dynamic)res.Data!["__type"]!; + Assert.Equal("Person", type.name); + Assert.Equal("OBJECT", type.kind); + + // fields should have the fields for OBJECT types + var fields = (IEnumerable)type.fields; + Assert.NotEmpty(fields); + + // inputFields should be null/empty for OBJECT types + var inputFields = type.inputFields as IEnumerable; + Assert.True(inputFields == null || !inputFields.Any()); + } + + private class TestInputType + { + public string Name { get; set; } = string.Empty; + public int Value { get; set; } + } } diff --git a/src/tests/EntityGraphQL.Tests/IntrospectionTests/MetaDataTests.cs b/src/tests/EntityGraphQL.Tests/IntrospectionTests/MetaDataTests.cs index e08e30ae..bfe1a3c7 100644 --- a/src/tests/EntityGraphQL.Tests/IntrospectionTests/MetaDataTests.cs +++ b/src/tests/EntityGraphQL.Tests/IntrospectionTests/MetaDataTests.cs @@ -18,10 +18,11 @@ public void Supports__typename() { var schemaProvider = SchemaBuilder.FromObject(); // Add a argument field with a require parameter - var tree = new GraphQLCompiler(schemaProvider).Compile( + var tree = GraphQLParser.Parse( @"query { - users { __typename id } -}" + users { __typename id } + }", + schemaProvider ); var users = tree.ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); diff --git a/src/tests/EntityGraphQL.Tests/IsAnyAndConvertersTests.cs b/src/tests/EntityGraphQL.Tests/IsAnyAndConvertersTests.cs new file mode 100644 index 00000000..3e1173cd --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/IsAnyAndConvertersTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EntityGraphQL.Compiler; +using EntityGraphQL.Compiler.EntityQuery; +using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Schema; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class IsAnyAndConvertersTests +{ + private readonly EqlCompileContext compileContext = new(new CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null)); + + private class WithVersion + { + public WithVersion(Version v, string name) + { + V = v; + Name = name; + } + + public Version V { get; set; } + public string Name { get; set; } = string.Empty; + } + + private class WithNullableName + { + public WithNullableName(string? name) + { + Name = name; + } + + public string? Name { get; set; } + } + + [Fact] + public void IsAny_For_Version_With_String_List_Uses_Converters() + { + var schema = SchemaBuilder.FromObject(); + schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + + var compiled = EntityQueryCompiler.Compile("v.isAny([\"1.2.3\", \"2.0.0\"]) ", schema, compileContext, schema.MethodProvider); + var data = new[] { new WithVersion(new Version(1, 2, 2), "A"), new WithVersion(new Version(1, 2, 3), "B"), new WithVersion(new Version(2, 0, 0), "C") }; + var res = data.Where((Func)compiled.LambdaExpression.Compile()).Select(d => d.Name).ToArray(); + Assert.Equal(new[] { "B", "C" }, res); + } + + [Fact] + public void IsAny_On_CustomType_Is_Auto_Extended_By_Converters() + { + var schema = SchemaBuilder.FromObject(); + // Adding a converter to Version should automatically enable isAny on Version + schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + var compiled = EntityQueryCompiler.Compile("v.isAny([\"1.2.3\"]) ", schema, compileContext, schema.MethodProvider); + var data = new[] { new WithVersion(new Version(1, 2, 2), "A"), new WithVersion(new Version(1, 2, 3), "B") }; + var res = data.Where((Func)compiled.LambdaExpression.Compile()).Select(d => d.Name).ToArray(); + Assert.Equal(new[] { "B" }, res); + } + + [Fact] + public void IsAny_With_Nullable_Field_And_List_With_Null_Works() + { + var schema = SchemaBuilder.FromObject(); + var compiled = EntityQueryCompiler.Compile("name.isAny([null, \"A\"]) ", schema, compileContext, schema.MethodProvider); + var data = new[] { new WithNullableName(null), new WithNullableName("A"), new WithNullableName("B") }; + var names = data.Where((Func)compiled.LambdaExpression.Compile()).Select(d => d.Name).ToArray(); + Assert.Equal(new string?[] { null, "A" }, names); + } + + [Fact] + public void Query_Mixed_Binary_CustomConverter_And_IsAny_With_Variable_Strings_Simulated() + { + // This test simulates variable conversion path by pre-converting a string list via converters + var schema = SchemaBuilder.FromObject(); + schema.AddCustomTypeConverter((s, _) => Version.Parse(s)); + + var compiled = EntityQueryCompiler.Compile("v >= \"1.2.3\"", schema, compileContext); + var data = new[] + { + new WithVersion(new Version(1, 2, 2), "A"), + new WithVersion(new Version(1, 2, 3), "B"), + new WithVersion(new Version(2, 0, 0), "C"), + new WithVersion(new Version(3, 0, 0), "D"), + }; + + // simulate $versions variable provided as strings and converted + var versionStrings = new[] { "1.2.3", "2.0.0" }; + var converted = (IEnumerable)ExpressionUtil.ConvertObjectType(versionStrings, typeof(List), schema)!; + var versionSet = converted.Cast().ToHashSet(); + + var pred = (Func)compiled.LambdaExpression.Compile(); + var res = data.Where(w => pred(w) && versionSet.Contains(w.V)).Select(w => w.Name).ToArray(); + Assert.Equal(new[] { "B", "C" }, res); + } +} diff --git a/src/tests/EntityGraphQL.Tests/MutationTests/MutationArgsTests.cs b/src/tests/EntityGraphQL.Tests/MutationTests/MutationArgsTests.cs index 2239785a..38ae6aa0 100644 --- a/src/tests/EntityGraphQL.Tests/MutationTests/MutationArgsTests.cs +++ b/src/tests/EntityGraphQL.Tests/MutationTests/MutationArgsTests.cs @@ -30,6 +30,28 @@ public void SupportsGenericClassArg() Assert.Equal(65, res.Data!["addPerson"]!); } + [Fact] + public void RequiredModifierOnInputMakesArgRequired() + { + var schema = SchemaBuilder.FromObject(); + schema.Mutation().Add("addPersonReq", ([GraphQLArguments] RequiredInputArgs args) => args.Age, new SchemaBuilderOptions { AutoCreateInputTypes = true }); + + var sdl = schema.ToGraphQLSchemaString(); + Assert.Contains("addPersonReq(name: String!, age: Int!): Int!", sdl); + + // Missing required arg should error + var gqlMissing = new QueryRequest { Query = @"mutation AddPersonReq { addPersonReq(age: 22) }" }; + var resMissing = schema.ExecuteRequestWithContext(gqlMissing, new TestDataContext(), null, null); + Assert.NotNull(resMissing.Errors); + Assert.Equal("Field 'addPersonReq' - missing required argument 'name'", resMissing.Errors![0].Message); + + // Providing all args should succeed + var gql = new QueryRequest { Query = @"mutation AddPersonReq { addPersonReq(name: ""Herb"", age: 22) }" }; + var res = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(res.Errors); + Assert.Equal(22, res.Data!["addPersonReq"]!); + } + [Fact] public void SupportsGenericClassArgAsInputType() { @@ -182,6 +204,12 @@ internal class InputExtraArgs public string? Token { get; set; } } +internal class RequiredInputArgs +{ + public required string Name { get; init; } + public required int Age { get; init; } +} + internal class Human { public int Age { get; set; } diff --git a/src/tests/EntityGraphQL.Tests/MutationTests/MutationMethodParameterTests.cs b/src/tests/EntityGraphQL.Tests/MutationTests/MutationMethodParameterTests.cs index ba97b850..9fb5c20a 100644 --- a/src/tests/EntityGraphQL.Tests/MutationTests/MutationMethodParameterTests.cs +++ b/src/tests/EntityGraphQL.Tests/MutationTests/MutationMethodParameterTests.cs @@ -11,7 +11,6 @@ public class MutationMethodParameterTests public void TestSeparateArguments_PrimitivesOnly() { var schemaProvider = SchemaBuilder.FromObject(); - schemaProvider.AddScalarType("DateTime", ""); schemaProvider.AddScalarType("decimal", ""); schemaProvider.AddMutationsFrom(new SchemaBuilderOptions { AutoCreateInputTypes = false }); // Add a argument field with a require parameter @@ -38,7 +37,6 @@ public void TestSeparateArguments_PrimitivesOnly() public void TestSeparateArguments_PrimitivesOnly_WithInlineDefaults() { var schemaProvider = SchemaBuilder.FromObject(); - schemaProvider.AddScalarType("DateTime", ""); schemaProvider.AddScalarType("decimal", ""); schemaProvider.AddMutationsFrom(new SchemaBuilderOptions { AutoCreateInputTypes = false }); // Add a argument field with a require parameter @@ -198,7 +196,6 @@ public void TestSeparateArguments_AutoAddInputTypes() public void TestChildArraysDontGetArguments() { var schemaProvider = new SchemaProvider(); - schemaProvider.AddScalarType("DateTime", ""); schemaProvider.AddScalarType("decimal", ""); schemaProvider.AddScalarType("char", ""); schemaProvider.PopulateFromContext(); diff --git a/src/tests/EntityGraphQL.Tests/MutationTests/MutationTests.cs b/src/tests/EntityGraphQL.Tests/MutationTests/MutationTests.cs index af4789b9..7b6df888 100644 --- a/src/tests/EntityGraphQL.Tests/MutationTests/MutationTests.cs +++ b/src/tests/EntityGraphQL.Tests/MutationTests/MutationTests.cs @@ -36,7 +36,7 @@ public void SupportsMutationOptional() { var schemaProvider = SchemaBuilder.FromObject(); schemaProvider.AddMutationsFrom(new SchemaBuilderOptions() { AutoCreateInputTypes = true }); - // Add a argument field with a require parameter + // Add a argument field with a optional parameter var gql = new QueryRequest { Query = @@ -183,17 +183,17 @@ public void TestErrorOnVariableTypeMismatch() { Query = @"mutation AddPerson($names: [String]) { # wrong variable type - addPersonInput(nameInput: $names) { - id name lastName - } - }", + addPersonInput(nameInput: $names) { + id name lastName + } + }", // Object does not match the var definition in the AddPerson operation Variables = new QueryVariables { { "names", new { name = "Lisa", lastName = "Simpson" } } }, }; var result = schemaProvider.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); Assert.NotNull(result.Errors); Assert.Single(result.Errors); - Assert.Equal("Field 'addPersonInput' - Supplied variable 'names' can not be applied to defined variable type '[String]'", result.Errors.First().Message); + Assert.Equal("Supplied variable 'names' can not be applied to defined variable type '[String]'", result.Errors.First().Message); } [Fact] @@ -363,10 +363,10 @@ public void SupportsSelectionFromConstantList() { Query = @"mutation AddPerson($name: String) { - addPersonAdvList(name: $name) { - id name projects { id } - } - }", + addPersonAdvList(name: $name) { + id name projects { id } + } + }", Variables = new QueryVariables { { "name", "Bill" } }, }; var testSchema = new TestDataContext(); @@ -542,9 +542,8 @@ public void TestUnnamedMutationOp() { Query = @"mutation { - doGreatThing - } - ", + doGreatThing + }", }; var testSchema = new TestDataContext(); @@ -618,9 +617,8 @@ public void TestRequiredGuid() { Query = @"mutation { - needsGuid - } - ", + needsGuid + }", }; var testSchema = new TestDataContext(); @@ -968,7 +966,7 @@ public void TestNullableGuidEmptyString() var testSchema = new TestDataContext(); var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, serviceCollection.BuildServiceProvider(), null); Assert.NotNull(results.Errors); - Assert.Equal("Field 'nullableGuidArgs' - Supplied variable 'id' can not be applied to defined variable type 'ID'", results.Errors[0].Message); + Assert.Equal("Supplied variable 'id' can not be applied to defined variable type 'ID'", results.Errors[0].Message); } [Fact] @@ -1077,7 +1075,7 @@ public void TestAddFromMultipleClassesImplementingInterface() var schemaProvider = SchemaBuilder.FromObject(); schemaProvider.Mutation().AddFrom(new SchemaBuilderOptions { AutoCreateInputTypes = true }); - Assert.Equal(34, schemaProvider.Mutation().SchemaType.GetFields().Count()); + Assert.Equal(35, schemaProvider.Mutation().SchemaType.GetFields().Count()); } public class NonAttributeMarkedMethod @@ -1154,7 +1152,7 @@ public void TestNoArgsOnInputType() var schema = SchemaBuilder.FromObject(); schema.AddInputType("InputObject", "Using an object in the arguments"); - var ex = Assert.Throws(() => schema.Type().AddField("invalid", new { id = (int?)null }, (ctx, args) => 8, "Invalid field")); + var ex = Assert.Throws(() => schema.Type().AddField("invalid", new { id = (int?)null }, (ctx, args) => 8, "Invalid field")); Assert.Equal($"Field 'invalid' on type 'InputObject' has arguments but is a GraphQL '{nameof(GqlTypes.InputObject)}' type and can not have arguments.", ex.Message); } diff --git a/src/tests/EntityGraphQL.Tests/MutationTests/OneOfInputTypeTests.cs b/src/tests/EntityGraphQL.Tests/MutationTests/OneOfInputTypeTests.cs index 232838d5..8dd85abf 100644 --- a/src/tests/EntityGraphQL.Tests/MutationTests/OneOfInputTypeTests.cs +++ b/src/tests/EntityGraphQL.Tests/MutationTests/OneOfInputTypeTests.cs @@ -89,7 +89,7 @@ query IntrospectionQuery { public void TestOneOfAttributeCanNotBeUsedOnNonInputTypes() { var schemaProvider = SchemaBuilder.Create(); - var ex = Assert.Throws(() => schemaProvider.AddType("InputObject", "Using an object in the arguments")); + var ex = Assert.Throws(() => schemaProvider.AddType("InputObject", "Using an object in the arguments")); Assert.Equal($"OneOfInputType marked with OneOfDirective directive which is not valid on a {nameof(GqlTypes.QueryObject)}", ex.Message); } @@ -104,7 +104,7 @@ private class InvalidOneOfInputType public void TestOneOfAttributeChecksFieldsAreNullable() { var schemaProvider = SchemaBuilder.Create(); - var ex = Assert.Throws(() => schemaProvider.AddInputType("InputObject", "Using an object in the arguments").AddAllFields()); + var ex = Assert.Throws(() => schemaProvider.AddInputType("InputObject", "Using an object in the arguments").AddAllFields()); Assert.Equal("InvalidOneOfInputType is a OneOf type but all its fields are not nullable. OneOf input types require all the field to be nullable.", ex.Message); } diff --git a/src/tests/EntityGraphQL.Tests/MutationTests/PeopleMutations.cs b/src/tests/EntityGraphQL.Tests/MutationTests/PeopleMutations.cs index 6313472d..60b81d51 100644 --- a/src/tests/EntityGraphQL.Tests/MutationTests/PeopleMutations.cs +++ b/src/tests/EntityGraphQL.Tests/MutationTests/PeopleMutations.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Linq.Expressions; -using System.Threading; using System.Threading.Tasks; using EntityGraphQL.Schema; @@ -206,7 +205,13 @@ public IEnumerable AddPersonReturnAllConst(TestDataContext db, PeopleMut [GraphQLMutation] public int AddPersonError(PeopleMutationsArgs args) { - throw new EntityGraphQLArgumentException("name", "Name can not be null"); + throw new EntityGraphQLException("Argument name can not be null"); + } + + [GraphQLMutation] + public Person? AddPersonNullableError(PeopleMutationsArgs args) + { + throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "Argument name can not be null"); } [GraphQLMutation] @@ -319,14 +324,14 @@ public async Task AddPersonWithDelayAsync(TestDataContext db, PeopleMuta { // Simulate async work that can be cancelled await System.Threading.Tasks.Task.Delay(50); - + var person = new Person { Id = 999, Name = args.Name ?? "Test Person", - LastName = "Delayed" + LastName = "Delayed", }; - + db.People.Add(person); return person; } diff --git a/src/tests/EntityGraphQL.Tests/ProcessArgumentValueTests.cs b/src/tests/EntityGraphQL.Tests/ProcessArgumentValueTests.cs index b7bc0d03..30dc94a1 100644 --- a/src/tests/EntityGraphQL.Tests/ProcessArgumentValueTests.cs +++ b/src/tests/EntityGraphQL.Tests/ProcessArgumentValueTests.cs @@ -1,6 +1,6 @@ +using System; using EntityGraphQL.Compiler; using EntityGraphQL.Schema; -using HotChocolate.Language; using Xunit; namespace EntityGraphQL.Tests; @@ -11,7 +11,7 @@ public class ProcessArgumentValueTests public void TestParseArgumentFloat() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new FloatValueNode(1.2), "arg", typeof(float)); + var res = GraphQLParser.ConvertArgumentValue(schema, 1.2, typeof(float)); Assert.Equal(1.2f, res); } @@ -19,7 +19,7 @@ public void TestParseArgumentFloat() public void TestParseArgumentDouble() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new FloatValueNode(1.2), "arg", typeof(double)); + var res = GraphQLParser.ConvertArgumentValue(schema, 1.2, typeof(double)); Assert.Equal(1.2d, res); } @@ -27,7 +27,7 @@ public void TestParseArgumentDouble() public void TestParseArgumentDecimal() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new FloatValueNode(1.2), "arg", typeof(decimal)); + var res = GraphQLParser.ConvertArgumentValue(schema, 1.2, typeof(decimal)); Assert.Equal(1.2m, res); } @@ -35,7 +35,7 @@ public void TestParseArgumentDecimal() public void TestParseArgumentFloatNoFraction() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new IntValueNode(1), "arg", typeof(float)); + var res = GraphQLParser.ConvertArgumentValue(schema, 1, typeof(float)); Assert.Equal(1f, res); } @@ -43,7 +43,7 @@ public void TestParseArgumentFloatNoFraction() public void TestParseArgumentDoubleNoFraction() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new IntValueNode(1), "arg", typeof(double)); + var res = GraphQLParser.ConvertArgumentValue(schema, 1, typeof(double)); Assert.Equal(1d, res); } @@ -51,7 +51,7 @@ public void TestParseArgumentDoubleNoFraction() public void TestParseArgumentDecimalNoFraction() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new IntValueNode(1), "arg", typeof(decimal)); + var res = GraphQLParser.ConvertArgumentValue(schema, 1, typeof(decimal)); Assert.Equal(1m, res); } @@ -59,7 +59,7 @@ public void TestParseArgumentDecimalNoFraction() public void TestParseArgumentFloatNull() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new FloatValueNode(1.2), "arg", typeof(float?)); + var res = GraphQLParser.ConvertArgumentValue(schema, 1.2, typeof(float?)); Assert.Equal(1.2f, res); } @@ -67,7 +67,7 @@ public void TestParseArgumentFloatNull() public void TestParseArgumentDoubleNull() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new FloatValueNode(1.2), "arg", typeof(double?)); + var res = GraphQLParser.ConvertArgumentValue(schema, 1.2, typeof(double?)); Assert.Equal(1.2d, res); } @@ -75,7 +75,7 @@ public void TestParseArgumentDoubleNull() public void TestParseArgumentDecimalNull() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new FloatValueNode(1.2), "arg", typeof(decimal?)); + var res = GraphQLParser.ConvertArgumentValue(schema, 1.2, typeof(decimal?)); Assert.Equal(1.2m, res); } @@ -83,7 +83,7 @@ public void TestParseArgumentDecimalNull() public void TestParseArgumentFloatNullValue() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new NullValueNode(null), "arg", typeof(float?)); + var res = GraphQLParser.ConvertArgumentValue(schema, null, typeof(float?)); Assert.Equal((float?)null, res); } @@ -91,7 +91,7 @@ public void TestParseArgumentFloatNullValue() public void TestParseArgumentDoubleNullValue() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new NullValueNode(null), "arg", typeof(double?)); + var res = GraphQLParser.ConvertArgumentValue(schema, null, typeof(double?)); Assert.Equal((double?)null, res); } @@ -99,7 +99,254 @@ public void TestParseArgumentDoubleNullValue() public void TestParseArgumentDecimalNullValue() { var schema = SchemaBuilder.FromObject(); - var res = QueryWalkerHelper.ProcessArgumentValue(schema, new NullValueNode(null), "arg", typeof(decimal?)); + var res = GraphQLParser.ConvertArgumentValue(schema, null, typeof(decimal?)); Assert.Equal((decimal?)null, res); } + + [Fact] + public void TestParseArgumentShort() + { + var schema = SchemaBuilder.FromObject(); + var res = GraphQLParser.ConvertArgumentValue(schema, 123, typeof(short)); + Assert.Equal((short)123, res); + } + + [Fact] + public void TestParseArgumentUShort() + { + var schema = SchemaBuilder.FromObject(); + var res = GraphQLParser.ConvertArgumentValue(schema, 123, typeof(ushort)); + Assert.Equal((ushort)123, res); + } + + [Fact] + public void TestParseArgumentUInt() + { + var schema = SchemaBuilder.FromObject(); + var res = GraphQLParser.ConvertArgumentValue(schema, 123, typeof(uint)); + Assert.Equal(123u, res); + } + + [Fact] + public void TestParseArgumentLong() + { + var schema = SchemaBuilder.FromObject(); + var res = GraphQLParser.ConvertArgumentValue(schema, 123, typeof(long)); + Assert.Equal(123L, res); + } + + [Fact] + public void TestParseArgumentULong() + { + var schema = SchemaBuilder.FromObject(); + var res = GraphQLParser.ConvertArgumentValue(schema, 123, typeof(ulong)); + Assert.Equal(123UL, res); + } + + [Fact] + public void TestMutationWithShortArgument() + { + var schema = SchemaBuilder.FromObject(); + schema.AddScalarType("Short", "A 16-bit signed integer"); + + schema.Mutation().Add("addShort", (short value) => value + 1); + + var gql = new QueryRequest { Query = "mutation { addShort(value: 100) }" }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal(101, (int)result.Data!["addShort"]!); + } + + [Fact] + public void TestMutationWithUShortArgument() + { + var schema = SchemaBuilder.FromObject(); + schema.AddScalarType("UShort", "A 16-bit unsigned integer"); + + schema.Mutation().Add("addUShort", (ushort value) => value + 1); + + var gql = new QueryRequest { Query = "mutation { addUShort(value: 100) }" }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal(101, (int)result.Data!["addUShort"]!); + } + + [Fact] + public void TestMutationWithUIntArgument() + { + var schema = SchemaBuilder.FromObject(); + schema.AddScalarType("UInt", "A 32-bit unsigned integer"); + + schema.Mutation().Add("addUInt", (uint value) => value + 1); + + var gql = new QueryRequest { Query = "mutation { addUInt(value: 100) }" }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal(101, Convert.ToInt32(result.Data!["addUInt"]!)); + } + + [Fact] + public void TestMutationWithLongArgument() + { + var schema = SchemaBuilder.FromObject(); + schema.AddScalarType("Long", "A 64-bit signed integer"); + + schema.Mutation().Add("addLong", (long value) => value + 1); + + var gql = new QueryRequest { Query = "mutation { addLong(value: 100) }" }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal(101, Convert.ToInt32(result.Data!["addLong"]!)); + } + + [Fact] + public void TestMutationWithULongArgument() + { + var schema = SchemaBuilder.FromObject(); + schema.AddScalarType("ULong", "A 64-bit unsigned integer"); + + schema.Mutation().Add("addULong", (ulong value) => value + 1); + + var gql = new QueryRequest { Query = "mutation { addULong(value: 100) }" }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal(101, Convert.ToInt32(result.Data!["addULong"]!)); + } + + [Fact] + public void TestBlockStringWithIndentation() + { + var schema = SchemaBuilder.FromObject(); + schema.Mutation().Add("echo", (string value) => value); + + var gql = new QueryRequest + { + Query = + @"mutation { + echo(value: """""" + Hello, + World! + Yours, + GraphQL. + """""") + }", + }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal("Hello,\n World!\nYours,\n GraphQL.", result.Data!["echo"]); + } + + [Fact] + public void TestBlockStringWithLeadingAndTrailingEmptyLines() + { + var schema = SchemaBuilder.FromObject(); + schema.Mutation().Add("echo", (string value) => value); + + var gql = new QueryRequest + { + Query = + @"mutation { + echo(value: """""" + + Content here + + """""") + }", + }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal("Content here", result.Data!["echo"]); + } + + [Fact] + public void TestBlockStringEmpty() + { + var schema = SchemaBuilder.FromObject(); + schema.Mutation().Add("echo", (string value) => value); + + var gql = new QueryRequest { Query = "mutation {\n echo(value: \"\"\"\"\"\")\n }" }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal("", result.Data!["echo"]); + } + + [Fact] + public void TestBlockStringNoIndentRemoval() + { + var schema = SchemaBuilder.FromObject(); + schema.Mutation().Add("echo", (string value) => value); + + var gql = new QueryRequest + { + Query = + @"mutation { + echo(value: """"""No indent +Still no indent"""""") + }", + }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal("No indent\nStill no indent", result.Data!["echo"]); + } + + [Fact] + public void TestBlockStringWithEscapedQuotes() + { + var schema = SchemaBuilder.FromObject(); + schema.Mutation().Add("echo", (string value) => value); + + var gql = new QueryRequest { Query = "mutation {\n echo(value: \"\"\"He said \\\"\\\"\\\"Hello\\\"\\\"\\\"\"\"\")\n }" }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal("He said \"\"\"Hello\"\"\"", result.Data!["echo"]); + } + + [Fact] + public void TestBlockStringWithCarriageReturns() + { + var schema = SchemaBuilder.FromObject(); + schema.Mutation().Add("echo", (string value) => value); + + var gql = new QueryRequest { Query = "mutation {\n echo(value: \"\"\"\r\n Line1\r\n Line2\r\n\"\"\") \n }" }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal("Line1\nLine2", result.Data!["echo"]); + } + + [Fact] + public void TestBlockStringWithLeadingAndTrailingEmptyLinesAndIndentation() + { + var schema = SchemaBuilder.FromObject(); + schema.Mutation().Add("echo", (string value) => value); + + var gql = new QueryRequest + { + Query = + @"mutation { + echo(value: """""" + + Content here + And here + + Here + + """""") + }", + }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + Assert.Equal("Content here\n And here\n\nHere", result.Data!["echo"]); + } } diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/ArgumentTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/ArgumentTests.cs index c29d02ce..3f2fead8 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/ArgumentTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/ArgumentTests.cs @@ -16,18 +16,23 @@ public class ArgumentTests [Fact] public void CanExecuteRequiredParameter() { - var tree = new GraphQLCompiler(SchemaBuilder.FromObject()).Compile( - @" - { - project(id: 55) { - name - } - }" + var tree = GraphQLParser.Parse( + @"{ + project(id: 55) { + id + name + } + }", + SchemaBuilder.FromObject() ); Assert.Single(tree.Operations.First().QueryFields); - var result = tree.ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); + var data = new TestDataContext().FillWithTestData(); + data.Projects.Add(new Project { Id = 53, Name = "Project 2" }); + var result = tree.ExecuteQuery(data, null, null); + Assert.Null(result.Errors); Assert.Equal("Project 3", ((dynamic)result.Data!["project"]!).name); + Assert.Equal(55, ((dynamic)result.Data!["project"]!).id); } [Fact] @@ -36,10 +41,11 @@ public void SupportsManyArguments() var schema = SchemaBuilder.FromObject(new SchemaBuilderOptions { AutoCreateFieldWithIdArguments = false }); // Add a argument field with a require parameter schema.Query().AddField("user", new { id = ArgumentHelper.Required(), something = true }, (ctx, param) => ctx.Users.Where(u => u.Id == param.id).FirstOrDefault(), "Return a user by ID"); - var tree = new GraphQLCompiler(schema).Compile( + var tree = GraphQLParser.Parse( @"query { user(id: 100, something: false) { id } - }" + }", + schema ); // db => db.Users.Where(u => u.Id == id).Select(u => new {id = u.Id}]).FirstOrDefault() dynamic result = tree.ExecuteQuery(new TestDataContext().FillWithTestData(), null, null).Data!["user"]!; @@ -103,8 +109,8 @@ public void ThrowsOnMissingRequiredArguments() { Query = @"query { - user { id } - }", + user { id } + }", }; var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); Assert.NotNull(result.Errors); @@ -117,10 +123,11 @@ public void SupportsArgumentsDefaultValue() var schema = SchemaBuilder.FromObject(); // Add a argument field with a default parameter schema.Query().AddField("me", new { id = 100 }, (ctx, param) => ctx.Users.Where(u => u.Id == param.id).FirstOrDefault(), "Return me, or someone else"); - var tree = new GraphQLCompiler(schema).Compile( + var tree = GraphQLParser.Parse( @"query { - me { id } - }" + me { id } + }", + schema ); dynamic result = tree.ExecuteQuery(new TestDataContext().FillWithTestData(), null, null).Data!["me"]!; @@ -138,11 +145,12 @@ public void SupportsDefaultArgumentsInNonRoot() var schema = SchemaBuilder.FromObject(); schema.AddEnum("HeightUnit", typeof(HeightUnit), "Unit of height measurement"); schema.Type().ReplaceField("height", new { unit = HeightUnit.Cm }, (p, param) => p.GetHeight(param.unit), "Return me, or someone else"); - var result = new GraphQLCompiler(schema) - .Compile( + var result = GraphQLParser + .Parse( @"query { people { id height } - }" + }", + schema ) .ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); @@ -164,11 +172,12 @@ public void SupportsArgumentsInNonRootAndEnum() var schema = SchemaBuilder.FromObject(); schema.AddEnum("HeightUnit", typeof(HeightUnit), "Unit of height measurement"); schema.Type().ReplaceField("height", new { unit = HeightUnit.Cm }, (p, param) => p.GetHeight(param.unit), "Return me, or someone else"); - var tree = new GraphQLCompiler(schema) - .Compile( + var tree = GraphQLParser + .Parse( @"query { people { height(unit: Meter) } - }" + }", + schema ) .ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); @@ -194,7 +203,7 @@ public void SupportsArgumentsInNonRootAndEnumAsVar() }", Variables = new QueryVariables { { "unitType", "Meter" } }, }; - var tree = new GraphQLCompiler(schema).Compile(gql).ExecuteQuery(new TestDataContext().FillWithTestData(), null, gql.Variables, null); + var tree = GraphQLParser.Parse(gql, schema).ExecuteQuery(new TestDataContext().FillWithTestData(), null, gql.Variables, null); dynamic result = tree.Data!["people"]!; Assert.Equal(1, Enumerable.Count(result)); @@ -209,11 +218,12 @@ public void SupportsArgumentsGuid() var schema = SchemaBuilder.FromObject(); MakePersonIdGuid(schema); // Add a argument field with a require parameter - var tree = new GraphQLCompiler(schema) - .Compile( + var tree = GraphQLParser + .Parse( @"query { person(id: ""cccccccc-bbbb-4444-1111-ccddeeff0033"") { id projects { id name } } - }" + }", + schema ) .ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); @@ -238,7 +248,7 @@ public void SupportsArgumentsGuidAsVar() }", Variables = new QueryVariables { { "id", "cccccccc-bbbb-4444-1111-ccddeeff0033" } }, }; - var tree = new GraphQLCompiler(schema).Compile(gql).ExecuteQuery(new TestDataContext().FillWithTestData(), null, gql.Variables); + var tree = GraphQLParser.Parse(gql, schema).ExecuteQuery(new TestDataContext().FillWithTestData(), null, gql.Variables); dynamic user = tree.Data!["person"]!; // we only have the fields requested @@ -254,11 +264,12 @@ public void SupportsArgumentsInGraph() MakePersonIdGuid(schema); schema.Type().AddField("project", new { pid = ArgumentHelper.Required() }, (p, args) => p.Projects.FirstOrDefault(s => s.Id == args.pid), "Return a specific project"); // Add a argument field with a require parameter - var tree = new GraphQLCompiler(schema) - .Compile( + var tree = GraphQLParser + .Parse( @"query { person(id: ""cccccccc-bbbb-4444-1111-ccddeeff0033"") { id project(pid: 55) { id name } } - }" + }", + schema ) .ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); @@ -275,14 +286,15 @@ public void QueryWithUnknownArgument() { var schema = SchemaBuilder.FromObject(); // Add a argument field with a require parameter - var e = Assert.Throws(() => + var e = Assert.Throws(() => { - var tree = new GraphQLCompiler(schema).Compile( + var tree = GraphQLParser.Parse( @" query MyQuery($limit: Int = 10) { people(limit: $limit) { id name projects { id name } } } - " + ", + schema ); }); Assert.Equal("No argument 'limit' found on field 'people'", e.Message); @@ -294,15 +306,15 @@ public void FloatArg() var schema = SchemaBuilder.FromObject(); schema.Query().ReplaceField("users", new { f = (float?)null }, (db, p) => db.Users, "Testing float"); - var gql = new GraphQLCompiler(schema).Compile( - @" - query { - users(f: 4.3) { id } - }" + var gql = GraphQLParser.Parse( + @"query { + users(f: 4.3) { id } + }", + schema ); var context = new TestDataContext().FillWithTestData(); var qr = gql.ExecuteQuery(context, null, null); - dynamic users = (dynamic)qr.Data!["users"]!; + dynamic users = qr.Data!["users"]!; // we only have the fields requested Assert.Equal(1, Enumerable.Count(users)); } @@ -313,11 +325,12 @@ public void StringArg() var schema = SchemaBuilder.FromObject(); schema.Query().ReplaceField("users", new { str = (string?)null }, (db, p) => db.Users.WhereWhen(u => u.Field2.Contains(p.str!), !string.IsNullOrEmpty(p.str)), "Testing string"); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { users(str: ""3"") { id } - }" + }", + schema ); var context = new TestDataContext().FillWithTestData(); var qr = gql.ExecuteQuery(context, null, null); @@ -332,11 +345,12 @@ public void ListArg() var schema = SchemaBuilder.FromObject(); schema.Query().ReplaceField("people", new { names = (List?)null }, (db, p) => db.People.WhereWhen(per => p.names!.Any(a => a == per.Name), p.names != null), "Testing list"); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { people(names: [""bill"", ""jill""]) { name } - }" + }", + schema ); var context = new TestDataContext().FillWithTestData(); context.People.Add( @@ -351,7 +365,7 @@ public void ListArg() } ); var qr = gql.ExecuteQuery(context, null, null); - dynamic people = (dynamic)qr.Data!["people"]!; + dynamic people = qr.Data!["people"]!; // we only have the fields requested Assert.Equal(2, context.People.Count); Assert.Equal(1, Enumerable.Count(people)); @@ -363,11 +377,12 @@ public void ArrayArg() var schema = SchemaBuilder.FromObject(); schema.Query().ReplaceField("people", new { names = (string[]?)null }, (db, p) => db.People.WhereWhen(per => p.names!.Any(a => a == per.Name), p.names != null), "Testing list"); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { people(names: [""bill"", ""jill""]) { name } - }" + }", + schema ); var context = new TestDataContext().FillWithTestData(); context.People.Add( @@ -405,11 +420,12 @@ public void EnumerableArg() "Testing list" ); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { people(names: [""bill"", ""jill""]) { name } - }" + }", + schema ); var context = new TestDataContext().FillWithTestData(); context.People.Add( @@ -424,7 +440,7 @@ public void EnumerableArg() } ); var qr = gql.ExecuteQuery(context, null, null); - dynamic people = (dynamic)qr.Data!["people"]!; + dynamic people = qr.Data!["people"]!; // we only have the fields requested Assert.Equal(2, context.People.Count); Assert.Equal(1, Enumerable.Count(people)); @@ -437,11 +453,11 @@ public void ObjectArg() schema.AddInputType("PersonArg", "PersonArgs").AddAllFields(); schema.Query().ReplaceField("people", new { options = (PersonArg?)null }, (db, p) => db.People.WhereWhen(per => per.Name == p.options!.name, p.options != null), "Testing list"); - var gql = new GraphQLCompiler(schema).Compile( - @" - query { - people(options: {name: ""jill""}) { name } - }" + var gql = GraphQLParser.Parse( + @"query { + people(options: {name: ""jill""}) { name } + }", + schema ); var context = new TestDataContext().FillWithTestData(); context.People.Add( @@ -468,14 +484,15 @@ public void TestSameNameArgsOnDifferentFields() var schema = SchemaBuilder.Create(); schema.AddType("Person info").AddAllFields(); schema.Query().AddField("people", db => db.People, "List of people"); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { people { task(id: 1) { id name } project(id: 2) { id name } } - }" + }", + schema ); var context = new TestDataContext(); context.People.Add( @@ -483,8 +500,8 @@ public void TestSameNameArgsOnDifferentFields() { Id = 99, Name = "jill", - Projects = [new Project { Id = 1, Name = "Project 1" }, new Project { Id = 2, Name = "Project 2" },], - Tasks = [new Task { Id = 1, Name = "Task 1" }, new Task { Id = 2, Name = "Task 2" },], + Projects = [new Project { Id = 1, Name = "Project 1" }, new Project { Id = 2, Name = "Project 2" }], + Tasks = [new Task { Id = 1, Name = "Task 1" }, new Task { Id = 2, Name = "Task 2" }], } ); var qr = gql.ExecuteQuery(context, null, null); @@ -504,20 +521,20 @@ public void TestSameNameArgsOnDifferentFieldsRoot() schema.AddType("Task info").AddAllFields(); schema.Query().AddField("task", new { id = ArgumentHelper.Required() }, (db, args) => db.Tasks.FirstOrDefault(t => t.Id == args.id), "Get task"); schema.Query().AddField("project", new { id = ArgumentHelper.Required() }, (db, args) => db.Projects.FirstOrDefault(t => t.Id == args.id), "Get project"); - var gql = new GraphQLCompiler(schema).Compile( - @" - query { - task(id: 1) { id name } - project(id: 2) { id name } - }" + var gql = GraphQLParser.Parse( + @"query { + task(id: 1) { id name } + project(id: 2) { id name } + }", + schema ); var context = new TestDataContext { - Projects = [new Project { Id = 1, Name = "Project 1" }, new Project { Id = 2, Name = "Project 2" },], + Projects = [new Project { Id = 1, Name = "Project 1" }, new Project { Id = 2, Name = "Project 2" }], Tasks = new List { - new Task { Id = 1, Name = "Task 1" }, - new Task { Id = 2, Name = "Task 2" }, + new() { Id = 1, Name = "Task 1" }, + new() { Id = 2, Name = "Task 2" }, }, }; var qr = gql.ExecuteQuery(context, null, null); @@ -537,14 +554,15 @@ public void TestSameNameArgsOnSameFieldWithAlias() var schema = SchemaBuilder.Create(); schema.AddType("Person info").AddAllFields(); schema.Query().AddField("people", db => db.People, "List of people"); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { people { task(id: 1) { id name } task2: task(id: 2) { id name } } - }" + }", + schema ); var context = new TestDataContext(); context.People.Add( @@ -552,7 +570,7 @@ public void TestSameNameArgsOnSameFieldWithAlias() { Id = 99, Name = "jill", - Tasks = [new Task { Id = 1, Name = "Task 1" }, new Task { Id = 2, Name = "Task 2" },], + Tasks = [new Task { Id = 1, Name = "Task 1" }, new Task { Id = 2, Name = "Task 2" }], } ); var qr = gql.ExecuteQuery(context, null, null); @@ -571,12 +589,13 @@ public void TestSameNameArgsOnSameFieldWithAliasRoot() schema.AddType("Task info").AddAllFields(); schema.Query().AddField("task", new { id = ArgumentHelper.Required() }, (db, args) => db.Tasks.FirstOrDefault(t => t.Id == args.id), "Get task"); schema.Query().AddField("project", new { id = ArgumentHelper.Required() }, (db, args) => db.Projects.FirstOrDefault(t => t.Id == args.id), "Get project"); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { task(id: 1) { id name } task2: task(id: 2) { id name } - }" + }", + schema ); var context = new TestDataContext { Tasks = [new Task { Id = 1, Name = "Task 1" }, new Task { Id = 2, Name = "Task 2" }] }; var qr = gql.ExecuteQuery(context, null, null); @@ -596,10 +615,11 @@ public void ObjectArgViaConstructor() schema.AddInputType("PersonArgConstructor", "PersonArgConstructors").AddAllFields(); schema.Query().ReplaceField("people", new { options = (PersonArgConstructor?)null }, (db, p) => db.People.WhereWhen(per => per.Name == p.options!.Name, p.options != null), "Testing list"); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @"query { people(options: {name: ""jill""}) { name } - }" + }", + schema ); var context = new TestDataContext().FillWithTestData(); context.People.Add( diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/ArgumentTrackerTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/ArgumentTrackerTests.cs index 5b80a18a..a714f999 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/ArgumentTrackerTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/ArgumentTrackerTests.cs @@ -283,7 +283,17 @@ public void TestInputTypePropertySetTrackingMutation_IsSet() { var schema = SchemaBuilder.FromObject(); schema.AddInputType(nameof(TestInputTracking)).AddAllFields(); - schema.Mutation().Add("doTest", (TestInputTracking input) => input); + schema + .Mutation() + .Add( + "doTest", + (TestInputTracking input) => + { + Assert.True(input.IsSet(nameof(TestInputTracking.Id))); + Assert.False(input.IsSet(nameof(TestInputTracking.Name))); + return true; + } + ); var gql = new QueryRequest { Query = """ @@ -303,9 +313,7 @@ mutation M ($input: TestInputTracking) { var testSchema = new TestDataContext(); var results = schema.ExecuteRequestWithContext(gql, testSchema, null, null); Assert.Null(results.Errors); - Assert.NotNull(results.Data!["doTest"]); - var testData = (IArgumentsTracker)results.Data!["doTest"]!; - Assert.True(testData.IsSet(nameof(TestInputTracking.Id))); + Assert.True((bool)results.Data!["doTest"]!); } [Fact] @@ -313,7 +321,18 @@ public void TestInputTypePropertySetTrackingMutation_NotSet() { var schema = SchemaBuilder.FromObject(); schema.AddInputType(nameof(TestInputTracking)).AddAllFields(); - schema.Mutation().Add("doTest", (TestInputTracking input) => input); + schema + .Mutation() + .Add( + "doTest", + (TestInputTracking input) => + { + Assert.False(input.IsSet(nameof(TestInputTracking.Id))); + Assert.False(input.IsSet(nameof(TestInputTracking.Name))); + + return true; + } + ); var gql = new QueryRequest { Query = """ @@ -326,9 +345,7 @@ mutation M () { var testSchema = new TestDataContext(); var results = schema.ExecuteRequestWithContext(gql, testSchema, null, null); Assert.Null(results.Errors); - Assert.NotNull(results.Data!["doTest"]); - var testData = (IArgumentsTracker)results.Data!["doTest"]!; - Assert.False(testData.IsSet(nameof(TestInputTracking.Id))); + Assert.True((bool)results.Data!["doTest"]!); } [Theory] @@ -341,17 +358,28 @@ public void TestNestedInputTypePropertySetTrackingMutation_IsSet(bool setParent, var schema = SchemaBuilder.FromObject(); schema.AddInputType(nameof(TestInputTracking)).AddAllFields(); schema.AddInputType(nameof(NestedTestInputTracking)).AddAllFields(); - schema.Mutation().Add("doTest", (NestedTestInputTracking input) => input); + schema + .Mutation() + .Add( + "doTest", + (NestedTestInputTracking input) => + { + return new NestedWhatIsSet { ParentIdSet = input.IsSet(nameof(NestedTestInputTracking.Id)), ChildIdSet = input.Child?.IsSet(nameof(TestInputTracking.Id)) ?? false }; + } + ); var gql = new QueryRequest { Query = $$""" mutation M () { - doTest(input : { + doTest(input : { {{(setParent ? "id: \"03d539f8-6bbc-4b62-8f7f-b55c7eb242e6\"" : "")}} child: { {{(setChild ? "id: \"03d539f8-6bbc-4b62-8f7f-b55c7eb242e7\"" : "")}} - } - }) + } + }) { + parentIdSet + childIdSet + } } """, }; @@ -360,10 +388,9 @@ mutation M () { var results = schema.ExecuteRequestWithContext(gql, testSchema, null, null); Assert.Null(results.Errors); Assert.NotNull(results.Data!["doTest"]); - var testData = (NestedTestInputTracking)results.Data!["doTest"]!; - Assert.NotNull(testData.Child); - Assert.Equal(setParent, testData.IsSet(nameof(NestedTestInputTracking.Id))); - Assert.Equal(setChild, testData.Child.IsSet(nameof(TestInputTracking.Id))); + dynamic testData = results.Data!["doTest"]!; + Assert.Equal(setParent, (bool)testData.parentIdSet); + Assert.Equal(setChild, (bool)testData.childIdSet); } [Fact] @@ -372,11 +399,19 @@ public void TestPersistedInputTypePropertySetTrackingMutation_IsSet() var testSchema = new TestDataContext(); var schema = SchemaBuilder.FromObject(); schema.AddInputType(nameof(TestInputTracking)).AddAllFields(); - schema.Mutation().Add("doTest", (TestInputTracking input) => input); + schema + .Mutation() + .Add( + "doTest", + (TestInputTracking input) => + { + return new WhatIsSet { IdSet = input.IsSet(nameof(TestInputTracking.Id)), NameSet = input.IsSet(nameof(TestInputTracking.Name)) }; + } + ); var query = """ mutation M ($input: TestInputTracking) { - doTest(input : $input) + doTest(input : $input) { nameSet idSet } } """; var hash = QueryCache.ComputeHash(query); @@ -401,9 +436,9 @@ mutation M ($input: TestInputTracking) { }; var results = schema.ExecuteRequestWithContext(gql, testSchema, null, null); - var testData = (IArgumentsTracker)results.Data!["doTest"]!; - Assert.True(testData.IsSet(nameof(TestInputTracking.Id))); - Assert.False(testData.IsSet(nameof(TestInputTracking.Name))); + Assert.Null(results.Errors); + Assert.True((bool)(results.Data!["doTest"]! as dynamic).idSet); + Assert.False((bool)(results.Data!["doTest"]! as dynamic).nameSet); gql.Query = null; gql.Variables = new QueryVariables() @@ -415,9 +450,9 @@ mutation M ($input: TestInputTracking) { }; results = schema.ExecuteRequestWithContext(gql, testSchema, null, null); - testData = (IArgumentsTracker)results.Data!["doTest"]!; - Assert.True(testData.IsSet(nameof(TestInputTracking.Name))); - Assert.False(testData.IsSet(nameof(TestInputTracking.Id))); + Assert.Null(results.Errors); + Assert.False((bool)(results.Data!["doTest"]! as dynamic).idSet); + Assert.True((bool)(results.Data!["doTest"]! as dynamic).nameSet); } [Fact] @@ -722,4 +757,16 @@ private class NestedTestInputTracking : ArgumentsTracker public Guid? Id { get; set; } public TestInputTracking? Child { get; set; } } + + private class WhatIsSet + { + public bool IdSet { get; set; } + public bool NameSet { get; set; } + } + + private class NestedWhatIsSet + { + public bool ParentIdSet { get; set; } + public bool ChildIdSet { get; set; } + } } diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/AsyncTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/AsyncTests.cs index 9647f4d6..67d5daab 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/AsyncTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/AsyncTests.cs @@ -1,7 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; -using EntityGraphQL.Compiler; +using System.Threading.Tasks; using EntityGraphQL.Schema; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -66,7 +67,7 @@ public void TestFieldRequiresGenericTask() { var schema = SchemaBuilder.FromObject(); // Task<> returns are now supported with automatic async resolution - Assert.Throws(() => + Assert.Throws(() => { schema.Type().AddField("age", "Returns persons age").ResolveAsync((ctx, srv) => srv.GetAgeAsyncNoResult(ctx.Birthday)); }); @@ -115,7 +116,6 @@ public async System.Threading.Tasks.Task TestCancellationTokenSupport() Assert.Equal(25, age1); // Test 2: With cancelled token should work for now since we're using sync method - // TODO: Add async test when proper CancellationToken support is exposed in public API var cts = new CancellationTokenSource(); cts.Cancel(); // Cancel immediately @@ -126,4 +126,165 @@ public async System.Threading.Tasks.Task TestCancellationTokenSupport() Assert.Equal("The operation was canceled.", result2.Errors[0].Message); Assert.Null(result2.Data); } + + [Fact] + public void TestAsyncGraphQLFieldReturnsCorrectSchemaType_Issue488() + { + // Test for https://github.com/EntityGraphQL/EntityGraphQL/issues/488 + var schema = SchemaBuilder.FromObject(); + var sdl = schema.ToGraphQLSchemaString(); + + // Verify it's defined as an array type (with square brackets) + // The async Task wrapper should be properly unwrapped to recognize the IEnumerable + Assert.Contains("jobs(search: String!): [Job!]!", sdl); + + // make sure it executes + var gql = new QueryRequest + { + Query = + @"{ + jobs(search: ""Dev"") { + id + name + } + }", + }; + var context = new JobContext(); + context.AllJobs.Add(new Job { Id = 1, Name = "DevOps Engineer" }); + context.AllJobs.Add(new Job { Id = 2, Name = "Marketing Manager" }); + var result = schema.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + var jobs = (IEnumerable)result.Data!["jobs"]!; + Assert.Single(jobs); // one job added so should have one result + } + + [Fact] + public void TestAsyncGraphQLFieldReturnsCorrectSchemaType_IQueryable_Issue488() + { + // Test for https://github.com/EntityGraphQL/EntityGraphQL/issues/488 + // Test async method returning IQueryable - verify schema type is correct + var schema = SchemaBuilder.FromObject(); + var sdl = schema.ToGraphQLSchemaString(); + + // Verify it's defined as an array type (the async Task wrapper should be unwrapped) + Assert.Contains("jobsQueryable(search: String!): [Job!]!", sdl); + + // make sure it executes + var gql = new QueryRequest + { + Query = + @"{ + jobsQueryable(search: ""Dev"") { + id + name + } + }", + }; + var context = new JobContext(); + context.AllJobs.Add(new Job { Id = 1, Name = "DevOps Engineer" }); + context.AllJobs.Add(new Job { Id = 2, Name = "Marketing Manager" }); + var result = schema.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + var jobs = (IEnumerable)result.Data!["jobsQueryable"]!; + Assert.Single(jobs); // one job added so should have one result + } + + [Fact] + public void TestAsyncGraphQLFieldReturnsCorrectSchemaType_Object_Issue488() + { + // Test for https://github.com/EntityGraphQL/EntityGraphQL/issues/488 + // Test async method returning an object + var schema = SchemaBuilder.FromObject(); + var sdl = schema.ToGraphQLSchemaString(); + + // Verify it's defined as Job type (not wrapped in array) + Assert.Contains("jobById(id: Int!): Job", sdl); + + var gql = new QueryRequest + { + Query = + @"{ + jobById(id: 1) { + id + name + } + }", + }; + var context = new JobContext(); + context.AllJobs.Add(new Job { Id = 1, Name = "DevOps Engineer" }); + context.AllJobs.Add(new Job { Id = 2, Name = "Marketing Manager" }); + var result = schema.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + dynamic job = result.Data!["jobById"]!; + Assert.Equal(1, job.id); + Assert.Equal("DevOps Engineer", job.name); + } + + [Fact] + public void TestAsyncGraphQLFieldReturnsCorrectSchemaType_Scalar_Issue488() + { + // Test for https://github.com/EntityGraphQL/EntityGraphQL/issues/488 + // Test async method returning a scalar + var schema = SchemaBuilder.FromObject(); + var sdl = schema.ToGraphQLSchemaString(); + + // Verify it's defined as Int type + Assert.Contains("jobCount: Int!", sdl); + + var gql = new QueryRequest { Query = @"{ jobCount }" }; + var context = new JobContext(); + context.AllJobs.Add(new Job { Id = 1, Name = "DevOps Engineer" }); + context.AllJobs.Add(new Job { Id = 2, Name = "Marketing Manager" }); + var result = schema.ExecuteRequestWithContext(gql, context, null, null); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + Assert.Equal(2, result.Data!["jobCount"]); + } + + private class JobContext + { + [GraphQLIgnore] + public List AllJobs { get; set; } = []; + + [GraphQLField("jobs", "Search for jobs")] + public async Task> JobSearch(string search) + { + // Simulate async operation + await System.Threading.Tasks.Task.Delay(1); + return AllJobs.Where(j => j.Name.Contains(search)); + } + + [GraphQLField("jobsQueryable", "Search for jobs returning IQueryable")] + public async Task> JobSearchQueryable(string search) + { + // Simulate async operation + await System.Threading.Tasks.Task.Delay(1); + return AllJobs.Where(j => j.Name.Contains(search)).AsQueryable(); + } + + [GraphQLField("jobById", "Get a job by ID")] + public async Task GetJobById(int id) + { + // Simulate async operation + await System.Threading.Tasks.Task.Delay(1); + return AllJobs.FirstOrDefault(j => j.Id == id); + } + + [GraphQLField("jobCount", "Get total job count")] + public async Task GetJobCount() + { + // Simulate async operation + await System.Threading.Tasks.Task.Delay(1); + return AllJobs.Count; + } + } + + private class Job + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + } } diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/DirectiveOnVisitTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/DirectiveOnVisitTests.cs index 19ee5485..70e8d025 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/DirectiveOnVisitTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/DirectiveOnVisitTests.cs @@ -13,7 +13,7 @@ public class DirectiveOnVisitTests public void TestOnVisitListFieldRoot() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.FIELD); + var directive = new MyDirective(ExecutableDirectiveLocation.Field); schema.AddDirective(directive); var query = new QueryRequest { @@ -25,14 +25,14 @@ people @myDirective { }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FIELD, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Field, directive.WasVisited); } [Fact] public void TestOnVisitObjectFieldRoot() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.FIELD); + var directive = new MyDirective(ExecutableDirectiveLocation.Field); schema.AddDirective(directive); var query = new QueryRequest { @@ -44,14 +44,14 @@ public void TestOnVisitObjectFieldRoot() }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FIELD, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Field, directive.WasVisited); } [Fact] public void TestOnVisitScalarFieldRoot() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.FIELD); + var directive = new MyDirective(ExecutableDirectiveLocation.Field); schema.AddDirective(directive); var query = new QueryRequest { @@ -61,14 +61,14 @@ totalPeople @myDirective }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FIELD, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Field, directive.WasVisited); } [Fact] public void TestOnVisitListField() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.FIELD); + var directive = new MyDirective(ExecutableDirectiveLocation.Field); schema.AddDirective(directive); var query = new QueryRequest { @@ -83,14 +83,14 @@ projects @myDirective { }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FIELD, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Field, directive.WasVisited); } [Fact] public void TestOnVisitObjectField() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.FIELD); + var directive = new MyDirective(ExecutableDirectiveLocation.Field); schema.AddDirective(directive); var query = new QueryRequest { @@ -105,14 +105,14 @@ manager @myDirective { }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FIELD, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Field, directive.WasVisited); } [Fact] public void TestOnVisitScalarField() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.FIELD); + var directive = new MyDirective(ExecutableDirectiveLocation.Field); schema.AddDirective(directive); var query = new QueryRequest { @@ -125,14 +125,14 @@ name @myDirective }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FIELD, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Field, directive.WasVisited); } [Fact] public void TestOnVisitFragmentDef() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.FRAGMENT_DEFINITION); + var directive = new MyDirective(ExecutableDirectiveLocation.FragmentDefinition); schema.AddDirective(directive); var query = new QueryRequest { @@ -147,14 +147,14 @@ fragment myFragment on Person @myDirective { }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FRAGMENT_DEFINITION, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.FragmentDefinition, directive.WasVisited); } [Fact] public void TestOnVisitFragmentSpread() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.FRAGMENT_SPREAD); + var directive = new MyDirective(ExecutableDirectiveLocation.FragmentSpread); schema.AddDirective(directive); var query = new QueryRequest { @@ -169,20 +169,20 @@ fragment myFragment on Person { }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FRAGMENT_SPREAD, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.FragmentSpread, directive.WasVisited); } [Fact] public void TestOnVisitInlineFragment() { var schema = SchemaBuilder.FromObject(new SchemaBuilderOptions { AutoCreateInterfaceTypes = true }); - var directive = new MyDirective(ExecutableDirectiveLocation.INLINE_FRAGMENT); + var directive = new MyDirective(ExecutableDirectiveLocation.InlineFragment); schema.AddDirective(directive); schema.Type().AddPossibleType(); schema.Type().AddPossibleType(); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @"query { animals { __typename @@ -195,21 +195,22 @@ ... on Cat { lives } } - }" + }", + schema ); var context = new TestUnionDataContext(); context.Animals.Add(new Dog() { Name = "steve", HasBone = true }); context.Animals.Add(new Cat() { Name = "george", Lives = 9 }); gql.ExecuteQuery(context, null, null); - Assert.Equal(ExecutableDirectiveLocation.INLINE_FRAGMENT, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.InlineFragment, directive.WasVisited); } [Fact] public void TestOnVisitMutationField() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.FIELD); + var directive = new MyDirective(ExecutableDirectiveLocation.Field); schema.AddDirective(directive); schema .Mutation() @@ -230,14 +231,14 @@ doStuff @myDirective { }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FIELD, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Field, directive.WasVisited); } [Fact] public void TestOnVisitMutationInnerField() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.FIELD); + var directive = new MyDirective(ExecutableDirectiveLocation.Field); schema.AddDirective(directive); schema .Mutation() @@ -258,14 +259,14 @@ id @myDirective }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FIELD, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Field, directive.WasVisited); } [Fact] public void TestOnVisitMutationStatement() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.MUTATION); + var directive = new MyDirective(ExecutableDirectiveLocation.Mutation); schema.AddDirective(directive); schema .Mutation() @@ -286,14 +287,14 @@ public void TestOnVisitMutationStatement() }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.MUTATION, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Mutation, directive.WasVisited); } [Fact] public void TestOnVisitQueryStatement() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.QUERY); + var directive = new MyDirective(ExecutableDirectiveLocation.Query); schema.AddDirective(directive); var query = new QueryRequest { @@ -303,7 +304,7 @@ public void TestOnVisitQueryStatement() }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.QUERY, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Query, directive.WasVisited); } [Fact] @@ -312,7 +313,7 @@ public void TestOnVisitSubscriptionStatement() var schema = SchemaBuilder.FromObject(); schema.AddType("Message info").AddAllFields(); schema.Subscription().AddFrom(); - var directive = new MyDirective(ExecutableDirectiveLocation.SUBSCRIPTION); + var directive = new MyDirective(ExecutableDirectiveLocation.Subscription); schema.AddDirective(directive); var query = new QueryRequest { @@ -322,7 +323,7 @@ public void TestOnVisitSubscriptionStatement() }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.SUBSCRIPTION, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Subscription, directive.WasVisited); } [Fact] @@ -331,7 +332,7 @@ public void TestOnVisitSubscriptionField() var schema = SchemaBuilder.FromObject(); schema.AddType("Message info").AddAllFields(); schema.Subscription().AddFrom(); - var directive = new MyDirective(ExecutableDirectiveLocation.FIELD); + var directive = new MyDirective(ExecutableDirectiveLocation.Field); schema.AddDirective(directive); var query = new QueryRequest { @@ -341,14 +342,14 @@ onMessage @myDirective { id } }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FIELD, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.Field, directive.WasVisited); } [Fact] public void TestOnVisitVariableDef() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.VARIABLE_DEFINITION); + var directive = new MyDirective(ExecutableDirectiveLocation.VariableDefinition); schema.AddDirective(directive); var query = new QueryRequest { @@ -358,14 +359,14 @@ public void TestOnVisitVariableDef() }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.VARIABLE_DEFINITION, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.VariableDefinition, directive.WasVisited); } [Fact] public void TestOnVisitCalledOnce() { var schema = SchemaBuilder.FromObject(); - var directive = new MyDirective(ExecutableDirectiveLocation.FRAGMENT_SPREAD); + var directive = new MyDirective(ExecutableDirectiveLocation.FragmentSpread); schema.AddDirective(directive); var query = new QueryRequest { @@ -381,7 +382,7 @@ fragment myFragment on Person { }", }; schema.ExecuteRequestWithContext(query, new TestDataContext().FillWithTestData(), null, null, null); - Assert.Equal(ExecutableDirectiveLocation.FRAGMENT_SPREAD, directive.WasVisited); + Assert.Equal(ExecutableDirectiveLocation.FragmentSpread, directive.WasVisited); Assert.Equal(1, directive.Calls); } } diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/DirectiveTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/DirectiveTests.cs index 0a01562e..30803008 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/DirectiveTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/DirectiveTests.cs @@ -339,24 +339,24 @@ internal class ExampleDirective : DirectiveProcessor public override string Description => "Actually does nothing"; - public override List Location => [ExecutableDirectiveLocation.FIELD]; + public override List Location => [ExecutableDirectiveLocation.Field]; } internal class FormatDirective : DirectiveProcessor { public override string Name => "format"; public override string Description => "Formats DateTime scalar values"; - public override List Location => [ExecutableDirectiveLocation.FIELD]; + public override List Location => [ExecutableDirectiveLocation.Field]; public override IGraphQLNode VisitNode(ExecutableDirectiveLocation location, IGraphQLNode? node, object? arguments) { - if (location == ExecutableDirectiveLocation.FIELD && arguments is FormatDirectiveArgs args) + if (location == ExecutableDirectiveLocation.Field && arguments is FormatDirectiveArgs args) { if (node is GraphQLScalarField fieldNode) { var expression = fieldNode.NextFieldContext!; if (expression.Type != typeof(DateTime) && expression.Type != typeof(DateTime?)) - throw new EntityGraphQLException("The format directive can only be used on DateTime fields"); + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "The format directive can only be used on DateTime fields"); if (expression.Type == typeof(DateTime?)) expression = Expression.Property(expression, "Value"); diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/FilteredFieldTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/FilteredFieldTests.cs index c737c175..8c3c906a 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/FilteredFieldTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/FilteredFieldTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using EntityGraphQL.Extensions; using EntityGraphQL.Schema; using EntityGraphQL.Schema.FieldExtensions; @@ -88,7 +89,7 @@ public void TestWhereWhenOnNonRootField() Assert.Equal("tasks", projectType.GetFields()[0].Name); } - [Fact(Skip = "Not implemented")] + [Fact] public void TestOffsetPagingWithOthersAndServices() { var schema = SchemaBuilder.FromObject(); @@ -99,7 +100,7 @@ public void TestOffsetPagingWithOthersAndServices() Id = 1, Name = "Jill", LastName = "Frank", - Birthday = DateTime.Now.AddYears(22), + Birthday = DateTime.Now.AddYears(-22), } ); data.People.Add( @@ -108,7 +109,7 @@ public void TestOffsetPagingWithOthersAndServices() Id = 2, Name = "Cheryl", LastName = "Frank", - Birthday = DateTime.Now.AddYears(10), + Birthday = DateTime.Now.AddYears(-10), } ); @@ -138,7 +139,7 @@ name id age lastName Assert.Equal("Jill", person1.name); } - [Theory(Skip = "Not implemented")] + [Theory] [InlineData(true)] [InlineData(false)] public void TestFilterWithServiceReference(bool separateServices) @@ -181,12 +182,123 @@ name id age lastName serviceCollection.AddSingleton(ager); var result = schema.ExecuteRequestWithContext(gql, data, serviceCollection.BuildServiceProvider(), null, new ExecutionOptions { ExecuteServiceFieldsSeparately = separateServices }); - Assert.Null(result.Errors); + // Both scenarios should now work - filtering with service fields is supported + Assert.Null(result.Errors); dynamic people = result.Data!["people"]!; Assert.Equal(1, Enumerable.Count(people)); var person1 = Enumerable.ElementAt(people, 0); Assert.Equal("Frank", person1.lastName); Assert.Equal("Jill", person1.name); } + + [Fact] + public void TestUseFilterOnFieldWithExistingArgumentsAddsFilterArgumentWithoutDefault() + { + var schema = SchemaBuilder.FromObject(); + + // Add a field with existing arguments then apply UseFilter + schema + .Type() + .ReplaceField("projects", new { search = (string?)null, limit = 10 }, (ctx, args) => ctx.Projects.Take(args.limit), "List of projects with search and limit") + .UseFilter(); + + // Get the GraphQL schema as SDL + var sdl = schema.ToGraphQLSchemaString(); + + // Look for the projects field definition and check arguments + Assert.Contains("projects(", sdl); + Assert.Contains("search: String", sdl); + Assert.Contains("limit: Int!", sdl); + Assert.Contains("filter: String", sdl); + + // Ensure filter argument doesn't have default value + Assert.DoesNotContain("filter: String = ", sdl); + } + + [Fact(Skip = "This test currently fails - see GitHub issue #378")] + public void ReproducesGitHubIssue378_CountMethodNotFoundOnOffsetPage() + { + var schema = SchemaBuilder.FromObject(); + + // Set up the exact hierarchical scenario from GitHub issue #378: + // incident -> enquiries (with filtering and paging) -> enquirerDaps (with filtering and paging) + schema.Type().GetField("enquiries", null).UseFilter().UseOffsetPaging(); + schema.Type().GetField("enquirerDaps", null).UseFilter().UseOffsetPaging(); + + var gql = new QueryRequest + { + Query = + @" + query { + incident(id: 1) { + id + enquiries(filter: ""enquirerDaps.selectMany(items).count() > 0"", skip: 0, take: null) { + items { + enquirerDaps(filter: ""dapId==209"") { + items { + dapId + } + } + } + } + } + }", + }; + + var context = new IncidentContext().FillWithIncidentData(); + var result = schema.ExecuteRequestWithContext(gql, context, null, null); + + // This test reproduces GitHub issue #378: + // https://github.com/EntityGraphQL/EntityGraphQL/issues/378 + Assert.Null(result.Errors); + dynamic incident = ((IDictionary)result.Data!)["incident"]; + Assert.Equal(3, incident.enquiries.items); + var enquiry = Enumerable.First(incident.enquiries.items); + Assert.Equal(1, enquiry.id); + Assert.Single(enquiry.enquirerDaps.items); + var enquirerDap = Enumerable.First(enquiry.enquirerDaps.items); + Assert.Equal(209, enquirerDap.dapId); + } + + private class IncidentContext + { + public List Incidents { get; set; } = []; + + public IncidentContext FillWithIncidentData() + { + var incident = new Incident { Id = 1 }; + + // Enquiry with matching enquirerDaps (should be returned) + var enquiryWithDaps = new Enquiry { Id = 1, EnquirerDaps = [new EnquirerDap { DapId = 209 }, new EnquirerDap { DapId = 210 }] }; + + // Enquiry with no enquirerDaps (should be filtered out) + var enquiryWithoutDaps = new Enquiry { Id = 2, EnquirerDaps = [] }; + + // Enquiry with non-matching enquirerDaps (should be filtered out) + var enquiryWithOtherDaps = new Enquiry { Id = 3, EnquirerDaps = [new EnquirerDap { DapId = 999 }] }; + + incident.Enquiries = [enquiryWithDaps, enquiryWithoutDaps, enquiryWithOtherDaps]; + Incidents.Add(incident); + + return this; + } + } + + private class Incident + { + public int Id { get; set; } + public List Enquiries { get; set; } = []; + } + + private class Enquiry + { + public int Id { get; set; } + public List EnquirerDaps { get; set; } = []; + } + + private class EnquirerDap + { + public int DapId { get; set; } + } } diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/FragmentTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/FragmentTests.cs index 572822d2..f70abd2c 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/FragmentTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/FragmentTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using EntityGraphQL.Compiler; @@ -15,7 +14,7 @@ public void SupportsFragmentSelectionSyntax() { var schemaProvider = SchemaBuilder.FromObject(); // Add a argument field with a require parameter - var tree = new GraphQLCompiler(schemaProvider).Compile( + var tree = GraphQLParser.Parse( @" query { people { ...info projects { id name } } @@ -23,7 +22,8 @@ public void SupportsFragmentSelectionSyntax() fragment info on Person { id name } - " + ", + schemaProvider ); Assert.Single(tree.Operations.First().QueryFields); @@ -41,17 +41,18 @@ public void SupportsFragmentWithDirective() { var schemaProvider = SchemaBuilder.FromObject(); // Add a argument field with a require parameter - var tree = new GraphQLCompiler(schemaProvider).Compile( + var tree = GraphQLParser.Parse( @" -query { - people { - ...info @skip(if: true) - projects { id name } } -} -fragment info on Person { - id name -} -" + query { + people { + ...info @skip(if: true) + projects { id name } } + } + fragment info on Person { + id name + } + ", + schemaProvider ); Assert.Single(tree.Operations.First().QueryFields); @@ -225,7 +226,7 @@ public void Test_InlineFragment_With_Service() catType.AddField("isAngry", "Is the cat angry").Resolve((cat, service) => service.IsAngry(cat.Id)); }); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { animals { @@ -234,7 +235,8 @@ ... on Cat { isAngry } } - }" + }", + schema ); var context = new TestUnionDataContext(); context.Animals.Add(new Dog() { Name = "steve", HasBone = true }); @@ -259,10 +261,9 @@ ... on Cat { public void TestFragmentSpreadMustNotFormCycles_DirectCycle() { var schemaProvider = SchemaBuilder.FromObject(); - var compiler = new GraphQLCompiler(schemaProvider); - var exception = Assert.Throws(() => - compiler.Compile( + var exception = Assert.Throws(() => + GraphQLParser.Parse( @" query { people { ...PersonInfo } @@ -271,7 +272,8 @@ fragment PersonInfo on Person { name ...PersonInfo } - " + ", + schemaProvider ) ); @@ -282,10 +284,9 @@ fragment PersonInfo on Person { public void TestFragmentSpreadMustNotFormCycles_IndirectCycle() { var schemaProvider = SchemaBuilder.FromObject(); - var compiler = new GraphQLCompiler(schemaProvider); - var exception = Assert.Throws(() => - compiler.Compile( + var exception = Assert.Throws(() => + GraphQLParser.Parse( @" query { people { ...PersonInfo } @@ -298,7 +299,8 @@ fragment ProjectInfo on Project { name ...PersonInfo } - " + ", + schemaProvider ) ); @@ -309,10 +311,9 @@ fragment ProjectInfo on Project { public void TestFragmentSpreadMustNotFormCycles_ComplexCycle() { var schemaProvider = SchemaBuilder.FromObject(); - var compiler = new GraphQLCompiler(schemaProvider); - var exception = Assert.Throws(() => - compiler.Compile( + var exception = Assert.Throws(() => + GraphQLParser.Parse( @" query { people { ...A } @@ -329,7 +330,8 @@ fragment C on Person { lastName ...A } - " + ", + schemaProvider ) ); @@ -340,9 +342,8 @@ fragment C on Person { public void TestFragmentSpreadMustNotFormCycles_ValidNoCycle() { var schemaProvider = SchemaBuilder.FromObject(); - var compiler = new GraphQLCompiler(schemaProvider); - var tree = compiler.Compile( + var tree = GraphQLParser.Parse( @" query { people { @@ -358,7 +359,8 @@ fragment ProjectInfo on Project { name id } - " + ", + schemaProvider ); Assert.NotNull(tree); @@ -370,14 +372,13 @@ fragment ProjectInfo on Project { public void TestFragmentSpreadMustNotFormCycles_ValidReusedFragment() { var schemaProvider = SchemaBuilder.FromObject(); - var compiler = new GraphQLCompiler(schemaProvider); - var tree = compiler.Compile( + var tree = GraphQLParser.Parse( @" query { - people { + people { ...PersonBasics - projects { + projects { ...ProjectBasics owner { ...PersonBasics } } @@ -391,13 +392,62 @@ fragment ProjectBasics on Project { name id } - " + ", + schemaProvider ); Assert.NotNull(tree); var qr = tree.ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); Assert.NotNull(qr.Data); } + + [Fact] + public void TestFragmentWithVariables() + { + // Issue #483: Variables should be supported in fragments + var schemaProvider = SchemaBuilder.FromObject(); + + var tree = GraphQLParser.Parse( + @" + query Projects($taskName: String) { + projects { + ...ProjectFragment + } + } + + fragment ProjectFragment on Project { + id + name + searchTasks(name: $taskName) { + id + name + } + } + ", + schemaProvider + ); + + Assert.NotNull(tree); + + var variables = new QueryVariables { { "taskName", "task 1" } }; + + var context = new TestDataContext().FillWithTestData(); + var qr = tree.ExecuteQuery(context, null, variables); + + Assert.Null(qr.Errors); + Assert.NotNull(qr.Data); + + dynamic projects = qr.Data!["projects"]!; + Assert.Single(projects); + + dynamic project = projects[0]; + Assert.Equal(55, project.id); + Assert.Equal("Project 3", project.name); + + // Should only get the task matching "task 1" + Assert.Single(project.searchTasks); + Assert.Equal("task 1", project.searchTasks[0].name); + } } internal class CatAngerService diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/InheritanceTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/InheritanceTests.cs index 83bdbb6c..df3a2aad 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/InheritanceTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/InheritanceTests.cs @@ -18,14 +18,15 @@ public class InheritanceTests public void TestInheritance() { var schemaProvider = new TestAbstractDataGraphSchema(); - var gql = new GraphQLCompiler(schemaProvider).Compile( + var gql = GraphQLParser.Parse( @" query { animals { __typename name } - }" + }", + schemaProvider ); var context = new TestAbstractDataContext(); context.Animals.Add(new Dog() { Name = "steve", HasBone = true }); @@ -54,7 +55,7 @@ public void TestAutoInheritance() public void TestInheritanceExtraFields() { var schemaProvider = new TestAbstractDataGraphSchema(); - var gql = new GraphQLCompiler(schemaProvider).Compile( + var gql = GraphQLParser.Parse( @" query { animals { @@ -68,7 +69,8 @@ ...on Dog { } } } - " + ", + schemaProvider ); var context = new TestAbstractDataContext(); @@ -92,7 +94,7 @@ ...on Dog { public void TestInheritancDuplicateFields() { var schemaProvider = new TestAbstractDataGraphSchema(); - var gql = new GraphQLCompiler(schemaProvider).Compile( + var gql = GraphQLParser.Parse( @" query { animals { @@ -106,7 +108,8 @@ ...on Dog { } } } - " + ", + schemaProvider ); var context = new TestAbstractDataContext(); @@ -142,7 +145,7 @@ ...on Dog { public void TestInheritanceExtraFieldsOnObjectDog() { var schemaProvider = new TestAbstractDataGraphSchema(); - var gql = new GraphQLCompiler(schemaProvider).Compile( + var gql = GraphQLParser.Parse( @" query { animal(id: 9) { @@ -156,7 +159,8 @@ ...on Dog { } } } - " + ", + schemaProvider ); var context = new TestAbstractDataContext(); @@ -183,7 +187,7 @@ ...on Dog { public void TestInheritanceExtraFieldsOnObjectCat() { var schemaProvider = new TestAbstractDataGraphSchema(); - var gql = new GraphQLCompiler(schemaProvider).Compile( + var gql = GraphQLParser.Parse( @" query { animal(id: 2) { @@ -197,7 +201,8 @@ ...on Dog { } } } - " + ", + schemaProvider ); var context = new TestAbstractDataContext(); @@ -231,7 +236,7 @@ ...on Dog { public void TestInheritanceExtraFieldsOnObjectCatUsingFragments() { var schemaProvider = new TestAbstractDataGraphSchema(); - var gql = new GraphQLCompiler(schemaProvider).Compile( + var gql = GraphQLParser.Parse( @" query { animal(id: 2) { @@ -249,7 +254,8 @@ ...on Dog { hasBone } } - " + ", + schemaProvider ); var context = new TestAbstractDataContext(); @@ -286,7 +292,7 @@ public void TestInheritanceReturnFromMutation() schemaProvider.Mutation().AddFrom(); - var gql = new GraphQLCompiler(schemaProvider).Compile( + var gql = GraphQLParser.Parse( @" mutation { testMutation(id: 1) { @@ -296,7 +302,8 @@ ... on Dog { } } } - " + ", + schemaProvider ); var context = new TestAbstractDataContext(); @@ -323,7 +330,7 @@ public void SupportsFragmentRepeatedFields() { // apollo client inserts __typename everywhere var schemaProvider = new TestAbstractDataGraphSchema(); - var gql = new GraphQLCompiler(schemaProvider).Compile( + var gql = GraphQLParser.Parse( @" query { dog(id: 9) { @@ -336,7 +343,8 @@ fragment animalFragment on Animal { __typename # type name on base Animal type name # also this builds p_animal.Name where we need p_dog.Name } - " + ", + schemaProvider ); var context = new TestAbstractDataContext(); @@ -360,7 +368,7 @@ fragment animalFragment on Animal { public void SelectFieldFromInheritedType() { var schemaProvider = new TestAbstractDataGraphSchema(); - var gql = new GraphQLCompiler(schemaProvider).Compile( + var gql = GraphQLParser.Parse( @" query { dogs { @@ -371,7 +379,8 @@ public void SelectFieldFromInheritedType() fragment dogFragment on Dog { name } - " + ", + schemaProvider ); var context = new TestAbstractDataContext(); @@ -394,7 +403,7 @@ fragment dogFragment on Dog { public void SelectFieldFromInheritedTypeWithServiceField() { var schemaProvider = new TestAbstractDataGraphSchema(); - var gql = new GraphQLCompiler(schemaProvider).Compile( + var gql = GraphQLParser.Parse( @" fragment frag on Dog { name @@ -408,7 +417,8 @@ fragment frag on Dog { } } } - " + ", + schemaProvider ); var context = new TestAbstractDataContext(); diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/InputTypeTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/InputTypeTests.cs index fb2a70b8..3ca2d74a 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/InputTypeTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/InputTypeTests.cs @@ -20,7 +20,7 @@ public void SupportsEnumInInputType_Introspection() @"query { __type(name: ""PeopleArgs"") { name - fields { + inputFields { name } } @@ -28,9 +28,9 @@ public void SupportsEnumInInputType_Introspection() }; var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); Assert.Null(result.Errors); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "unit"); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "dayOfWeek"); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "name"); + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "unit"); + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "dayOfWeek"); + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "name"); } [Fact] @@ -45,7 +45,7 @@ public void SupportsEnumInInputTypeAsList_Introspection() @"query { __type(name: ""PeopleArgs"") { name - fields { + inputFields { name } } @@ -53,9 +53,9 @@ public void SupportsEnumInInputTypeAsList_Introspection() }; var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); Assert.Null(result.Errors); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "unit"); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "dayOfWeek"); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "name"); + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "unit"); + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "dayOfWeek"); + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "name"); } [Fact] @@ -79,17 +79,25 @@ public void SupportsEnumInInputTypeAsListInMutation_Introspection() @"query { __type(name: ""PeopleArgs"") { name + kind fields { name } + inputFields { + name + } } }", }; var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); Assert.Null(result.Errors); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "unit"); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "dayOfWeek"); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "name"); + // Per GraphQL spec, fields should be null for INPUT_OBJECT types + Assert.Equal("INPUT_OBJECT", ((dynamic)result.Data!["__type"]!).kind); + Assert.Null(((dynamic)result.Data!["__type"]!).fields); + // inputFields should have the fields + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "unit"); + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "dayOfWeek"); + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "name"); } [Fact] @@ -113,17 +121,25 @@ public void SupportsEnumInInputTypeAsListInMutationArgs_Introspection() @"query { __type(name: ""PeopleArgs"") { name + kind fields { name } + inputFields { + name + } } }", }; var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); Assert.Null(result.Errors); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "unit"); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "dayOfWeek"); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "name"); + // Per GraphQL spec, fields should be null for INPUT_OBJECT types + Assert.Equal("INPUT_OBJECT", ((dynamic)result.Data!["__type"]!).kind); + Assert.Null(((dynamic)result.Data!["__type"]!).fields); + // inputFields should have the fields + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "unit"); + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "dayOfWeek"); + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "name"); } class TaskInput : Task { } @@ -216,8 +232,12 @@ public void SupportsQueryTypeAsInputTypeIntrospection() @"query { __type(name: ""UserInput"") { name + kind fields { name + } + inputFields { + name type { name ofType { kind name } } } } @@ -225,9 +245,13 @@ public void SupportsQueryTypeAsInputTypeIntrospection() }; var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); Assert.Null(result.Errors); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "id"); - Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).fields, f => f.name == "tasks"); - Assert.Equal("TaskInput", ((dynamic)result.Data!["__type"]!).fields[1].type.ofType.name); + // Per GraphQL spec, fields should be null for INPUT_OBJECT types + Assert.Equal("INPUT_OBJECT", ((dynamic)result.Data!["__type"]!).kind); + Assert.Null(((dynamic)result.Data!["__type"]!).fields); + // inputFields should have the fields + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "id"); + Assert.Contains((IEnumerable)((dynamic)result.Data!["__type"]!).inputFields, f => f.name == "tasks"); + Assert.Equal("TaskInput", ((dynamic)result.Data!["__type"]!).inputFields[1].type.ofType.name); } [Fact] diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/ListEdgeCasesTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/ListEdgeCasesTests.cs index 5779a3c3..6e3b0f31 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/ListEdgeCasesTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/ListEdgeCasesTests.cs @@ -17,14 +17,14 @@ public void WildcardQueriesHonorRemovedFieldsOnList() // empty schema var schema = SchemaBuilder.FromObject(); schema.Type().RemoveField(p => p.Id); - var ex = Assert.Throws( - () => - new GraphQLCompiler(schema).Compile( - @" + var ex = Assert.Throws(() => + GraphQLParser.Parse( + @" { people - }" - ) + }", + schema + ) ); Assert.Equal("Field 'people' requires a selection set defining the fields you would like to select.", ex.Message); } @@ -36,14 +36,14 @@ public void WildcardQueriesHonorRemovedFieldsOnListFromEmpty() var schema = SchemaBuilder.Create(); schema.AddType("Person").AddField("name", p => p.Name, "Person's name"); schema.Query().AddField("people", p => p.People, "People"); - var ex = Assert.Throws( - () => - new GraphQLCompiler(schema).Compile( - @" + var ex = Assert.Throws(() => + GraphQLParser.Parse( + @" { people - }" - ) + }", + schema + ) ); Assert.Equal("Field 'people' requires a selection set defining the fields you would like to select.", ex.Message); } @@ -51,11 +51,12 @@ public void WildcardQueriesHonorRemovedFieldsOnListFromEmpty() [Fact] public void CanParseQueryWithCollection() { - var tree = new GraphQLCompiler(SchemaBuilder.FromObject()).Compile( + var tree = GraphQLParser.Parse( @" { people { id name projects { name } } - }" + }", + SchemaBuilder.FromObject() ); // People.Select(p => new { Id = p.Id, Name = p.Name, User = new { Field1 = p.User.Field1 }) var result = tree.ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); @@ -77,8 +78,9 @@ public void CanParseQueryWithCollection() [Fact] public void CanParseQueryWithCollectionDeep() { - var tree = new GraphQLCompiler(SchemaBuilder.FromObject()).Compile( + var tree = GraphQLParser.Parse( @" + { people { id projects { @@ -86,7 +88,8 @@ public void CanParseQueryWithCollectionDeep() tasks { id name } } } - }" + }", + SchemaBuilder.FromObject() ); var result = tree.ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); Assert.Equal(1, Enumerable.Count((dynamic)result.Data!["people"]!)); diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/QueryTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/QueryTests.cs index 9b530082..ed4e7978 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/QueryTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/QueryTests.cs @@ -15,15 +15,16 @@ public class QueryTests public void CanParseSimpleQuery() { var objectSchemaProvider = SchemaBuilder.FromObject(); - var tree = new GraphQLCompiler(objectSchemaProvider).Compile( - @" -{ - people { id name } -}" + var tree = GraphQLParser.Parse( + @"{ + people { id name } + }", + objectSchemaProvider ); Assert.Single(tree.Operations); Assert.Single(tree.Operations.First().QueryFields); var result = tree.ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); + Assert.Null(result.Errors); Assert.NotNull(result.Data); Assert.Single(result.Data); var person = Enumerable.ElementAt((dynamic)result.Data["people"]!, 0); @@ -57,11 +58,12 @@ public void CanQueryExtendedFields() { var objectSchemaProvider = SchemaBuilder.FromObject(); objectSchemaProvider.Type().AddField("thing", p => p.Id + " - " + p.Name, "A weird field I want"); - var tree = new GraphQLCompiler(objectSchemaProvider).Compile( + var tree = GraphQLParser.Parse( @" { people { id thing } -}" +}", + objectSchemaProvider ); Assert.Single(tree.Operations); Assert.Single(tree.Operations.First().QueryFields); @@ -80,13 +82,14 @@ public void CanRemoveFields() { var schema = SchemaBuilder.FromObject(); schema.Type().RemoveField(p => p.Id); - var ex = Assert.Throws(() => + var ex = Assert.Throws(() => { - var tree = new GraphQLCompiler(schema).Compile( + var tree = GraphQLParser.Parse( @" { people { id } - }" + }", + schema ); }); Assert.Equal("Field 'id' not found on type 'Person'", ex.Message); @@ -99,14 +102,13 @@ public void WildcardQueriesHonorRemovedFieldsOnObject() var schema = SchemaBuilder.Create(); schema.AddType("Person").AddField("name", p => p.Name, "Person's name"); schema.Query().AddField("person", new { id = ArgumentHelper.Required() }, (p, args) => p.People.FirstOrDefault(p => p.Id == args.id), "Person"); - var ex = Assert.Throws( - () => - new GraphQLCompiler(schema).Compile( - @" - { - person(id: 1) - }" - ) + var ex = Assert.Throws(() => + GraphQLParser.Parse( + @"{ + person(id: 1) + }", + schema + ) ); Assert.Equal("Field 'person' requires a selection set defining the fields you would like to select.", ex.Message); } @@ -114,12 +116,13 @@ public void WildcardQueriesHonorRemovedFieldsOnObject() [Fact] public void CanParseMultipleEntityQuery() { - var tree = new GraphQLCompiler(SchemaBuilder.FromObject()).Compile( + var tree = GraphQLParser.Parse( @" { people { id name } users { id } - }" + }", + SchemaBuilder.FromObject() ); Assert.Single(tree.Operations); @@ -142,11 +145,12 @@ public void CanParseMultipleEntityQuery() [Fact] public void CanParseQueryWithRelation() { - var tree = new GraphQLCompiler(SchemaBuilder.FromObject()).Compile( + var tree = GraphQLParser.Parse( @" { people { id name user { field1 } } -}" +}", + SchemaBuilder.FromObject() ); // People.Select(p => new { Id = p.Id, Name = p.Name, User = new { Field1 = p.User.Field1 }) var result = tree.ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); @@ -166,7 +170,7 @@ public void CanParseQueryWithRelation() [Fact] public void CanParseQueryWithRelationDeep() { - var tree = new GraphQLCompiler(SchemaBuilder.FromObject()).Compile( + var tree = GraphQLParser.Parse( @" { people { @@ -176,7 +180,8 @@ id name nestedRelation { id name } } } - }" + }", + SchemaBuilder.FromObject() ); // People.Select(p => new { Id = p.Id, Name = p.Name, User = new { Field1 = p.User.Field1, NestedRelation = new { Id = p.User.NestedRelation.Id, Name = p.User.NestedRelation.Name } }) var result = tree.ExecuteQuery(new TestDataContext().FillWithTestData(), null, null); @@ -201,10 +206,9 @@ id name [Fact] public void FailsNonExistingField() { - var ex = Assert.Throws( - () => - new GraphQLCompiler(SchemaBuilder.FromObject()).Compile( - @" + var ex = Assert.Throws(() => + GraphQLParser.Parse( + @" { people { id projects { @@ -212,8 +216,9 @@ public void FailsNonExistingField() blahs { id name } } } - }" - ) + }", + SchemaBuilder.FromObject() + ) ); Assert.Equal("Field 'blahs' not found on type 'Project'", ex.Message); } @@ -221,18 +226,18 @@ public void FailsNonExistingField() [Fact] public void FailsNonExistingField2() { - var ex = Assert.Throws( - () => - new GraphQLCompiler(SchemaBuilder.FromObject()).Compile( - @" + var ex = Assert.Throws(() => + GraphQLParser.Parse( + @" { people { id projects { name3 } } - }" - ) + }", + SchemaBuilder.FromObject() + ) ); Assert.Equal("Field 'name3' not found on type 'Project'", ex.Message); } @@ -240,13 +245,14 @@ public void FailsNonExistingField2() [Fact] public void TestAlias() { - var tree = new GraphQLCompiler(SchemaBuilder.FromObject()).Compile( + var tree = GraphQLParser.Parse( @" { projects { n: name } - }" + }", + SchemaBuilder.FromObject() ); Assert.Single(tree.Operations.First().QueryFields); @@ -257,14 +263,15 @@ public void TestAlias() [Fact] public void TestAliasDeep() { - var tree = new GraphQLCompiler(SchemaBuilder.FromObject()).Compile( + var tree = GraphQLParser.Parse( @"{ people { id projects { n: name } } - }" + }", + SchemaBuilder.FromObject() ); Assert.Single(tree.Operations.First().QueryFields); @@ -319,11 +326,11 @@ public void DateScalarsTest() public void TestTopLevelScalar() { var schemaProvider = SchemaBuilder.FromObject(); - var gql = new GraphQLCompiler(schemaProvider).Compile( - @" -query { - totalPeople -}" + var gql = GraphQLParser.Parse( + @"query { + totalPeople + }", + schemaProvider ); var context = new TestDataContext(); @@ -355,7 +362,7 @@ public void TestNoArgumentsOnEnum() { var schema = SchemaBuilder.FromObject(); - var ex = Assert.Throws(() => schema.Type().AddField("invalid", new { id = (int?)null }, (ctx, args) => 8, "Invalid field")); + var ex = Assert.Throws(() => schema.Type().AddField("invalid", new { id = (int?)null }, (ctx, args) => 8, "Invalid field")); Assert.Equal("Field 'invalid' on type 'Gender' has arguments but is a GraphQL 'Enum' type and can not have arguments.", ex.Message); } @@ -364,7 +371,7 @@ public void TestNoFieldsOnScalar() { var schema = SchemaBuilder.FromObject(); - var ex = Assert.Throws(() => schema.Type().AddField("invalid", (ctx) => 8, "Invalid field")); + var ex = Assert.Throws(() => schema.Type().AddField("invalid", (ctx) => 8, "Invalid field")); Assert.Equal("Cannot add field 'invalid' to type 'String', as 'String' is a scalar type and can not have fields.", ex.Message); } diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/ServiceFieldTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/ServiceFieldTests.cs index 1de9e4de..38cf8670 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/ServiceFieldTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/ServiceFieldTests.cs @@ -1049,14 +1049,14 @@ public void TestCollectionToSingleWithServiceTypeInCollection() { Query = @"query { - task(id: 1) { # context collection to single - project { - config { # service - type + task(id: 1) { # context collection to single + project { + config { # service + type + } + } } - } - } - }", + }", }; var context = new TestDataContext @@ -1074,7 +1074,7 @@ public void TestCollectionToSingleWithServiceTypeInCollection() var res = schema.ExecuteRequestWithContext(gql, context, serviceCollection.BuildServiceProvider(), null); Assert.Null(res.Errors); Assert.Equal(1, configSrv.CallCount); - dynamic project = (dynamic)res.Data!["task"]!; + dynamic project = res.Data!["task"]!; } [Fact] @@ -1402,14 +1402,13 @@ public void TestRootServiceFieldBackToContext() serviceCollection.AddSingleton(service); // what we want to test here is that ctx.People.FirstOrDefault().Id is pulled up into the pre-services expression - var graphQLCompiler = new GraphQLCompiler(schema); - var compiledQuery = graphQLCompiler.Compile(gql); + var compiledQuery = GraphQLParser.Parse(gql, schema); var query = compiledQuery.Operations[0]; var node = query.QueryFields[0]; // first stage without services var expression = node.GetNodeExpression( - new CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null)), + new CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null), serviceCollection.BuildServiceProvider(), new Dictionary(), query.OpVariableParameter, @@ -1978,6 +1977,137 @@ public void TestGeneratedNamesDoNotCollide() Assert.Equal("getProjectId", Enumerable.ElementAt(project.GetType().GetFields(), 1).Name); } + public class ContextTest + { + public int GetProjectsCalled { get; private set; } + public int GetProjectCalled { get; private set; } + + public async Task GetProject() + { + GetProjectCalled += 1; + return await System.Threading.Tasks.Task.FromResult( + new Project() + { + Id = 1, + Name = "test", + CreatedBy = 1, + } + ); + } + + public async Task> GetProjects() + { + GetProjectsCalled += 1; + return await System.Threading.Tasks.Task.FromResult( + new[] + { + new Project() + { + Id = 1, + Name = "test", + CreatedBy = 1, + }, + } + ); + } + } + + [Fact] + public void TestTopLevelAsyncService() + { + var schema = SchemaBuilder.FromObject(); + + // Here the expression project is extracted and the expression is used for a field name, which is a duplicate of the project field + schema.Query().AddField("getProject", "Something").ResolveAsync((context, us) => us.GetProject()); + + var gql = new QueryRequest + { + Query = + @"{ + getProject { id name createdBy } + }", + }; + + var context = new TestDataContext(); + + var serviceCollection = new ServiceCollection(); + var srv = new ContextTest(); + serviceCollection.AddSingleton(srv); + serviceCollection.AddSingleton(context); + + var res = schema.ExecuteRequest(gql, serviceCollection.BuildServiceProvider(), null); + Assert.Null(res.Errors); + Assert.NotNull(res.Data); + dynamic project = res.Data["getProject"]!; + Assert.Equal(1, project.id); + Assert.Equal("test", project.name); + Assert.Equal(1, project.createdBy); + Assert.Equal(1, srv.GetProjectCalled); + } + + [Fact] + public void TestTopLevelAsyncServiceList() + { + var schema = SchemaBuilder.FromObject(); + + // Here the expression project is extracted and the expression is used for a field name, which is a duplicate of the project field + schema.Query().AddField("getProjects", "Something").ResolveAsync((context, us) => us.GetProjects()); + + var gql = new QueryRequest + { + Query = + @"{ + getProjects { id name createdBy } + }", + }; + + var context = new TestDataContext(); + + var serviceCollection = new ServiceCollection(); + var srv = new ContextTest(); + serviceCollection.AddSingleton(srv); + serviceCollection.AddSingleton(context); + + var res = schema.ExecuteRequest(gql, serviceCollection.BuildServiceProvider(), null); + Assert.Null(res.Errors); + Assert.NotNull(res.Data); + dynamic projects = res.Data["getProjects"]!; + var project = Enumerable.First(projects); + Assert.Equal(1, project.id); + Assert.Equal("test", project.name); + Assert.Equal(1, project.createdBy); + Assert.Equal(1, srv.GetProjectsCalled); + } + + [Fact] + public void TestResolveAsyncWithServiceAndAnonymousArgsShouldNotTreatArgsAsService() + { + // Reproduces issue #487: https://github.com/EntityGraphQL/EntityGraphQL/issues/487 + var schema = SchemaBuilder.FromObject(); + + schema + .Query() + .AddField("testField", new { resourceId = (int?)null }, "Test field with optional argument") + .ResolveAsync((ctx, args, service) => service.GetDataAsync(args.resourceId)); + + var serviceCollection = new ServiceCollection(); + var testService = new TestResolveAsyncService(); + serviceCollection.AddSingleton(testService); + + var query = + @" + query { + testField(resourceId: 42) + } + "; + + var result = schema.ExecuteRequestWithContext(new QueryRequest { Query = query }, new TestDataContext(), serviceCollection.BuildServiceProvider(), null); + + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + Assert.Equal("Data for resource 42", result.Data["testField"]); + } + public class ConfigService { public ConfigService() @@ -2307,3 +2437,12 @@ public class ProjectConfig public int Id { get; set; } public string Type { get; set; } = ""; } + +public class TestResolveAsyncService +{ + public async Task GetDataAsync(int? resourceId) + { + await System.Threading.Tasks.Task.CompletedTask; + return resourceId.HasValue ? $"Data for resource {resourceId}" : "No resource specified"; + } +} diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/UnionTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/UnionTests.cs index 29543f05..73f54ce0 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/UnionTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/UnionTests.cs @@ -20,7 +20,7 @@ public void TestAutoUnion() Assert.True(schema.GetSchemaType(typeof(Cat), false, null).GqlType == GqlTypes.QueryObject); Assert.True(schema.GetSchemaType(typeof(Dog), false, null).GqlType == GqlTypes.QueryObject); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { animals { @@ -34,7 +34,8 @@ ... on Cat { lives } } - }" + }", + schema ); var context = new TestUnionDataContext(); context.Animals.Add(new Dog() { Name = "steve", HasBone = true }); @@ -65,7 +66,7 @@ public void TestAutoUnion_NoTypeName() Assert.True(schema.GetSchemaType(typeof(Cat), false, null).GqlType == GqlTypes.QueryObject); Assert.True(schema.GetSchemaType(typeof(Dog), false, null).GqlType == GqlTypes.QueryObject); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { animals { @@ -78,7 +79,8 @@ ... on Cat { lives } } - }" + }", + schema ); var context = new TestUnionDataContext(); context.Animals.Add(new Dog() { Name = "steve", HasBone = true }); @@ -109,7 +111,7 @@ public void TestAutoUnion_DogOnly() Assert.True(schema.GetSchemaType(typeof(Cat), false, null).GqlType == GqlTypes.QueryObject); Assert.True(schema.GetSchemaType(typeof(Dog), false, null).GqlType == GqlTypes.QueryObject); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { animals { @@ -118,7 +120,8 @@ ... on Dog { hasBone } } - }" + }", + schema ); var context = new TestUnionDataContext(); context.Animals.Add(new Dog() { Name = "steve", HasBone = true }); @@ -149,7 +152,7 @@ public void TestAutoUnion_CatOnly() Assert.Equal(GqlTypes.QueryObject, schema.GetSchemaType(typeof(Cat), false, null).GqlType); Assert.Equal(GqlTypes.QueryObject, schema.GetSchemaType(typeof(Dog), false, null).GqlType); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @" query { animals { @@ -158,7 +161,8 @@ ... on Cat { lives } } - }" + }", + schema ); var context = new TestUnionDataContext(); context.Animals.Add(new Dog() { Name = "steve", HasBone = true }); @@ -189,12 +193,13 @@ public void TestAutoUnion_TypeOnly() Assert.Equal(GqlTypes.QueryObject, schema.GetSchemaType(typeof(Cat), false, null).GqlType); Assert.Equal(GqlTypes.QueryObject, schema.GetSchemaType(typeof(Dog), false, null).GqlType); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( @"query { animals { __typename } - }" + }", + schema ); var context = new TestUnionDataContext(); context.Animals.Add(new Dog() { Name = "steve", HasBone = true }); @@ -225,7 +230,7 @@ public void TestAutoUnion_InterfaceTypedField_ObjectProjection() Assert.Equal(GqlTypes.QueryObject, schema.GetSchemaType(typeof(Cat), null).GqlType); Assert.Equal(GqlTypes.QueryObject, schema.GetSchemaType(typeof(Dog), null).GqlType); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( """ query { petOwner(id: 1) { @@ -253,7 +258,8 @@ ... on Dog { } } } - """ + """, + schema ); var context = new TestUnionDataContext2(); context.PetOwners.Add( @@ -290,7 +296,7 @@ public void TestAutoUnion_InterfaceTypedField() Assert.Equal(GqlTypes.QueryObject, schema.GetSchemaType(typeof(Cat), null).GqlType); Assert.Equal(GqlTypes.QueryObject, schema.GetSchemaType(typeof(Dog), null).GqlType); - var gql = new GraphQLCompiler(schema).Compile( + var gql = GraphQLParser.Parse( """ query { petOwners { @@ -308,7 +314,8 @@ ... on Dog { } } } - """ + """, + schema ); var context = new TestUnionDataContext2(); context.PetOwners.Add( diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/VariableTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/VariableTests.cs index f116e62c..37bf9468 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/VariableTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/VariableTests.cs @@ -20,14 +20,13 @@ public void QueryWithVariable() var gql = new QueryRequest { Query = - @" - query MyQuery($limit: Int) { - people(limit: $limit) { id name } - } - ", + @"query MyQuery($limit: Int) { + people(limit: $limit) { id name } + } +", Variables = new QueryVariables { { "limit", 5 } }, }; - var tree = new GraphQLCompiler(schema).Compile(gql); + var tree = GraphQLParser.Parse(gql, schema); Assert.Single(tree.Operations.First().QueryFields); TestDataContext context = new TestDataContext().FillWithTestData(); @@ -47,12 +46,13 @@ public void QueryWithDefaultArguments() var schema = SchemaBuilder.FromObject(); schema.Query().ReplaceField("people", new { limit = ArgumentHelper.Required() }, (db, p) => db.People.Take(p.limit), "List of people with limit"); // Add a argument field with a require parameter - var tree = new GraphQLCompiler(schema).Compile( + var tree = GraphQLParser.Parse( @" query MyQuery($limit: Int = 10) { people(limit: $limit) { id name } } - " + ", + schema ); Assert.Single(tree.Operations.First().QueryFields); @@ -75,12 +75,13 @@ public void QueryWithDefaultArgumentsOverrideCodeDefault() schema.Query().ReplaceField("people", new { limit = 5 }, (db, p) => db.People.Take(p.limit), "List of people with limit"); // should use gql default of 6 - var tree = new GraphQLCompiler(schema).Compile( + var tree = GraphQLParser.Parse( @" query MyQuery($limit: Int = 6) { people(limit: $limit) { id name } } - " + ", + schema ); Assert.Single(tree.Operations.First().QueryFields); @@ -116,10 +117,8 @@ public void QueryVariableDefinitionRequiredBySchemaItIsNotRequired() var testSchema = new TestDataContext(); var results = schemaProvider.ExecuteRequestWithContext(gql, testSchema, serviceCollection.BuildServiceProvider(), null); Assert.NotNull(results.Errors); - Assert.Equal( - "Field 'nullableGuidArgs' - Supplied variable 'id' is null while the variable definition is non-null. Please update query document or supply a non-null value.", - results.Errors[0].Message - ); + Assert.Null(results.Data); + Assert.Equal("Supplied variable 'id' is null while the variable definition is non-null. Please update query document or supply a non-null value.", results.Errors[0].Message); } [Fact] diff --git a/src/tests/EntityGraphQL.Tests/RoleAuthorizationTests.cs b/src/tests/EntityGraphQL.Tests/RoleAuthorizationTests.cs index 2d3b3d4e..b2c1222d 100644 --- a/src/tests/EntityGraphQL.Tests/RoleAuthorizationTests.cs +++ b/src/tests/EntityGraphQL.Tests/RoleAuthorizationTests.cs @@ -14,8 +14,8 @@ public void TestAttributeOnTypeFromObject() { var schema = SchemaBuilder.FromObject(); - Assert.Single(schema.Type().RequiredAuthorization!.Roles); - Assert.Equal("admin", schema.Type().RequiredAuthorization!.Roles.ElementAt(0).ElementAt(0)); + Assert.Single(schema.Type().RequiredAuthorization!.GetRoles()!); + Assert.Equal("admin", schema.Type().RequiredAuthorization!.GetRoles()!.ElementAt(0).ElementAt(0)); } [Fact] @@ -24,8 +24,8 @@ public void TestAttributeOnTypeAddType() var schema = new SchemaProvider(); schema.AddType("Project", "All about the project"); - Assert.Single(schema.Type().RequiredAuthorization!.Roles); - Assert.Equal("admin", schema.Type().RequiredAuthorization!.Roles.ElementAt(0).ElementAt(0)); + Assert.Single(schema.Type().RequiredAuthorization!.GetRoles()!); + Assert.Equal("admin", schema.Type().RequiredAuthorization!.GetRoles()!.ElementAt(0).ElementAt(0)); } [Fact] @@ -33,12 +33,12 @@ public void TestMethodOnType() { var schema = SchemaBuilder.FromObject(); - Assert.Empty(schema.Type().RequiredAuthorization!.Roles); + Assert.Null(schema.Type().RequiredAuthorization); schema.Type().RequiresAnyRole("admin"); - Assert.Single(schema.Type().RequiredAuthorization!.Roles); - Assert.Equal("admin", schema.Type().RequiredAuthorization!.Roles.ElementAt(0).ElementAt(0)); + Assert.Single(schema.Type().RequiredAuthorization!.GetRoles()!); + Assert.Equal("admin", schema.Type().RequiredAuthorization!.GetRoles()!.ElementAt(0).ElementAt(0)); } [Fact] @@ -46,8 +46,8 @@ public void TestAttributeOnField() { var schema = SchemaBuilder.FromObject(); - Assert.Single(schema.Type().GetField("type", null).RequiredAuthorization!.Roles); - Assert.Equal("can-type", schema.Type().GetField("type", null).RequiredAuthorization!.Roles.ElementAt(0).ElementAt(0)); + Assert.Single(schema.Type().GetField("type", null).RequiredAuthorization!.GetRoles()!); + Assert.Equal("can-type", schema.Type().GetField("type", null).RequiredAuthorization!.GetRoles()!.ElementAt(0).ElementAt(0)); } [Fact] @@ -56,8 +56,8 @@ public void TestAttributeOnFieldAddField() var schema = new SchemaProvider(); schema.AddType("Project", "All about the project").AddField(p => p.Type, "The type info"); - Assert.Single(schema.Type().GetField("type", null).RequiredAuthorization!.Roles); - Assert.Equal("can-type", schema.Type().GetField("type", null).RequiredAuthorization!.Roles.ElementAt(0).ElementAt(0)); + Assert.Single(schema.Type().GetField("type", null).RequiredAuthorization!.GetRoles()!); + Assert.Equal("can-type", schema.Type().GetField("type", null).RequiredAuthorization!.GetRoles()!.ElementAt(0).ElementAt(0)); } [Fact] @@ -67,8 +67,8 @@ public void TestMethodOnField() schema.AddType("Task", "All about tasks").AddField(p => p.IsActive, "Is it active").RequiresAnyRole("admin"); - Assert.Single(schema.Type().GetField("isActive", null).RequiredAuthorization!.Roles); - Assert.Equal("admin", schema.Type().GetField("isActive", null).RequiredAuthorization!.Roles.ElementAt(0).ElementAt(0)); + Assert.Single(schema.Type().GetField("isActive", null).RequiredAuthorization!.GetRoles()!); + Assert.Equal("admin", schema.Type().GetField("isActive", null).RequiredAuthorization!.GetRoles()!.ElementAt(0).ElementAt(0)); } [Fact] @@ -78,9 +78,9 @@ public void TestRequiresAnyRoleMany() schema.AddType("Task", "All about tasks").AddField(p => p.IsActive, "Is it active").RequiresAnyRole("admin", "something-else"); - Assert.Single(schema.Type().GetField("isActive", null).RequiredAuthorization!.Roles); - Assert.Equal("admin", schema.Type().GetField("isActive", null).RequiredAuthorization!.Roles.ElementAt(0).ElementAt(0)); - Assert.Equal("something-else", schema.Type().GetField("isActive", null).RequiredAuthorization!.Roles.ElementAt(0).ElementAt(1)); + Assert.Single(schema.Type().GetField("isActive", null).RequiredAuthorization!.GetRoles()!); + Assert.Equal("admin", schema.Type().GetField("isActive", null).RequiredAuthorization!.GetRoles()!.ElementAt(0).ElementAt(0)); + Assert.Equal("something-else", schema.Type().GetField("isActive", null).RequiredAuthorization!.GetRoles()!.ElementAt(0).ElementAt(1)); } [Fact] @@ -275,7 +275,8 @@ public void TestMutationAuth() var result = schema.ExecuteRequestWithContext(gql, new RolesDataContext(), null, new ClaimsPrincipal(claims)); Assert.NotNull(result.Errors); - Assert.Equal("Field 'needsAuth' - You are not authorized to access the 'needsAuth' field on type 'Mutation'.", result.Errors.First().Message); + Assert.Equal("You are not authorized to access the 'needsAuth' field on type 'Mutation'.", result.Errors.First().Message); + Assert.Equal(["needsAuth"], result.Errors.First().Path); claims = new ClaimsIdentity([new Claim(ClaimTypes.Role, "can-mutate")], "authed"); result = schema.ExecuteRequestWithContext(gql, new RolesDataContext(), null, new ClaimsPrincipal(claims)); diff --git a/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaBuilderFromObjectTests.cs b/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaBuilderFromObjectTests.cs index 66255819..1cd66baa 100644 --- a/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaBuilderFromObjectTests.cs +++ b/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaBuilderFromObjectTests.cs @@ -106,11 +106,10 @@ public void DoesNotSupportSameFieldDifferentArguments() // Graphql doesn't support "field overloading" var schemaProvider = SchemaBuilder.FromObject(); // user(id: ID) already created - var ex = Assert.Throws( - () => - schemaProvider - .Query() - .AddField("people", new { monkey = ArgumentHelper.Required() }, (ctx, param) => ctx.People.Where(u => u.Id == param.monkey).FirstOrDefault(), "Return a user by ID") + var ex = Assert.Throws(() => + schemaProvider + .Query() + .AddField("people", new { monkey = ArgumentHelper.Required() }, (ctx, param) => ctx.People.Where(u => u.Id == param.monkey).FirstOrDefault(), "Return a user by ID") ); Assert.Equal("Field 'people' already exists on type 'Query'. Use ReplaceField() if this is intended.", ex.Message); } diff --git a/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaProviderTests.cs b/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaProviderTests.cs index eb9f1169..9dee25ee 100644 --- a/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaProviderTests.cs +++ b/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaProviderTests.cs @@ -132,7 +132,7 @@ public void Scalar_SpecifiedBy_ErrorsOnObjectType() { var schema = new TestObjectGraphSchema(); - var ex = Assert.Throws(() => + var ex = Assert.Throws(() => { schema.Type().SpecifiedBy("https://www.example.com"); }); diff --git a/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaValidateTests.cs b/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaValidateTests.cs index 80c390df..20a4f6c9 100644 --- a/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaValidateTests.cs +++ b/src/tests/EntityGraphQL.Tests/SchemaTests/SchemaValidateTests.cs @@ -1,4 +1,3 @@ -using EntityGraphQL.Compiler; using EntityGraphQL.Schema; using Xunit; @@ -12,7 +11,7 @@ public void TestMissingTypeError() { var schema = SchemaBuilder.Create(); schema.Query().AddField("people", ctx => ctx.People, "People"); - var ex = Assert.Throws(() => schema.Validate()); + var ex = Assert.Throws(() => schema.Validate()); Assert.Equal("Field 'people' on type 'Query' returns type 'EntityGraphQL.Tests.Person' that is not in the schema", ex.Message); } @@ -22,7 +21,7 @@ public void TestMissingTypeErrorNonRoot() var schema = SchemaBuilder.Create(); schema.Query().AddField("people", ctx => ctx.People, "People"); schema.AddType("A person").AddField("tasks", p => p.Tasks, "Tasks"); - var ex = Assert.Throws(() => schema.Validate()); + var ex = Assert.Throws(() => schema.Validate()); Assert.Equal("Field 'tasks' on type 'Person' returns type 'EntityGraphQL.Tests.Task' that is not in the schema", ex.Message); } } diff --git a/src/tests/EntityGraphQL.Tests/SchemaTests/ToGraphQLSchemaStringTests.cs b/src/tests/EntityGraphQL.Tests/SchemaTests/ToGraphQLSchemaStringTests.cs index a9b58626..ca3e2f6d 100644 --- a/src/tests/EntityGraphQL.Tests/SchemaTests/ToGraphQLSchemaStringTests.cs +++ b/src/tests/EntityGraphQL.Tests/SchemaTests/ToGraphQLSchemaStringTests.cs @@ -339,12 +339,6 @@ public void TestGetArgDefaultValue_Enum() Assert.Equal("Alternitive", SchemaGenerator.GetArgDefaultValue(new DefaultArgValue(true, Genre.Alternitive), (e) => e)); } - [Fact] - public void TestGetArgDefaultValue_Filter() - { - Assert.Equal("", SchemaGenerator.GetArgDefaultValue(new DefaultArgValue(true, new EntityQueryType()), (e) => e)); - } - [Fact] public void TestGetArgDefaultValue_Object() { diff --git a/src/tests/EntityGraphQL.Tests/SerializationTests.cs b/src/tests/EntityGraphQL.Tests/SerializationTests.cs index ee87e224..b2cf7e62 100644 --- a/src/tests/EntityGraphQL.Tests/SerializationTests.cs +++ b/src/tests/EntityGraphQL.Tests/SerializationTests.cs @@ -22,8 +22,8 @@ public void JsonNewtonsoft() var schemaProvider = SchemaBuilder.FromObject(); schemaProvider.AddInputType("InputObject", "Using an object in the arguments"); schemaProvider.AddMutationsFrom(); - schemaProvider.AddCustomTypeConverter(new JObjectTypeConverter()); - schemaProvider.AddCustomTypeConverter(new JTokenTypeConverter()); + schemaProvider.AddCustomTypeConverter(JObjectTypeConverter.ChangeType); + schemaProvider.AddCustomTypeConverter(JTokenTypeConverter.ChangeType); // Simulate a JSON request with JSON.NET // variables will end up having JObjects var gql = JsonConvert.DeserializeObject( @@ -59,8 +59,8 @@ public void JsonNewtonsoftArray() // test that even though we don't know about JArray they are IEnumerable and can easily be handled var schemaProvider = SchemaBuilder.FromObject(); schemaProvider.AddMutationsFrom(); - schemaProvider.AddCustomTypeConverter(new JObjectTypeConverter()); - schemaProvider.AddCustomTypeConverter(new JTokenTypeConverter()); + schemaProvider.AddCustomTypeConverter(JObjectTypeConverter.ChangeType); + schemaProvider.AddCustomTypeConverter(JTokenTypeConverter.ChangeType); var gql = JsonConvert.DeserializeObject( @" @@ -86,8 +86,8 @@ public void JsonNewtonsoftArray2() // test that even though we don't know about JArray they are IEnumerable and can easily be handled var schemaProvider = SchemaBuilder.FromObject(); schemaProvider.AddMutationsFrom(); - schemaProvider.AddCustomTypeConverter(new JObjectTypeConverter()); - schemaProvider.AddCustomTypeConverter(new JTokenTypeConverter()); + schemaProvider.AddCustomTypeConverter(JObjectTypeConverter.ChangeType); + schemaProvider.AddCustomTypeConverter(JTokenTypeConverter.ChangeType); var gql = JsonConvert.DeserializeObject( @" @@ -112,9 +112,9 @@ public void JsonNewtonsoftJValue() { var schemaProvider = SchemaBuilder.FromObject(); schemaProvider.AddMutationsFrom(new SchemaBuilderOptions() { AutoCreateInputTypes = true }); - schemaProvider.AddCustomTypeConverter(new JObjectTypeConverter()); - schemaProvider.AddCustomTypeConverter(new JTokenTypeConverter()); - schemaProvider.AddCustomTypeConverter(new JValueTypeConverter()); + schemaProvider.AddCustomTypeConverter(JObjectTypeConverter.ChangeType); + schemaProvider.AddCustomTypeConverter(JTokenTypeConverter.ChangeType); + schemaProvider.AddCustomTypeConverter(JValueTypeConverter.ChangeType); var gql = JsonConvert.DeserializeObject( @" @@ -165,33 +165,27 @@ public void TextJsonJsonElement() } } -internal class JObjectTypeConverter : ICustomTypeConverter +internal static class JObjectTypeConverter { - public Type Type => typeof(JObject); - - public object ChangeType(object value, Type toType, ISchemaProvider schema) + public static object ChangeType(object value, Type toType, ISchemaProvider schema) { // Default JSON deserializer will deserialize child objects in QueryVariables as this JSON type return ((JObject)value).ToObject(toType)!; } } -internal class JTokenTypeConverter : ICustomTypeConverter +internal static class JTokenTypeConverter { - public Type Type => typeof(JToken); - - public object ChangeType(object value, Type toType, ISchemaProvider schema) + public static object ChangeType(object value, Type toType, ISchemaProvider schema) { // Default JSON deserializer will deserialize child objects in QueryVariables as this JSON type return ((JToken)value).ToObject(toType)!; } } -internal class JValueTypeConverter : ICustomTypeConverter +internal static class JValueTypeConverter { - public Type Type => typeof(JValue); - - public object ChangeType(object value, Type toType, ISchemaProvider schema) + public static object ChangeType(object value, Type toType, ISchemaProvider schema) { return ((JValue)value).ToString(); } diff --git a/src/tests/EntityGraphQL.Tests/SubscriptionTests/SubscriptionTests.cs b/src/tests/EntityGraphQL.Tests/SubscriptionTests/SubscriptionTests.cs index f0662de7..b171d6fc 100644 --- a/src/tests/EntityGraphQL.Tests/SubscriptionTests/SubscriptionTests.cs +++ b/src/tests/EntityGraphQL.Tests/SubscriptionTests/SubscriptionTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using EntityGraphQL.Compiler; using EntityGraphQL.Schema; using EntityGraphQL.Subscriptions; @@ -73,7 +74,7 @@ public void TestSubscriptionMakesOperation() onMessage { id text } }", }; - var res = new GraphQLCompiler(schema).Compile(gql.Query, null); + var res = GraphQLParser.Parse(gql, schema); Assert.Single(res.Operations); Assert.Single(res.Operations[0].QueryFields); Assert.Equal("onMessage", res.Operations[0].QueryFields[0].Name); @@ -138,6 +139,33 @@ public void TestArguments() var res = schema.ExecuteRequestWithContext(gql, new TestDataContext(), services.BuildServiceProvider(), null); Assert.Null(res.Errors); } + + [Fact] + public void TestSubscriptionReturnsListResult() + { + var schema = new SchemaProvider(); + schema.AddType("Message info").AddAllFields(); + schema + .Subscription() + .Add( + "onMessages", + (ChatService chat) => + { + return chat.SubscribeToList(); + } + ); + var gql = new QueryRequest + { + Query = + @"subscription { + onMessages { id text } + }", + }; + var services = new ServiceCollection(); + services.AddSingleton(new ChatService()); + var res = schema.ExecuteRequestWithContext(gql, new TestDataContext(), services.BuildServiceProvider(), null); + Assert.Null(res.Errors); + } } internal class TestSubscriptions @@ -158,6 +186,7 @@ internal class Message internal class ChatService { private readonly Broadcaster broadcaster = new(); + private readonly Broadcaster> listBroadcaster = new(); public Message PostMessage(string message) { @@ -172,4 +201,9 @@ public IObservable Subscribe() { return broadcaster; } + + public IObservable> SubscribeToList() + { + return listBroadcaster; + } } diff --git a/src/tests/EntityGraphQL.Tests/TestDataContext.cs b/src/tests/EntityGraphQL.Tests/TestDataContext.cs index 11b51fb7..669e1c3a 100644 --- a/src/tests/EntityGraphQL.Tests/TestDataContext.cs +++ b/src/tests/EntityGraphQL.Tests/TestDataContext.cs @@ -94,7 +94,7 @@ public class Person // fake an error public string Error { - get => throw new EntityGraphQLException("Field failed to execute", new Dictionary { { "code", 1 } }); + get => throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, "Field failed to execute", new Dictionary { { "code", 1 } }); set => throw new Exception("Field failed to execute"); } diff --git a/src/tests/EntityGraphQL.Tests/ValidationTests.cs b/src/tests/EntityGraphQL.Tests/ValidationTests.cs index c3cbc0aa..7b3dde72 100644 --- a/src/tests/EntityGraphQL.Tests/ValidationTests.cs +++ b/src/tests/EntityGraphQL.Tests/ValidationTests.cs @@ -30,13 +30,13 @@ public void TestValidationAttributesOnMutationArgs() var results = schema.ExecuteRequestWithContext(gql, testContext, null, null); Assert.NotNull(results.Errors); Assert.Equal(7, results.Errors.Count); - Assert.Equal("Field 'addMovie' - Title is required", results.Errors[0].Message); - Assert.Equal("Field 'addMovie' - Date is required", results.Errors[1].Message); - Assert.Equal("Field 'addMovie' - Genre must be specified", results.Errors[2].Message); - Assert.Equal("Field 'addMovie' - Price must be between $1 and $100", results.Errors[3].Message); - Assert.Equal("Field 'addMovie' - Rating must be less than 5 characters", results.Errors[4].Message); - Assert.Equal("Field 'addMovie' - Actor is required", results.Errors[5].Message); - Assert.Equal("Field 'addMovie' - Character must be less than 5 characters", results.Errors[6].Message); + Assert.Contains("Field 'addMovie' - Title is required", results.Errors.Select(e => e.Message)); + Assert.Contains("Field 'addMovie' - Date is required", results.Errors.Select(e => e.Message)); + Assert.Contains("Field 'addMovie' - Genre must be specified", results.Errors.Select(e => e.Message)); + Assert.Contains("Field 'addMovie' - Price must be between $1 and $100", results.Errors.Select(e => e.Message)); + Assert.Contains("Field 'addMovie' - Rating must be less than 5 characters", results.Errors.Select(e => e.Message)); + Assert.Contains("Field 'addMovie' - Actor is required", results.Errors.Select(e => e.Message)); + Assert.Contains("Field 'addMovie' - Character must be less than 5 characters", results.Errors.Select(e => e.Message)); } [Fact] @@ -131,6 +131,36 @@ public void TestGraphQLValidatorWithInlineArgs() Assert.Equal("Test Error", results.Errors[0].Message); } + [Fact] + public void TestErrorContainsAliasPath_WithGraphQLValidator() + { + var schema = SchemaBuilder.FromObject(); + + schema.AddMutationsFrom(new SchemaBuilderOptions() { AutoCreateInputTypes = true }); + + var gql = new QueryRequest + { + Query = + @"mutation Mutate($arg: CastMemberArg) { + a: updateCastMemberWithGraphQLValidator(arg: $arg) + b: updateCastMemberWithGraphQLValidator(arg: $arg) + }", + Variables = new QueryVariables() { { "arg", new { Actor = "Neil", Character = "Barn" } } }, + }; + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient(); + var testContext = new ValidationTestsContext(); + var results = schema.ExecuteRequestWithContext(gql, testContext, serviceCollection.BuildServiceProvider(), null); + Assert.NotNull(results.Errors); + Assert.Equal(2, results.Errors.Count); + Assert.Equal("Test Error", results.Errors[0].Message); + var paths = results.Errors.Where(e => e.Path != null).SelectMany(e => e.Path!); + Assert.Equal(2, paths.Count()); + Assert.Contains("a", paths); + Assert.Contains("b", paths); + } + [Fact] public void TestCustomValidationDelegateOnMutation() { @@ -386,6 +416,94 @@ public void TestCustomValidationAttributeOnQueryFieldArgs() Assert.Equal("Field 'movies' - Empty or null Title is an invalid search term", results.Errors[1].Message); } + [Fact] + public void TestValidationAttributesOnGraphQLInputType() + { + var schema = SchemaBuilder.FromObject(); + schema.AddMutationsFrom(new SchemaBuilderOptions { AutoCreateInputTypes = true }); + var gql = new QueryRequest + { + Query = + @"mutation { + addPersonWithGraphQLInputType(person: { + personName: ""ThisNameIsTooLong"", + age: 150 + }) { + id + } + }", + }; + + var testContext = new ValidationTestsContext(); + var results = schema.ExecuteRequestWithContext(gql, testContext, null, null); + + Assert.NotNull(results.Errors); + Assert.True(results.Errors.Count >= 1, $"Expected at least 1 error but got {results.Errors.Count}. Errors: {string.Join(", ", results.Errors.Select(e => e.Message))}"); + Assert.Contains(results.Errors, e => e.Message.Contains("Person name must be less than 5 characters")); + Assert.Contains(results.Errors, e => e.Message.Contains("Age must be between 0 and 100")); + } + + [Fact] + public void TestValidationAttributesOnGraphQLInputTypePassValidData() + { + var schema = SchemaBuilder.FromObject(); + schema.AddMutationsFrom(new SchemaBuilderOptions { AutoCreateInputTypes = true }); + var gql = new QueryRequest + { + Query = + @"mutation { + addPersonWithGraphQLInputType(person: { + personName: ""John"", + age: 25 + }) { + id + } + }", + }; + + var testContext = new ValidationTestsContext(); + var results = schema.ExecuteRequestWithContext(gql, testContext, null, null); + + // Debug info in case of unexpected errors + if (results.Errors != null) + { + Assert.Fail($"Unexpected errors: {string.Join(", ", results.Errors.Select(e => e.Message))}"); + } + + Assert.Null(results.Errors); + Assert.NotNull(results.Data); + } + + [Fact] + public void TestNestedValidationAttributesOnGraphQLInputType() + { + var schema = SchemaBuilder.FromObject(); + schema.AddMutationsFrom(new SchemaBuilderOptions { AutoCreateInputTypes = true }); + var gql = new QueryRequest + { + Query = + @"mutation { + addPersonWithNestedGraphQLInputType(person: { + personName: ""John"", + age: 25, + address: { + street: ""ThisStreetNameIsWayTooLong"", + zipCode: ""12"" + } + }) { + id + } + }", + }; + + var testContext = new ValidationTestsContext(); + var results = schema.ExecuteRequestWithContext(gql, testContext, null, null); + Assert.NotNull(results.Errors); + Assert.Equal(2, results.Errors.Count); + Assert.Contains(results.Errors, e => e.Message.Contains("Street must be less than 20 characters")); + Assert.Contains(results.Errors, e => e.Message.Contains("Zip code must be exactly 5 characters")); + } + private static bool AddPerson(PersonArgs args) { return true; @@ -472,6 +590,7 @@ internal class MovieQueryArgsWithValidator internal class ValidationTestsContext { public List Movies { get; set; } = []; + public List People { get; set; } = []; } internal class ValidationTestsMutations @@ -507,6 +626,20 @@ public static bool UpdateCastMemberWithGraphQLValidator(CastMemberArg arg, IGrap validator.AddError("Test Error"); return true; } + + [GraphQLMutation] + public static Expression> AddPersonWithGraphQLInputType([GraphQLInputType] PersonMutationArgs person) + { + var newPerson = new Person { Id = new Random().Next(), Name = person.PersonName }; + return c => c.People.SingleOrDefault(p => p.Id == newPerson.Id); + } + + [GraphQLMutation] + public static Expression> AddPersonWithNestedGraphQLInputType([GraphQLInputType] PersonWithAddressMutationArgs person) + { + var newPerson = new Person { Id = new Random().Next(), Name = person.PersonName }; + return c => c.People.SingleOrDefault(p => p.Id == newPerson.Id); + } } [GraphQLArguments] @@ -552,3 +685,34 @@ internal class PersonArgsWithValidator { public string Name { get; set; } = string.Empty; } + +[GraphQLInputType] +internal class PersonMutationArgs +{ + [StringLength(5, ErrorMessage = "Person name must be less than 5 characters")] + public string PersonName { get; set; } = default!; + + [Range(0, 100, ErrorMessage = "Age must be between 0 and 100")] + public int Age { get; set; } +} + +[GraphQLInputType] +internal class PersonWithAddressMutationArgs +{ + [StringLength(5, ErrorMessage = "Person name must be less than 5 characters")] + public string PersonName { get; set; } = default!; + + [Range(0, 100, ErrorMessage = "Age must be between 0 and 100")] + public int Age { get; set; } + + public AddressInput Address { get; set; } = default!; +} + +internal class AddressInput +{ + [StringLength(20, ErrorMessage = "Street must be less than 20 characters")] + public string Street { get; set; } = default!; + + [StringLength(5, MinimumLength = 5, ErrorMessage = "Zip code must be exactly 5 characters")] + public string ZipCode { get; set; } = default!; +} diff --git a/src/tests/Test.App/Program.cs b/src/tests/Test.App/Program.cs index a187b979..10dfd866 100644 --- a/src/tests/Test.App/Program.cs +++ b/src/tests/Test.App/Program.cs @@ -29,7 +29,7 @@ var app = builder.Build(); -app.MapGraphQL(followSpec: true); +app.MapGraphQL(); app.Run(); diff --git a/src/tests/Test.App/Test.App.csproj b/src/tests/Test.App/Test.App.csproj index e304c0a1..1d6fc413 100644 --- a/src/tests/Test.App/Test.App.csproj +++ b/src/tests/Test.App/Test.App.csproj @@ -1,13 +1,10 @@ - - net9.0 + net8.0;net9.0;net10.0 enable enable - -