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 3a0b1783..dc861eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,111 @@ +# 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 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 + +## Fixes + +- #466 Include enum-typed input fields in introspection per GraphQL spec + # 5.7.0 ## Changes +- QueryInfo Support: Added optional query execution information that can be included in the result extensions. Enable with `ExecutionOptions.IncludeQueryInfo = true` to get metadata about: + - Operation type (Query, Mutation, Subscription) + - Operation name + - Types queried and their selected fields + - Total number of types and fields queried + - This is useful for query analysis, debugging, and monitoring GraphQL usage patterns - Fix #441 (PR #455) - Have your argument class (including an `InputType`) class inherit `ArgumentsTracker` or implement `IArgumentsTracker` to be able to tell if a variable is just the dotnet default value or is set from the query (variable or inline) with `args.IsSet`. You can also include `IArgumentsTracker` as an argument in your mutations etc. This is useful if you method usings simple arguments e.g. `MyMutation(Guid id, string name)` vs. a `[GraphQLArguments]` object. - #461 - `EntityGraphQLEndpointRouteExtensions.MapGraphQL` now supports chunked requests - `RequiredAuthorization` is not settable allowing you to change or overwrite it after creation. Also now has `Clear()` methods to remove authentication diff --git a/README.md b/README.md index 3bc97244..87a89d1d 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,6 @@ This sets up 1 end point: - `POST` at `/graphql` where the body of the post is a GraphQL query - You can authorize that route how you would any ASP.NET route. See Authorization below for details on having parts of the schema requiring Authorization/Claims -_Note - As of version 1.1+ the EntityGraphQL.AspNet extension helper uses System.Text.Json. Previous versions used JSON.NET._ - ## 3. Build awesome applications You can now make a request to your API. For example 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 60da0f05..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()); }); @@ -373,10 +373,33 @@ See the serialization tests for [an example with Newtonsoft.Json](https://github ## Threading and async execution -`EntityGraphQL` executes each request (`schemaProvider.ExecuteRequest(...)`) in a single thread. First, `EntityGraphQL` compiles the whole GraphQL request document then selects the operation to execute. For a mutation operation all top-level mutations individually, in the order it appears in the document, as [required by GraphQL](https://graphql.org/learn/queries/#multiple-fields-in-mutations). For a query operation, `EntityGraphQL` starts each query in the order it appears in the document. Finally, it awaits all queries, the async portion of which is allowed to execute in parallel. +`EntityGraphQL` executes each request (`schemaProvider.ExecuteRequest(...)`) in a single thread. First, `EntityGraphQL` compiles the whole GraphQL request document then selects the operation to execute. For a mutation operation all top-level mutations are executed individually, in the order it appears in the document, as [required by GraphQL](https://graphql.org/learn/queries/#multiple-fields-in-mutations). For a query operation, `EntityGraphQL` executes each query in the order it appears in the document sequentially. Finally, it awaits all queries, the async portion of which is allowed to execute in parallel. Since a GraphQL request is processed with a single thread, database contexts can be scoped services like they do for ordinary web services. Likewise, queries (and mutations) that call external web services can safely use the single-threaded `HttpContext` accessor to access `HttpContext.RequestAborted` to cancel the dependent request if the GraphQL request is aborted. +### Async Fields + +EntityGraphQL provides comprehensive support for asynchronous field resolution using the `ResolveAsync` method. This allows you to integrate with external services, APIs, and perform long-running operations while maintaining control over concurrency and performance. + +```csharp +// Basic async field with service injection +schema.Type() + .AddField("weather", "Current weather data") + .ResolveAsync((person, weatherService) => + weatherService.GetWeatherAsync(person.Location)); + +// With concurrency control +schema.Type() + .AddField("profile", "External profile data") + .ResolveAsync((person, service) => + service.GetProfileAsync(person.Id), + maxConcurrency: 5); // Limit to 5 concurrent operations +``` + +EntityGraphQL supports hierarchical concurrency control at field, service, and query levels to help you manage resource usage effectively. + +For comprehensive information about async fields, concurrency control, error handling, and best practices, see [Async Fields](./schema-creation/async-fields). + ## Tracking Argument Values: IArgumentsTracker EntityGraphQL provides a way to help you determine if an argument or input property was explicitly set by the user in a query or mutation, or if it is just the default .NET value. This is useful for distinguishing between "not provided" and "provided as null/default". diff --git a/docs/docs/integration.md b/docs/docs/integration.md index e19b8090..e4006537 100644 --- a/docs/docs/integration.md +++ b/docs/docs/integration.md @@ -9,3 +9,71 @@ Being GraphQL there are many tools that integrate well with EntityGraphQL. EntityGraphQL supports GraphQL introspection queries so tools like GraphiQL etc can work against your schema. You can use `schema.ToGraphQLSchemaString()` to produce a GraphQL schema file. This works well as input to the Apollo code gen tools. + +## Query Information & Monitoring + +EntityGraphQL can provide detailed information about executed queries through the `QueryInfo` feature. This is useful for: + +- Query analysis and optimization +- Debugging complex queries +- Monitoring GraphQL usage patterns +- Understanding which types and fields are being accessed + +### Enabling Query Information + +To include query execution information in your results, set `IncludeQueryInfo = true` in your execution options: + +```cs +var options = new ExecutionOptions +{ + IncludeQueryInfo = true +}; + +var result = schema.ExecuteRequestWithContext(request, context, serviceProvider, user, options); +``` + +### ASP.NET Integration + +When using EntityGraphQL.AspNet, you can enable query info globally: + +```cs +app.MapGraphQL(options: new ExecutionOptions +{ + IncludeQueryInfo = true +}); +``` + +### Query Information Output + +When enabled, query information is included in the `extensions` field of the GraphQL response: + +```json +{ + "data": { + "people": [{ "name": "John", "projects": [{ "name": "Project A" }] }] + }, + "extensions": { + "queryInfo": { + "operationType": "Query", + "operationName": "GetPeople", + "totalTypesQueried": 3, // Includes the Query Type + "totalFieldsQueried": 6, + "typesQueried": { + "Query": ["people"], + "Person": ["name", "projects"], + "Project": ["name"] + } + } + } +} +``` + +### Query Information Properties + +- **operationType**: The type of GraphQL operation (Query, Mutation, or Subscription) +- **operationName**: The name of the operation (if provided in the query) +- **totalTypesQueried**: Total number of types accessed in the query +- **totalFieldsQueried**: Total number of fields selected across all types +- **typesQueried**: Dictionary mapping type names to the list of fields selected from each type + +Note: Fragment spreads are expanded and their fields are counted, but the fragment spread itself is not counted as a field. 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/async-fields.md b/docs/docs/schema-creation/async-fields.md new file mode 100644 index 00000000..46752ddc --- /dev/null +++ b/docs/docs/schema-creation/async-fields.md @@ -0,0 +1,234 @@ +--- +sidebar_position: 3 +--- + +# Async Fields + +EntityGraphQL provides comprehensive support for asynchronous field resolution, allowing you to integrate with external services, databases, and APIs while maintaining control over performance and concurrency. + +EntityGraphQL handles async execution by compiling all fields into expression trees first, then executing them with proper concurrency control and task coordination. + +## Basic Async Field Setup + +### Using ResolveAsync + +The `ResolveAsync` method allows you to define fields that execute asynchronously using injected services: + +```csharp +public class WeatherService +{ + private readonly HttpClient httpClient; + + public WeatherService(HttpClient httpClient) + { + this.httpClient = httpClient; + } + + public async Task GetWeatherAsync(string location) + { + var response = await httpClient.GetAsync($"/weather?location={location}"); + return await response.Content.ReadFromJsonAsync(); + } +} + +public class Person +{ + public int Id { get; set; } + public string Name { get; set; } + public string Location { get; set; } +} + +// Add async field to your schema +var schema = SchemaBuilder.FromObject(); + +schema.Type() + .AddField("weather", "Current weather for this person's location") + .ResolveAsync((person, weatherService) => + weatherService.GetWeatherAsync(person.Location)); +``` + +## Concurrency Control + +EntityGraphQL provides three levels of concurrency control to help you manage resource usage and prevent overwhelming external services. + +All concurrency limits apply only to the currently executing query. + +### Field-Level Concurrency + +Limit concurrency for individual fields: + +```csharp +schema.Type() + .AddField("expensiveOperation", "Resource-intensive operation") + .ResolveAsync((person, service) => + service.DoExpensiveWorkAsync(person.Id), + maxConcurrency: 5); // Only 5 concurrent operations for this field +``` + +### Service-Level Concurrency + +Configure concurrency limits for entire services across all fields that use them: + +```csharp +var executionOptions = new ExecutionOptions +{ + ServiceConcurrencyLimits = new Dictionary + { + [typeof(WeatherService)] = 10, // Max 10 concurrent weather calls + [typeof(DatabaseService)] = 3, // Max 3 concurrent database operations + [typeof(EmailService)] = 2 // Max 2 concurrent email sends + } +}; + +var result = await schema.ExecuteRequestAsync(query, context, serviceProvider, executionOptions); +``` + +### Query-Level Concurrency + +Set a global limit for all async operations in a single query: + +```csharp +var executionOptions = new ExecutionOptions +{ + MaxQueryConcurrency = 20 // Maximum 20 concurrent operations across entire query +}; + +var result = await schema.ExecuteRequestAsync(query, context, serviceProvider, executionOptions); +``` + +### Hierarchical Concurrency Control + +EntityGraphQL applies concurrency limits hierarchically - each level respects the limits above it: + +1. **Query Level**: Global limit for the entire query +2. **Service Level**: Limit per service type +3. **Field Level**: Limit per individual field + +For example, with these settings: + +- Query limit: 50 +- WeatherService limit: 10 +- Field limit: 3 + +The field will never exceed 3 concurrent operations, the WeatherService will never exceed 10 concurrent operations, and the entire query will never exceed 50 concurrent operations. + +```csharp +var executionOptions = new ExecutionOptions +{ + MaxQueryConcurrency = 50, + ServiceConcurrencyLimits = new Dictionary + { + [typeof(WeatherService)] = 10 + } +}; + +schema.Type() + .AddField("weather", "Weather data") + .ResolveAsync((person, service) => + service.GetWeatherAsync(person.Location), + maxConcurrency: 3); +``` + +### Implementation Overview + +You can view the implementation in `ConcurrencyLimitFieldExtension` and `ConcurrencyLimiterRegistry`. It uses `SemaphoreSlim` to control the concurrency limits. + +This controls when the async method _starts_. Taking the `GetWeatherAsync` example above, if we are resolving `weather` within a list of 100 people `GetWeatherAsync` will only be started/called 3 at a time. + +If you have no limits set up (the default) all `async` fields will start at the same time. + +## Cancellation Support + +EntityGraphQL provides comprehensive support for `CancellationToken` to enable cooperative cancellation of long-running operations. This allows you to gracefully handle request timeouts, client disconnections, and manual cancellation. + +### Basic CancellationToken Usage + +#### In Service Methods + +Your service methods can accept a `CancellationToken` parameter, which EntityGraphQL will automatically provide: + +```csharp +public class WeatherService +{ + private readonly HttpClient httpClient; + + public WeatherService(HttpClient httpClient) + { + this.httpClient = httpClient; + } + + public async Task GetWeatherAsync(string location, CancellationToken cancellationToken = default) + { + // Pass cancellationToken to async operations + var response = await httpClient.GetAsync($"/weather?location={location}", cancellationToken); + return await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + } +} +``` + +#### In ResolveAsync Methods + +Use the `ResolveAsync` overload that accepts a `CancellationToken` parameter: + +```csharp +schema.Type() + .AddField("weather", "Current weather for this person's location") + .ResolveAsync((person, weatherService, cancellationToken) => + weatherService.GetWeatherAsync(person.Location, cancellationToken)); +``` + +### ASP.NET Core Integration + +When using EntityGraphQL with ASP.NET Core, the framework automatically uses `HttpContext.RequestAborted` as the cancellation token. This means operations are cancelled when: + +- The client disconnects +- The request times out +- The server is shutting down + +```csharp +// In your controller or minimal API +app.MapPost("/graphql", async (HttpContext context, GraphQLRequest request) => +{ + var result = await schema.ExecuteRequestAsync( + request.Query, + myContext, + context.RequestServices, + cancellationToken: context.RequestAborted); // Automatically handled by EntityGraphQL.AspNet + + return result; +}); +``` + +### Manual Cancellation + +You can also provide your own `CancellationToken` for scenarios like: + +- Custom timeout policies +- Manual cancellation based on business logic +- Testing scenarios + +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); // 30-second timeout + +var result = await schema.ExecuteRequestAsync( + query, + context, + serviceProvider, + cancellationToken: cts.Token); +``` + +### Concurrency and Cancellation + +Cancellation works seamlessly with EntityGraphQL's concurrency control features. When a cancellation is requested: + +1. All pending async operations receive the cancellation signal +2. Operations waiting for semaphore slots are cancelled immediately +3. Currently executing operations can respond to cancellation cooperatively + +```csharp +schema.Type() + .AddField("expensiveOperation", "Resource-intensive operation") + .ResolveAsync((person, service, cancellationToken) => + service.DoExpensiveWorkAsync(person.Id, cancellationToken), + maxConcurrency: 5); // Concurrency limits + cancellation support +``` diff --git a/docs/docs/schema-creation/fields.md b/docs/docs/schema-creation/fields.md index 4ed772b5..5c5f6678 100644 --- a/docs/docs/schema-creation/fields.md +++ b/docs/docs/schema-creation/fields.md @@ -261,3 +261,24 @@ query { ``` In both cases, only the `name` and `city` filters will be applied, even though `city` is explicitly set to `null`. + +## Async Fields + +For fields that need to perform asynchronous operations, EntityGraphQL provides the `ResolveAsync` method with comprehensive concurrency control. + +```csharp +// Basic async field +schema.Type() + .AddField("externalData", "Data from external API") + .ResolveAsync((person, service) => + service.GetDataAsync(person.Id)); + +// With concurrency limiting +schema.Type() + .AddField("weatherData", "Current weather") + .ResolveAsync((person, service) => + service.GetWeatherAsync(person.Location), + maxConcurrency: 5); +``` + +For comprehensive documentation on async fields, concurrency control, best practices, and integration patterns, see [Async Fields](./async-fields). diff --git a/docs/docs/schema-creation/mutations.md b/docs/docs/schema-creation/mutations.md index b9a31b75..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. @@ -480,3 +478,124 @@ mutation { } } ``` + +## Async Mutations + +EntityGraphQL supports asynchronous mutations by returning `Task` from your mutation methods. This is useful for operations that call external services, perform database operations, or other async work. + +```csharp +public class PeopleMutations +{ + [GraphQLMutation("Add a new person asynchronously")] + public async Task>> AddPersonAsync( + DemoContext db, + string firstName, + string lastName, + EmailService emailService) + { + var person = new Person + { + FirstName = firstName, + LastName = lastName, + }; + + db.People.Add(person); + await db.SaveChangesAsync(); + + // Send welcome email asynchronously + await emailService.SendWelcomeEmailAsync(person); + + return (ctx) => ctx.People.First(p => p.Id == person.Id); + } + + [GraphQLMutation("Update person with external validation")] + public async Task UpdatePersonWithValidationAsync( + int personId, + string newEmail, + ValidationService validator, + DemoContext db) + { + // Validate email with external service + var isValid = await validator.ValidateEmailAsync(newEmail); + if (!isValid) + { + throw new InvalidOperationException("Email validation failed"); + } + + var person = await db.People.FindAsync(personId); + if (person == null) + { + return false; + } + + person.Email = newEmail; + await db.SaveChangesAsync(); + return true; + } +} +``` + +### Mutations vs. Async Fields + +- **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/other-data-sources.md b/docs/docs/schema-creation/other-data-sources.md index 01c213d7..cf2ae480 100644 --- a/docs/docs/schema-creation/other-data-sources.md +++ b/docs/docs/schema-creation/other-data-sources.md @@ -278,6 +278,67 @@ var result = data.Select(p => new { }); ``` +## Async Bulk Data Loading + +For scenarios where your bulk data loading operations are asynchronous (e.g., making HTTP calls to external APIs, async database operations), you can use `ResolveBulkAsync`. This works similarly to `ResolveBulk` but supports `Task` return types and includes concurrency limiting. + +```cs +schema.UpdateType(type => +{ + type.AddField("createdBy", "Get the user details of user that created this project") + // normal async service to fetch the User object for creator of the Project type + .ResolveAsync((proj, users) => users.GetUserByIdAsync(proj.CreatedById)) + // Async bulk service used to fetch many User objects + .ResolveBulkAsync(proj => proj.CreatedById, (ids, srv) => srv.GetAllUsersAsync(ids)); +}); +``` + +The async bulk loader method signature needs to match the following: + +```cs +public Task> MethodName(IEnumerable data) {} + +// Example of this above +public async Task> GetAllUsersAsync(IEnumerable data) +{ + // Make async calls to external API, database, etc. + var users = await externalApiClient.GetUsersAsync(data); + return users.ToDictionary(u => u.Id, u => u); +} +``` + +### Concurrency Limiting + +`ResolveBulkAsync` supports concurrency limiting to prevent overwhelming external services or hitting rate limits. You can specify the maximum number of concurrent bulk operations: + +```cs +schema.UpdateType(type => +{ + type.AddField("createdBy", "Get the user details of user that created this project") + .ResolveAsync((proj, users) => users.GetUserByIdAsync(proj.CreatedById)) + // Limit to 5 concurrent bulk operations + .ResolveBulkAsync( + proj => proj.CreatedById, + (ids, srv) => srv.GetAllUsersAsync(ids), + maxConcurrency: 5 + ); +}); +``` + +Concurrency can also be configured at different levels: + +1. **Field level**: Using the `maxConcurrency` parameter as shown above +2. **Query level**: Set `ExecutionOptions.MaxConcurrency` when executing the query +3. **Service level**: Configure in your dependency injection container + +When multiple concurrency limits are specified, all will be applied. + +:::info + +The concurrency limiting applies to how many bulk loader operations can run simultaneously, not to individual items within a single bulk operation. This helps prevent overwhelming external services while still allowing efficient batching of requests. + +::: + ## Limitation using services with `[GraphQLField]` method fields Because EntityGraphQL handles service fields by executing an expression without those fields and _rewriting_ the expressions to work with the resulting type - see [How EntityGraphQL handles services](../library-compatibility/entity-framework) section for more details - you cannot use services with a method as EntityGraphQL cannot rewrite and data may be missing. 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/docusaurus.config.js b/docs/docusaurus.config.js index 07b67a37..ff689efa 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -1,14 +1,14 @@ // @ts-check // Note: type annotations allow type checking and IDEs autocompletion -const {themes} = require('prism-react-renderer'); +const { themes } = require('prism-react-renderer'); const lightCodeTheme = themes.github; const darkCodeTheme = themes.dracula; /** @type {import('@docusaurus/types').Config} */ const config = { title: 'Entity GraphQL', - tagline: 'A modern .NET Core GraphQL library', + tagline: 'A modern .NET GraphQL library', url: 'https://entitygraphql.github.io', baseUrl: '/', onBrokenLinks: 'throw', @@ -114,10 +114,10 @@ const config = { apiKey: '1382a55cce60fc56fb5c6f05fb12443e', indexName: 'entitygraphql', contextualSearch: true, - + // Optional: Algolia search parameters searchParameters: {}, - + // Optional: path for search page that enabled by default (`false` to disable it) searchPagePath: 'search', //... other Algolia params diff --git a/docs/package-lock.json b/docs/package-lock.json index 8dc351c9..06f5ad89 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8,60 +8,132 @@ "name": "entity-graphql-docs", "version": "0.0.0", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/preset-classic": "3.7.0", - "@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.7.0", + "@docusaurus/module-type-aliases": "3.9.2", "gh-pages": "6.3.0" }, "engines": { "node": ">=18.0" } }, - "node_modules/@algolia/autocomplete-core": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", - "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", - "license": "MIT", + "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": { - "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", - "@algolia/autocomplete-shared": "1.17.7" + "@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/@algolia/autocomplete-plugin-algolia-insights": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", - "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", - "license": "MIT", + "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": { - "@algolia/autocomplete-shared": "1.17.7" + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" }, "peerDependencies": { - "search-insights": ">= 1 < 3" + "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.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.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.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.19.2", + "@algolia/autocomplete-shared": "1.19.2" } }, - "node_modules/@algolia/autocomplete-preset-algolia": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", - "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "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.7" + "@algolia/autocomplete-shared": "1.19.2" }, "peerDependencies": { - "@algolia/client-search": ">= 4.9.1 < 6", - "algoliasearch": ">= 4.9.1 < 6" + "search-insights": ">= 1 < 3" } }, "node_modules/@algolia/autocomplete-shared": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", - "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "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", @@ -69,99 +141,99 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.19.0.tgz", - "integrity": "sha512-dMHwy2+nBL0SnIsC1iHvkBao64h4z+roGelOz11cxrDBrAdASxLxmfVMop8gmodQ2yZSacX0Rzevtxa+9SqxCw==", + "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.19.0", - "@algolia/requester-browser-xhr": "5.19.0", - "@algolia/requester-fetch": "5.19.0", - "@algolia/requester-node-http": "5.19.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.19.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.19.0.tgz", - "integrity": "sha512-CDW4RwnCHzU10upPJqS6N6YwDpDHno7w6/qXT9KPbPbt8szIIzCHrva4O9KIfx1OhdsHzfGSI5hMAiOOYl4DEQ==", + "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.19.0", - "@algolia/requester-browser-xhr": "5.19.0", - "@algolia/requester-fetch": "5.19.0", - "@algolia/requester-node-http": "5.19.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.19.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.19.0.tgz", - "integrity": "sha512-2ERRbICHXvtj5kfFpY5r8qu9pJII/NAHsdgUXnUitQFwPdPL7wXiupcvZJC7DSntOnE8AE0lM7oDsPhrJfj5nQ==", + "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.19.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.19.0.tgz", - "integrity": "sha512-xPOiGjo6I9mfjdJO7Y+p035aWePcbsItizIp+qVyfkfZiGgD+TbNxM12g7QhFAHIkx/mlYaocxPY/TmwPzTe+A==", + "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.19.0", - "@algolia/requester-browser-xhr": "5.19.0", - "@algolia/requester-fetch": "5.19.0", - "@algolia/requester-node-http": "5.19.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.19.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.19.0.tgz", - "integrity": "sha512-B9eoce/fk8NLboGje+pMr72pw+PV7c5Z01On477heTZ7jkxoZ4X92dobeGuEQop61cJ93Gaevd1of4mBr4hu2A==", + "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.19.0", - "@algolia/requester-browser-xhr": "5.19.0", - "@algolia/requester-fetch": "5.19.0", - "@algolia/requester-node-http": "5.19.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.19.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.19.0.tgz", - "integrity": "sha512-6fcP8d4S8XRDtVogrDvmSM6g5g6DndLc0pEm1GCKe9/ZkAzCmM3ZmW1wFYYPxdjMeifWy1vVEDMJK7sbE4W7MA==", + "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.19.0", - "@algolia/requester-browser-xhr": "5.19.0", - "@algolia/requester-fetch": "5.19.0", - "@algolia/requester-node-http": "5.19.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.19.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.19.0.tgz", - "integrity": "sha512-Ctg3xXD/1VtcwmkulR5+cKGOMj4r0wC49Y/KZdGQcqpydKn+e86F6l3tb3utLJQVq4lpEJud6kdRykFgcNsp8Q==", + "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.19.0", - "@algolia/requester-browser-xhr": "5.19.0", - "@algolia/requester-fetch": "5.19.0", - "@algolia/requester-node-http": "5.19.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" @@ -174,138 +246,125 @@ "license": "MIT" }, "node_modules/@algolia/ingestion": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.19.0.tgz", - "integrity": "sha512-LO7w1MDV+ZLESwfPmXkp+KLeYeFrYEgtbCZG6buWjddhYraPQ9MuQWLhLLiaMlKxZ/sZvFTcZYuyI6Jx4WBhcg==", + "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.19.0", - "@algolia/requester-browser-xhr": "5.19.0", - "@algolia/requester-fetch": "5.19.0", - "@algolia/requester-node-http": "5.19.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.19.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.19.0.tgz", - "integrity": "sha512-Mg4uoS0aIKeTpu6iv6O0Hj81s8UHagi5TLm9k2mLIib4vmMtX7WgIAHAcFIaqIZp5D6s5EVy1BaDOoZ7buuJHA==", + "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.19.0", - "@algolia/requester-browser-xhr": "5.19.0", - "@algolia/requester-fetch": "5.19.0", - "@algolia/requester-node-http": "5.19.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.19.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.19.0.tgz", - "integrity": "sha512-PbgrMTbUPlmwfJsxjFhal4XqZO2kpBNRjemLVTkUiti4w/+kzcYO4Hg5zaBgVqPwvFDNQ8JS4SS3TBBem88u+g==", + "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.19.0", - "@algolia/requester-browser-xhr": "5.19.0", - "@algolia/requester-fetch": "5.19.0", - "@algolia/requester-node-http": "5.19.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.19.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.19.0.tgz", - "integrity": "sha512-GfnhnQBT23mW/VMNs7m1qyEyZzhZz093aY2x8p0era96MMyNv8+FxGek5pjVX0b57tmSCZPf4EqNCpkGcGsmbw==", + "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.19.0" + "@algolia/client-common": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.19.0.tgz", - "integrity": "sha512-oyTt8ZJ4T4fYvW5avAnuEc6Laedcme9fAFryMD9ndUTIUe/P0kn3BuGcCLFjN3FDmdrETHSFkgPPf1hGy3sLCw==", + "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.19.0" + "@algolia/client-common": "5.45.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.19.0.tgz", - "integrity": "sha512-p6t8ue0XZNjcRiqNkb5QAM0qQRAKsCiebZ6n9JjWA+p8fWf8BvnhO55y2fO28g3GW0Imj7PrAuyBuxq8aDVQwQ==", + "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.19.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.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", - "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", + "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.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", + "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": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@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.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", @@ -330,15 +389,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "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.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@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" }, "engines": { @@ -346,25 +405,25 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -383,17 +442,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "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.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@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.28.5", "semver": "^6.3.1" }, "engines": { @@ -413,13 +472,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", - "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "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.25.9", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -439,56 +498,65 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", - "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "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.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "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.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -498,35 +566,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -536,14 +604,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -553,79 +621,79 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "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" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "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.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@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.27.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", - "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "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.0", - "@babel/types": "^7.27.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "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.27.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -635,13 +703,13 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", - "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "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.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -651,12 +719,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", - "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -666,12 +734,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -681,14 +749,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -698,13 +766,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "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.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -738,12 +806,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -753,12 +821,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -768,12 +836,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -783,12 +851,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -814,12 +882,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -829,14 +897,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", - "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -846,14 +914,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -863,12 +931,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", - "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -878,12 +946,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", - "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "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.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -893,13 +961,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -909,13 +977,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "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.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -925,17 +993,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "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.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "globals": "^11.1.0" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -945,13 +1013,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -961,12 +1029,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "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.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -976,13 +1045,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", - "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -992,12 +1061,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", - "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1007,13 +1076,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1023,12 +1092,28 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", - "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -1038,12 +1123,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", - "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "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.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1053,12 +1138,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", - "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1068,13 +1153,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", - "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1084,14 +1169,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1101,12 +1186,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", - "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1116,12 +1201,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1131,12 +1216,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", - "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "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.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1146,12 +1231,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1161,13 +1246,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", - "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1177,13 +1262,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", - "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1193,15 +1278,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", - "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "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.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1211,13 +1296,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", - "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1227,13 +1312,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1243,12 +1328,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", - "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1258,12 +1343,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", - "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1273,12 +1358,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", - "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1288,14 +1373,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", - "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "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.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" + "@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.4" }, "engines": { "node": ">=6.9.0" @@ -1305,13 +1392,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1321,12 +1408,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", - "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1336,13 +1423,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "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.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1352,12 +1439,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1367,13 +1454,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1383,14 +1470,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", - "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1400,12 +1487,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1415,12 +1502,12 @@ } }, "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.25.9.tgz", - "integrity": "sha512-Ncw2JFsJVuvfRsa2lSHiC55kETQVLSnsYGQ1JDDwkUeWGTL/8Tom8aLTnlqgoeuopWrbbGndrc9AlLYrIosrow==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", + "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1430,12 +1517,12 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz", - "integrity": "sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1445,16 +1532,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz", - "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1464,12 +1551,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz", - "integrity": "sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", "license": "MIT", "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.25.9" + "@babel/plugin-transform-react-jsx": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1479,13 +1566,13 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz", - "integrity": "sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1495,13 +1582,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", - "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "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.25.9", - "regenerator-transform": "^0.15.2" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1511,13 +1597,13 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", - "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1527,12 +1613,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", - "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1542,16 +1628,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz", - "integrity": "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==", + "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.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", - "babel-plugin-polyfill-regenerator": "^0.6.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "engines": { @@ -1571,12 +1657,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1586,13 +1672,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1602,12 +1688,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", - "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1617,12 +1703,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", - "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1632,12 +1718,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", - "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1647,16 +1733,16 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz", - "integrity": "sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==", + "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.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@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" }, "engines": { "node": ">=6.9.0" @@ -1666,12 +1752,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", - "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1681,13 +1767,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", - "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1697,13 +1783,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", - "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1713,13 +1799,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", - "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1729,79 +1815,80 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", - "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "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.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.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.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.25.9", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.25.9", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.25.9", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.25.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.25.9", - "@babel/plugin-transform-typeof-symbol": "^7.25.9", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@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.5", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@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.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.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.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.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.4", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^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.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", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.38.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "engines": { @@ -1835,17 +1922,17 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.26.3.tgz", - "integrity": "sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw==", + "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.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-transform-react-display-name": "^7.25.9", - "@babel/plugin-transform-react-jsx": "^7.25.9", - "@babel/plugin-transform-react-jsx-development": "^7.25.9", - "@babel/plugin-transform-react-pure-annotations": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^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" }, "engines": { "node": ">=6.9.0" @@ -1855,16 +1942,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", - "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", + "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.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-typescript": "^7.25.9" + "@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.28.5" }, "engines": { "node": ">=6.9.0" @@ -1886,58 +1973,57 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz", - "integrity": "sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==", + "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.30.2", - "regenerator-runtime": "^0.14.0" + "core-js-pure": "^3.43.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", - "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "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.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "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.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1947,15 +2033,16 @@ "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" } }, "node_modules/@csstools/cascade-layer-name-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.4.tgz", - "integrity": "sha512-7DFHlPuIxviKYZrOiwVU/PiHLm3lLUR23OMuEEtfEOQTOp9hzQ2JjdY6X5H18RVuUPJqSCI+qNnD5iOLMVE0bA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", + "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", "funding": [ { "type": "github", @@ -1971,14 +2058,14 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/color-helpers": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz", - "integrity": "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==", + "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", @@ -1995,9 +2082,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.1.tgz", - "integrity": "sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "funding": [ { "type": "github", @@ -2013,14 +2100,14 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz", - "integrity": "sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==", + "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", @@ -2033,21 +2120,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.0.1", - "@csstools/css-calc": "^2.1.1" + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", - "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "funding": [ { "type": "github", @@ -2063,13 +2150,13 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", - "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "funding": [ { "type": "github", @@ -2086,9 +2173,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz", - "integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", "funding": [ { "type": "github", @@ -2104,14 +2191,43 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@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.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.1.tgz", - "integrity": "sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", + "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", "funding": [ { "type": "github", @@ -2157,9 +2273,9 @@ } }, "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -2170,9 +2286,38 @@ } }, "node_modules/@csstools/postcss-color-function": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.7.tgz", - "integrity": "sha512-aDHYmhNIHR6iLw4ElWhf+tRqqaXwKnMl0YsQ/X105Zc4dQwe6yJpMrTN6BwOoESrkDjOYMOfORviSSLeDTJkdQ==", + "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", @@ -2185,10 +2330,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.7", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@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": { @@ -2199,9 +2344,38 @@ } }, "node_modules/@csstools/postcss-color-mix-function": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.7.tgz", - "integrity": "sha512-e68Nev4CxZYCLcrfWhHH4u/N1YocOfTmw67/kVX5Rb7rnguqqLyxPjhHWjSBX8o4bmyuukmNf3wrUSU3//kT7g==", + "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", + "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-mix-variadic-function-arguments": { + "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", @@ -2214,10 +2388,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.7", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@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": { @@ -2228,9 +2402,9 @@ } }, "node_modules/@csstools/postcss-content-alt-text": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.4.tgz", - "integrity": "sha512-YItlZUOuZJCBlRaCf8Aucc1lgN41qYGALMly0qQllrxYJhiyzlI6RxOTMUvtWk+KhS8GphMDsDhKQ7KTPfEMSw==", + "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", @@ -2243,9 +2417,38 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.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-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", + "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": { @@ -2256,9 +2459,9 @@ } }, "node_modules/@csstools/postcss-exponential-functions": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.6.tgz", - "integrity": "sha512-IgJA5DQsQLu/upA3HcdvC6xEMR051ufebBTIXZ5E9/9iiaA7juXWz1ceYj814lnDYP/7eWjZnw0grRJlX4eI6g==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", + "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", "funding": [ { "type": "github", @@ -2271,9 +2474,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.1", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2309,9 +2512,9 @@ } }, "node_modules/@csstools/postcss-gamut-mapping": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.7.tgz", - "integrity": "sha512-gzFEZPoOkY0HqGdyeBXR3JP218Owr683u7KOZazTK7tQZBE8s2yhg06W1tshOqk7R7SWvw9gkw2TQogKpIW8Xw==", + "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", @@ -2324,9 +2527,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.7", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2336,9 +2539,9 @@ } }, "node_modules/@csstools/postcss-gradients-interpolation-method": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.7.tgz", - "integrity": "sha512-WgEyBeg6glUeTdS2XT7qeTFBthTJuXlS9GFro/DVomj7W7WMTamAwpoP4oQCq/0Ki2gvfRYFi/uZtmRE14/DFA==", + "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", @@ -2351,10 +2554,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.7", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@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": { @@ -2365,9 +2568,9 @@ } }, "node_modules/@csstools/postcss-hwb-function": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.7.tgz", - "integrity": "sha512-LKYqjO+wGwDCfNIEllessCBWfR4MS/sS1WXO+j00KKyOjm7jDW2L6jzUmqASEiv/kkJO39GcoIOvTTfB3yeBUA==", + "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", @@ -2380,10 +2583,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.7", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@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": { @@ -2394,9 +2597,9 @@ } }, "node_modules/@csstools/postcss-ic-unit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.0.tgz", - "integrity": "sha512-9QT5TDGgx7wD3EEMN3BSUG6ckb6Eh5gSPT5kZoVtUuAonfPmLDJyPhqR4ntPpMYhUKAMVKAg3I/AgzqHMSeLhA==", + "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", @@ -2409,7 +2612,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, @@ -2421,9 +2624,9 @@ } }, "node_modules/@csstools/postcss-initial": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.0.tgz", - "integrity": "sha512-dv2lNUKR+JV+OOhZm9paWzYBXOCi+rJPqJ2cJuhh9xd8USVrd0cBEPczla81HNOyThMQWeCcdln3gZkQV2kYxA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz", + "integrity": "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==", "funding": [ { "type": "github", @@ -2443,9 +2646,9 @@ } }, "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.1.tgz", - "integrity": "sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", "funding": [ { "type": "github", @@ -2491,9 +2694,9 @@ } }, "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -2504,9 +2707,9 @@ } }, "node_modules/@csstools/postcss-light-dark-function": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.7.tgz", - "integrity": "sha512-ZZ0rwlanYKOHekyIPaU+sVm3BEHCe+Ha0/px+bmHe62n0Uc1lL34vbwrLYn6ote8PHlsqzKeTQdIejQCJ05tfw==", + "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", @@ -2519,9 +2722,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.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": { @@ -2623,9 +2826,9 @@ } }, "node_modules/@csstools/postcss-logical-viewport-units": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.3.tgz", - "integrity": "sha512-OC1IlG/yoGJdi0Y+7duz/kU/beCwO+Gua01sD6GtOtLi7ByQUpcIqs7UE/xuRPay4cHgOMatWdnDdsIDjnWpPw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", + "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", "funding": [ { "type": "github", @@ -2638,7 +2841,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-tokenizer": "^3.0.3", + "@csstools/css-tokenizer": "^3.0.4", "@csstools/utilities": "^2.0.0" }, "engines": { @@ -2649,9 +2852,9 @@ } }, "node_modules/@csstools/postcss-media-minmax": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.6.tgz", - "integrity": "sha512-J1+4Fr2W3pLZsfxkFazK+9kr96LhEYqoeBszLmFjb6AjYs+g9oDAw3J5oQignLKk3rC9XHW+ebPTZ9FaW5u5pg==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", + "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", "funding": [ { "type": "github", @@ -2664,10 +2867,10 @@ ], "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.1", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/media-query-list-parser": "^4.0.2" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" }, "engines": { "node": ">=18" @@ -2677,9 +2880,9 @@ } }, "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.4.tgz", - "integrity": "sha512-AnGjVslHMm5xw9keusQYvjVWvuS7KWK+OJagaG0+m9QnIjZsrysD2kJP/tr/UJIyYtMCtu8OkUd+Rajb4DqtIQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", + "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", "funding": [ { "type": "github", @@ -2692,9 +2895,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/media-query-list-parser": "^4.0.2" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" }, "engines": { "node": ">=18" @@ -2755,9 +2958,9 @@ } }, "node_modules/@csstools/postcss-oklab-function": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.7.tgz", - "integrity": "sha512-I6WFQIbEKG2IO3vhaMGZDkucbCaUSXMxvHNzDdnfsTCF5tc0UlV3Oe2AhamatQoKFjBi75dSEMrgWq3+RegsOQ==", + "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", @@ -2770,10 +2973,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.7", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@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": { @@ -2784,9 +2987,9 @@ } }, "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.0.0.tgz", - "integrity": "sha512-XQPtROaQjomnvLUSy/bALTR5VCtTVUFwYs1SblvYgLSeTo2a/bMNwUwo2piXw5rTv/FEYiy5yPSXBqg9OKUx7Q==", + "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", @@ -2809,9 +3012,9 @@ } }, "node_modules/@csstools/postcss-random-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-1.0.2.tgz", - "integrity": "sha512-vBCT6JvgdEkvRc91NFoNrLjgGtkLWt47GKT6E2UDn3nd8ZkMBiziQ1Md1OiKoSsgzxsSnGKG3RVdhlbdZEkHjA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", + "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", "funding": [ { "type": "github", @@ -2824,9 +3027,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.1", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2836,9 +3039,9 @@ } }, "node_modules/@csstools/postcss-relative-color-syntax": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.7.tgz", - "integrity": "sha512-apbT31vsJVd18MabfPOnE977xgct5B1I+Jpf+Munw3n6kKb1MMuUmGGH+PT9Hm/fFs6fe61Q/EWnkrb4bNoNQw==", + "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", @@ -2851,10 +3054,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.7", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@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": { @@ -2890,9 +3093,9 @@ } }, "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -2903,9 +3106,9 @@ } }, "node_modules/@csstools/postcss-sign-functions": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.1.tgz", - "integrity": "sha512-MslYkZCeMQDxetNkfmmQYgKCy4c+w9pPDfgOBCJOo/RI1RveEUdZQYtOfrC6cIZB7sD7/PHr2VGOcMXlZawrnA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", + "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", "funding": [ { "type": "github", @@ -2918,9 +3121,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.1", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2930,9 +3133,9 @@ } }, "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.6.tgz", - "integrity": "sha512-/dwlO9w8vfKgiADxpxUbZOWlL5zKoRIsCymYoh1IPuBsXODKanKnfuZRr32DEqT0//3Av1VjfNZU9yhxtEfIeA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", + "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", "funding": [ { "type": "github", @@ -2945,9 +3148,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.1", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -2957,9 +3160,9 @@ } }, "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.1.tgz", - "integrity": "sha512-xPZIikbx6jyzWvhms27uugIc0I4ykH4keRvoa3rxX5K7lEhkbd54rjj/dv60qOCTisoS+3bmwJTeyV1VNBrXaw==", + "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", @@ -2972,7 +3175,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/color-helpers": "^5.0.1", + "@csstools/color-helpers": "^5.1.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -2983,9 +3186,9 @@ } }, "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.6.tgz", - "integrity": "sha512-c4Y1D2Why/PeccaSouXnTt6WcNHJkoJRidV2VW9s5gJ97cNxnLgQ4Qj8qOqkIR9VmTQKJyNcbF4hy79ZQnWD7A==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", + "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", "funding": [ { "type": "github", @@ -2998,9 +3201,9 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-calc": "^2.1.1", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" }, "engines": { "node": ">=18" @@ -3062,28 +3265,15 @@ "node": ">=10.0.0" } }, - "node_modules/@docsearch/css": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", - "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", - "license": "MIT" - }, - "node_modules/@docsearch/react": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", - "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "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", - "dependencies": { - "@algolia/autocomplete-core": "1.17.7", - "@algolia/autocomplete-preset-algolia": "1.17.7", - "@docsearch/css": "3.8.2", - "algoliasearch": "^5.14.2" - }, "peerDependencies": { - "@types/react": ">= 16.8.0 < 19.0.0", - "react": ">= 16.8.0 < 19.0.0", - "react-dom": ">= 16.8.0 < 19.0.0", - "search-insights": ">= 1 < 3" + "@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": { @@ -3094,16 +3284,55 @@ }, "react-dom": { "optional": true - }, - "search-insights": { - "optional": true + } + } + }, + "node_modules/@docsearch/css": { + "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": "4.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.3.2.tgz", + "integrity": "sha512-74SFD6WluwvgsOPqifYOviEEVwDxslxfhakTlra+JviaNcs7KK/rjsPj89kVEoQc9FUxRkAofaJnHIR7pb4TSQ==", + "license": "MIT", + "dependencies": { + "@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", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true } } }, "node_modules/@docusaurus/babel": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.7.0.tgz", - "integrity": "sha512-0H5uoJLm14S/oKV3Keihxvh8RV+vrid+6Gv+2qhuzbqHanawga8tYnsdpjEyt36ucJjqlby2/Md2ObWjA02UXQ==", + "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", @@ -3116,42 +3345,41 @@ "@babel/runtime": "^7.25.9", "@babel/runtime-corejs3": "^7.25.9", "@babel/traverse": "^7.25.9", - "@docusaurus/logger": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@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.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.7.0.tgz", - "integrity": "sha512-CUUT9VlSGukrCU5ctZucykvgCISivct+cby28wJwCC/fkQFgAHRp/GKv2tx38ZmXb7nacrKzFTcp++f9txUYGg==", + "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.7.0", - "@docusaurus/cssnano-preset": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@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.2", + "clean-css": "^5.3.3", "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.8.1", + "css-loader": "^6.11.0", "css-minimizer-webpack-plugin": "^5.0.1", "cssnano": "^6.1.2", "file-loader": "^6.2.0", "html-minifier-terser": "^7.2.0", - "mini-css-extract-plugin": "^2.9.1", + "mini-css-extract-plugin": "^2.9.2", "null-loader": "^4.0.1", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "postcss-preset-env": "^10.1.0", - "react-dev-utils": "^12.0.1", + "postcss": "^8.5.4", + "postcss-loader": "^7.3.4", + "postcss-preset-env": "^10.2.1", "terser-webpack-plugin": "^5.3.9", "tslib": "^2.6.0", "url-loader": "^4.1.1", @@ -3159,7 +3387,7 @@ "webpackbar": "^6.0.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/faster": "*" @@ -3171,18 +3399,18 @@ } }, "node_modules/@docusaurus/core": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.7.0.tgz", - "integrity": "sha512-b0fUmaL+JbzDIQaamzpAFpTviiaU4cX3Qz8cuo14+HGBCwa0evEK0UYCBFY3n4cLzL8Op1BueeroUD2LYAIHbQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/babel": "3.7.0", - "@docusaurus/bundler": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "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", @@ -3190,19 +3418,19 @@ "combine-promises": "^1.1.0", "commander": "^5.1.0", "core-js": "^3.31.1", - "del": "^6.1.1", "detect-port": "^1.5.1", "escape-html": "^1.0.3", "eta": "^2.2.0", "eval": "^0.1.8", + "execa": "5.1.1", "fs-extra": "^11.1.1", "html-tags": "^3.3.1", "html-webpack-plugin": "^5.6.0", "leven": "^3.1.0", "lodash": "^4.17.21", + "open": "^8.4.0", "p-map": "^4.0.0", "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", "react-loadable-ssr-addon-v5-slorber": "^1.0.1", @@ -3211,19 +3439,19 @@ "react-router-dom": "^5.3.4", "semver": "^7.5.4", "serve-handler": "^6.1.6", - "shelljs": "^0.8.5", + "tinypool": "^1.0.2", "tslib": "^2.6.0", "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", @@ -3232,49 +3460,49 @@ } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.7.0.tgz", - "integrity": "sha512-X9GYgruZBSOozg4w4dzv9uOz8oK/EpPVQXkp0MM6Tsgp/nRIU9hJzJ0Pxg1aRa3xCeEQTOimZHcocQFlLwYajQ==", + "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", - "postcss": "^8.4.38", + "postcss": "^8.5.4", "postcss-sort-media-queries": "^5.2.0", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/logger": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.7.0.tgz", - "integrity": "sha512-z7g62X7bYxCYmeNNuO9jmzxLQG95q9QxINCwpboVcNff3SJiHJbGrarxxOVMVmAh1MsrSfxWkVGv4P41ktnFsA==", + "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.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.7.0.tgz", - "integrity": "sha512-OFBG6oMjZzc78/U3WNPSHs2W9ZJ723ewAcvVJaqS0VgyeUfmzUV8f1sv+iUHA0DtwiR5T5FjOxj6nzEE8LY6VA==", + "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.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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", "estree-util-value-to-estree": "^3.0.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", - "image-size": "^1.0.2", + "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-string": "^4.0.0", "rehype-raw": "^7.0.0", @@ -3291,7 +3519,7 @@ "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3299,17 +3527,17 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz", - "integrity": "sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg==", + "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.7.0", + "@docusaurus/types": "3.9.2", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", "@types/react-router-dom": "*", - "react-helmet-async": "npm:@slorber/react-helmet-async@*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" }, "peerDependencies": { @@ -3318,24 +3546,24 @@ } }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.7.0.tgz", - "integrity": "sha512-EFLgEz6tGHYWdPU0rK8tSscZwx+AsyuBW/r+tNig2kbccHYGUJmZtYN38GjAa3Fda4NU+6wqUO5kTXQSRBQD3g==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "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", "lodash": "^4.17.21", - "reading-time": "^1.5.0", + "schema-dts": "^1.1.2", "srcset": "^4.0.0", "tslib": "^2.6.0", "unist-util-visit": "^5.0.0", @@ -3343,7 +3571,7 @@ "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", @@ -3352,31 +3580,32 @@ } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.7.0.tgz", - "integrity": "sha512-GXg5V7kC9FZE4FkUZA8oo/NrlRb06UwuICzI6tcbzj0+TVgjq/mpUXXzSgKzMS82YByi4dY2Q808njcBCyy6tQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "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", "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "schema-dts": "^1.1.2", "tslib": "^2.6.0", "utility-types": "^3.10.0", "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3384,43 +3613,59 @@ } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.7.0.tgz", - "integrity": "sha512-YJSU3tjIJf032/Aeao8SZjFOrXJbz/FACMveSMjLyMH4itQyZ2XgUIzt4y+1ISvvk5zrW4DABVT2awTCqBkx0Q==", + "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.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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", "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "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.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, "node_modules/@docusaurus/plugin-debug": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.7.0.tgz", - "integrity": "sha512-Qgg+IjG/z4svtbCNyTocjIwvNTNEwgRjSXXSJkKVG0oWoH0eX/HAPiu+TS1HBwRPQV+tTYPWLrUypYFepfujZA==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", "fs-extra": "^11.1.1", - "react-json-view-lite": "^1.2.0", + "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", @@ -3428,18 +3673,18 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.7.0.tgz", - "integrity": "sha512-otIqiRV/jka6Snjf+AqB360XCeSv7lQC+DKYW+EUZf6XbuE8utz5PeUQ8VuOcD8Bk5zvT1MC4JKcd5zPfDuMWA==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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", @@ -3447,19 +3692,19 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.7.0.tgz", - "integrity": "sha512-M3vrMct1tY65ModbyeDaMoA+fNJTSPe5qmchhAbtqhDD/iALri0g9LrEpIOwNaoLmm6lO88sfBUADQrSRSGSWA==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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", @@ -3467,18 +3712,18 @@ } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.7.0.tgz", - "integrity": "sha512-X8U78nb8eiMiPNg3jb9zDIVuuo/rE1LjGDGu+5m5CX4UBZzjMy+klOY2fNya6x8ACyE/L3K2erO1ErheP55W/w==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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", @@ -3486,23 +3731,23 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.7.0.tgz", - "integrity": "sha512-bTRT9YLZ/8I/wYWKMQke18+PF9MV8Qub34Sku6aw/vlZ/U+kuEuRpQ8bTcNOjaTSfYsWkK4tTwDMHK2p5S86cA==", + "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.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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", @@ -3510,22 +3755,22 @@ } }, "node_modules/@docusaurus/plugin-svgr": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.7.0.tgz", - "integrity": "sha512-HByXIZTbc4GV5VAUkZ2DXtXv1Qdlnpk3IpuImwSnEzCDBkUMYcec5282hPjn6skZqB25M1TYCmWS91UbhBGxQg==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@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", @@ -3533,28 +3778,29 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.7.0.tgz", - "integrity": "sha512-nPHj8AxDLAaQXs+O6+BwILFuhiWbjfQWrdw2tifOClQoNfuXDjfjogee6zfx6NGHWqshR23LrcN115DmkHC91Q==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/plugin-content-blog": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/plugin-content-pages": "3.7.0", - "@docusaurus/plugin-debug": "3.7.0", - "@docusaurus/plugin-google-analytics": "3.7.0", - "@docusaurus/plugin-google-gtag": "3.7.0", - "@docusaurus/plugin-google-tag-manager": "3.7.0", - "@docusaurus/plugin-sitemap": "3.7.0", - "@docusaurus/plugin-svgr": "3.7.0", - "@docusaurus/theme-classic": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-search-algolia": "3.7.0", - "@docusaurus/types": "3.7.0" + "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", @@ -3562,31 +3808,30 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.7.0.tgz", - "integrity": "sha512-MnLxG39WcvLCl4eUzHr0gNcpHQfWoGqzADCly54aqCofQX6UozOS9Th4RK3ARbM9m7zIRv3qbhggI53dQtx/hQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/plugin-content-blog": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/plugin-content-pages": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-translations": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "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", - "postcss": "^8.4.26", + "postcss": "^8.5.4", "prism-react-renderer": "^2.3.0", "prismjs": "^1.29.0", "react-router-dom": "^5.3.4", @@ -3595,7 +3840,7 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3603,15 +3848,15 @@ } }, "node_modules/@docusaurus/theme-common": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.7.0.tgz", - "integrity": "sha512-8eJ5X0y+gWDsURZnBfH0WabdNm8XMCXHv8ENy/3Z/oQKwaB/EHt5lP9VsTDTf36lKEp0V6DjzjFyFIB+CetL0A==", + "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.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@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": "*", @@ -3622,7 +3867,7 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", @@ -3631,21 +3876,21 @@ } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.7.0.tgz", - "integrity": "sha512-Al/j5OdzwRU1m3falm+sYy9AaB93S1XF1Lgk9Yc6amp80dNxJVplQdQTR4cYdzkGtuQqbzUA8+kaoYYO0RbK6g==", - "license": "MIT", - "dependencies": { - "@docsearch/react": "^3.8.1", - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-translations": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", - "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", @@ -3654,7 +3899,7 @@ "utility-types": "^3.10.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -3662,26 +3907,27 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.7.0.tgz", - "integrity": "sha512-Ewq3bEraWDmienM6eaNK7fx+/lHMtGDHQyd1O+4+3EsDxxUmrzPkV7Ct3nBWTuE0MsoZr3yNwQVKjllzCMuU3g==", + "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.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz", - "integrity": "sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ==", + "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", @@ -3710,15 +3956,16 @@ } }, "node_modules/@docusaurus/utils": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.7.0.tgz", - "integrity": "sha512-e7zcB6TPnVzyUaHMJyLSArKa2AG3h9+4CfvKXKKWNx6hRs+p0a+u7HHTJBgo6KW2m+vqDnuIHK4X+bhmoghAFA==", + "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.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@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", "fs-extra": "^11.1.1", "github-slugger": "^1.5.0", @@ -3728,40 +3975,40 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "micromatch": "^4.0.5", + "p-queue": "^6.6.2", "prompts": "^2.4.2", "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", "tslib": "^2.6.0", "url-loader": "^4.1.1", "utility-types": "^3.10.0", "webpack": "^5.88.1" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/utils-common": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.7.0.tgz", - "integrity": "sha512-IZeyIfCfXy0Mevj6bWNg7DG7B8G+S6o6JVpddikZtWyxJguiQ7JYr0SIZ0qWd8pGNuMyVwriWmbWqMnK7Y5PwA==", + "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.7.0", + "@docusaurus/types": "3.9.2", "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@docusaurus/utils-validation": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.7.0.tgz", - "integrity": "sha512-w8eiKk8mRdN+bNfeZqC4nyFoxNyI1/VExMKAzD9tqpJfLLbsa46Wfn5wcKH761g9WkKh36RtFV49iL9lh1DYBA==", + "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.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@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", @@ -3769,7 +4016,7 @@ "tslib": "^2.6.0" }, "engines": { - "node": ">=18.0" + "node": ">=20.0" } }, "node_modules/@hapi/hoek": { @@ -3817,60 +4064,173 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "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/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, - "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==", - "engines": { - "node": ">=6.0.0" + "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/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@jridgewell/resolve-uri": { + "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.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "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.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "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", @@ -3878,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", @@ -3914,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" @@ -3962,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" } @@ -3974,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" }, @@ -3984,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", @@ -4000,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": { @@ -4055,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", @@ -4316,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" }, @@ -4323,28 +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/acorn": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", - "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, "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": "*", @@ -4409,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": { @@ -4424,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": "*", @@ -4436,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==", @@ -4482,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": "*" @@ -4531,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", @@ -4554,33 +4908,29 @@ "license": "MIT" }, "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "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": "*" } }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" - }, "node_modules/@types/prismjs": { "version": "1.26.5", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", @@ -4593,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": { @@ -4644,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": { @@ -4664,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", @@ -4683,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": "*", @@ -4709,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": "*" @@ -4733,11 +5083,20 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", - "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "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", @@ -4940,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" @@ -4951,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", @@ -4976,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" } @@ -4984,6 +5356,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -4992,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", @@ -5038,33 +5429,34 @@ } }, "node_modules/algoliasearch": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.19.0.tgz", - "integrity": "sha512-zrLtGhC63z3sVLDDKGW+SlCRN9eJHFTgdEmoAOpsVh6wgGL1GgTTDou7tpCBjevzgIvi3AIyDAQO3Xjbg5eqZg==", - "license": "MIT", - "dependencies": { - "@algolia/client-abtesting": "5.19.0", - "@algolia/client-analytics": "5.19.0", - "@algolia/client-common": "5.19.0", - "@algolia/client-insights": "5.19.0", - "@algolia/client-personalization": "5.19.0", - "@algolia/client-query-suggestions": "5.19.0", - "@algolia/client-search": "5.19.0", - "@algolia/ingestion": "1.19.0", - "@algolia/monitoring": "1.19.0", - "@algolia/recommend": "5.19.0", - "@algolia/requester-browser-xhr": "5.19.0", - "@algolia/requester-fetch": "5.19.0", - "@algolia/requester-node-http": "5.19.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.22.6", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.22.6.tgz", - "integrity": "sha512-F2gSb43QHyvZmvH/2hxIjbk/uFdO2MguQYTFP7J+RowMW1csjIODMobEnpLI8nbLQuzZnGZdIxl5Bpy1k9+CFQ==", + "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" @@ -5077,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" } @@ -5084,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", @@ -5142,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" } @@ -5150,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" }, @@ -5164,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" @@ -5213,18 +5611,10 @@ "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", "dev": true }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", "funding": [ { "type": "opencollective", @@ -5241,11 +5631,11 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "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.0.1", + "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -5285,13 +5675,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", - "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.3", + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { @@ -5308,25 +5698,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", - "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3" + "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -5345,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", @@ -5363,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": { @@ -5431,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", @@ -5476,9 +5882,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -5495,10 +5901,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "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" @@ -5510,12 +5917,28 @@ "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/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "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", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -5525,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" } @@ -5533,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", @@ -5546,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", @@ -5576,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", @@ -5589,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" @@ -5608,6 +6022,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } @@ -5616,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" @@ -5625,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" }, @@ -5645,9 +6062,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "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", @@ -5678,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" @@ -5777,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", @@ -5798,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" } @@ -5820,6 +6237,7 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } @@ -5828,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" }, @@ -5839,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" } @@ -5847,6 +6267,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", "engines": { "node": ">=6" } @@ -5855,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" }, @@ -5863,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" }, @@ -5879,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", @@ -5931,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" }, @@ -5941,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", @@ -5959,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" } @@ -5977,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" } @@ -6006,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" @@ -6059,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", @@ -6098,9 +6536,9 @@ } }, "node_modules/consola": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.3.3.tgz", - "integrity": "sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" @@ -6145,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", @@ -6225,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.40.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", - "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "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.24.3" + "browserslist": "^4.28.0" }, "funding": { "type": "opencollective", @@ -6248,9 +6675,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.40.0.tgz", - "integrity": "sha512-AtDzVIgRrmRKQai62yuSIN5vNiQjcJakJb4fbhVw3ehxx7Lohphvw9SGNWKhLFqSxC4ilD0g/L1huAYFQU3Q6A==", + "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": { @@ -6294,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", @@ -6307,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" }, @@ -6321,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" }, @@ -6354,9 +6784,9 @@ } }, "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -6367,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" @@ -6379,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", @@ -6428,9 +6858,9 @@ } }, "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -6542,9 +6972,9 @@ } }, "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -6571,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" }, @@ -6582,9 +7013,9 @@ } }, "node_modules/cssdb": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.2.3.tgz", - "integrity": "sha512-9BDG5XmJrJQQnJ51VFxXCAtpZ5ebDlAREmO8sxMOVU0aSxN/gocbctjIG5LMh3WBUq+xTlb/jw2LoljBEqraTA==", + "version": "8.4.3", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.4.3.tgz", + "integrity": "sha512-8aaDS5nVqMXmYjlmmJpqlDJosiqbl2NJkYuSFOXR6RTY14qNosMrqT4t7O+EUm+OdduQg3GNI2ZwC03No1Y58Q==", "funding": [ { "type": "opencollective", @@ -6751,11 +7182,12 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6767,9 +7199,9 @@ } }, "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -6783,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" }, @@ -6797,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" }, @@ -6808,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" } @@ -6816,26 +7251,44 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", "engines": { "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/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" } @@ -6861,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" } @@ -6882,27 +7336,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6938,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" @@ -6948,37 +7382,11 @@ "bin": { "detect": "bin/detect-port.js", "detect-port": "bin/detect-port.js" - } - }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" }, "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detect-port-alt/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "node": ">= 4.0.0" } }, - "node_modules/detect-port-alt/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -7019,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" } @@ -7046,7 +7455,8 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "5.0.3", @@ -7081,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" @@ -7090,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" }, @@ -7104,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" } @@ -7125,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", @@ -7139,9 +7554,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.79", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.79.tgz", - "integrity": "sha512-nYOxJNxQ9Om4EC88BE4pPoNI8xwSFf8pU/BAeOl4Hh/b/i6V4biTAzwV7pXi3ARKeoYO5JZKMIXTryXSVer5RA==", + "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": { @@ -7153,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", @@ -7190,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" @@ -7214,9 +7631,10 @@ } }, "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" } @@ -7240,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" @@ -7301,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" }, @@ -7311,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" }, @@ -7328,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" @@ -7353,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" }, @@ -7364,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" } @@ -7372,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" } @@ -7445,9 +7871,9 @@ } }, "node_modules/estree-util-value-to-estree": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.3.3.tgz", - "integrity": "sha512-Db+m1WSD4+mUO7UgMeKkAwdbfNWwIxLt48XF2oFU9emPfXkIu+k5/nlOj313v7wqtAPo0f9REhUvznFrPkG8CQ==", + "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" @@ -7492,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" }, @@ -7530,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", @@ -7558,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" @@ -7636,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", @@ -7666,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", @@ -7686,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", @@ -7794,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", @@ -7868,14 +8322,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -7962,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", @@ -7981,161 +8427,42 @@ } } }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, + "node_modules/form-data-encoder": { + "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": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-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": ">= 14.17" } }, - "node_modules/fork-ts-checker-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/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-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/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/form-data-encoder": { - "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==", - "engines": { - "node": ">= 14.17" - } - }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" + "node": ">= 0.6" } }, "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" } }, @@ -8161,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" @@ -8188,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" } @@ -8202,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", @@ -8248,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" }, @@ -8384,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", @@ -8414,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" }, @@ -8433,58 +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/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -8520,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", @@ -8544,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" }, @@ -8581,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", @@ -8597,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" }, @@ -8617,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" } @@ -8649,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" }, @@ -8669,16 +8941,16 @@ } }, "node_modules/hast-util-from-parse5": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.2.tgz", - "integrity": "sha512-SfMzfdAi/zAoZ1KkFEyyeXBn7u/ShQrfd675ZEE9M3qj+PMFX05xubzRyF76CCSJu8au9jgVxDV1+okFvgZU4A==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" @@ -8727,9 +8999,9 @@ } }, "node_modules/hast-util-to-estree": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.1.tgz", - "integrity": "sha512-IWtwwmPskfSmma9RpzCappDUitC8t5jhAynHhc1m2+5trOgsrp7txscUSavc5Ic8PATyAjfrCK1wgtxh2cICVQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -8743,9 +9015,9 @@ "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", - "style-to-object": "^1.0.0", + "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" }, @@ -8755,9 +9027,9 @@ } }, "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz", - "integrity": "sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -8770,9 +9042,9 @@ "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", - "style-to-object": "^1.0.0", + "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" }, @@ -8800,6 +9072,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -8814,15 +9096,15 @@ } }, "node_modules/hastscript": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.0.tgz", - "integrity": "sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" }, "funding": { @@ -8834,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" } @@ -8842,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", @@ -8855,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" } @@ -8907,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", @@ -8963,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" }, @@ -8981,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", @@ -9015,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" } @@ -9023,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", @@ -9059,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", @@ -9086,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": { @@ -9145,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" @@ -9162,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", @@ -9195,13 +9479,10 @@ } }, "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, "bin": { "image-size": "bin/image-size.js" }, @@ -9209,19 +9490,11 @@ "node": ">=16.x" } }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -9237,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" } @@ -9245,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" } @@ -9253,6 +9528,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", "engines": { "node": ">=8" } @@ -9266,43 +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/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "engines": { - "node": ">= 0.10" - } - }, "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" } @@ -9343,12 +9608,14 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-binary-path": { "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" }, @@ -9360,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" }, @@ -9368,11 +9636,15 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9392,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" }, @@ -9423,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" } @@ -9448,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" @@ -9463,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" }, @@ -9491,18 +9812,11 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/is-path-inside": { "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" } @@ -9540,14 +9854,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "engines": { - "node": ">=6" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -9563,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" }, @@ -9580,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" } @@ -9587,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", @@ -9678,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" @@ -9704,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", @@ -9744,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" } @@ -9752,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" } @@ -9760,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" } @@ -9768,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" }, @@ -9779,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" } @@ -9811,14 +10135,20 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "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": { @@ -9851,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", @@ -9898,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" } @@ -9906,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" }, @@ -9968,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", @@ -9978,13 +10323,14 @@ } }, "node_modules/mdast-util-directive": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz", - "integrity": "sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", + "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", @@ -10096,9 +10442,9 @@ } }, "node_modules/mdast-util-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", - "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^2.0.0", @@ -10168,9 +10514,9 @@ "license": "MIT" }, "node_modules/mdast-util-gfm-footnote": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", - "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -10268,9 +10614,9 @@ } }, "node_modules/mdast-util-mdx-jsx": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.3.tgz", - "integrity": "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -10324,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", @@ -10394,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": { @@ -10416,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", @@ -10436,9 +10790,9 @@ } }, "node_modules/micromark": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", - "integrity": "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "funding": [ { "type": "GitHub Sponsors", @@ -10471,9 +10825,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz", - "integrity": "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "funding": [ { "type": "GitHub Sponsors", @@ -10870,9 +11224,9 @@ "license": "MIT" }, "node_modules/micromark-extension-gfm-table": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", - "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -11029,9 +11383,9 @@ "license": "MIT" }, "node_modules/micromark-extension-mdx-expression": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz", - "integrity": "sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", "funding": [ { "type": "GitHub Sponsors", @@ -11111,12 +11465,11 @@ "license": "MIT" }, "node_modules/micromark-extension-mdx-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.1.tgz", - "integrity": "sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", "license": "MIT", "dependencies": { - "@types/acorn": "^4.0.0", "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", @@ -11395,9 +11748,9 @@ "license": "MIT" }, "node_modules/micromark-factory-mdx-expression": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.2.tgz", - "integrity": "sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", "funding": [ { "type": "GitHub Sponsors", @@ -11927,9 +12280,9 @@ "license": "MIT" }, "node_modules/micromark-util-events-to-acorn": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz", - "integrity": "sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", "funding": [ { "type": "GitHub Sponsors", @@ -11942,7 +12295,6 @@ ], "license": "MIT", "dependencies": { - "@types/acorn": "^4.0.0", "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", @@ -12096,9 +12448,9 @@ "license": "MIT" }, "node_modules/micromark-util-subtokenize": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.3.tgz", - "integrity": "sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "funding": [ { "type": "GitHub Sponsors", @@ -12150,9 +12502,9 @@ "license": "MIT" }, "node_modules/micromark-util-types": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", - "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "funding": [ { "type": "GitHub Sponsors", @@ -12279,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" }, @@ -12287,9 +12640,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", - "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "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", @@ -12313,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" }, @@ -12327,23 +12681,25 @@ "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" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -12359,9 +12715,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -12388,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" @@ -12415,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" } @@ -12446,6 +12805,18 @@ "node": ">=0.10.0" } }, + "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", @@ -12468,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" }, @@ -12496,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", @@ -12553,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" @@ -12620,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", @@ -12647,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", @@ -12672,10 +13037,20 @@ "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" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -12710,6 +13085,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" }, @@ -12720,15 +13096,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, "engines": { "node": ">=8" } @@ -12737,6 +13145,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "engines": { "node": ">=6" } @@ -12745,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", @@ -12762,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" @@ -12771,6 +13182,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -12807,6 +13219,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -12827,12 +13240,12 @@ "license": "ISC" }, "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { - "entities": "^4.5.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -12851,6 +13264,18 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12864,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" @@ -12878,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", @@ -12896,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" } @@ -12903,7 +13322,8 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" }, "node_modules/path-to-regexp": { "version": "1.9.0", @@ -12954,77 +13374,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "engines": { - "node": ">=4" - } - }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -13041,7 +13394,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -13075,9 +13428,9 @@ } }, "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -13119,9 +13472,9 @@ } }, "node_modules/postcss-color-functional-notation": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.7.tgz", - "integrity": "sha512-EZvAHsvyASX63vXnyXOIynkxhaHRSsdb7z6yiXKIovGXAolW4cMZ3qoh7k3VdTsLBS6VGdksGfIo3r6+waLoOw==", + "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", @@ -13134,10 +13487,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.7", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@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": { @@ -13234,9 +13587,9 @@ } }, "node_modules/postcss-custom-media": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.5.tgz", - "integrity": "sha512-SQHhayVNgDvSAdX9NQ/ygcDQGEY+aSF4b/96z7QUX6mqL5yl/JgG/DywcF6fW9XbnCRE+aVYk+9/nqGuzOPWeQ==", + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", + "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", "funding": [ { "type": "github", @@ -13249,10 +13602,10 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/media-query-list-parser": "^4.0.2" + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" }, "engines": { "node": ">=18" @@ -13262,9 +13615,9 @@ } }, "node_modules/postcss-custom-properties": { - "version": "14.0.4", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.4.tgz", - "integrity": "sha512-QnW8FCCK6q+4ierwjnmXF9Y9KF8q0JkbgVfvQEMa93x1GT8FvOiUevWCN2YLaOWyByeDX8S6VFbZEeWoAoXs2A==", + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", + "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", "funding": [ { "type": "github", @@ -13277,9 +13630,9 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, @@ -13291,9 +13644,9 @@ } }, "node_modules/postcss-custom-selectors": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.4.tgz", - "integrity": "sha512-ASOXqNvDCE0dAJ/5qixxPeL1aOVGHGW2JwSy7HyjWNbnWTQCl+fDc968HY1jCmZI0+BaYT5CxsOiUhavpG/7eg==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", + "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", "funding": [ { "type": "github", @@ -13306,9 +13659,9 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", "postcss-selector-parser": "^7.0.0" }, "engines": { @@ -13319,9 +13672,9 @@ } }, "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -13357,9 +13710,9 @@ } }, "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -13433,9 +13786,9 @@ } }, "node_modules/postcss-double-position-gradients": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.0.tgz", - "integrity": "sha512-JkIGah3RVbdSEIrcobqj4Gzq0h53GG4uqDPsho88SgY84WnpkTpI0k50MFK/sX7XqVisZ6OqUfFnoUO6m1WWdg==", + "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", @@ -13448,7 +13801,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, @@ -13485,9 +13838,9 @@ } }, "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -13523,9 +13876,9 @@ } }, "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -13593,9 +13946,9 @@ } }, "node_modules/postcss-lab-function": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.7.tgz", - "integrity": "sha512-+ONj2bpOQfsCKZE2T9VGMyVVdGcGUpr7u3SVfvkJlvhTRmDCfY25k4Jc8fubB9DclAPR4+w8uVtDZmdRgdAHig==", + "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", @@ -13608,10 +13961,10 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^3.0.7", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@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": { @@ -13644,9 +13997,9 @@ } }, "node_modules/postcss-logical": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.0.0.tgz", - "integrity": "sha512-HpIdsdieClTjXLOyYdUPAX/XQASNIwdKt5hoZW08ZOAiI+tbV0ta1oclkpVkW5ANU+xJvk3KkA0FejkjGLXUkg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", + "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", "funding": [ { "type": "github", @@ -13812,9 +14165,9 @@ } }, "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -13840,9 +14193,9 @@ } }, "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -13868,9 +14221,9 @@ } }, "node_modules/postcss-nesting": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.1.tgz", - "integrity": "sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", "funding": [ { "type": "github", @@ -13883,7 +14236,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/selector-resolve-nested": "^3.0.0", + "@csstools/selector-resolve-nested": "^3.1.0", "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, @@ -13895,9 +14248,9 @@ } }, "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz", - "integrity": "sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", "funding": [ { "type": "github", @@ -13939,9 +14292,9 @@ } }, "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -14182,9 +14535,9 @@ } }, "node_modules/postcss-preset-env": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.1.3.tgz", - "integrity": "sha512-9qzVhcMFU/MnwYHyYpJz4JhGku/4+xEiPTmhn0hj3IxnUYlEF9vbh7OC1KoLAnenS6Fgg43TKNp9xcuMeAi4Zw==", + "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", @@ -14197,62 +14550,66 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-cascade-layers": "^5.0.1", - "@csstools/postcss-color-function": "^4.0.7", - "@csstools/postcss-color-mix-function": "^3.0.7", - "@csstools/postcss-content-alt-text": "^2.0.4", - "@csstools/postcss-exponential-functions": "^2.0.6", + "@csstools/postcss-alpha-function": "^1.0.1", + "@csstools/postcss-cascade-layers": "^5.0.2", + "@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.7", - "@csstools/postcss-gradients-interpolation-method": "^5.0.7", - "@csstools/postcss-hwb-function": "^4.0.7", - "@csstools/postcss-ic-unit": "^4.0.0", - "@csstools/postcss-initial": "^2.0.0", - "@csstools/postcss-is-pseudo-class": "^5.0.1", - "@csstools/postcss-light-dark-function": "^2.0.7", + "@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.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", "@csstools/postcss-logical-resize": "^3.0.0", - "@csstools/postcss-logical-viewport-units": "^3.0.3", - "@csstools/postcss-media-minmax": "^2.0.6", - "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.4", + "@csstools/postcss-logical-viewport-units": "^3.0.4", + "@csstools/postcss-media-minmax": "^2.0.9", + "@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.7", - "@csstools/postcss-progressive-custom-properties": "^4.0.0", - "@csstools/postcss-random-function": "^1.0.2", - "@csstools/postcss-relative-color-syntax": "^3.0.7", + "@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.12", "@csstools/postcss-scope-pseudo-class": "^4.0.1", - "@csstools/postcss-sign-functions": "^1.1.1", - "@csstools/postcss-stepped-value-functions": "^4.0.6", - "@csstools/postcss-text-decoration-shorthand": "^4.0.1", - "@csstools/postcss-trigonometric-functions": "^4.0.6", + "@csstools/postcss-sign-functions": "^1.1.4", + "@csstools/postcss-stepped-value-functions": "^4.0.9", + "@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.19", - "browserslist": "^4.23.1", + "autoprefixer": "^10.4.21", + "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.2.3", + "cssdb": "^8.4.2", "postcss-attribute-case-insensitive": "^7.0.1", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^7.0.7", + "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.5", - "postcss-custom-properties": "^14.0.4", - "postcss-custom-selectors": "^8.0.4", + "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.0", + "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.7", - "postcss-logical": "^8.0.0", - "postcss-nesting": "^13.0.1", + "postcss-lab-function": "^7.0.12", + "postcss-logical": "^8.1.0", + "postcss-nesting": "^13.0.2", "postcss-opacity-percentage": "^3.0.0", "postcss-overflow-shorthand": "^6.0.0", "postcss-page-break": "^3.0.4", @@ -14294,9 +14651,9 @@ } }, "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -14387,9 +14744,9 @@ } }, "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", @@ -14480,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" @@ -14526,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" @@ -14545,9 +14904,9 @@ } }, "node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", "funding": { "type": "github", @@ -14557,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", @@ -14581,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" }, @@ -14610,15 +14980,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -14642,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" }, @@ -14653,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" } @@ -14694,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", @@ -14704,169 +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-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "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-error-overlay": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", - "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" - }, "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", @@ -14875,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": { @@ -14885,15 +15134,15 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-json-view-lite": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-1.5.0.tgz", - "integrity": "sha512-nWqA1E4jKPklL2jvHWs6s+7Na0qNgw9HCP6xehdQJeg6nPBTFZgGwyko9Q0oj+jQWKTTVRS30u0toM5wiuL3iw==", + "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": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/react-loadable": { @@ -14912,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" }, @@ -14927,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", @@ -14946,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" }, @@ -14958,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", @@ -14989,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" }, @@ -14996,23 +15250,6 @@ "node": ">=8.10.0" } }, - "node_modules/reading-time": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", - "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==", - "license": "MIT" - }, - "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", - "dependencies": { - "resolve": "^1.1.6" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/recma-build-jsx": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", @@ -15029,9 +15266,9 @@ } }, "node_modules/recma-jsx": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.0.tgz", - "integrity": "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", "license": "MIT", "dependencies": { "acorn-jsx": "^5.0.0", @@ -15043,6 +15280,9 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/recma-parse": { @@ -15077,17 +15317,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", - "dependencies": { - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -15095,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" @@ -15111,36 +15340,28 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "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" }, @@ -15152,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" }, @@ -15169,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", @@ -15226,14 +15436,15 @@ "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" } }, "node_modules/remark-directive": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.0.tgz", - "integrity": "sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", + "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -15279,9 +15490,9 @@ } }, "node_modules/remark-gfm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", - "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -15297,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", @@ -15327,9 +15538,9 @@ } }, "node_modules/remark-rehype": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", - "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -15362,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", @@ -15374,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", @@ -15389,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", @@ -15402,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" }, @@ -15416,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", @@ -15429,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" } @@ -15444,6 +15661,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", @@ -15484,17 +15702,21 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "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.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -15502,12 +15724,14 @@ "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", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", "engines": { "node": ">=4" } @@ -15515,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" }, @@ -15549,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", @@ -15581,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", @@ -15620,7 +15844,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -15629,23 +15854,30 @@ "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", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "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", @@ -15701,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" }, @@ -15718,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" }, @@ -15728,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", @@ -15792,12 +16007,6 @@ "node": ">= 0.8" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/send/node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -15817,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" @@ -15968,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" }, @@ -15985,34 +16196,23 @@ "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" } }, - "node_modules/shelljs": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", - "dependencies": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "bin": { - "shjs": "bin/shjs" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -16088,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", @@ -16107,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", @@ -16185,12 +16387,12 @@ } }, "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "license": "BSD-3-Clause", "engines": { - "node": ">= 8" + "node": ">= 12" } }, "node_modules/source-map-js": { @@ -16206,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" @@ -16215,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" } @@ -16287,9 +16491,9 @@ } }, "node_modules/std-env": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", - "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "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": { @@ -16305,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", @@ -16318,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" }, @@ -16329,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" }, @@ -16374,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" }, @@ -16432,13 +16640,22 @@ "node": ">=0.8.0" } }, + "node_modules/style-to-js": { + "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.14" + } + }, "node_modules/style-to-object": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", - "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "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": { @@ -16461,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" }, @@ -16472,6 +16690,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -16486,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" @@ -16519,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" }, @@ -16545,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" @@ -16577,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", @@ -16613,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" }, @@ -16652,12 +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/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "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", @@ -16666,14 +16886,25 @@ "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", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -16704,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", @@ -16746,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" }, @@ -16799,27 +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/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "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", @@ -16853,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" @@ -16893,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" }, @@ -16904,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" @@ -16971,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", @@ -17002,9 +17241,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "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", @@ -17022,7 +17261,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -17035,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", @@ -17062,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", @@ -17083,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" }, @@ -17091,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" }, @@ -17105,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", @@ -17145,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", @@ -17214,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", @@ -17223,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", @@ -17255,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", @@ -17295,9 +17542,9 @@ } }, "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -17309,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" @@ -17340,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" @@ -17421,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" @@ -17474,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": { @@ -17532,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" @@ -17568,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" } @@ -17615,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" }, @@ -17622,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", @@ -17738,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" }, @@ -17752,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" }, @@ -17772,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", @@ -17785,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" }, @@ -17796,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" }, @@ -17807,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" }, @@ -17820,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", @@ -17857,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" }, @@ -17886,18 +18161,10 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "engines": { - "node": ">= 6" - } - }, "node_modules/yocto-queue": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", - "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "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" @@ -17906,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 28e498ea..7c2e57cf 100644 --- a/docs/package.json +++ b/docs/package.json @@ -15,15 +15,15 @@ "typecheck": "tsc" }, "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/preset-classic": "3.7.0", - "@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.7.0", + "@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/AsyncResolutionBenchmarks.cs b/src/Benchmarks/AsyncResolutionBenchmarks.cs new file mode 100644 index 00000000..7a23c420 --- /dev/null +++ b/src/Benchmarks/AsyncResolutionBenchmarks.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using EntityGraphQL; +using EntityGraphQL.Extensions; +using EntityGraphQL.Schema; + +namespace Benchmarks; + +/// +/// Benchmarks for testing different async field resolution strategies +/// Tests the performance impact of concurrent vs sequential async processing +/// +/// Was testing a MaxDegreeOfParallelism at the resolving Task level but it didn't really make a difference so removed it. +/// Although it is not the best test as we're not hitting resources like HTTP requests or DB connections. +/// +/// Have introduced limiting to control how many async tasks start +/// +/// BenchmarkDotNet v0.15.2, macOS 26.0 (25A5338b) [Darwin 25.0.0] +/// Apple M1 Max, 1 CPU, 10 logical and 10 physical cores +/// .NET SDK 9.0.301 +/// +/// | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +/// |---------------------------------------- |------------:|---------:|---------:|----------:|--------:|-----------:| +/// | SmallCollection_WithAsyncFields | 296.1 us | 2.26 us | 2.22 us | 18.5547 | 3.9063 | 118.81 KB | +/// | LargeCollection_WithAsyncFields | 13,304.9 us | 35.31 us | 31.30 us | 1015.6250 | - | 6251.58 KB | +/// | LargeCollection_WithAsyncFields_Limited | 13,305.3 us | 38.34 us | 33.98 us | 1015.6250 | 15.6250 | 6264.8 KB | +/// +/// +[MemoryDiagnoser] +public class AsyncResolutionBenchmarks : BaseBenchmark +{ + private readonly string smallCollectionQuery = + @"{ + movies { + id + name + asyncRating + asyncDescription + } + }"; + + private readonly string largeCollectionQuery = + @"{ + moviesBig { + id + name + asyncRating + asyncDescription + actors { + id + firstName + asyncBio + asyncAwards + } + } + }"; + + private readonly string largeCollectionQueryLimited = + @"{ + moviesBig { + id + name + asyncRatingLimit + asyncDescriptionLimit + actors { + id + firstName + asyncBioLimit + asyncAwardsLimit + } + } + }"; + + private readonly QueryRequest smallGql; + private readonly QueryRequest largeGql; + private readonly QueryRequest largeGqlLimit; + private readonly BenchmarkContext context; + + public AsyncResolutionBenchmarks() + { + smallGql = new QueryRequest { Query = smallCollectionQuery }; + largeGql = new QueryRequest { Query = largeCollectionQuery }; + largeGqlLimit = new QueryRequest { Query = largeCollectionQueryLimited }; + context = GetContext(); + } + + [GlobalSetup] + public void Setup() + { + // Add async fields to Movie + Schema.Type().AddField("asyncRating", "Async rating").ResolveAsync((movie, srv) => srv.GetAsyncRatingAsync(movie.Id)); + Schema.Type().AddField("asyncDescription", "Async description").ResolveAsync((movie, srv) => srv.GetAsyncDescriptionAsync(movie.Id)); + Schema.Type().AddField("asyncRatingLimit", "Async rating").ResolveAsync((movie, srv) => srv.GetAsyncRatingAsync(movie.Id), 5); + Schema.Type().AddField("asyncDescriptionLimit", "Async description").ResolveAsync((movie, srv) => srv.GetAsyncDescriptionAsync(movie.Id), 5); + + // Add async fields to Person (actors) + Schema.Type().AddField("asyncBio", "Async bio").ResolveAsync((person, srv) => srv.GetAsyncBioAsync(person.Id)); + Schema.Type().AddField("asyncAwards", "Async awards").ResolveAsync((person, srv) => srv.GetAsyncAwardsAsync(person.Id)); + Schema.Type().AddField("asyncBioLimit", "Async bio").ResolveAsync((person, srv) => srv.GetAsyncBioAsync(person.Id), 5); + Schema.Type().AddField("asyncAwardsLimit", "Async awards").ResolveAsync((person, srv) => srv.GetAsyncAwardsAsync(person.Id), 5); + + // Replace movies field to return a controlled set + Schema + .Query() + .ReplaceField( + "movies", + (ctx) => ctx.Movies.Take(100), // Small collection for testing + "List of movies with async fields" + ); + + Schema + .Query() + .AddField( + "moviesBig", + (ctx) => ctx.Movies.Take(1000), // Larger collection + "List of movies with async fields" + ); + } + + [Benchmark] + public async Task SmallCollection_WithAsyncFields() + { + var result = await Schema.ExecuteRequestWithContextAsync(smallGql, context, null, null, new ExecutionOptions { EnableQueryCache = false }); + return result; + } + + [Benchmark] + public async Task LargeCollection_WithAsyncFields() + { + var result = await Schema.ExecuteRequestWithContextAsync(largeGql, context, null, null, new ExecutionOptions { EnableQueryCache = false }); + return result; + } + + [Benchmark] + public async Task LargeCollection_WithAsyncFields_Limited() + { + // Test with concurrency limit + var result = await Schema.ExecuteRequestWithContextAsync(largeGqlLimit, context, null, null, new ExecutionOptions { EnableQueryCache = false }); + return result; + } + + public class FakeServices + { + // Simulate async operations with controlled delays + public async Task GetAsyncRatingAsync(Guid movieId) + { + await Task.Delay(50); // Simulate async work (API call, etc.) + return 7.5f + movieId.GetHashCode() % 10 * 0.3f; + } + + public async Task GetAsyncDescriptionAsync(Guid movieId) + { + await Task.Delay(75); // Simulate async work + return $"Async description for movie {movieId}"; + } + + public async Task GetAsyncBioAsync(Guid personId) + { + await Task.Delay(100); // Simulate async work + return $"Async bio for person {personId}"; + } + + public async Task> GetAsyncAwardsAsync(Guid personId) + { + await Task.Delay(110); // Simulate async work + return new List { "Best Actor", "Best Supporting" }; + } + } +} diff --git a/src/Benchmarks/Benchmarks.csproj b/src/Benchmarks/Benchmarks.csproj index 03e97437..1053045d 100644 --- a/src/Benchmarks/Benchmarks.csproj +++ b/src/Benchmarks/Benchmarks.csproj @@ -1,22 +1,23 @@ - Exe net8.0;net9.0 - 13.0 false enable + pdbonly + true - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + - diff --git a/src/Benchmarks/CompileAllStagesBenchmarks.cs b/src/Benchmarks/CompileAllStagesBenchmarks.cs index 953eb92d..bd703534 100644 --- a/src/Benchmarks/CompileAllStagesBenchmarks.cs +++ b/src/Benchmarks/CompileAllStagesBenchmarks.cs @@ -9,18 +9,6 @@ namespace Benchmarks; /// On a Apple M1 Max 64GB ram /// Command: `dotnet run -c Debug --framework net8.0` to skip execution /// -/// 5.2.1 (5.0 & 5.1 were basically the same) -/// | Method | Mean | Error | StdDev | Gen 0 | Allocated | -/// |--------------- |----------:|---------:|---------:|--------:|----------:| -/// | CompileNoCache | 116.21 us | 0.399 us | 0.354 us | 42.4805 | 87 KB | -/// | CompileCache | 95.66 us | 0.388 us | 0.363 us | 33.5693 | 69 KB | -/// -/// 5.3.0 -/// | Method | Mean | Error | StdDev | Gen 0 | Allocated | -/// |--------------- |----------:|---------:|---------:|--------:|----------:| -/// | CompileNoCache | 125.85 us | 0.650 us | 0.543 us | 44.4336 | 91 KB | -/// | CompileCache | 97.15 us | 0.396 us | 0.371 us | 33.9355 | 69 KB | -/// /// 5.6.0 with net9.0 /// On a Apple M1 Max 64GB ram /// Command: `dotnet run -c Debug --framework net8.0` to skip execution @@ -29,6 +17,15 @@ namespace Benchmarks; /// |--------------- |---------:|---------:|---------:|--------:|-------:|----------:| /// | CompileNoCache | 97.04 us | 0.795 us | 0.744 us | 15.6250 | 0.9766 | 95.79 KB | /// | CompileCache | 80.42 us | 0.190 us | 0.177 us | 12.5732 | 0.4883 | 77.08 KB | +/// +/// BenchmarkDotNet v0.15.2, macOS 26.0 (25A5338b) [Darwin 25.0.0] +/// Apple M1 Max, 1 CPU, 10 logical and 10 physical cores +/// .NET SDK 9.0.301 +/// | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +/// |--------------- |---------:|---------:|---------:|--------:|-------:|----------:| +/// | CompileNoCache | 93.56 us | 1.776 us | 1.662 us | 15.3809 | 0.7324 | 94.25 KB | +/// | CompileCache | 77.24 us | 1.524 us | 1.425 us | 12.2070 | 0.2441 | 75.39 KB | +/// /// [MemoryDiagnoser] public class CompileAllStagesBenchmarks : BaseBenchmark diff --git a/src/Benchmarks/CompileFiltersBenchmarks.cs b/src/Benchmarks/CompileFiltersBenchmarks.cs index 057611a9..902e9f50 100644 --- a/src/Benchmarks/CompileFiltersBenchmarks.cs +++ b/src/Benchmarks/CompileFiltersBenchmarks.cs @@ -13,20 +13,6 @@ namespace Benchmarks; /// Command: `dotnet run -c Debug` /// To not actually execute and skip EF /// -/// 4.2.0 -/// | Method | Mean | Error | StdDev | Gen 0 | Allocated | -/// |-------------------------- |------------:|----------:|----------:|---------:|----------:| -/// | PlainDbSet | 41.61 us | 0.326 us | 0.305 us | 9.5825 | 20 KB | -/// | SetOfBasicWhereStatements | 434.38 us | 3.326 us | 2.777 us | 77.6367 | 159 KB | -/// | LargerSetOfWhereWhens | 4,982.14 us | 17.287 us | 14.435 us | 664.0625 | 1,377 KB | -/// -/// 5.3.0 -/// | Method | Mean | Error | StdDev | Gen 0 | Allocated | -/// |-------------------------- |------------:|---------:|---------:|---------:|----------:| -/// | PlainDbSet | 20.73 us | 0.079 us | 0.066 us | 7.9346 | 16 KB | -/// | SetOfBasicWhereStatements | 158.70 us | 0.547 us | 0.511 us | 41.5039 | 85 KB | -/// | LargerSetOfWhereWhens | 2,515.82 us | 5.926 us | 5.543 us | 457.0313 | 934 KB | -/// /// 5.6.0 with net9.0 /// /// | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | @@ -34,6 +20,18 @@ namespace Benchmarks; /// | PlainDbSet | 18.98 us | 0.091 us | 0.200 us | 3.2959 | - | 20.83 KB | /// | SetOfBasicWhereStatements | 70.65 us | 0.208 us | 0.194 us | 11.9629 | 0.3662 | 73.73 KB | /// | LargerSetOfWhereWhens | 1,055.51 us | 1.878 us | 1.757 us | 123.0469 | 23.4375 | 765.25 KB | +/// +/// 5.8.0 +/// BenchmarkDotNet v0.15.2, macOS 26.0 (25A5338b) [Darwin 25.0.0] +/// Apple M1 Max, 1 CPU, 10 logical and 10 physical cores +/// .NET SDK 9.0.301 +/// +/// | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +/// |-------------------------- |-----------:|---------:|----------:|---------:|--------:|-----------:| +/// | PlainDbSet | 972.5 us | 9.54 us | 12.74 us | 109.3750 | 15.6250 | 671.66 KB | +/// | SetOfBasicWhereStatements | 1,462.2 us | 27.33 us | 51.99 us | 117.1875 | 15.6250 | 740.14 KB | +/// | LargerSetOfWhereWhens | 3,910.9 us | 65.65 us | 109.69 us | 234.3750 | 62.5000 | 1489.08 KB | +/// /// [MemoryDiagnoser] public class CompileFiltersBenchmarks : BaseBenchmark diff --git a/src/Benchmarks/CompileGqlDocumentOnlyBenchmarks.cs b/src/Benchmarks/CompileGqlDocumentOnlyBenchmarks.cs index 0999d85e..025e823f 100644 --- a/src/Benchmarks/CompileGqlDocumentOnlyBenchmarks.cs +++ b/src/Benchmarks/CompileGqlDocumentOnlyBenchmarks.cs @@ -10,32 +10,35 @@ namespace Benchmarks; /// Benchmarks to test just the string graphql document to EntityGraphQL IGraphQLNode compilation. /// Not to the expression that will be executed /// -/// BenchmarkDotNet v0.14.0, macOS Sequoia 15.1 (24B83) [Darwin 24.1.0] +/// BenchmarkDotNet v0.15.2, macOS 26.0 (25A5338b) [Darwin 25.0.0] /// Apple M1 Max, 1 CPU, 10 logical and 10 physical cores -/// .NET SDK 9.0.100 +/// .NET SDK 9.0.301 +/// 5.7.1 +/// | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +/// |-------------------------------------------------- |---------:|----------:|----------:|-------:|-------:|----------:| +/// | Query_SingleObjectWithArg | 4.899 us | 0.0515 us | 0.0481 us | 1.5564 | - | 9.54 KB | +/// | Query_SingleObjectWithArg_IncludeSubObject | 6.261 us | 0.0081 us | 0.0072 us | 2.0905 | 0.0534 | 12.82 KB | +/// | Query_SingleObjectWithArg_IncludeSubObjectAndList | 7.780 us | 0.0166 us | 0.0139 us | 2.6855 | 0.0916 | 16.47 KB | +/// | 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 | /// -/// 5.6.0 - HotChocolate.Language 13.9.11 +/// 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 | /// -/// | Method | Job | Toolchain | IterationCount | LaunchCount | WarmupCount | Mean | Error | StdDev | -/// |-------------------------------------------------- |----------- |----------------------- |--------------- |------------ |------------ |---------:|----------:|----------:| -/// | Query_SingleObjectWithArg | Job-KXSWTB | InProcessEmitToolchain | Default | Default | Default | 5.065 us | 0.0098 us | 0.0092 us | -/// | Query_SingleObjectWithArg_IncludeSubObject | Job-KXSWTB | InProcessEmitToolchain | Default | Default | Default | 6.470 us | 0.0129 us | 0.0108 us | -/// | Query_SingleObjectWithArg_IncludeSubObjectAndList | Job-KXSWTB | InProcessEmitToolchain | Default | Default | Default | 7.895 us | 0.0152 us | 0.0127 us | -/// | Query_List | Job-KXSWTB | InProcessEmitToolchain | Default | Default | Default | 2.462 us | 0.0069 us | 0.0061 us | -/// | Query_ListWithTakeArg | Job-KXSWTB | InProcessEmitToolchain | Default | Default | Default | 8.604 us | 0.0080 us | 0.0075 us | -/// | Query_SingleObjectWithArg | ShortRun | Default | 3 | 1 | 3 | 4.910 us | 0.3644 us | 0.0200 us | -/// | Query_SingleObjectWithArg_IncludeSubObject | ShortRun | Default | 3 | 1 | 3 | 6.357 us | 0.2872 us | 0.0157 us | -/// | Query_SingleObjectWithArg_IncludeSubObjectAndList | ShortRun | Default | 3 | 1 | 3 | 7.760 us | 0.0774 us | 0.0042 us | -/// | Query_List | ShortRun | Default | 3 | 1 | 3 | 2.547 us | 0.0216 us | 0.0012 us | -/// | Query_ListWithTakeArg | ShortRun | Default | 3 | 1 | 3 | 8.600 us | 1.3116 us | 0.0719 us | /// -[ShortRunJob] +[MemoryDiagnoser] public class CompileGqlDocumentOnlyBenchmarks : BaseBenchmark { [Benchmark] public void Query_SingleObjectWithArg() { - new GraphQLCompiler(Schema).Compile( + GraphQLParser.Parse( new QueryRequest { Query = @@ -44,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 = @@ -63,14 +67,15 @@ id name dob } } }", - } + }, + Schema ); } [Benchmark] public void Query_SingleObjectWithArg_IncludeSubObjectAndList() { - new GraphQLCompiler(Schema).Compile( + GraphQLParser.Parse( new QueryRequest { Query = @@ -85,14 +90,15 @@ id name dob } } }", - } + }, + Schema ); } [Benchmark] public void Query_List() { - new GraphQLCompiler(Schema).Compile( + GraphQLParser.Parse( new QueryRequest { Query = @@ -101,7 +107,8 @@ public void Query_List() id name released } }", - } + }, + Schema ); } @@ -114,7 +121,7 @@ public void ModifyField() [Benchmark] public void Query_ListWithTakeArg() { - new GraphQLCompiler(Schema).Compile( + GraphQLParser.Parse( new QueryRequest { Query = @@ -123,7 +130,8 @@ public void Query_ListWithTakeArg() id name released } }", - } + }, + Schema ); } } diff --git a/src/Benchmarks/EqlBenchmarks.cs b/src/Benchmarks/EqlBenchmarks.cs index 1884de93..d2578532 100644 --- a/src/Benchmarks/EqlBenchmarks.cs +++ b/src/Benchmarks/EqlBenchmarks.cs @@ -11,13 +11,6 @@ namespace Benchmarks; /// On a Apple M1 Max 64GB ram /// Command: `dotnet run -c Release` /// -/// 5.3.0 -/// | Method | Toolchain | IterationCount | LaunchCount | WarmupCount | Mean | Error | StdDev | -/// |-------------------------------- |--------------- |--------------- |------------ |------------ |---------:|----------:|---------:| -/// | SimpleExpression | Default | 3 | 1 | 3 | 14.16 us | 0.948 us | 0.052 us | -/// | ComplexExpression | Default | 3 | 1 | 3 | 51.51 us | 85.437 us | 4.683 us | -/// | ComplexWithMethodCallExpression | Default | 3 | 1 | 3 | 66.06 us | 5.265 us | 0.289 us | -/// /// 5.4.0 with Parlot replacing Antlr /// | SimpleExpression | Default | 3 | 1 | 3 | 6.380 us | 0.2524 us | 0.0138 us | /// | ComplexExpression | Default | 3 | 1 | 3 | 24.810 us | 2.7608 us | 0.1513 us | @@ -33,35 +26,66 @@ namespace Benchmarks; /// | SimpleExpression | Default | 3 | 1 | 3 | 10.62 us | 3.848 us | 0.211 us | /// | ComplexExpression | Default | 3 | 1 | 3 | 32.84 us | 23.009 us | 1.261 us | /// | ComplexWithMethodCallExpression | Default | 3 | 1 | 3 | 53.69 us | 9.651 us | 0.529 us | +/// +/// 5.8.0 Parlot 1.4.2 +/// .NET SDK 9.0.301 +/// +/// | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +/// |-------------------------------- |----------:|-----------:|----------:|-------:|-------:|----------:| +/// | SimpleExpression | 3.477 us | 0.1119 us | 0.0061 us | 1.0872 | 0.0076 | 6.68 KB | +/// | ComplexExpression | 22.977 us | 17.0218 us | 0.9330 us | 5.3711 | - | 34.25 KB | +/// | ComplexWithMethodCallExpression | 46.528 us | 18.6464 us | 1.0221 us | 8.7891 | - | 55.91 KB | +/// /// -[ShortRunJob] +[ShortRunJob, MemoryDiagnoser] public class EqlBenchmarks : BaseBenchmark { + private Movie _data = new(Guid.NewGuid(), "foo", 3, new DateTime(2021, 1, 1), new Person(Guid.NewGuid(), "Jimmy", "Rum", new DateTime(1978, 2, 4), []), [], new MovieGenre("Action")); + private Expression _context = Expression.Parameter(typeof(Movie), "m"); + [Benchmark] - public void SimpleExpression() + public object SimpleExpression() { var expressionStr = "name == \"foo\""; - var data = new Movie(Guid.NewGuid(), "foo", 3, new DateTime(2021, 1, 1), new Person(Guid.NewGuid(), "Jimmy", "Rum", new DateTime(1978, 2, 4), []), [], new MovieGenre("Action")); - var context = Expression.Parameter(data.GetType(), "m"); - 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; } [Benchmark] - public void ComplexExpression() + public object ComplexExpression() { var expressionStr = "name == \"foo\" && director.name == \"Jimmy\" && director.dob > \"1978-02-04\" && genre.name == \"Action\" && rating > 3"; - var data = new Movie(Guid.NewGuid(), "foo", 3, new DateTime(2021, 1, 1), new Person(Guid.NewGuid(), "Jimmy", "Rum", new DateTime(1978, 2, 4), []), [], new MovieGenre("Action")); - var context = Expression.Parameter(data.GetType(), "m"); - 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; } [Benchmark] - public void ComplexWithMethodCallExpression() + 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 data = new Movie(Guid.NewGuid(), "foo", 3, new DateTime(2021, 1, 1), new Person(Guid.NewGuid(), "Jimmy", "Rum", new DateTime(1978, 2, 4), []), [], new MovieGenre("Action")); - var context = Expression.Parameter(data.GetType(), "m"); - 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/Model/BenchmarkContext.cs b/src/Benchmarks/Model/BenchmarkContext.cs index 68e3493c..e0ef9a3c 100644 --- a/src/Benchmarks/Model/BenchmarkContext.cs +++ b/src/Benchmarks/Model/BenchmarkContext.cs @@ -20,6 +20,8 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity().HasKey(d => d.Name); builder.Entity().HasKey(d => d.Id); builder.Entity().HasOne(d => d.Director); + builder.Entity().HasMany(d => d.Actors); + builder.Entity().HasMany(d => d.Movies); builder.Entity().HasMany(d => d.DirectorOf); } } diff --git a/src/Benchmarks/Model/Person.cs b/src/Benchmarks/Model/Person.cs index ca34dd52..79b34ca7 100644 --- a/src/Benchmarks/Model/Person.cs +++ b/src/Benchmarks/Model/Person.cs @@ -25,4 +25,5 @@ public Person(Guid id, string firstName, string lastName, DateTime dob, IEnumera public string LastName { get; set; } public DateTime Dob { get; set; } public IEnumerable DirectorOf { get; set; } = []; + public IEnumerable Movies { get; set; } = []; } diff --git a/src/Benchmarks/Program.cs b/src/Benchmarks/Program.cs index 84db66d6..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; @@ -7,7 +6,7 @@ class Program { static void Main(string[] args) { - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, new DebugInProcessConfig()); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); // var bench = new CompileStagesBenchmarks(); // for (int i = 0; i < 1; i++) diff --git a/src/EntityGraphQL.AspNet/EntityGraphQL.AspNet.csproj b/src/EntityGraphQL.AspNet/EntityGraphQL.AspNet.csproj index 66876359..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.6.2 + 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 375c2957..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,93 +37,75 @@ 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; } - var isChunked = context.Request.Headers.TransferEncoding - .Any(h => h is not null && h.Equals("chunked", StringComparison.OrdinalIgnoreCase)); + var isChunked = context.Request.Headers.TransferEncoding.Any(h => h is not null && h.Equals("chunked", StringComparison.OrdinalIgnoreCase)); - if (!isChunked && (context.Request.ContentLength == null || - context.Request.ContentLength == 0)) + if (!isChunked && (context.Request.ContentLength == null || context.Request.ContentLength == 0)) { context.Response.StatusCode = StatusCodes.Status400BadRequest; return; } 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 gqlResult = await schema.ExecuteRequestAsync(query, context.RequestServices, context.User, options); + 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(); - 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"; - } + try + { + var gqlResult = await schema.ExecuteRequestAsync(query, context.RequestServices, context.User, options, context.RequestAborted); + + 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.AspNet/WebSockets/GraphQLWebSocketServer.cs b/src/EntityGraphQL.AspNet/WebSockets/GraphQLWebSocketServer.cs index bc662592..2af185cd 100644 --- a/src/EntityGraphQL.AspNet/WebSockets/GraphQLWebSocketServer.cs +++ b/src/EntityGraphQL.AspNet/WebSockets/GraphQLWebSocketServer.cs @@ -137,7 +137,7 @@ await CloseConnectionAsync( { var request = graphQLWSMessage.Payload; // executing this sets up the observers etc. We don't return any data until we have an event - var result = await schema.ExecuteRequestAsync(request, Context.RequestServices, Context.User, options)!; + var result = await schema.ExecuteRequestAsync(request, Context.RequestServices, Context.User, options, Context.RequestAborted)!; if (result.Errors != null) { await SendErrorAsync(graphQLWSMessage.Id, result.Errors); diff --git a/src/EntityGraphQL/BulkFieldResolver.cs b/src/EntityGraphQL/BulkFieldResolver.cs index 203e445a..68cf4c8c 100644 --- a/src/EntityGraphQL/BulkFieldResolver.cs +++ b/src/EntityGraphQL/BulkFieldResolver.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; +using System.Threading.Tasks; using EntityGraphQL.Compiler; namespace EntityGraphQL.Schema; @@ -17,6 +18,8 @@ public class BulkFieldResolver : IBulkFieldRe public string Name { get; } public ParameterExpression? BulkArgParam => null; + public bool IsAsync => false; + public int? MaxConcurrency => null; public BulkFieldResolver( string name, @@ -43,18 +46,81 @@ public class BulkFieldResolverWithArgs ExtractedFields { get; } public string Name { get; } public ParameterExpression? BulkArgParam => fieldExpression.Parameters[1]; + public bool IsAsync => false; + public int? MaxConcurrency => null; public BulkFieldResolverWithArgs( string name, Expression, TParams, TService, IDictionary>> fieldExpression, Expression> dataSelector, - IEnumerable extractedFields + IEnumerable extractedFields + ) + { + this.fieldExpression = fieldExpression; + this.dataSelector = dataSelector; + ExtractedFields = extractedFields; + Name = name; + } +} + +public class AsyncBulkFieldResolver : IBulkFieldResolver +{ + private readonly Expression, TService, Task>>> fieldExpression; + private readonly Expression> dataSelector; + + public LambdaExpression FieldExpression => fieldExpression; + public LambdaExpression DataSelector => dataSelector; + + public IEnumerable ExtractedFields { get; } + public string Name { get; } + + public ParameterExpression? BulkArgParam => null; + public bool IsAsync => true; + public int? MaxConcurrency { get; } + + public AsyncBulkFieldResolver( + string name, + Expression, TService, Task>>> fieldExpression, + Expression> dataSelector, + IEnumerable extractedFields, + int? maxConcurrency = null + ) + { + this.fieldExpression = fieldExpression; + this.dataSelector = dataSelector; + ExtractedFields = extractedFields; + Name = name; + MaxConcurrency = maxConcurrency; + } +} + +public class AsyncBulkFieldResolverWithArgs : IBulkFieldResolver +{ + private readonly Expression, TParams, TService, Task>>> fieldExpression; + private readonly Expression> dataSelector; + + public LambdaExpression FieldExpression => fieldExpression; + public LambdaExpression DataSelector => dataSelector; + + public IEnumerable ExtractedFields { get; } + public string Name { get; } + public ParameterExpression? BulkArgParam => fieldExpression.Parameters[1]; + public bool IsAsync => true; + public int? MaxConcurrency { get; } + + public AsyncBulkFieldResolverWithArgs( + string name, + Expression, TParams, TService, Task>>> fieldExpression, + Expression> dataSelector, + IEnumerable extractedFields, + int? maxConcurrency = null ) { this.fieldExpression = fieldExpression; this.dataSelector = dataSelector; ExtractedFields = extractedFields; Name = name; + MaxConcurrency = maxConcurrency; } } @@ -65,4 +131,6 @@ public interface IBulkFieldResolver IEnumerable ExtractedFields { get; } string Name { get; } ParameterExpression? BulkArgParam { get; } + bool IsAsync { get; } + int? MaxConcurrency { get; } } 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 new file mode 100644 index 00000000..f35ceb2d --- /dev/null +++ b/src/EntityGraphQL/Compiler/EntityQuery/DefaultMethodImplementations.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Extensions; + +namespace EntityGraphQL.Compiler.EntityQuery; + +/// +/// Contains all the default in-built method implementations for the EqlMethodProvider. +/// These methods handle the built-in filter language operations like contains, where, orderBy, etc. +/// +internal static class DefaultMethodImplementations +{ + #region String Method Implementations + + internal static Expression MakeStringContainsMethod(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.Contains), [typeof(string)])!, predicate); + } + + internal static Expression MakeStringStartsWithMethod(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.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), [typeof(string)])!, predicate); + } + + internal static Expression MakeStringToLowerMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + ExpectArgsCount(0, args, methodName); + return Expression.Call(context, typeof(string).GetMethod(nameof(string.ToLower), Type.EmptyTypes)!); + } + + internal static Expression MakeStringToUpperMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + ExpectArgsCount(0, args, methodName); + return Expression.Call(context, typeof(string).GetMethod(nameof(string.ToUpper), Type.EmptyTypes)!); + } + + #endregion + + #region Enumerable Method Implementations + + internal static Expression MakeWhereMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + 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), [argContext.Type], context, lambda); + } + + internal static Expression MakeAnyMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + 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), [argContext.Type], context, lambda); + } + + internal static Expression MakeFirstMethod(Expression context, Expression argContext, string methodName, Expression[] args) => + MakeOptionalFilterArgumentCall(context, argContext, methodName, args, nameof(Enumerable.First)); + + internal static Expression MakeFirstOrDefaultMethod(Expression context, Expression argContext, string methodName, Expression[] args) => + MakeOptionalFilterArgumentCall(context, argContext, methodName, args, nameof(Enumerable.FirstOrDefault)); + + internal static Expression MakeLastMethod(Expression context, Expression argContext, string methodName, Expression[] args) => + MakeOptionalFilterArgumentCall(context, argContext, methodName, args, nameof(Enumerable.Last)); + + internal static Expression MakeLastOrDefaultMethod(Expression context, Expression argContext, string methodName, Expression[] args) => + MakeOptionalFilterArgumentCall(context, argContext, methodName, args, nameof(Enumerable.LastOrDefault)); + + internal static Expression MakeCountMethod(Expression context, Expression argContext, string methodName, Expression[] args) => + MakeOptionalFilterArgumentCall(context, argContext, methodName, args, "Count"); + + private static Expression MakeOptionalFilterArgumentCall(Expression context, Expression argContext, string methodName, Expression[] args, string actualMethodName) + { + ExpectArgsCountBetween(0, 1, args, methodName); + + var allArgs = new List { context }; + if (args.Length == 1) + { + var predicate = ConvertTypeIfWeCan(methodName, args[0], typeof(bool)); + allArgs.Add(Expression.Lambda(predicate, (ParameterExpression)argContext)); + } + + 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), [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), [argContext.Type], context, amount); + } + + internal static Expression MakeOrderByMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + ExpectArgsCount(1, args, methodName); + var column = args[0]; + var lambda = Expression.Lambda(column, (ParameterExpression)argContext); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.OrderBy), [argContext.Type, column.Type], context, lambda); + } + + internal static Expression MakeOrderByDescMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + ExpectArgsCount(1, args, methodName); + var column = args[0]; + var lambda = Expression.Lambda(column, (ParameterExpression)argContext); + 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 + + #region Special Method Implementations + + internal static Expression MakeIsAnyMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + ExpectArgsCount(1, args, methodName); + var array = args[0]; + 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), [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), [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) + { + ExpectArgsCount(1, args, methodName); + var condition = args[0]; + var lambda = Expression.Lambda(condition, (ParameterExpression)argContext); + return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.All), [argContext.Type], context, lambda); + } + + internal static Expression MakeSumMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + if (args.Length == 0) + { + // Sum() - direct sum of numeric elements + 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), [argContext.Type], context, lambda); + } + else + { + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); + } + } + + internal static Expression MakeMinMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + if (args.Length == 0) + { + 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), [argContext.Type], context, lambda); + } + else + { + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); + } + } + + internal static Expression MakeMaxMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + if (args.Length == 0) + { + 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), [argContext.Type], context, lambda); + } + else + { + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); + } + } + + internal static Expression MakeAverageMethod(Expression context, Expression argContext, string methodName, Expression[] args) + { + if (args.Length == 0) + { + 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), [argContext.Type], context, lambda); + } + else + { + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' expects 0 or 1 arguments but {args.Length} were supplied"); + } + } + + internal static Expression GetContextFromEnumerable(Expression context) + { + if (context.Type.IsEnumerableOrArray()) + { + 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(); + return t != null ? Expression.Parameter(t, $"p_{t.Name}") : context; + } + + #endregion + + #region Helper Methods + + private static void ExpectArgsCount(int count, Expression[] args, string method) + { + if (args.Length != count) + 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 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) + { + if (expected == argExp.Type) + return argExp; + + try + { + return Expression.Convert(argExp, expected); + } + catch (Exception 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 + ); + } + } + + #endregion +} diff --git a/src/EntityGraphQL/Compiler/EntityQuery/DefaultMethodProvider.cs b/src/EntityGraphQL/Compiler/EntityQuery/DefaultMethodProvider.cs deleted file mode 100644 index d15ceaad..00000000 --- a/src/EntityGraphQL/Compiler/EntityQuery/DefaultMethodProvider.cs +++ /dev/null @@ -1,307 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using EntityGraphQL.Compiler.Util; -using EntityGraphQL.Extensions; - -namespace EntityGraphQL.Compiler.EntityQuery; - -/// -/// The default method provider for Ling Queries. Implements the useful Linq functions for -/// querying and filtering your data requests. -/// -public class DefaultMethodProvider : IMethodProvider -{ - // Map of the method names and a function that makes the Expression.Call - private readonly Dictionary, Dictionary>> supportedMethods = - new() - { - { - (Type t) => t == typeof(string), - new(StringComparer.OrdinalIgnoreCase) - { - { "contains", MakeStringContainsMethod }, - { "startsWith", MakeStringStartsWithMethod }, - { "endsWith", MakeStringEndsWithMethod }, - { "toLower", MakeStringToLowerMethod }, - { "toUpper", MakeStringToUpperMethod }, - } - }, - { - (Type t) => t.IsEnumerableOrArray(), - new(StringComparer.OrdinalIgnoreCase) - { - { "where", MakeWhereMethod }, - { "filter", MakeWhereMethod }, - { "first", MakeFirstMethod }, - { "firstOrDefault", MakeFirstOrDefaultMethod }, - { "last", MakeLastMethod }, - { "lastOrDefault", MakeLastOrDefaultMethod }, - { "take", MakeTakeMethod }, - { "skip", MakeSkipMethod }, - { "count", MakeCountMethod }, - { "any", MakeAnyMethod }, - { "orderby", MakeOrderByMethod }, - { "orderByDesc", MakeOrderByDescMethod }, - } - }, - { - (Type t) => - t == typeof(string) - || t == typeof(long) - || t == typeof(long?) - || t == typeof(int) - || t == typeof(int?) - || t == typeof(short) - || t == typeof(short?) - || t == typeof(byte) - || t == typeof(byte?) - || t == typeof(double) - || t == typeof(double?) - || t == typeof(float) - || t == typeof(float?) - || t == typeof(decimal) - || t == typeof(decimal?) - || t == typeof(uint) - || t == typeof(uint?) - || t == typeof(ulong) - || t == typeof(ulong?) - || t == typeof(ushort) - || t == typeof(ushort?) - || t == typeof(sbyte) - || t == typeof(sbyte?) - || t == typeof(char) - || t == typeof(char?) - || t == typeof(DateTime) - || t == typeof(DateTime?) - || t == typeof(Guid) - || t == typeof(Guid?) - || t == typeof(DateTimeOffset) - || t == typeof(DateTimeOffset?) -#if NET5_0_OR_GREATER - || t == typeof(DateOnly) - || t == typeof(DateOnly?) - || t == typeof(TimeOnly) - || t == typeof(TimeOnly?) -#endif - , - new(StringComparer.OrdinalIgnoreCase) { { "isAny", MakeIsAnyMethod } } - }, - }; - - public bool EntityTypeHasMethod(Type context, string methodName) - { - foreach (var item in supportedMethods) - { - if (item.Key(context) && item.Value.ContainsKey(methodName)) - return true; - } - - return false; - } - - public Expression GetMethodContext(Expression context, string methodName) - { - // some methods have a context of the element type in the list, other is just the original context - // need some way for the method compiler to tells us that - // return _supportedMethods[methodName](context); - return GetContextFromEnumerable(context); - } - - public Expression MakeCall(Expression context, Expression argContext, string methodName, IEnumerable? args, Type type) - { - foreach (var item in supportedMethods) - { - if (item.Key(type) && item.Value.TryGetValue(methodName, out var func)) - { - return func(context, argContext, methodName, args != null ? args.ToArray() : []); - } - } - - throw new EntityGraphQLCompilerException($"Unsupported method {methodName}"); - } - - private static Expression MakeWhereMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - ExpectArgsCount(1, args, methodName); - var predicate = args.First(); - predicate = ConvertTypeIfWeCan(methodName, predicate, typeof(bool)); - var lambda = Expression.Lambda(predicate, (ParameterExpression)argContext); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Where), [argContext.Type], context, lambda); - } - - private static Expression MakeAnyMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - ExpectArgsCount(1, args, methodName); - var predicate = args.First(); - predicate = ConvertTypeIfWeCan(methodName, predicate, typeof(bool)); - var lambda = Expression.Lambda(predicate, (ParameterExpression)argContext); - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Any), [argContext.Type], context, lambda); - } - - private static Expression MakeFirstMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - return MakeOptionalFilterArgumentCall(context, argContext, methodName, args, nameof(Enumerable.First)); - } - - private static Expression MakeFirstOrDefaultMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - return MakeOptionalFilterArgumentCall(context, argContext, methodName, args, nameof(Enumerable.FirstOrDefault)); - } - - private static Expression MakeCountMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - return MakeOptionalFilterArgumentCall(context, argContext, methodName, args, "Count"); - } - - private static Expression MakeOptionalFilterArgumentCall(Expression context, Expression argContext, string methodName, Expression[] args, string actualMethodName) - { - ExpectArgsCountBetween(0, 1, args, methodName); - - var allArgs = new List { context }; - if (args.Length == 1) - { - var predicate = args.First(); - predicate = ConvertTypeIfWeCan(methodName, predicate, typeof(bool)); - allArgs.Add(Expression.Lambda(predicate, (ParameterExpression)argContext)); - } - - return ExpressionUtil.MakeCallOnQueryable(actualMethodName, [argContext.Type], allArgs.ToArray()); - } - - private static Expression MakeLastMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - return MakeOptionalFilterArgumentCall(context, argContext, methodName, args, nameof(Enumerable.Last)); - } - - private static Expression MakeLastOrDefaultMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - return MakeOptionalFilterArgumentCall(context, argContext, methodName, args, nameof(Enumerable.LastOrDefault)); - } - - private static Expression MakeTakeMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - ExpectArgsCount(1, args, methodName); - var amount = args.First(); - amount = ConvertTypeIfWeCan(methodName, amount, typeof(int)); - - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Take), [argContext.Type], context, amount); - } - - private static Expression MakeSkipMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - ExpectArgsCount(1, args, methodName); - var amount = args.First(); - amount = ConvertTypeIfWeCan(methodName, amount, typeof(int)); - - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.Skip), [argContext.Type], context, amount); - } - - private static Expression MakeOrderByMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - ExpectArgsCount(1, args, methodName); - var column = args.First(); - var lambda = Expression.Lambda(column, (ParameterExpression)argContext); - - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.OrderBy), [argContext.Type, column.Type], context, lambda); - } - - private static Expression MakeOrderByDescMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - ExpectArgsCount(1, args, methodName); - var column = args.First(); - var lambda = Expression.Lambda(column, (ParameterExpression)argContext); - - return ExpressionUtil.MakeCallOnQueryable(nameof(Queryable.OrderByDescending), [argContext.Type, column.Type], context, lambda); - } - - private static Expression MakeIsAnyMethod(Expression context, Expression argContext, string methodName, Expression[] args) - { - ExpectArgsCount(1, args, methodName); - var array = args.First(); - var arrayType = array.Type.GetEnumerableOrArrayType() ?? throw new EntityGraphQLCompilerException("Could not get element type from enumerable/array"); - var isQueryable = typeof(IQueryable).IsAssignableFrom(array.Type); - if (context.Type.IsNullableType()) - { - var call = isQueryable ? - 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), [arrayType], array, Expression.Convert(context, arrayType)) : - ExpressionUtil.MakeCallOnEnumerable(nameof(Enumerable.Contains), [arrayType], array, Expression.Convert(context, arrayType)); - } - - private static Expression MakeStringContainsMethod(Expression context, Expression argContext, string methodName, Expression[] args) => - MakeStringMethod(string.Empty.Contains, context, methodName, args); - - private static Expression MakeStringStartsWithMethod(Expression context, Expression argContext, string methodName, Expression[] args) => - MakeStringMethod(string.Empty.StartsWith, context, methodName, args); - - private static Expression MakeStringEndsWithMethod(Expression context, Expression argContext, string methodName, Expression[] args) => - MakeStringMethod(string.Empty.EndsWith, context, methodName, args); - - private static Expression MakeStringToLowerMethod(Expression context, Expression argContext, string methodName, Expression[] args) => - MakeStringMethod(string.Empty.ToLower, context, methodName, args); - - private static Expression MakeStringToUpperMethod(Expression context, Expression argContext, string methodName, Expression[] args) => - MakeStringMethod(string.Empty.ToUpper, context, methodName, args); - - private static MethodCallExpression MakeStringMethod(Func method, Expression context, string methodName, Expression[] args) - { - ExpectArgsCount(0, args, methodName); - return Expression.Call(context, method.Method); - } - - private static MethodCallExpression MakeStringMethod(Func method, Expression context, string methodName, Expression[] args) - { - ExpectArgsCount(1, args, methodName); - var predicate = args.First(); - predicate = ConvertTypeIfWeCan(methodName, predicate, typeof(string)); - return Expression.Call(context, method.Method, predicate); - } - - private 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"); - return Expression.Parameter(type, $"p_{type.Name}"); - } - var t = context.Type.GetEnumerableOrArrayType(); - if (t != null) - return Expression.Parameter(t, $"p_{t.Name}"); - return 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"); - } - - 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"); - } - - private static Expression ConvertTypeIfWeCan(string methodName, Expression argExp, Type expected) - { - if (expected != argExp.Type) - { - try - { - return Expression.Convert(argExp, expected); - } - catch (Exception ex) - { - throw new EntityGraphQLCompilerException($"Method '{methodName}' expects parameter that evaluates to a '{expected}' result but found result type '{argExp.Type}'", ex); - } - } - return argExp; - } -} diff --git a/src/EntityGraphQL/Compiler/EntityQuery/EQLMethodAttribute.cs b/src/EntityGraphQL/Compiler/EntityQuery/EQLMethodAttribute.cs new file mode 100644 index 00000000..32ebb059 --- /dev/null +++ b/src/EntityGraphQL/Compiler/EntityQuery/EQLMethodAttribute.cs @@ -0,0 +1,30 @@ +using System; + +namespace EntityGraphQL.Compiler.EntityQuery; + +/// +/// Marks an extension method as safe for use in GraphQL filter expressions. +/// Only methods marked with this attribute will be exposed to the filter language when using EqlMethodProvider. +/// +[AttributeUsage(AttributeTargets.Method)] +public class EqlMethodAttribute : Attribute +{ + /// + /// The name to use for this method in filter expressions. If not provided, uses the method name converted to camelCase. + /// + public string? MethodName { get; set; } + + /// + /// Initializes a new instance of the GraphQLFilterMethodAttribute. + /// + public EqlMethodAttribute() { } + + /// + /// Initializes a new instance of the GraphQLFilterMethodAttribute with a specific method name. + /// + /// The name to use for this method in filter expressions + public EqlMethodAttribute(string methodName) + { + MethodName = methodName; + } +} diff --git a/src/EntityGraphQL/Compiler/EntityQuery/EntityQueryCompiler.cs b/src/EntityGraphQL/Compiler/EntityQuery/EntityQueryCompiler.cs index 566989cf..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, new DefaultMethodProvider()); + 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)); @@ -41,12 +44,12 @@ public static CompiledQueryResult Compile(string query, ISchemaProvider? schemaP ParameterExpression? contextParam = null; - methodProvider ??= new DefaultMethodProvider(); + methodProvider ??= new EqlMethodProvider(); 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 DefaultMethodProvider(); - var expression = CompileQuery(query, context, schemaProvider, requestContext, methodProvider, executionOptions) ?? throw new EntityGraphQLCompilerException("Failed to compile expression"); + methodProvider ??= new EqlMethodProvider(); + 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,12 +80,10 @@ private static Expression CompileQuery( ISchemaProvider? schemaProvider, QueryRequestContext requestContext, IMethodProvider methodProvider, - ExecutionOptions executionOptions + EqlCompileContext compileContext ) { - var compileContext = new CompileContext(executionOptions, null, requestContext); - var expressionParser = new EntityQueryParser(context, schemaProvider, requestContext, methodProvider, compileContext); - var expression = expressionParser.Parse(query); + 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 new file mode 100644 index 00000000..860f3b31 --- /dev/null +++ b/src/EntityGraphQL/Compiler/EntityQuery/EqlMethodProvider.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using EntityGraphQL.Extensions; + +namespace EntityGraphQL.Compiler.EntityQuery; + +/// +/// A unified method provider that registers all methods (default and custom) using a consistent registration system. +/// This replaces the need for separate DefaultMethodProvider and ExtensibleMethodProvider classes. +/// +public class EqlMethodProvider : IMethodProvider +{ + private readonly Dictionary registeredMethods; + private readonly HashSet isAnySupportedTypes = new(); + + public EqlMethodProvider() + { + registeredMethods = new Dictionary(StringComparer.OrdinalIgnoreCase); + InitializeIsAnySupportedTypes(); + RegisterDefaultMethods(); + } + + #region Public Registration Methods + + public void RegisterMethods() => RegisterMethods(typeof(T)); + + public void RegisterMethods(Type extensionType) + { + ValidateNotNull(extensionType, nameof(extensionType)); + + var methods = extensionType.GetMethods(BindingFlags.Public | BindingFlags.Static).Where(m => m.GetCustomAttribute() != null); + + foreach (var method in methods) + { + try + { + RegisterMethod(method); + } + catch (InvalidOperationException) + { /* Skip invalid methods */ + } + } + } + + public void RegisterMethod(MethodInfo method) + { + var parameters = method.GetParameters(); + RegisterMethod(method, parameters.Length > 0 ? parameters[0].ParameterType : typeof(object)); + } + + /// + /// Registers a method for use in the language system. + /// + /// The MethodInfo used to call the method + /// The context type for the method. Limits the types that the method can be called on. + /// An optional name to use for the method + /// Additional arguments to pass to the method call. Useful for methods like those in EF.Functions. These are prefixed. + public void RegisterMethod(MethodInfo method, Type methodContextType, string? filterMethodName = null, Expression[]? extraArgs = null) + { + var methodName = filterMethodName ?? GetCamelCaseMethodName(method.Name); + ValidateMethodName(methodName); + ValidateUniqueMethodName(methodName); + + var methodInfo = new RegisteredMethodInfo + { + Method = method, + MethodContextType = methodContextType, + MethodName = methodName, + Origin = MethodOrigin.Custom, + MakeCallFunc = (context, argContext, methodName, args) => + { + var instance = method.IsStatic ? null : context; + var allArgs = method.IsStatic ? [context, .. args] : args; + if (extraArgs != null && extraArgs.Length > 0) + allArgs = [.. extraArgs, .. allArgs]; + var call = Expression.Call(instance, method, allArgs); + return call; + }, + }; + + registeredMethods[methodName] = methodInfo; + } + + public void RegisterMethod(Type methodContextType, string filterMethodName, Func makeCallFunc) + { + RegisterMethodInternal(methodContextType, filterMethodName, makeCallFunc, MethodOrigin.Custom); + } + + public void RegisterMethod(Func typePredicate, string filterMethodName, Func makeCallFunc) + { + RegisterMethodInternal(typeof(object), filterMethodName, makeCallFunc, MethodOrigin.Custom, typePredicate); + } + + private void RegisterMethodInternal( + Type methodContextType, + string filterMethodName, + Func makeCallFunc, + MethodOrigin origin, + Func? typePredicate = null + ) + { + ValidateMethodName(filterMethodName); + ValidateUniqueMethodName(filterMethodName); + + var methodInfo = new RegisteredMethodInfo + { + Method = null!, + MethodContextType = methodContextType, + MethodName = filterMethodName, + MakeCallFunc = makeCallFunc, + Origin = origin, + TypePredicate = typePredicate, + }; + + registeredMethods[filterMethodName] = methodInfo; + } + + private void RegisterMethodWithTypePredicate(Func typePredicate, string filterMethodName, Func makeCallFunc) + { + RegisterMethodInternal(typeof(object), filterMethodName, makeCallFunc, MethodOrigin.Default, typePredicate); + } + + #endregion + + #region Query Methods + + public IReadOnlyCollection GetRegisteredMethods() => registeredMethods.Values.ToList().AsReadOnly(); + + public IReadOnlyCollection GetCustomRegisteredMethods() => registeredMethods.Values.Where(m => m.Origin == MethodOrigin.Custom).ToList().AsReadOnly(); + + public void ClearAllMethods() => registeredMethods.Clear(); + + public void ClearCustomMethods() + { + var customKeys = registeredMethods.Where(kvp => kvp.Value.Origin == MethodOrigin.Custom).Select(kvp => kvp.Key).ToList(); + + foreach (var key in customKeys) + registeredMethods.Remove(key); + } + + public bool EntityTypeHasMethod(Type context, string methodName) => registeredMethods.TryGetValue(methodName, out var method) && IsTypeCompatible(context, method); + + public Expression GetMethodContext(Expression context, string methodName) => DefaultMethodImplementations.GetContextFromEnumerable(context); + + public Expression MakeCall(Expression context, Expression argContext, string methodName, IEnumerable? args, Type type) + { + if (!registeredMethods.TryGetValue(methodName, out var method)) + throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Unsupported method {methodName}"); + + if (!IsTypeCompatible(type, method)) + 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 EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Method '{methodName}' does not have a MakeCallFunc defined"); + } + + #endregion + + #region Default Method Registration + + private void RegisterDefaultMethods() + { + // String methods + RegisterMethodInternal(typeof(string), "contains", DefaultMethodImplementations.MakeStringContainsMethod, MethodOrigin.Default); + RegisterMethodInternal(typeof(string), "startsWith", DefaultMethodImplementations.MakeStringStartsWithMethod, MethodOrigin.Default); + RegisterMethodInternal(typeof(string), "endsWith", DefaultMethodImplementations.MakeStringEndsWithMethod, MethodOrigin.Default); + RegisterMethodInternal(typeof(string), "toLower", DefaultMethodImplementations.MakeStringToLowerMethod, MethodOrigin.Default); + RegisterMethodInternal(typeof(string), "toUpper", DefaultMethodImplementations.MakeStringToUpperMethod, MethodOrigin.Default); + + // Enumerable methods - these need special type predicates for efficiency + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "where", DefaultMethodImplementations.MakeWhereMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "filter", DefaultMethodImplementations.MakeWhereMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "first", DefaultMethodImplementations.MakeFirstMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "firstOrDefault", DefaultMethodImplementations.MakeFirstOrDefaultMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "last", DefaultMethodImplementations.MakeLastMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "lastOrDefault", DefaultMethodImplementations.MakeLastOrDefaultMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "take", DefaultMethodImplementations.MakeTakeMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "skip", DefaultMethodImplementations.MakeSkipMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "count", DefaultMethodImplementations.MakeCountMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "any", DefaultMethodImplementations.MakeAnyMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "all", DefaultMethodImplementations.MakeAllMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "orderby", DefaultMethodImplementations.MakeOrderByMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "orderByDesc", DefaultMethodImplementations.MakeOrderByDescMethod); + RegisterMethodWithTypePredicate(t => t.IsEnumerableOrArray(), "sum", DefaultMethodImplementations.MakeSumMethod); + 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 void InitializeIsAnySupportedTypes() + { + isAnySupportedTypes.Clear(); + var defaults = new[] + { + typeof(string), + typeof(long), + typeof(long?), + typeof(int), + typeof(int?), + typeof(short), + typeof(short?), + typeof(byte), + typeof(byte?), + typeof(double), + typeof(double?), + typeof(float), + typeof(float?), + typeof(decimal), + typeof(decimal?), + typeof(uint), + typeof(uint?), + typeof(ulong), + typeof(ulong?), + typeof(ushort), + typeof(ushort?), + typeof(sbyte), + typeof(sbyte?), + typeof(char), + typeof(char?), + typeof(DateTime), + typeof(DateTime?), + typeof(Guid), + typeof(Guid?), + typeof(DateTimeOffset), + typeof(DateTimeOffset?), + typeof(TimeSpan), + typeof(TimeSpan?) +#if NET8_0_OR_GREATER + , + typeof(DateOnly), + typeof(DateOnly?), + typeof(TimeOnly), + 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); + } + } + } + + private Func CreateIsAnyTypePredicate() + { + return t => isAnySupportedTypes.Contains(t); + } + + #endregion + + #region Validation Helpers + + private static void ValidateNotNull(object? value, string paramName) + { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(value); +#else + if (value == null) + throw new ArgumentNullException(paramName); +#endif + } + + private static void ValidateMethodName(string methodName) + { + if (string.IsNullOrWhiteSpace(methodName)) + throw new ArgumentException("Filter method name cannot be empty"); + } + + private void ValidateUniqueMethodName(string methodName) + { + if (registeredMethods.ContainsKey(methodName)) + throw new InvalidOperationException($"A method with name '{methodName}' is already registered"); + } + + private static bool IsTypeCompatible(Type contextType, RegisteredMethodInfo methodInfo) + { + return methodInfo.TypePredicate?.Invoke(contextType) ?? methodInfo.MethodContextType.IsAssignableFrom(contextType); + } + + private static string GetCamelCaseMethodName(string methodName) => methodName.Length > 0 ? char.ToLowerInvariant(methodName[0]) + methodName.Substring(1) : methodName; + + #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 f8642d22..1d01ae31 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/CallPath.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/CallPath.cs @@ -2,22 +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) { - return parts[0].Compile(context, schema, requestContext, methodProvider); + if (parts[0] is CallExpression ce) + { + 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, parser, schema, requestContext, methodProvider); } var exp = parts.Aggregate( context!, @@ -25,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; @@ -39,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, @@ -47,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; @@ -62,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 d3176012..7149c0ed 100644 --- a/src/EntityGraphQL/Compiler/EntityQuery/Grammar/EntityQueryParser.cs +++ b/src/EntityGraphQL/Compiler/EntityQuery/Grammar/EntityQueryParser.cs @@ -10,6 +10,7 @@ namespace EntityGraphQL.Compiler.EntityQuery.Grammar; public sealed class EntityQueryParser { + public static readonly EntityQueryParser Instance; private const string MultiplyChar = "*"; private const string DivideChar = "/"; private const string ModChar = "%"; @@ -54,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"); @@ -62,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); @@ -73,12 +75,13 @@ public sealed class EntityQueryParser private static readonly Parser strExp = SkipWhiteSpace(new StringLiteral(StringLiteralQuotes.SingleOrDouble)) .Then(static s => new EqlExpression(Expression.Constant(s.ToString()))); - private readonly Expression? context; - private readonly ISchemaProvider? schema; - private readonly QueryRequestContext requestContext; - private readonly IMethodProvider methodProvider; - public EntityQueryParser(Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider, CompileContext compileContext) + // 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 var expression = Deferred(); @@ -89,28 +92,63 @@ public EntityQueryParser(Expression? context, ISchemaProvider? schema, QueryRequ 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(x => new IdentityExpression(x.Item1.ToString()!, 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(x => new EqlExpression(Expression.NewArrayInit(x.Item2[0].Type, x.Item2.Select(e => e.Compile(context, schema, requestContext, 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)); - var callPath = Separated(dot, OneOf(call, constArray, identifier)).Then(p => new CallPath(p, compileContext)); + var callPath = Separated(dot, OneOf(call, constArray, identifier)).Then(static (c, p) => new CallPath(p, ((EntityQueryParseContext)c).CompileContext)); var nullExp = Terms.Text("null").AndSkip(Not(identifier)).Then(static _ => new EqlExpression(Expression.Constant(null))); var trueExp = Terms.Text("true").AndSkip(Not(identifier)).Then(static _ => new EqlExpression(Expression.Constant(true))); 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(x => new EqlExpression(Expression.Negate(x.Item2.Compile(context, schema, requestContext, 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 )* ; @@ -155,10 +193,11 @@ public EntityQueryParser(Expression? context, ISchemaProvider? schema, QueryRequ expression.Parser = conditional.Or(logicalBinary); grammar = expression; - this.context = context; - this.schema = schema; - this.requestContext = requestContext; - this.methodProvider = methodProvider; + } + + static EntityQueryParser() + { + Instance = new EntityQueryParser(); } private static IExpression HandleBinary((IExpression, IReadOnlyList<(string, IExpression)>) x) @@ -198,9 +237,30 @@ private static IExpression HandleBinary((IExpression, IReadOnlyList<(string, IEx return binaryExp; } - public Expression Parse(string query) + private sealed class EntityQueryParseContext : ParseContext { - var result = grammar.Parse(query) ?? throw new EntityGraphQLCompilerException("Failed to parse query"); - return result.Compile(context, schema, requestContext, methodProvider); + public EntityQueryParseContext(string query, Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider, EqlCompileContext compileContext) + : base(new Parlot.Scanner(query)) + { + Context = context; + Schema = schema; + RequestContext = requestContext; + MethodProvider = methodProvider; + CompileContext = compileContext; + } + + public Expression? Context { get; } + public ISchemaProvider? Schema { get; } + public QueryRequestContext RequestContext { get; } + public IMethodProvider MethodProvider { get; } + public EqlCompileContext CompileContext { get; } + } + + 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 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/EntityQuery/RegisteredMethodInfo.cs b/src/EntityGraphQL/Compiler/EntityQuery/RegisteredMethodInfo.cs new file mode 100644 index 00000000..cfacceaf --- /dev/null +++ b/src/EntityGraphQL/Compiler/EntityQuery/RegisteredMethodInfo.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace EntityGraphQL.Compiler.EntityQuery; + +/// +/// Contains information about a registered method for use in filter expressions. +/// This unified class handles both extension methods and direct methods. +/// +public class RegisteredMethodInfo +{ + /// + /// The MethodInfo of the method. + /// + public MethodInfo Method { get; set; } = null!; + + /// + /// The name to use for this method in filter expressions. + /// + public string MethodName { get; set; } = null!; + + /// + /// The type that this method can be called on. + /// + public Type MethodContextType { get; set; } = null!; + + /// + /// Dynamic type predicate function for efficient type checking. + /// When provided, this takes precedence over MethodContextType for type compatibility checks. + /// + public Func? TypePredicate { get; set; } + + /// + /// Custom function to make the expression call for delegate methods + /// + public Func? MakeCallFunc { get; set; } + + /// + /// Indicates whether this is a default method or a custom method + /// + public MethodOrigin Origin { get; set; } +} + +/// +/// Indicates whether the method is a default method or a custom method. +/// +public enum MethodOrigin +{ + /// + /// A default method that comes pre-registered (like contains, startsWith, etc.) + /// + Default, + + /// + /// A custom method registered by the user + /// + Custom, +} diff --git a/src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLField.cs b/src/EntityGraphQL/Compiler/GqlNodes/BaseGraphQLField.cs index 840cc2d3..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,15 @@ 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)); + } + + /// + /// Checks if this field or any of its child fields are async (return Task) + /// + public virtual bool HasAsyncFieldsAtOrBelow(IReadOnlyDictionary fragments) + { + return Field?.IsAsync == true || QueryFields.Any(f => f.HasAsyncFieldsAtOrBelow(fragments)); } /// @@ -225,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); } @@ -365,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}" + ); } } @@ -399,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 727802a2..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); @@ -170,7 +177,16 @@ ParameterReplacer replacer listExpressionPath.Insert(0, parentNode); parentNode = parentNode.ParentNode; } - compileContext.AddBulkResolver(bulkResolver.Name, bulkResolver.DataSelector, (LambdaExpression)bulkFieldExpr, bulkResolver.ExtractedFields, listExpressionPath); + compileContext.AddBulkResolver( + bulkResolver.Name, + bulkResolver.DataSelector, + (LambdaExpression)bulkFieldExpr, + bulkResolver.ExtractedFields, + listExpressionPath, + bulkResolver.FieldExpression.Parameters.First().Type, + bulkResolver.IsAsync, + bulkResolver.MaxConcurrency + ); compileContext.AddServices(field.Field!.Services); } } diff --git a/src/EntityGraphQL/Compiler/GqlNodes/CompileContext.cs b/src/EntityGraphQL/Compiler/GqlNodes/CompileContext.cs index c9a47f27..7c46a74d 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/CompileContext.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/CompileContext.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq.Expressions; +using System.Threading; using EntityGraphQL.Schema; +using EntityGraphQL.Schema.FieldExtensions; namespace EntityGraphQL.Compiler; @@ -12,12 +15,23 @@ public class CompileContext private readonly Dictionary constantParameters = []; private readonly Dictionary constantParametersForField = []; - public CompileContext(ExecutionOptions options, Dictionary? bulkData, QueryRequestContext requestContext) + 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; } = []; @@ -27,6 +41,10 @@ public CompileContext(ExecutionOptions options, Dictionary? bulk public ParameterExpression? BulkParameter { get; } 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) { @@ -55,10 +73,25 @@ public void AddBulkResolver( LambdaExpression dataSelection, LambdaExpression fieldExpression, IEnumerable extractedFields, - List listExpressionPath + List listExpressionPath, + Type serviceType + ) + { + BulkResolvers.Add(new CompiledBulkFieldResolver(name, dataSelection, fieldExpression, extractedFields, listExpressionPath, serviceType)); + } + + public void AddBulkResolver( + string name, + LambdaExpression dataSelection, + LambdaExpression fieldExpression, + IEnumerable extractedFields, + List listExpressionPath, + Type serviceType, + bool isAsync, + int? maxConcurrency ) { - BulkResolvers.Add(new CompiledBulkFieldResolver(name, dataSelection, fieldExpression, extractedFields, listExpressionPath)); + BulkResolvers.Add(new CompiledBulkFieldResolver(name, dataSelection, fieldExpression, extractedFields, listExpressionPath, serviceType, isAsync, maxConcurrency)); } public void AddArgsToCompileContext( @@ -67,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/CompiledBulkFieldResolver.cs b/src/EntityGraphQL/Compiler/GqlNodes/CompiledBulkFieldResolver.cs index e24574ef..a21eaffd 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/CompiledBulkFieldResolver.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/CompiledBulkFieldResolver.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq.Expressions; using EntityGraphQL.Compiler.Util; @@ -10,7 +11,10 @@ public class CompiledBulkFieldResolver( LambdaExpression dataSelection, LambdaExpression fieldExpression, IEnumerable extractedFields, - List listExpressionPath + List listExpressionPath, + Type serviceType, + bool isAsync = false, + int? maxConcurrency = null ) { public string Name { get; private set; } = name; @@ -18,6 +22,9 @@ List listExpressionPath public LambdaExpression FieldExpression { get; private set; } = fieldExpression; public IEnumerable ExtractedFields { get; } = extractedFields; public List ListExpressionPath { get; } = listExpressionPath; + public bool IsAsync { get; } = isAsync; + public int? MaxConcurrency { get; } = maxConcurrency; + public Type ServiceType { get; } = serviceType; public Expression GetBulkSelectionExpression(Expression newContextParam, List listExpressionPath, ParameterReplacer replacer, bool isRoot = true) { diff --git a/src/EntityGraphQL/Compiler/GqlNodes/ExecutableGraphQLStatement.cs b/src/EntityGraphQL/Compiler/GqlNodes/ExecutableGraphQLStatement.cs index c146366a..942183f3 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/ExecutableGraphQLStatement.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/ExecutableGraphQLStatement.cs @@ -1,13 +1,18 @@ using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using System.Reflection; +using System.Threading; using System.Threading.Tasks; using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Directives; using EntityGraphQL.Extensions; using EntityGraphQL.Schema; +using EntityGraphQL.Schema.FieldExtensions; using Microsoft.Extensions.DependencyInjection; namespace EntityGraphQL.Compiler; @@ -25,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; } @@ -45,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) { @@ -62,18 +77,32 @@ 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 + QueryRequestContext requestContext, + CancellationToken cancellationToken = default ) { 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 { @@ -82,60 +111,129 @@ QueryRequestContext requestContext // } // 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) { - try + if (directive.VisitNode(DirectiveLocation, Schema, this, Arguments, null, null) == null) + return (result, allErrors); + } + try + { + IArgumentsTracker? docVariables = BuildDocumentVariables(ref variables); + CompileContext compileContext = new(options, null, requestContext, OpVariableParameter, docVariables, cancellationToken); + + foreach (var fieldNode in QueryFields) { -#if DEBUG - Stopwatch? timer = null; - if (options.IncludeDebugInfo) + cancellationToken.ThrowIfCancellationRequested(); + + try { - timer = new Stopwatch(); - timer.Start(); - } -#endif - var contextToUse = GetContextToUse(context, serviceProvider!, fieldNode)!; + var contextToUse = GetContextToUse(context, serviceProvider!, fieldNode)!; + + var expandedFields = fieldNode.Expand(compileContext, fragments, false, NextFieldContext!, OpVariableParameter, docVariables).Cast(); + if (!expandedFields.Any()) + continue; - (var data, var didExecute) = await CompileAndExecuteNodeAsync(new CompileContext(options, null, requestContext), contextToUse, serviceProvider, fragments, fieldNode, docVariables); + foreach (var expandedField in expandedFields) + { + try + { #if DEBUG - if (options.IncludeDebugInfo) - { - timer?.Stop(); - result[$"__{fieldNode.Name}_timeMs"] = timer?.ElapsedMilliseconds; - } + Stopwatch? timer = null; + if (options.IncludeDebugInfo) + { + timer = new Stopwatch(); + timer.Start(); + } #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; + var (data, didExecute, fieldErrors) = await ExecuteOperationField(compileContext, expandedField, contextToUse, serviceProvider, fragments, docVariables); - if (didExecute) - result[fieldNode.Name] = data; - } - catch (EntityGraphQLValidationException) - { - throw; - } - catch (EntityGraphQLFieldException) - { - throw; - } - catch (Exception ex) - { - throw new EntityGraphQLFieldException(fieldNode.Name, ex); +#if DEBUG + if (options.IncludeDebugInfo) + { + timer?.Stop(); + result[$"__{expandedField.Name}_timeMs"] = timer?.ElapsedMilliseconds; + } +#endif + + 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; + + 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; } @@ -156,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); } } } @@ -188,73 +287,90 @@ 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; - - 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); - 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 = ResolveBulkLoaders(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); + 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, null, - contextChanged: true, + false, replacer ); - contextParam = newContextType; + 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); - return data; } - private static Dictionary ResolveBulkLoaders( + private static async Task> ResolveBulkLoadersAsync( CompileContext compileContext, IServiceProvider? serviceProvider, BaseGraphQLField node, @@ -266,8 +382,13 @@ ParameterExpression newContextParam var bulkData = new Dictionary(); if (compileContext.BulkResolvers?.Count > 0) { + var bulkTasks = new List(); + var bulkResults = new Dictionary>(); + foreach (var bulkResolver in compileContext.BulkResolvers) { + compileContext.CancellationToken.ThrowIfCancellationRequested(); + // rebuild list expression on new context var toReplace = node.Field!.ResolveExpression!; var listExpression = bulkResolver.GetBulkSelectionExpression(newContextParam, bulkResolver.ListExpressionPath.GetRange(1, bulkResolver.ListExpressionPath.Count - 1), replacer); @@ -291,21 +412,112 @@ ParameterExpression newContextParam var bulkDataArgs = Expression.Lambda(listExpression, newContextParam).Compile().DynamicInvoke([runningContext]); var parameters = new List { bulkResolver.FieldExpression.Parameters.First() }; var allArgs = new List { bulkDataArgs }; - var bulkLoader = GraphQLHelper.InjectServices(serviceProvider!, compileContext.Services, allArgs, bulkResolver.FieldExpression.Body, parameters, replacer); + var bulkLoader = GraphQLHelper.InjectServices( + serviceProvider!, + compileContext.Services, + allArgs, + bulkResolver.FieldExpression.Body, + parameters, + replacer, + compileContext.CancellationToken + ); if (compileContext.ConstantParameters.Any()) { parameters.AddRange(compileContext.ConstantParameters.Keys); allArgs.AddRange(compileContext.ConstantParameters.Values); } - var dataLoaded = Expression.Lambda(bulkLoader, parameters).Compile().DynamicInvoke([.. allArgs])!; - bulkData[bulkResolver.Name] = dataLoaded; + var lambdaExpression = Expression.Lambda(bulkLoader, parameters); + + // Handle async bulk resolvers with concurrency control + if (bulkResolver.IsAsync) + { + var bulkTask = ExecuteBulkResolverWithConcurrencyAsync(lambdaExpression, [.. allArgs], bulkResolver, compileContext); + bulkResults[bulkResolver.Name] = bulkTask!; + bulkTasks.Add(bulkTask); + } + else + { + var dataLoaded = lambdaExpression.Compile().DynamicInvoke([.. allArgs])!; + bulkData[bulkResolver.Name] = dataLoaded; + } + } + + // Wait for all async bulk resolvers to complete + if (bulkTasks.Count > 0) + { + await Task.WhenAll(bulkTasks); + + // Collect results from async operations + foreach (var kvp in bulkResults) + { + bulkData[kvp.Key] = await kvp.Value; + } } } return bulkData; } + private static async Task ExecuteBulkResolverWithConcurrencyAsync(LambdaExpression lambdaExpression, object?[] args, CompiledBulkFieldResolver bulkResolver, CompileContext compileContext) + { + // Generate semaphore configurations for bulk resolver concurrency limiting + var semaphoreConfigs = GetBulkResolverSemaphoreConfigs(bulkResolver, compileContext); + + if (semaphoreConfigs.Count > 0) + { + // Use the existing ExecuteWithConcurrencyLimitAsync method + return await ConcurrencyLimitFieldExtension.ExecuteWithConcurrencyLimitAsync( + lambdaExpression, + semaphoreConfigs, + compileContext.ConcurrencyLimiterRegistry, + args.Where(a => a != null).ToArray()!, + compileContext.CancellationToken + ) ?? new object(); + } + + // No concurrency limiting, execute directly + var result = lambdaExpression.Compile().DynamicInvoke(args); + + if (result is Task task) + { + await task; + + // Get result from Task + var taskType = task.GetType(); + var resultProperty = taskType.GetProperty(nameof(Task.Result)); + if (resultProperty != null) + { + return resultProperty.GetValue(task)!; + } + } + + return result!; + } + + private static List<(string scopeKey, int maxConcurrency)> GetBulkResolverSemaphoreConfigs(CompiledBulkFieldResolver bulkResolver, CompileContext compileContext) + { + var configs = new List<(string scopeKey, int maxConcurrency)>(); + + // Query-level limit + if (compileContext.ExecutionOptions.MaxQueryConcurrency.HasValue) + { + configs.Add(("query_global", compileContext.ExecutionOptions.MaxQueryConcurrency.Value)); + } + + var serviceMax = compileContext.ExecutionOptions.ServiceConcurrencyLimits.GetValueOrDefault(bulkResolver.ServiceType); + if (serviceMax > 0) + configs.Add(($"service_{bulkResolver.ServiceType.AssemblyQualifiedName}", serviceMax)); + + // Bulk resolver-specific limit + if (bulkResolver.MaxConcurrency.HasValue) + { + configs.Add(($"bulk_{bulkResolver.Name}", bulkResolver.MaxConcurrency.Value)); + } + + return configs; + } + private static async Task<(object? result, bool didExecute)> ExecuteExpressionAsync( Expression? expression, object context, @@ -314,7 +526,9 @@ ParameterExpression newContextParam ParameterReplacer replacer, CompileContext compileContext, BaseGraphQLField node, - bool isFinal + bool isFinal, + IReadOnlyDictionary fragments, + bool isSecondExec ) { // they had a query with a directive that was skipped, resulting in an empty query? @@ -329,7 +543,7 @@ bool isFinal // inject dependencies into the fullSelection if (serviceProvider != null) { - expression = GraphQLHelper.InjectServices(serviceProvider, compileContext.Services, allArgs, expression, parameters, replacer); + expression = GraphQLHelper.InjectServices(serviceProvider, compileContext.Services, allArgs, expression, parameters, replacer, compileContext.CancellationToken); } if (compileContext.ConstantParameters.Any()) @@ -356,11 +570,15 @@ bool isFinal 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()); + // Resolve any nested async results in the returned object graph if the query contains async fields + if (res != null && node.HasAsyncFieldsAtOrBelow(fragments) && (!compileContext.ExecutionOptions.ExecuteServiceFieldsSeparately || isSecondExec)) + res = await ResolveAsyncResultsRecursive(res, compileContext.CancellationToken); + return (res, true); } @@ -374,4 +592,363 @@ public void AddDirectives(IEnumerable graphQLDirectives) { Directives.AddRange(graphQLDirectives); } + + /// + /// Recursively walks the object graph and awaits any async values (Task, ValueTask, IAsyncEnumerable) + /// + private static async Task ResolveAsyncResultsRecursive(object obj, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var type = obj.GetType(); + + // Handle Task and internal async state machines - await it and recursively resolve the result + if (obj is Task task) + { + await task; + + // Get the result from Task + var taskType = task.GetType(); + // Try to get Result property + var resultProp = taskType.GetProperty(nameof(Task.Result)); + if (resultProp != null) + { + var taskResult = resultProp.GetValue(task); + return taskResult != null ? await ResolveAsyncResultsRecursive(taskResult, cancellationToken) : null; + } + + return null; // Task (not Task) + } + + // ValueTask + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + var asTaskMethod = type.GetMethod("AsTask", BindingFlags.Public | BindingFlags.Instance); + if (asTaskMethod != null) + { + var valueTaskAsTask = (Task?)asTaskMethod.Invoke(obj, null); + if (valueTaskAsTask != null) + { + await valueTaskAsTask; + var resultProperty = valueTaskAsTask.GetType().GetProperty(nameof(Task.Result)); + var taskResult = resultProperty?.GetValue(valueTaskAsTask); + return taskResult != null ? await ResolveAsyncResultsRecursive(taskResult, cancellationToken) : null; + } + } + return null; + } + + // IAsyncEnumerable - buffer to a list + if (ImplementsIAsyncEnumerable(type)) + { + return await BufferAsyncEnumerable(obj, cancellationToken); + } + + // Handle collections (but not strings) + if (obj is IEnumerable enumerable and not string) + { + var items = enumerable.Cast().ToArray(); + var resolvedItems = new List(items.Length); + + // Process items concurrently + var tasks = items.Select(async item => + { + cancellationToken.ThrowIfCancellationRequested(); + return item != null ? await ResolveAsyncResultsRecursive(item, cancellationToken) : null; + }); + + var results = await Task.WhenAll(tasks); + resolvedItems.AddRange(results); + + // Try to preserve the original collection type + var originalType = obj.GetType(); + + // Handle arrays + if (originalType.IsArray) + { + var elementType = originalType.GetElementType()!; + var array = Array.CreateInstance(elementType, resolvedItems.Count); + for (int i = 0; i < resolvedItems.Count; i++) + { + array.SetValue(resolvedItems[i], i); + } + return array; + } + + return resolvedItems; + } + + // Handle complex objects (including anonymous types and dynamic types) + if (type.IsClass && type != typeof(string) && !type.IsPrimitive) + { + return await ResolveComplexObject(obj, type, cancellationToken); + } + + return obj; + } + + /// + /// Handles complex objects including anonymous types, dynamic types, and regular classes + /// + private static async Task ResolveComplexObject(object obj, Type type, CancellationToken cancellationToken = default) + { + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); + + // For anonymous types and dynamically generated types, try to reconstruct the object + if (IsAnonymousOrDynamicType(type)) + { + // all types should be anonymous types built by LinqRuntimeTypeBuilder + return await RebuildDynamicTypeWithResolvedFields(obj, type, properties, fields, cancellationToken); + } + + // For regular mutable objects, we can modify in place + foreach (var prop in properties.Where(p => p.CanRead && p.CanWrite)) + { + var value = prop.GetValue(obj); + if (value != null && ContainsAsyncValue(value)) + { + var resolvedValue = await ResolveAsyncResultsRecursive(value, cancellationToken); + prop.SetValue(obj, resolvedValue); + } + } + + // Fields are typically readonly, but let's try anyway + foreach (var field in fields.Where(f => !f.IsInitOnly)) + { + var value = field.GetValue(obj); + if (value != null && ContainsAsyncValue(value)) + { + var resolvedValue = await ResolveAsyncResultsRecursive(value, cancellationToken); + field.SetValue(obj, resolvedValue); + } + } + + return obj; + } + + /// + /// Checks if a value is or contains async operations + /// + private static bool ContainsAsyncValue(object? value) + { + if (value == null) + return false; + var t = value.GetType(); + if (typeof(Task).IsAssignableFrom(t)) + return true; + if (t == typeof(ValueTask) || (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ValueTask<>))) + return true; + if (ImplementsIAsyncEnumerable(t)) + return true; + return false; + } + + /// + /// Determines if a type is anonymous or dynamically generated (like those created by EntityGraphQL) + /// + private static bool IsAnonymousOrDynamicType(Type type) + { + return type.Namespace?.StartsWith("EntityGraphQL", StringComparison.Ordinal) == true || type.Assembly.IsDynamic; + } + + /// + /// Rebuilds a dynamic type with resolved field types (converting Task fields to T fields) + /// + private static async Task RebuildDynamicTypeWithResolvedFields(object obj, Type originalType, PropertyInfo[] properties, FieldInfo[] fields, CancellationToken cancellationToken = default) + { + var fieldTypeMap = new Dictionary(); + var fieldValues = new Dictionary(); + + // Process properties + foreach (var prop in properties.Where(p => p.CanRead)) + { + var value = prop.GetValue(obj); + var resolvedValue = value != null ? await ResolveAsyncResultsRecursive(value, cancellationToken) : null; + + fieldValues[prop.Name] = resolvedValue; + // If original type was Task, use T. Otherwise use the resolved value type or original type + fieldTypeMap[prop.Name] = GetResolvedFieldType(prop.PropertyType, resolvedValue); + } + + // Process fields + foreach (var field in fields) + { + var value = field.GetValue(obj); + var resolvedValue = value != null ? await ResolveAsyncResultsRecursive(value, cancellationToken) : null; + + fieldValues[field.Name] = resolvedValue; + // If original type was Task, use T. Otherwise use the resolved value type or original type + fieldTypeMap[field.Name] = GetResolvedFieldType(field.FieldType, resolvedValue); + } + + // Create new dynamic type with resolved field types + var newType = LinqRuntimeTypeBuilder.GetDynamicType(fieldTypeMap, originalType.Name); + var newInstance = Activator.CreateInstance(newType); + + if (newInstance != null) + { + // Set field values on the new instance + foreach (var kvp in fieldValues) + { + var newField = newType.GetField(kvp.Key); + if (newField != null) + { + newField.SetValue(newInstance, kvp.Value); + } + } + } + + return newInstance; + } + + /// + /// Gets the resolved field type - if the original type was Task, returns T, otherwise returns the resolved value type + /// + private static Type GetResolvedFieldType(Type originalType, object? resolvedValue) + { + // If the original type was Task, extract T + if (typeof(Task).IsAssignableFrom(originalType) && originalType.IsGenericType) + { + var taskGenericType = originalType.GetGenericArguments().FirstOrDefault(); + if (taskGenericType != null) + return taskGenericType; + } + // If the original type was ValueTask, extract T + if (originalType.IsGenericType && originalType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + var vtGeneric = originalType.GetGenericArguments().FirstOrDefault(); + if (vtGeneric != null) + return vtGeneric; + } + // If the original type was IAsyncEnumerable, convert to IEnumerable + if (originalType.IsGenericType && originalType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) + { + var t = originalType.GetGenericArguments()[0]; + return typeof(IEnumerable<>).MakeGenericType(t); + } + + // Otherwise use the resolved value's type, or fall back to original type + return resolvedValue?.GetType() ?? originalType; + } + + private static bool ImplementsIAsyncEnumerable(Type type) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) + return true; + return type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)); + } + + internal static async Task BufferAsyncEnumerable(object asyncEnumerableObj, CancellationToken cancellationToken = default) + { + // Use reflection to get the GetAsyncEnumerator method since we don't know the generic type at compile time + var asyncEnumerableType = asyncEnumerableObj.GetType(); + + // Get the element type from the original IAsyncEnumerable first + var elementType = typeof(object); + var asyncEnumerableInterface = asyncEnumerableType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)); + + if (asyncEnumerableInterface != null) + { + elementType = asyncEnumerableInterface.GetGenericArguments()[0]; + } + + // Create the properly typed list upfront + var listType = typeof(List<>).MakeGenericType(elementType); + var typedList = Activator.CreateInstance(listType)!; + var addMethod = listType.GetMethod("Add")!; + + var getEnumeratorMethod = asyncEnumerableType.GetMethod("GetAsyncEnumerator", BindingFlags.Public | BindingFlags.Instance); + + if (getEnumeratorMethod == null) + { + // Try to find it on interfaces + var interfaces = asyncEnumerableType.GetInterfaces(); + foreach (var iface in interfaces) + { + if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) + { + getEnumeratorMethod = iface.GetMethod("GetAsyncEnumerator"); + break; + } + } + } + + if (getEnumeratorMethod != null) + { + var enumerator = getEnumeratorMethod.Invoke(asyncEnumerableObj, [cancellationToken]); + if (enumerator != null) + { + var enumeratorType = enumerator.GetType(); + + // Find the IAsyncEnumerator interface on the enumerator + var asyncEnumeratorInterface = enumeratorType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IAsyncEnumerator<>)); + + MethodInfo? moveNextMethod = null; + PropertyInfo? currentProperty = null; + MethodInfo? disposeMethod = null; + + if (asyncEnumeratorInterface != null) + { + // Get methods from the interface + moveNextMethod = asyncEnumeratorInterface.GetMethod("MoveNextAsync"); + currentProperty = asyncEnumeratorInterface.GetProperty("Current"); + disposeMethod = asyncEnumeratorInterface.GetMethod("DisposeAsync"); + } + else + { + // Fallback: try to get methods directly from the type + moveNextMethod = enumeratorType.GetMethod("MoveNextAsync"); + currentProperty = enumeratorType.GetProperty("Current"); + disposeMethod = enumeratorType.GetMethod("DisposeAsync"); + } + + if (moveNextMethod != null && currentProperty != null) + { + try + { + while (true) + { + var moveNextTask = moveNextMethod.Invoke(enumerator, null); + if (moveNextTask is ValueTask valueTask) + { + var result = await valueTask; + if (!result) + break; + + var current = currentProperty.GetValue(enumerator); + if (current != null) + { + var resolvedCurrent = await ResolveAsyncResultsRecursive(current, cancellationToken); + addMethod.Invoke(typedList, [resolvedCurrent]); + } + } + else + { + break; // Unexpected return type + } + } + } + finally + { + if (disposeMethod != null) + { + var disposeTask = disposeMethod.Invoke(enumerator, null); + if (disposeTask is ValueTask disposeValueTask) + await disposeValueTask; + } + } + } + } + } + + 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 dda79177..81d92182 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLDocument.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLDocument.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Threading; using System.Threading.Tasks; using EntityGraphQL.Schema; -using Microsoft.Extensions.DependencyInjection; namespace EntityGraphQL.Compiler; @@ -55,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; } @@ -70,7 +70,7 @@ public QueryResult ExecuteQuery( ExecutionOptions? options = null ) { - return ExecuteQueryAsync(context, services, variables, operationName, requestContext, options).GetAwaiter().GetResult(); + return ExecuteQueryAsync(context, services, variables, operationName, requestContext, options, CancellationToken.None).GetAwaiter().GetResult(); } /// @@ -90,31 +90,30 @@ public async Task ExecuteQueryAsync( QueryVariables? variables, string? operationName, QueryRequestContext? requestContext, - ExecutionOptions? options = null + ExecutionOptions? options = null, + CancellationToken cancellationToken = default ) { // 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) - ) + 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) @@ -123,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; @@ -141,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 99e521cd..9974918b 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLMutationStatement.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLMutationStatement.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; @@ -20,115 +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 + 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); - foreach (var field in QueryFields) + if (methodValidator?.HasErrors == true) { - 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 c15d970c..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) - ); - - 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 da0d0734..0450eb25 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLQueryStatement.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLQueryStatement.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq.Expressions; using System.Threading.Tasks; @@ -10,29 +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 + 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); + (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 20573d3f..9d392cb6 100644 --- a/src/EntityGraphQL/Compiler/GqlNodes/GraphQLSubscriptionStatement.cs +++ b/src/EntityGraphQL/Compiler/GqlNodes/GraphQLSubscriptionStatement.cs @@ -1,9 +1,9 @@ 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.Directives; using EntityGraphQL.Extensions; @@ -25,81 +25,49 @@ 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 + QueryRequestContext requestContext, + CancellationToken cancellationToken = default ) 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) { - 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); -#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( @@ -107,24 +75,24 @@ QueryRequestContext requestContext TContext context, IServiceProvider? serviceProvider, IArgumentsTracker? docVariables, - ExecutionOptions executionOptions, - QueryRequestContext requestContext + 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); } @@ -137,14 +105,26 @@ QueryRequestContext requestContext { 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 5807c61a..ad6a6fc8 100644 --- a/src/EntityGraphQL/Compiler/Util/CompileHelper.cs +++ b/src/EntityGraphQL/Compiler/Util/CompileHelper.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Threading; using EntityGraphQL.Compiler.Util; using EntityGraphQL.Schema; @@ -15,7 +16,8 @@ public static Expression InjectServices( List allArgs, Expression expression, List parameters, - ParameterReplacer replacer + ParameterReplacer replacer, + CancellationToken cancellationToken = default ) { foreach (var serviceParam in services) @@ -24,13 +26,21 @@ ParameterReplacer replacer // serviceProvider.GetService is used and the rules registered with the service provider are used // e.g. a new instance or a singleton etc. var srvParam = Expression.Parameter(serviceParam.Type, $"exec_{serviceParam.Name}"); - parameters.Add(srvParam); - var service = serviceProvider.GetService(serviceParam.Type) ?? throw new EntityGraphQLExecutionException($"Service {serviceParam.Type.Name} not found in service provider"); - allArgs.Add(service); - expression = replacer.Replace(expression, serviceParam, srvParam); - } + parameters.Add(srvParam); + if (serviceParam.Type == typeof(CancellationToken)) + { + allArgs.Add(cancellationToken); + } + else + { + var service = + serviceProvider.GetService(serviceParam.Type) + ?? throw new EntityGraphQLException(GraphQLErrorCategory.ExecutionError, $"Service {serviceParam.Type.Name} not found in service provider"); + allArgs.Add(service); + } + } return expression; } @@ -45,7 +55,7 @@ public static void ValidateAndReplaceFieldArgs( ParameterReplacer replacer, ref object? argumentValue, ref Expression result, - List validationErrors, + HashSet validationErrors, ParameterExpression? newArgParam ) { @@ -64,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 97ce211c..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,93 +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 - Expression expression = EntityQueryCompiler.CompileWith(query, contextParam, schemaProvider, new QueryRequestContext(null, null), new ExecutionOptions()).ExpressionResult; + contextParam ??= Expression.Parameter(queryType, $"q_{queryType.Name}"); + Expression expression = EntityQueryCompiler + .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 74c39544..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; } @@ -330,4 +330,27 @@ protected override Expression VisitMethodCall(MethodCallExpression node) return Expression.Call(callOn, node.Method, node.Arguments.Select(base.Visit).ToArray()!); } + + protected override Expression VisitConstant(ConstantExpression node) + { + if (node.Type == toReplaceType) + return newParam!; + + if (node.Value is Expression exp) + { + if (exp == toReplace) + return newParam!; + + var newExp = base.Visit(node.Value as Expression); + return Expression.Constant(newExp); + } + + if (node.Value is List enumerable) + { + var newValues = enumerable.Select(item => (ParameterExpression)base.Visit(item)).ToList(); + return Expression.Constant(newValues); + } + + return base.VisitConstant(node); + } } 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 3f9c7615..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.6.2 - 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 c4e5a6d9..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; @@ -33,10 +32,11 @@ public abstract class BaseField : IField public List Services { get; set; } = []; public IReadOnlyCollection> Validators => ArgumentValidators; - [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; } + /// + /// Indicates if this field returns a Task and requires async resolution + /// + public bool IsAsync { get; internal set; } + public Expression? ResolveExpression { get; protected set; } #endregion IField properties @@ -48,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) { @@ -102,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, @@ -138,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}"); @@ -159,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 /// @@ -267,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/ExecutionOptions.cs b/src/EntityGraphQL/Schema/ExecutionOptions.cs index 32c915a2..b7bfc097 100644 --- a/src/EntityGraphQL/Schema/ExecutionOptions.cs +++ b/src/EntityGraphQL/Schema/ExecutionOptions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq.Expressions; namespace EntityGraphQL.Schema; @@ -59,6 +60,21 @@ public class ExecutionOptions /// public bool IncludeQueryInfo { get; set; } + /// + /// Service-level concurrency limits. Key is the service type, value is the max concurrent operations. + /// This provides centralized control over concurrency for all fields using specific services. + /// Useful to help rate limiting or throttling. + /// Example: { [typeof(TmdbService)] = 5, [typeof(DatabaseService)] = 10 } + /// + public Dictionary ServiceConcurrencyLimits { get; set; } = []; + + /// + /// Global query-level concurrency limit. If set, no more than this many async operations + /// will run concurrently across the entire query execution, regardless of service type. + /// This overrides any individual field or service limits. + /// + public int? MaxQueryConcurrency { get; set; } + #if DEBUG /// /// Include timing information about query execution diff --git a/src/EntityGraphQL/Schema/Field.cs b/src/EntityGraphQL/Schema/Field.cs index 83b273f5..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,16 +177,17 @@ 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 - if (FieldParam != null && !contextChanged) + // if (FieldParam != null) + if (FieldParam != null && (!contextChanged || IsAsync)) { 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) @@ -200,7 +201,7 @@ ParameterReplacer replacer Expression fieldExpression, ParameterReplacer replacer, Expression context, - IGraphQLNode? parentNode, + BaseGraphQLField? fieldNode, ParameterExpression? docParam, IArgumentsTracker? docVariables, bool servicesPass, @@ -209,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; @@ -219,52 +220,39 @@ 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); return (result, newArgParam); } - protected void SetUpField(LambdaExpression fieldExpression, bool withServices, bool hasArguments) + 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; @@ -274,9 +262,20 @@ protected void SetUpField(LambdaExpression fieldExpression, bool withServices, b if (fieldExpression.Body.NodeType == ExpressionType.Call) returnType = ((MethodCallExpression)fieldExpression.Body).Type; - if (typeof(Task).IsAssignableFrom(returnType)) - throw new EntityGraphQLCompilerException($"Field '{Name}' is returning a Task please resolve your async method with .GetAwaiter().GetResult()"); + // do the above before unwrapping Task<> + returnType = SchemaBuilder.GetReturnType(returnType, out bool returnsAsync); + if (returnsAsync) + IsAsync = true; + + if (!isAsync && IsAsync) + 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 new file mode 100644 index 00000000..247fd34a --- /dev/null +++ b/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimitFieldExtension.cs @@ -0,0 +1,198 @@ +using System; +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; +using EntityGraphQL.Extensions; + +namespace EntityGraphQL.Schema.FieldExtensions; + +/// +/// Field extension that wraps async field expressions with concurrency control +/// Uses expression wrapping pattern similar to WrapObjectProjectionFieldForNullCheck +/// +public class ConcurrencyLimitFieldExtension : BaseFieldExtension +{ + private readonly int? maxConcurrency; + private readonly List? serviceTypes; + + /// + /// Create a service-based concurrency limit (resolved from ExecutionOptions) + /// + /// The service type to apply limits to + /// Optional override for the service limit + public ConcurrencyLimitFieldExtension(IEnumerable? serviceTypes, int? maxConcurrency = null) + { + this.serviceTypes = serviceTypes?.ToList(); + this.maxConcurrency = maxConcurrency; + } + + public override (Expression? expression, ParameterExpression? originalArgParam, ParameterExpression? newArgParam, object? argumentValue) GetExpressionAndArguments( + IField field, + BaseGraphQLField fieldNode, + Expression expression, + ParameterExpression? argumentParam, + dynamic? arguments, + Expression context, + bool servicesPass, + ParameterReplacer parameterReplacer, + ParameterExpression? originalArgParam, + CompileContext compileContext + ) + { + // Only apply concurrency control during the service pass for async expressions + if (!servicesPass || !IsAsyncExpression(expression)) + { + return (expression, originalArgParam, argumentParam, arguments); + } + + // Generate the semaphore configurations for hierarchical limiting + var semaphoreConfigs = GetSemaphoreConfigs(field, compileContext.ExecutionOptions); + + // Skip if no concurrency limits are configured + if (semaphoreConfigs.Count == 0) + { + return (expression, originalArgParam, argumentParam, arguments); + } + + // Wrap the async expression with hierarchical concurrency control + var newExp = WrapAsyncExpressionWithConcurrencyLimit(field, expression, semaphoreConfigs, compileContext.ConcurrencyLimiterRegistry, compileContext.CancellationToken); + return (newExp, originalArgParam, argumentParam, arguments); + } + + private static bool IsAsyncExpression(Expression expression) => expression.Type.IsAsyncGenericType(); + + /// + /// Wraps an async field expression with hierarchical concurrency limiting + /// Similar to ExpressionUtil.WrapObjectProjectionFieldForNullCheck + /// + private static MethodCallExpression WrapAsyncExpressionWithConcurrencyLimit( + IField field, + Expression asyncExpression, + List<(string scopeKey, int maxConcurrency)> semaphoreConfigs, + ConcurrencyLimiterRegistry concurrencyLimiterRegistry, + CancellationToken cancellationToken + ) + { + List expArgs = [field.FieldParam!, .. field.Services]; + Expression asyncExpressionExp = Expression.Lambda(asyncExpression, expArgs); + + // Create an array expression containing the dynamic arguments + var expArgsArray = Expression.NewArrayInit(typeof(object), expArgs.Cast()); + + // Convert semaphore configs to a constant expression + var semaphoreConfigsConstant = Expression.Constant(semaphoreConfigs); + + Expression[] arguments = [asyncExpressionExp, semaphoreConfigsConstant, Expression.Constant(concurrencyLimiterRegistry), expArgsArray, Expression.Constant(cancellationToken)]; + + var call = Expression.Call(typeof(ConcurrencyLimitFieldExtension), nameof(ExecuteWithConcurrencyLimitAsync), null, arguments); + return call; + } + + private List<(string scopeKey, int maxConcurrency)> GetSemaphoreConfigs(IField field, ExecutionOptions executionOptions) + { + var configs = new List<(string scopeKey, int maxConcurrency)>(); + + // 1. Global query limit (if specified) + var globalLimit = executionOptions.MaxQueryConcurrency ?? 0; + if (globalLimit > 0) + { + configs.Add(("global_query", globalLimit)); + } + + // 2. Service-specific limit (if specified) + if (serviceTypes != null) + { + foreach (var serviceType in serviceTypes) + { + var serviceLimit = executionOptions.ServiceConcurrencyLimits.GetValueOrDefault(serviceType); + if (serviceLimit > 0) + { + configs.Add(($"service_{serviceType.AssemblyQualifiedName}", serviceLimit)); + } + } + } + + // 3. Field-specific limit (if specified) + if (maxConcurrency.HasValue) + { + configs.Add(($"field_{field.FromType.Name}.{field.Name}_{maxConcurrency}", maxConcurrency.Value)); + } + + return configs; + } + + /// + /// Runtime execution method that applies hierarchical concurrency limiting to async operations + /// This gets called during expression execution, not compilation + /// + public static async Task ExecuteWithConcurrencyLimitAsync( + LambdaExpression asyncOperationExp, + List<(string scopeKey, int maxConcurrency)> semaphoreConfigs, + ConcurrencyLimiterRegistry concurrencyLimiterRegistry, + object[] expArgs, + CancellationToken cancellationToken + ) + { + // Get all semaphores for hierarchical limiting + var semaphores = semaphoreConfigs.Select(config => concurrencyLimiterRegistry.GetSemaphore(config.scopeKey, config.maxConcurrency)).ToList(); + + // Acquire all semaphores in order (query -> service -> field) + foreach (var semaphore in semaphores) + { + await semaphore.WaitAsync(cancellationToken); + } + + try + { + // Execute the async operation + var asyncOperation = asyncOperationExp.Compile().DynamicInvoke(expArgs); + if (asyncOperation is null) + { + return null; + } + if (asyncOperation is Task task) + { + await task; + + // Get the result from Task + var taskType = task.GetType(); + if (taskType.IsGenericType) + { + var resultProperty = taskType.GetProperty(nameof(Task.Result)); + return resultProperty?.GetValue(task); + } + return null; + } + // Handle ValueTask + var type = asyncOperation.GetType(); + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + var asTaskMethod = type.GetMethod(nameof(ValueTask.AsTask)); + if (asTaskMethod != null) + { + var taskToAwait = (Task?)asTaskMethod.Invoke(asyncOperation, null); + if (taskToAwait != null) + { + await taskToAwait; + var resultProperty = taskToAwait.GetType().GetProperty(nameof(ValueTask.Result)); + return resultProperty?.GetValue(taskToAwait); + } + } + } + + return asyncOperation; // Not async, return as-is + } + finally + { + // Release all semaphores in reverse order (field -> service -> query) + for (int i = semaphores.Count - 1; i >= 0; i--) + { + semaphores[i].Release(); + } + } + } +} diff --git a/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimiterRegistry.cs b/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimiterRegistry.cs new file mode 100644 index 00000000..98c0dde9 --- /dev/null +++ b/src/EntityGraphQL/Schema/FieldExtensions/AsyncFields/ConcurrencyLimiterRegistry.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; + +namespace EntityGraphQL.Schema.FieldExtensions; + +/// +/// Per-request registry for semaphores used in concurrency limiting +/// Request-scoped or application-scoped depending on your DI setup +/// +public class ConcurrencyLimiterRegistry +{ + private readonly ConcurrentDictionary semaphores = new(); + + public SemaphoreSlim GetSemaphore(string key, int maxConcurrency) + { + return semaphores.GetOrAdd(key, _ => new SemaphoreSlim(maxConcurrency, maxConcurrency)); + } + + /// + /// Call this at the end of request processing to clean up per-request semaphores + /// + public void ClearRequestSemaphores() + { + foreach (var kvp in semaphores.ToList()) + { + if (kvp.Key.StartsWith("field_", StringComparison.CurrentCultureIgnoreCase) || kvp.Key.StartsWith("query_", StringComparison.CurrentCultureIgnoreCase)) + { + if (semaphores.TryRemove(kvp.Key, out var semaphore)) + { + semaphore.Dispose(); + } + } + } + } + + /// + /// For testing or application shutdown + /// + public void ClearAllSemaphores() + { + foreach (var semaphore in semaphores.Values) + { + semaphore.Dispose(); + } + semaphores.Clear(); + } +} 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 e663475e..1f273bba 100644 --- a/src/EntityGraphQL/Schema/FieldToResolve.cs +++ b/src/EntityGraphQL/Schema/FieldToResolve.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Threading.Tasks; using EntityGraphQL.Compiler; using EntityGraphQL.Compiler.Util; +using EntityGraphQL.Schema.FieldExtensions; namespace EntityGraphQL.Schema; @@ -23,7 +25,29 @@ public Field ResolveBulk(Expression new GraphQLExtractedField(Schema, i.Key, i.Value, keyParam))!; ExtractedFieldsFromServices!.AddRange(fields); - BulkResolver = new BulkFieldResolverWithArgs($"bulk_{Name}", fieldExpression, dataSelector, fields); + BulkResolver = new BulkFieldResolverWithArgs($"bulk_{FromType.Name}.{Name}", fieldExpression, dataSelector, fields); + Services.Add(fieldExpression.Parameters[2]); + return this; + } + + /// + /// Add async bulk resolver for this field with optional concurrency limiting + /// + /// Expression to select keys for bulk loading + /// Async expression to resolve bulk data + /// Maximum number of concurrent bulk operations + /// The field with async bulk resolver configured + public Field ResolveBulkAsync( + Expression> dataSelector, + Expression, TParams, TService, Task>>> fieldExpression, + int? maxConcurrency = null + ) + { + var extractor = new ExpressionExtractor(); + var keyParam = dataSelector.Parameters.First(); + var fields = extractor.Extract(dataSelector, keyParam, false)?.Select(i => new GraphQLExtractedField(Schema, i.Key, i.Value, keyParam))!; + ExtractedFieldsFromServices!.AddRange(fields); + BulkResolver = new AsyncBulkFieldResolverWithArgs($"bulk_{FromType.Name}.{Name}", fieldExpression, dataSelector, fields, maxConcurrency); Services.Add(fieldExpression.Parameters[2]); return this; } @@ -44,7 +68,29 @@ public Field ResolveBulk(Expression new GraphQLExtractedField(Schema, i.Key, i.Value, keyParam))!; ExtractedFieldsFromServices!.AddRange(fields); - BulkResolver = new BulkFieldResolver($"bulk_{Name}", fieldExpression, dataSelector, fields); + BulkResolver = new BulkFieldResolver($"bulk_{FromType.Name}.{Name}", fieldExpression, dataSelector, fields); + Services.Add(fieldExpression.Parameters[1]); + return this; + } + + /// + /// Add async bulk resolver for this field with optional concurrency limiting + /// + /// Expression to select keys for bulk loading + /// Async expression to resolve bulk data + /// Maximum number of concurrent bulk operations + /// The field with async bulk resolver configured + public Field ResolveBulkAsync( + Expression> dataSelector, + Expression, TService, Task>>> fieldExpression, + int? maxConcurrency = null + ) + { + var extractor = new ExpressionExtractor(); + var keyParam = dataSelector.Parameters.First(); + var fields = extractor.Extract(dataSelector, keyParam, false)?.Select(i => new GraphQLExtractedField(Schema, i.Key, i.Value, keyParam))!; + ExtractedFieldsFromServices!.AddRange(fields); + BulkResolver = new AsyncBulkFieldResolver($"bulk_{FromType.Name}.{Name}", fieldExpression, dataSelector, fields, maxConcurrency); Services.Add(fieldExpression.Parameters[1]); return this; } @@ -60,72 +106,137 @@ 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); + SetUpField(fieldExpression, true, true, false); return this; } - public FieldWithContextAndArgs Resolve(Expression> fieldExpression) + public FieldWithContextAndArgs Resolve(Expression> fieldExpression) { - SetUpField(fieldExpression, true, true); + 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); + 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); + 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); + SetUpField(fieldExpression, true, true, false); Services = [fieldExpression.Parameters[2], fieldExpression.Parameters[3], fieldExpression.Parameters[4], fieldExpression.Parameters[5]]; return this; } - [Obsolete("Use Resolve")] - public FieldWithContextAndArgs ResolveWithServices( - Expression> fieldExpression - ) => Resolve(fieldExpression); - public FieldWithContextAndArgs Resolve( - Expression> fieldExpression + Expression> fieldExpression ) { - SetUpField(fieldExpression, true, true); + SetUpField(fieldExpression, true, true, false); Services = [fieldExpression.Parameters[2], fieldExpression.Parameters[3], fieldExpression.Parameters[4], fieldExpression.Parameters[5], fieldExpression.Parameters[6]]; return this; } - [Obsolete("Use Resolve")] - public FieldWithContextAndArgs ResolveWithServices( - Expression> fieldExpression - ) => Resolve(fieldExpression); + public FieldWithContextAndArgs ResolveAsync(Expression> fieldExpression, int? maxConcurrency = null) => + ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync(Expression>> fieldExpression, int? maxConcurrency = null) => + ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync(Expression> fieldExpression, int? maxConcurrency = null) => + ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContextAndArgs ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + private FieldWithContextAndArgs ResolveAsyncImpl(LambdaExpression fieldExpression, int? maxConcurrency) + { + 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 EntityGraphQLSchemaException("Async field expression must return Task not Task as the field needs a result."); + } + + // Add concurrency limiting + Extensions.Add(new ConcurrencyLimitFieldExtension(serviceTypes, maxConcurrency)); + + return this; + } } /// @@ -139,64 +250,131 @@ public FieldToResolve(ISchemaProvider schema, ISchemaType fromType, string name, public Field Resolve(Expression> fieldExpression) { - SetUpField(fieldExpression, false, false); + SetUpField(fieldExpression, false, false, false); return this; } public FieldWithContext Resolve(Expression> fieldExpression) { - SetUpField(fieldExpression, true, false); + SetUpField(fieldExpression, true, false, false); Services = [fieldExpression.Parameters[1]]; return this; } - [Obsolete("Use Resolve")] - public FieldWithContext ResolveWithService(Expression> fieldExpression) => Resolve(fieldExpression); - public FieldWithContext Resolve(Expression> fieldExpression) { - SetUpField(fieldExpression, true, false); + SetUpField(fieldExpression, true, false, false); Services = [fieldExpression.Parameters[1], fieldExpression.Parameters[2]]; return this; } - [Obsolete("Use Resolve")] - public FieldWithContext ResolveWithServices(Expression> fieldExpression) => Resolve(fieldExpression); - public FieldWithContext Resolve(Expression> fieldExpression) { - SetUpField(fieldExpression, true, false); + SetUpField(fieldExpression, true, false, false); Services = [fieldExpression.Parameters[1], fieldExpression.Parameters[2], fieldExpression.Parameters[3]]; return this; } - [Obsolete("Use Resolve")] - public FieldWithContext ResolveWithServices(Expression> fieldExpression) => - Resolve(fieldExpression); - public FieldWithContext Resolve(Expression> fieldExpression) { - SetUpField(fieldExpression, true, false); + SetUpField(fieldExpression, true, false, false); Services = [fieldExpression.Parameters[1], fieldExpression.Parameters[2], fieldExpression.Parameters[3], fieldExpression.Parameters[4]]; return this; } - [Obsolete("Use Resolve")] - public FieldWithContext ResolveWithServices( - Expression> fieldExpression - ) => Resolve(fieldExpression); - public FieldWithContext Resolve( Expression> fieldExpression ) { - SetUpField(fieldExpression, true, false); + SetUpField(fieldExpression, true, false, false); Services = [fieldExpression.Parameters[1], fieldExpression.Parameters[2], fieldExpression.Parameters[3], fieldExpression.Parameters[4], fieldExpression.Parameters[5]]; return this; } - [Obsolete("Use Resolve")] - public FieldWithContext ResolveWithServices( - Expression> fieldExpression - ) => Resolve(fieldExpression); + /// + /// Resolve an async field with optional concurrency limiting + /// + /// The async resolver function + /// Maximum number of concurrent operations for this field + /// The resolved field with concurrency limiting applied + public FieldWithContext ResolveAsync(Expression> fieldExpression, int? maxConcurrency = null) => + ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync(Expression>> fieldExpression, int? maxConcurrency = null) => + ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync(Expression>> fieldExpression, int? maxConcurrency = null) => + ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync(Expression> fieldExpression, int? maxConcurrency = null) => + ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync(Expression>> fieldExpression, int? maxConcurrency = null) => + ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync(Expression> fieldExpression, int? maxConcurrency = null) => + ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync( + Expression> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync( + Expression> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + public FieldWithContext ResolveAsync( + Expression>> fieldExpression, + int? maxConcurrency = null + ) => ResolveAsyncImpl(fieldExpression, maxConcurrency); + + private FieldWithContext ResolveAsyncImpl(LambdaExpression fieldExpression, int? maxConcurrency) + { + SetUpField(fieldExpression, true, false, true); + Services = [.. fieldExpression.Parameters.Skip(1)]; + 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 EntityGraphQLSchemaException("Async field expression must return Task not Task as the field needs a result."); + } + + // Add concurrency limiting + Extensions.Add(new ConcurrencyLimitFieldExtension(serviceTypes, maxConcurrency)); + + return this; + } } 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 b421f8ca..54983c4e 100644 --- a/src/EntityGraphQL/Schema/IField.cs +++ b/src/EntityGraphQL/Schema/IField.cs @@ -76,12 +76,13 @@ public interface IField /// Services required to be injected for this fields selection /// List Services { get; } - 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; } + /// + /// Indicates if this field returns a Task and requires async resolution + /// + bool IsAsync { get; } + + IReadOnlyCollection> Validators { 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. @@ -90,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, @@ -112,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 6db72bd2..dd3234fd 100644 --- a/src/EntityGraphQL/Schema/ISchemaProvider.cs +++ b/src/EntityGraphQL/Schema/ISchemaProvider.cs @@ -1,9 +1,31 @@ using System; using System.Collections.Generic; +using EntityGraphQL.Compiler.EntityQuery; using EntityGraphQL.Directives; 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 @@ -20,7 +42,17 @@ 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); @@ -49,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); @@ -80,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 b5aabe57..75f5cd45 100644 --- a/src/EntityGraphQL/Schema/MethodField.cs +++ b/src/EntityGraphQL/Schema/MethodField.cs @@ -21,7 +21,7 @@ public abstract class MethodField : BaseField { public override GraphQLQueryFieldType FieldType { get; } protected MethodInfo Method { get; set; } - public bool IsAsync { get; protected set; } + public new bool IsAsync { get; protected set; } public MethodField( ISchemaProvider schema, @@ -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 3a494d7f..dc7bd841 100644 --- a/src/EntityGraphQL/Schema/SchemaIntrospection.cs +++ b/src/EntityGraphQL/Schema/SchemaIntrospection.cs @@ -7,7 +7,6 @@ using EntityGraphQL.Directives; #endif - namespace EntityGraphQL.Schema; public static class SchemaIntrospection @@ -29,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) ); @@ -117,10 +116,6 @@ private static List BuildInputTypes(ISchemaProvider schema) if (field.ResolveExpression?.NodeType == System.Linq.Expressions.ExpressionType.Call) continue; - // Skipping ENUM type - if (field.ReturnType.TypeDotnet.IsEnum) - continue; - inputValues.Add(new InputValue(field.Name, BuildType(schema, field.ReturnType, field.ReturnType.TypeDotnet, true)) { Description = field.Description }); } @@ -208,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 327a3b2d..7ca9c1fa 100644 --- a/src/EntityGraphQL/Schema/SchemaProvider.cs +++ b/src/EntityGraphQL/Schema/SchemaProvider.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Reflection; using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; using EntityGraphQL.Compiler; +using EntityGraphQL.Compiler.EntityQuery; using EntityGraphQL.Compiler.Util; using EntityGraphQL.Directives; using EntityGraphQL.Extensions; @@ -28,6 +30,7 @@ public class SchemaProvider : ISchemaProvider, IDisposable public Type SubscriptionType => subscriptionType.SchemaType.TypeDotnet; public Func SchemaFieldNamer { get; } public IGqlAuthorizationService AuthorizationService { get; set; } + public EqlMethodProvider MethodProvider { get; set; } private readonly Dictionary schemaTypes = []; private readonly Dictionary directives = []; private readonly QueryCache queryCache; @@ -36,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; } = []; @@ -67,9 +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(); @@ -83,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 @@ -156,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 + /// + /// 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) + { + 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 /// - /// - public void AddCustomTypeConverter(ICustomTypeConverter typeConverter) + /// 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) { - TypeConverters.Add(typeConverter.Type, typeConverter); + 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) @@ -171,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"); } @@ -207,7 +411,21 @@ public QueryResult ExecuteRequest(QueryRequest gql, IServiceProvider serviceProv /// public async Task ExecuteRequestAsync(QueryRequest gql, IServiceProvider serviceProvider, ClaimsPrincipal? user, ExecutionOptions? options = null) { - return await DoExecuteRequestAsync(gql, default, serviceProvider, user, options); + return await DoExecuteRequestAsync(gql, default, serviceProvider, user, options, CancellationToken.None); + } + + /// + /// Execute a query using this schema with cancellation support. + /// + /// The query + /// A service provider used for looking up dependencies of field selections and mutations + /// ClaimsPrincipal user to check access against + /// Execution options + /// Cancellation token for async operations + /// + public async Task ExecuteRequestAsync(QueryRequest gql, IServiceProvider serviceProvider, ClaimsPrincipal? user, ExecutionOptions? options, CancellationToken cancellationToken) + { + return await DoExecuteRequestAsync(gql, default, serviceProvider, user, options, cancellationToken); } /// @@ -235,10 +453,17 @@ public QueryResult ExecuteRequestWithContext(QueryRequest gql, TContextType cont /// public async Task ExecuteRequestWithContextAsync(QueryRequest gql, TContextType context, IServiceProvider? serviceProvider, ClaimsPrincipal? user, ExecutionOptions? options = null) { - return await DoExecuteRequestAsync(gql, context, serviceProvider, user, options); + return await DoExecuteRequestAsync(gql, context, serviceProvider, user, options, CancellationToken.None); } - private async Task DoExecuteRequestAsync(QueryRequest gql, TContextType? overwriteContext, IServiceProvider? serviceProvider, ClaimsPrincipal? user, ExecutionOptions? options) + private async Task DoExecuteRequestAsync( + QueryRequest gql, + TContextType? overwriteContext, + IServiceProvider? serviceProvider, + ClaimsPrincipal? user, + ExecutionOptions? options, + CancellationToken cancellationToken = default + ) { QueryResult result; try @@ -247,23 +472,23 @@ private async Task DoExecuteRequestAsync(QueryRequest gql, TContext 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); } } @@ -274,7 +499,7 @@ private async Task DoExecuteRequestAsync(QueryRequest gql, TContext if (options.EnableQueryCache) compiledQuery = CompileQueryWithCache(gql, options); else - compiledQuery = graphQLCompiler.Compile(gql); + compiledQuery = GraphQLParser.Parse(gql, this); } } else if (options.EnableQueryCache) @@ -286,23 +511,29 @@ private async Task DoExecuteRequestAsync(QueryRequest gql, TContext // 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(overwriteContext, serviceProvider, gql.Variables, gql.OperationName, new QueryRequestContext(AuthorizationService, user), options); + result = await compiledQuery.ExecuteQueryAsync( + overwriteContext, + serviceProvider, + gql.Variables, + gql.OperationName, + new QueryRequestContext(AuthorizationService, user), + options, + cancellationToken + ); } catch (Exception ex) { - result = HandleException(ex); + return HandleException(ex); } return result; @@ -314,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; @@ -347,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); } @@ -534,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. @@ -554,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(); @@ -595,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; } @@ -630,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); @@ -891,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 @@ -904,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"); } /// @@ -925,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); } @@ -983,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 da17c947..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,9 +368,24 @@ 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().SelectMany(s => s.GetTypes()).Where(p => TypeDotnet.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract); + var types = AppDomain + .CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic) // Skip dynamic assemblies to avoid race conditions with runtime type generation and we shouldn't need them + .SelectMany(s => + { + try + { + return s.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + // Return the types that loaded successfully, filtering out nulls + return ex.Types.OfType(); + } + }) + .Where(p => TypeDotnet.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract); foreach (var type in types) { @@ -428,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); @@ -437,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; @@ -454,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 new file mode 100644 index 00000000..7df352b8 --- /dev/null +++ b/src/tests/EntityGraphQL.EF.Tests/EqlMethodProviderEFMethodsTests.cs @@ -0,0 +1,215 @@ +using System.Linq.Expressions; +using EntityGraphQL.Compiler.EntityQuery; +using EntityGraphQL.Schema; +using EntityGraphQL.Schema.FieldExtensions; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace EntityGraphQL.EF.Tests; + +public class EqlMethodProviderEFMethodsTests +{ + [Fact] + public void RegisterEFMethod_ShouldAllowUsingEFFunctionsInFilter() + { + var schema = SchemaBuilder.FromObject(); + RegisterEFLike(schema.MethodProvider); + schema.Query().ReplaceField("movies", db => db.Movies, "Get all movies").UseFilter(); + + using var factory = new TestDbContextFactory(); + var data = factory.CreateContext(); + + // Add test data + var movie1 = new Movie("The Matrix") { Id = 1, Released = new DateTime(1999, 3, 31) }; + var movie2 = new Movie("Matrix Reloaded") { Id = 2, Released = new DateTime(2003, 5, 15) }; + var movie3 = new Movie("Inception") { Id = 3, Released = new DateTime(2010, 7, 16) }; + + data.Movies.AddRange(movie1, movie2, movie3); + data.SaveChanges(); + + var gql = new QueryRequest + { + Query = + @"query { + movies(filter: ""name.like(\""Matrix%\""))"") { + id + name + } + }", + }; + + // Act + var result = schema.ExecuteRequestWithContext(gql, data, null, null); + + // Assert + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + + dynamic movies = ((IDictionary)result.Data!)["movies"]!; + var moviesList = movies as IEnumerable; + + // The EF.Functions.Like method is working - let's verify at least one movie is found + Assert.True(moviesList!.Count() >= 1); // Should find at least one Matrix movie + + // Verify we found at least one movie containing "Matrix" + var movieArray = moviesList!.ToArray(); + Assert.Contains(movieArray, m => ((dynamic)m).name.ToString().Contains("Matrix")); + } + + [Fact] + public void RegisterMultipleEFMethods_ShouldAllowUsingEFFunctionsWithBuiltInMethods() + { + var schema = SchemaBuilder.FromObject(); + RegisterEFLike(schema.MethodProvider); + schema.Query().ReplaceField("actors", db => db.Actors, "Get all actors").UseFilter(); + + using var factory = new TestDbContextFactory(); + var data = factory.CreateContext(); + + var actor1 = new Actor("John Doe") { Id = 1, Birthday = new DateTime(1980, 1, 1) }; + var actor2 = new Actor("jane smith") { Id = 2, Birthday = new DateTime(1985, 5, 15) }; + var actor3 = new Actor("BOB WILSON") { Id = 3, Birthday = new DateTime(1975, 12, 25) }; + + data.Actors.AddRange(actor1, actor2, actor3); + data.SaveChanges(); + + var gql = new QueryRequest + { + Query = + @"query { + actors(filter: ""name.like(\""J%\"") && name.contains(\""o\""))"") { + id + name + } + }", + }; + + // Act + var result = schema.ExecuteRequestWithContext(gql, data, null, null); + + // Assert + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + + dynamic actors = ((IDictionary)result.Data!)["actors"]!; + var actorsList = actors as IEnumerable; + + // Should find actors that start with 'J' AND contain 'o' - demonstrates EF + built-in methods working together + Assert.True(actorsList!.Count() >= 1); + + var actorArray = actorsList!.ToArray(); + Assert.Contains(actorArray, a => ((dynamic)a).name == "John Doe"); + } + + [Fact] + public void RegisterEFMethod_ShouldWorkWithDateTimeFunctions() + { + var schema = SchemaBuilder.FromObject(); + RegisterEFDatePart(schema.MethodProvider); + schema.Query().ReplaceField("movies", db => db.Movies, "Get all movies").UseFilter(); + + using var factory = new TestDbContextFactory(); + var data = factory.CreateContext(); + + var movie1 = new Movie("Movie 1999") { Id = 1, Released = new DateTime(1999, 3, 31) }; + var movie2 = new Movie("Movie 2010") { Id = 2, Released = new DateTime(2010, 7, 16) }; + var movie3 = new Movie("Movie 1999-2") { Id = 3, Released = new DateTime(1999, 12, 25) }; + + data.Movies.AddRange(movie1, movie2, movie3); + data.SaveChanges(); + + var gql = new QueryRequest + { + Query = + @"query { + movies(filter: ""released.datePart(\""year\"") == 1999"") { + id + name + } + }", + }; + + // Act + var result = schema.ExecuteRequestWithContext(gql, data, null, null); + + // Assert + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + + dynamic movies = ((IDictionary)result.Data!)["movies"]!; + var moviesList = movies as IEnumerable; + Assert.Equal(2, moviesList!.Count()); // Should find both 1999 movies + } + + [Fact] + public void EqlMethodProvider_ShouldCoexistWithDefaultMethods() + { + var schema = SchemaBuilder.FromObject(); + RegisterEFLike(schema.MethodProvider); + schema.Query().ReplaceField("actors", db => db.Actors, "Get all actors").UseFilter(); + + using var factory = new TestDbContextFactory(); + var data = factory.CreateContext(); + + var actor1 = new Actor("Alice Cooper") { Id = 1, Birthday = new DateTime(1980, 1, 1) }; + var actor2 = new Actor("Bob Dylan") { Id = 2, Birthday = new DateTime(1985, 5, 15) }; + + data.Actors.AddRange(actor1, actor2); + data.SaveChanges(); + + var gql = new QueryRequest + { + Query = + @"query { + actors(filter: ""name.like(\""A%\"") && name.contains(\""Cooper\""))"") { + id + name + } + }", + }; + + // Act + var result = schema.ExecuteRequestWithContext(gql, data, null, null); + + // Assert + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + + dynamic actors = ((IDictionary)result.Data!)["actors"]!; + var actorsList = actors as IEnumerable; + Assert.Single(actorsList!); // Should find "Alice Cooper" + + var actor = actorsList!.First(); + Assert.Equal("Alice Cooper", ((dynamic)actor).name); + } + + /// + /// Helper method to register EF.Functions.Like with the EqlMethodProvider. + /// This demonstrates the simple approach using the EF registration helper. + /// + private static void RegisterEFLike(EqlMethodProvider provider) + { + // Using the EF helper - this is now much simpler! + var likeMethod = typeof(DbFunctionsExtensions).GetMethod(nameof(DbFunctionsExtensions.Like), [typeof(DbFunctions), typeof(string), typeof(string)])!; + var extraArgs = Expression.Property(null, typeof(Microsoft.EntityFrameworkCore.EF), nameof(Microsoft.EntityFrameworkCore.EF.Functions)); + provider.RegisterMethod(likeMethod, typeof(string), "like", [extraArgs]); + } + + /// + /// Helper method to register a simplified date part function for testing + /// + private static void RegisterEFDatePart(EqlMethodProvider provider) + { + provider.RegisterMethod( + methodContextType: typeof(DateTime), + filterMethodName: "datePart", + makeCallFunc: (context, argContext, methodName, args) => + { + if (args.Length != 1) + 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.EF.Tests/Services.cs b/src/tests/EntityGraphQL.EF.Tests/Services.cs index 6574dce2..5ee7aeb8 100644 --- a/src/tests/EntityGraphQL.EF.Tests/Services.cs +++ b/src/tests/EntityGraphQL.EF.Tests/Services.cs @@ -54,3 +54,17 @@ public ProjectConfig[] GetList(int count, int from = 0) return configs.ToArray(); } } + +public class CancellationTestService +{ + public async Task GetAgeWithDelayAsync(DateTime? birthday, CancellationToken cancellationToken) + { + // Simulate some async work + await Task.Delay(10, cancellationToken); + + // Check for cancellation + cancellationToken.ThrowIfCancellationRequested(); + + return birthday.HasValue ? (int)(DateTime.Now - birthday.Value).TotalDays / 365 : 0; + } +} 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/ConcurrencyTests.cs b/src/tests/EntityGraphQL.Tests/ConcurrencyTests.cs new file mode 100644 index 00000000..513e9346 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/ConcurrencyTests.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EntityGraphQL.Schema; +using EntityGraphQL.Schema.FieldExtensions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class ConcurrencyTests +{ + [Fact] + public void TestFieldConcurrencyLimitExtension() + { + var numMovies = 10; + var schema = SchemaBuilder.FromObject(); + var data = new TestMovieContext + { + // Create multiple movies to trigger multiple concurrent operations + Movies = Enumerable.Range(0, numMovies).Select(i => new Movie { Id = Guid.NewGuid(), Name = $"Movie {i}" }).ToList(), + }; + + // Add field with concurrency limit and use TimedSlowService to track concurrency + var timedService = new TimedSlowService(); + var field = schema.Type().AddField("slowOperation", "Slow operation").ResolveAsync((movie, service) => service.DoTimedWorkAsync(movie.Id), maxConcurrency: 2); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(timedService); + + var query = new QueryRequest { Query = "{ movies { name slowOperation } }" }; + + var result = schema.ExecuteRequestWithContext(query, data, serviceCollection.BuildServiceProvider(), null); + Assert.Null(result.Errors); + + // Verify all operations completed + dynamic moviesResult = result.Data!["movies"]!; + Assert.Equal(numMovies, Enumerable.Count(moviesResult)); + + // Verify each movie has the expected result + for (int i = 0; i < numMovies; i++) + { + Assert.Equal($"Movie {i}", moviesResult[i].name); + Assert.StartsWith("Timed result", moviesResult[i].slowOperation); + } + + // Most importantly: Verify max concurrent operations never exceeded 2 + Assert.True(timedService.MaxConcurrentOperations <= 2, $"Expected max 2 concurrent operations, but saw {timedService.MaxConcurrentOperations}"); + + // Also verify that we actually had some concurrency (more than 1) + Assert.True(timedService.MaxConcurrentOperations > 1, $"Expected some concurrency, but max was only {timedService.MaxConcurrentOperations}"); + } + + [Fact] + public void TestServiceConcurrencyLimit() + { + var numMovies = 10; + var schema = SchemaBuilder.FromObject(); + var data = new TestMovieContext + { + // Create multiple movies to trigger multiple concurrent operations + Movies = Enumerable.Range(0, numMovies).Select(i => new Movie { Id = Guid.NewGuid(), Name = $"Movie {i}" }).ToList(), + }; + + // Add field that uses the service extension constructor (without field-specific limit) + var timedService = new TimedSlowService(); + var field = schema.Type().AddField("slowOperation", "Slow operation"); + + // Set up the field resolver + field.ResolveAsync((movie, service) => service.DoTimedWorkAsync(movie.Id)); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(timedService); + + // Create execution options with service-level concurrency limit + var executionOptions = new ExecutionOptions { ServiceConcurrencyLimits = { [typeof(TimedSlowService)] = 3 } }; + + var query = new QueryRequest { Query = "{ movies { name slowOperation } }" }; + + var result = schema.ExecuteRequestWithContext(query, data, serviceCollection.BuildServiceProvider(), null, executionOptions); + Assert.Null(result.Errors); + + // Verify all operations completed + dynamic moviesResult = result.Data!["movies"]!; + Assert.Equal(numMovies, Enumerable.Count(moviesResult)); + + // Verify each movie has the expected result + for (int i = 0; i < numMovies; i++) + { + Assert.Equal($"Movie {i}", moviesResult[i].name); + Assert.StartsWith("Timed result", moviesResult[i].slowOperation); + } + + // Most importantly: Verify max concurrent operations never exceeded 3 (the service limit) + Assert.True(timedService.MaxConcurrentOperations <= 3, $"Expected max 3 concurrent operations, but saw {timedService.MaxConcurrentOperations}"); + + // Also verify that we actually had some concurrency (more than 1) + Assert.True(timedService.MaxConcurrentOperations > 1, $"Expected some concurrency, but max was only {timedService.MaxConcurrentOperations}"); + } + + [Fact] + public void TestMaxQueryConcurrencyViaExecutionOptions() + { + var numMovies = 10; + var schema = SchemaBuilder.FromObject(); + var data = new TestMovieContext + { + // Create multiple movies to trigger multiple concurrent operations + Movies = Enumerable.Range(0, numMovies).Select(i => new Movie { Id = Guid.NewGuid(), Name = $"Movie {i}" }).ToList(), + }; + + // Add field that uses global concurrency extension (no specific service or field limits) + var timedService = new TimedSlowService(); + var field = schema.Type().AddField("slowOperation", "Slow operation"); + + // Set up the field resolver + field.ResolveAsync((movie, service) => service.DoTimedWorkAsync(movie.Id)); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(timedService); + + // Create execution options with global query-level concurrency limit + var executionOptions = new ExecutionOptions { MaxQueryConcurrency = 4 }; + + var query = new QueryRequest { Query = "{ movies { name slowOperation } }" }; + + var result = schema.ExecuteRequestWithContext(query, data, serviceCollection.BuildServiceProvider(), null, executionOptions); + Assert.Null(result.Errors); + + // Verify all operations completed + dynamic moviesResult = result.Data!["movies"]!; + Assert.Equal(numMovies, Enumerable.Count(moviesResult)); + + // Verify each movie has the expected result + for (int i = 0; i < numMovies; i++) + { + Assert.Equal($"Movie {i}", moviesResult[i].name); + Assert.StartsWith("Timed result", moviesResult[i].slowOperation); + } + + // Most importantly: Verify max concurrent operations never exceeded 4 (the global query limit) + Assert.True(timedService.MaxConcurrentOperations <= 4, $"Expected max 4 concurrent operations, but saw {timedService.MaxConcurrentOperations}"); + + // Also verify that we actually had some concurrency (more than 1) + Assert.True(timedService.MaxConcurrentOperations > 1, $"Expected some concurrency, but max was only {timedService.MaxConcurrentOperations}"); + } + + [Theory] + [InlineData(2, 5, 8, 2, "query")] + [InlineData(10, 3, 7, 3, "service")] + [InlineData(8, 6, 4, 4, "field")] + private void TestConcurrencyScenario(int queryLimit, int serviceLimit, int fieldLimit, int expectedMax, string mostRestrictiveLevel) + { + var numMovies = 12; // More than any limit to ensure we hit the restrictions + var schema = SchemaBuilder.FromObject(); + var data = new TestMovieContext { Movies = Enumerable.Range(0, numMovies).Select(i => new Movie { Id = Guid.NewGuid(), Name = $"Movie {i}" }).ToList() }; + + var timedService = new TimedSlowService(); + var field = schema.Type().AddField("slowOperation", "Slow operation"); + + field.ResolveAsync((movie, service) => service.DoTimedWorkAsync(movie.Id), maxConcurrency: fieldLimit); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(timedService); + + var executionOptions = new ExecutionOptions { MaxQueryConcurrency = queryLimit, ServiceConcurrencyLimits = { [typeof(TimedSlowService)] = serviceLimit } }; + + var query = new QueryRequest { Query = "{ movies { name slowOperation } }" }; + var result = schema.ExecuteRequestWithContext(query, data, serviceCollection.BuildServiceProvider(), null, executionOptions); + + Assert.Null(result.Errors); + + // Verify the most restrictive limit was enforced + Assert.True( + timedService.MaxConcurrentOperations == expectedMax, + $"Expected max {expectedMax} concurrent operations ({mostRestrictiveLevel} limit), but saw {timedService.MaxConcurrentOperations}" + ); + + // Reset for next test + timedService.Reset(); + } + + [Fact] + public void TestConcurrencyLimiterRegistry() + { + var concurrencyLimiterRegistry = new ConcurrencyLimiterRegistry(); + // Test the semaphore registry + concurrencyLimiterRegistry.ClearAllSemaphores(); + + var semaphore1 = concurrencyLimiterRegistry.GetSemaphore("test1", 5); + var semaphore2 = concurrencyLimiterRegistry.GetSemaphore("test1", 5); // Same key + var semaphore3 = concurrencyLimiterRegistry.GetSemaphore("test2", 3); // Different key + + // Same key should return same semaphore + Assert.Same(semaphore1, semaphore2); + Assert.NotSame(semaphore1, semaphore3); + + // Test cleanup + concurrencyLimiterRegistry.ClearAllSemaphores(); + var semaphore4 = concurrencyLimiterRegistry.GetSemaphore("test1", 5); + Assert.NotSame(semaphore1, semaphore4); // Should be new after cleanup + } + + [Fact] + public void TestConcurrencyLimiterRegistryRequestCleanup() + { + var concurrencyLimiterRegistry = new ConcurrencyLimiterRegistry(); + concurrencyLimiterRegistry.ClearAllSemaphores(); + + // Create field and query semaphores (should be cleaned) + var fieldSemaphore = concurrencyLimiterRegistry.GetSemaphore("field_test_5", 5); + var querySemaphore = concurrencyLimiterRegistry.GetSemaphore("query_test", 10); + + // Create service semaphore (should not be cleaned) + var serviceSemaphore = concurrencyLimiterRegistry.GetSemaphore("service_TestService", 3); + + concurrencyLimiterRegistry.ClearRequestSemaphores(); + + // Field and query semaphores should be gone, service should remain + var newFieldSemaphore = concurrencyLimiterRegistry.GetSemaphore("field_test_5", 5); + var newQuerySemaphore = concurrencyLimiterRegistry.GetSemaphore("query_test", 10); + var sameServiceSemaphore = concurrencyLimiterRegistry.GetSemaphore("service_TestService", 3); + + Assert.NotSame(fieldSemaphore, newFieldSemaphore); + Assert.NotSame(querySemaphore, newQuerySemaphore); + Assert.Same(serviceSemaphore, sameServiceSemaphore); + + concurrencyLimiterRegistry.ClearAllSemaphores(); + } + + public class TestMovieContext + { + public List Movies { get; set; } = new(); + } + + public class Movie + { + public Guid Id { get; set; } + public string Name { get; set; } = ""; + } + + public class SlowService + { + public async Task DoSlowWorkAsync(Guid movieId) + { + await System.Threading.Tasks.Task.Delay(50); // Small delay for testing + return $"Slow result for {movieId}"; + } + } +} + +public class TimedSlowService +{ + private int _currentConcurrentOperations = 0; + public int MaxConcurrentOperations { get; private set; } = 0; + + public async Task DoTimedWorkAsync(Guid movieId) + { + var current = Interlocked.Increment(ref _currentConcurrentOperations); + MaxConcurrentOperations = Math.Max(MaxConcurrentOperations, current); + + try + { + await System.Threading.Tasks.Task.Delay(100); // 100ms delay + return $"Timed result for {movieId}"; + } + finally + { + Interlocked.Decrement(ref _currentConcurrentOperations); + } + } + + public void Reset() + { + _currentConcurrentOperations = 0; + MaxConcurrentOperations = 0; + } +} 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 042ca35d..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)) }, @@ -331,14 +361,9 @@ public void CompilesEnumSimple() { var schema = SchemaBuilder.FromObject(); var param = Expression.Parameter(typeof(Person)); - var expressionParser = new EntityQueryParser( - param, - schema, - new QueryRequestContext(null, null), - new DefaultMethodProvider(), - new CompileContext(executionOptions, null, new QueryRequestContext(null, null)) - ); - var exp = expressionParser.Parse("gender == Female"); + + 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); Assert.True(res); @@ -348,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); @@ -358,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 @@ -379,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 @@ -402,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); }); } @@ -413,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)); } @@ -422,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/DefaultMethodProviderTests.cs b/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderDefaultMethodTests.cs similarity index 61% rename from src/tests/EntityGraphQL.Tests/EntityQuery/DefaultMethodProviderTests.cs rename to src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderDefaultMethodTests.cs index 5e23036e..29dff141 100644 --- a/src/tests/EntityGraphQL.Tests/EntityQuery/DefaultMethodProviderTests.cs +++ b/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderDefaultMethodTests.cs @@ -6,15 +6,20 @@ namespace EntityGraphQL.Compiler.EntityQuery.Tests; -public class DefaultMethodProviderTests +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() { - var exp = EntityQueryCompiler.Compile(@"people.first(guid == ""6492f5fe-0869-4279-88df-7f82f8e87a67"")", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as Person; + var exp = EntityQueryCompiler.Compile( + @"people.first(guid == ""6492f5fe-0869-4279-88df-7f82f8e87a67"")", + SchemaBuilder.FromObject(), + compileContext, + new EqlMethodProvider() + ); + var result = exp.Execute(new EqlMethodTestSchema()) as Person; Assert.NotNull(result); Assert.Equal(new Guid("6492f5fe-0869-4279-88df-7f82f8e87a67"), result.Guid); } @@ -22,8 +27,8 @@ public void CompilesFirst() [Fact] public void CompilesWhere() { - var exp = EntityQueryCompiler.Compile(@"people.where(name == ""bob"")", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as IEnumerable; + 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); } @@ -31,8 +36,8 @@ public void CompilesWhere() [Fact] public void CompilesWhere2() { - var exp = EntityQueryCompiler.Compile(@"people.where(name == ""Luke"")", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as IEnumerable; + 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); } @@ -40,8 +45,8 @@ public void CompilesWhere2() [Fact] public void FailsWhereNoParameter() { - var ex = Assert.Throws( - () => EntityQueryCompiler.Compile("people.where()", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()) + 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); } @@ -49,8 +54,8 @@ public void FailsWhereNoParameter() [Fact] public void FailsWhereWrongParameterType() { - var ex = Assert.Throws( - () => EntityQueryCompiler.Compile("people.where(name)", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()) + 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); } @@ -58,8 +63,8 @@ public void FailsWhereWrongParameterType() [Fact] public void CompilesFirstWithPredicate() { - var exp = EntityQueryCompiler.Compile(@"people.first(name == ""Luke"")", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as Person; + 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); } @@ -67,8 +72,8 @@ public void CompilesFirstWithPredicate() [Fact] public void CompilesFirstNoPredicate() { - var exp = EntityQueryCompiler.Compile("people.first()", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as Person; + 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); } @@ -76,8 +81,8 @@ public void CompilesFirstNoPredicate() [Fact] public void CompilesTake() { - var context = new TestSchema(); - var exp = EntityQueryCompiler.Compile("people.take(1)", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); + var context = new EqlMethodTestSchema(); + 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); @@ -89,8 +94,8 @@ public void CompilesTake() [Fact] public void CompilesSkip() { - var context = new TestSchema(); - var exp = EntityQueryCompiler.Compile("people.Skip(1)", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); + var context = new EqlMethodTestSchema(); + 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()); @@ -102,8 +107,8 @@ public void CompilesSkip() [Fact] public void CompilesMethodsChained() { - var exp = EntityQueryCompiler.Compile("people.where(id == 9).take(2)", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as IEnumerable; + 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()); Assert.Equal("Bob", result.ElementAt(0).Name); @@ -114,8 +119,8 @@ public void CompilesMethodsChained() [Fact] public void CompilesStringContains() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.contains(""ob""))", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as IEnumerable; + 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()); Assert.Equal("Bob", result.ElementAt(0).Name); @@ -126,8 +131,8 @@ public void CompilesStringContains() [Fact] public void CompilesStringStartsWith() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.startsWith(""Bo""))", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as IEnumerable; + 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()); Assert.Equal("Bob", result.ElementAt(0).Name); @@ -137,8 +142,8 @@ public void CompilesStringStartsWith() [Fact] public void CompilesStringEndsWith() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.endsWith(""b""))", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as IEnumerable; + 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); Assert.Equal("Bob", result.ElementAt(0).Name); @@ -147,8 +152,8 @@ public void CompilesStringEndsWith() [Fact] public void CompilesStringToLower() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.toLower() == ""bob"")", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as IEnumerable; + 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); Assert.Equal("Bob", result.ElementAt(0).Name); @@ -157,8 +162,8 @@ public void CompilesStringToLower() [Fact] public void CompilesStringToUpper() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.toUpper() == ""BOB"")", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as IEnumerable; + 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); Assert.Equal("Bob", result.ElementAt(0).Name); @@ -167,8 +172,13 @@ public void CompilesStringToUpper() [Fact] public void CompilesAndConvertsStringToGuid() { - var exp = EntityQueryCompiler.Compile(@"people.where(guid == ""6492f5fe-0869-4279-88df-7f82f8e87a67"")", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var result = exp.Execute(new TestSchema()) as IEnumerable; + var exp = EntityQueryCompiler.Compile( + @"people.where(guid == ""6492f5fe-0869-4279-88df-7f82f8e87a67"")", + SchemaBuilder.FromObject(), + compileContext, + new EqlMethodProvider() + ); + var result = exp.Execute(new EqlMethodTestSchema()) as IEnumerable; Assert.NotNull(result); Assert.Single(result); Assert.Equal("Luke", result.ElementAt(0).Name); @@ -177,8 +187,8 @@ public void CompilesAndConvertsStringToGuid() [Fact] public void SupportUseFilterIsAnyMethod() { - var exp = EntityQueryCompiler.Compile(@"people.where(name.isAny([""Bob"", ""Robin""]))", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var data = new TestSchema(); + 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); Assert.NotNull(result); @@ -190,8 +200,8 @@ public void SupportUseFilterIsAnyMethod() [Fact] public void SupportUseFilterIsAnyMethodOnNullable() { - var exp = EntityQueryCompiler.Compile(@"people.where(age.isAny([99, 44]))", SchemaBuilder.FromObject(), executionOptions, new DefaultMethodProvider()); - var data = new TestSchema(); + 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; Assert.NotNull(result); @@ -200,7 +210,7 @@ public void SupportUseFilterIsAnyMethodOnNullable() } // This would be your Entity/Object graph you use with EntityFramework - private class TestSchema + private class EqlMethodTestSchema { public IEnumerable People => [ diff --git a/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderTests.cs b/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderTests.cs new file mode 100644 index 00000000..c2ca5a72 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/EntityQuery/EqlMethodProviderTests.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using EntityGraphQL.Compiler; +using EntityGraphQL.Compiler.EntityQuery; +using EntityGraphQL.Schema; +using Xunit; + +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() + { + var provider = new EqlMethodProvider(); + + var regexMethod = typeof(SharedTestExtensions).GetMethod(nameof(SharedTestExtensions.Regex)); + Assert.NotNull(regexMethod); + + provider.RegisterMethod(regexMethod!, typeof(string), "regex"); + + // Should have registered the direct method + var registeredMethods = provider.GetCustomRegisteredMethods(); + Assert.Single(registeredMethods); + + var regexMethodInfo = registeredMethods.First(); + Assert.Equal("regex", regexMethodInfo.MethodName); + Assert.Equal(typeof(string), regexMethodInfo.MethodContextType); + } + + [Fact] + public void TestNamingConflicts() + { + var provider = new EqlMethodProvider(); + + var regexMethod = typeof(SharedTestExtensions).GetMethod(nameof(SharedTestExtensions.Regex)); + Assert.NotNull(regexMethod); + + // First registration should succeed + provider.RegisterMethod(regexMethod!, typeof(string), "regex"); + + // Second registration with same name should throw + Assert.Throws(() => provider.RegisterMethod(regexMethod!, typeof(string), "regex")); + } + + [Fact] + public void TestClearAllMethods() + { + var provider = new EqlMethodProvider(); + + // Register both extension and direct methods + provider.RegisterMethods(typeof(SharedTestExtensions)); + + // Should have registered methods + Assert.True(provider.GetRegisteredMethods().Count > 0); + + provider.ClearAllMethods(); + + // Should be empty after clearing + Assert.Empty(provider.GetRegisteredMethods()); + } + + [Fact] + public void TestTypeCompatibility() + { + var provider = new EqlMethodProvider(); + + var regexMethod = typeof(SharedTestExtensions).GetMethod(nameof(SharedTestExtensions.Regex)); + var matchesMethod = typeof(SharedTestExtensions).GetMethod(nameof(SharedTestExtensions.MatchesLength)); + + Assert.NotNull(regexMethod); + Assert.NotNull(matchesMethod); + + provider.RegisterMethod(regexMethod!, typeof(string), "regex"); + provider.RegisterMethod(matchesMethod!, typeof(double), "matchesLength"); + + // Should only work on correct types + Assert.True(provider.EntityTypeHasMethod(typeof(string), "regex")); + Assert.False(provider.EntityTypeHasMethod(typeof(int), "regex")); + + Assert.True(provider.EntityTypeHasMethod(typeof(double), "matchesLength")); + Assert.False(provider.EntityTypeHasMethod(typeof(string), "matchesLength")); + } + + [Fact] + public void TestAutoNamingWithCamelCase() + { + var provider = new EqlMethodProvider(); + + var regexMethod = typeof(SharedTestExtensions).GetMethod(nameof(SharedTestExtensions.Regex)); + Assert.NotNull(regexMethod); + + // Register without specifying name - should auto-generate camelCase name + provider.RegisterMethod(regexMethod!, typeof(string)); + + var registeredMethods = provider.GetCustomRegisteredMethods(); + Assert.Single(registeredMethods); + + var methodInfo = registeredMethods.First(); + Assert.Equal("regex", methodInfo.MethodName); + } + + [Fact] + public void EqlMethodProvider_Should_Allow_Custom_Method_Registration() + { + // Arrange + var provider = new EqlMethodProvider(); + + // Register a custom method for testing + provider.RegisterMethod(typeof(string), "customTest", (context, argContext, methodName, args) => Expression.Constant(true)); + + // Act & Assert + Assert.True(provider.EntityTypeHasMethod(typeof(string), "customTest")); + Assert.False(provider.EntityTypeHasMethod(typeof(int), "customTest")); + } + + [Fact] + public void EqlMethodProvider_Should_Prevent_Method_Name_Conflicts() + { + // Arrange + var provider = new EqlMethodProvider(); + + // Act & Assert + var exception = Assert.Throws(() => provider.RegisterMethod(typeof(string), "contains", (context, argContext, methodName, args) => Expression.Constant(true))); + + Assert.Contains("already registered", exception.Message); + } + + [Fact] + public void EqlMethodProvider_Should_Allow_Clearing_Custom_Methods_Only() + { + // Arrange + var provider = new EqlMethodProvider(); + + // Register a custom method + provider.RegisterMethod(typeof(string), "customMethod", (context, argContext, methodName, args) => Expression.Constant(true)); + + Assert.True(provider.EntityTypeHasMethod(typeof(string), "customMethod")); + Assert.True(provider.EntityTypeHasMethod(typeof(string), "contains")); // Default method + + // Act + provider.ClearCustomMethods(); + + // Assert + Assert.False(provider.EntityTypeHasMethod(typeof(string), "customMethod")); + Assert.True(provider.EntityTypeHasMethod(typeof(string), "contains")); // Default method should remain + } + + [Fact] + public void EqlMethodProvider_Test_AddingStaticMethod() + { + var provider = new EqlMethodProvider(); + // Register an instance method + 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(), compileContext, provider); + var result = exp.Execute(new EqlMethodTestSchema()) as Person; + Assert.NotNull(result); + Assert.Equal("Bob", result.Name); + } + + [Fact] + public void EqlMethodProvider_Test_AddingInstanceMethod() + { + var provider = new EqlMethodProvider(); + // Register an instance method + 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(), compileContext, provider); + var result = exp.Execute(new EqlMethodTestSchema()) as Person; + Assert.NotNull(result); + Assert.Equal("Robin Hood", result.GetFullName()); + } + + [Fact] + public void EqlMethodProvider_Test_AddingCustomMakeCallFunc() + { + var provider = new EqlMethodProvider(); + provider.RegisterMethod( + t => t == typeof(int) || t == typeof(string), + "isOne", + (context, argContext, methodName, args) => + { + if (context.Type == typeof(int)) + { + return Expression.MakeBinary(ExpressionType.Equal, context, Expression.Constant(1)); + } + return Expression.MakeBinary(ExpressionType.Equal, context, Expression.Constant("1")); + } + ); + + 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(), compileContext, provider); + Assert.False(exp.Execute(new EqlMethodTestSchema()) as bool?); + + exp = EntityQueryCompiler.Compile(@"oneStr.isOne()", SchemaBuilder.FromObject(), compileContext, provider); + Assert.True(exp.Execute(new EqlMethodTestSchema()) as bool?); + exp = EntityQueryCompiler.Compile(@"notOneStr.isOne()", SchemaBuilder.FromObject(), compileContext, provider); + Assert.False(exp.Execute(new EqlMethodTestSchema()) as bool?); + } + + private class EqlMethodTestSchema + { + public int One => 1; + public int NotOne => 2; + public string OneStr => "1"; + public string NotOneStr => "11"; + public IEnumerable People => + [ + new Person + { + Id = 9, + Name = "Bob", + Guid = Guid.NewGuid(), + }, + new Person(), + new Person + { + Id = 9, + Name = "Boba", + Guid = Guid.NewGuid(), + }, + new Person + { + Id = 9, + Name = "Robin", + LastName = "Hood", + Guid = Guid.NewGuid(), + Age = 44, + }, + ]; + } + + private class Person + { + public int Id { get; set; } = 99; + public string Name { get; set; } = "Luke"; + public string LastName { get; set; } = "Lasty"; + public Guid Guid { get; set; } = new Guid("6492f5fe-0869-4279-88df-7f82f8e87a67"); + public int? Age { get; set; } + + public string? GetFullName() => $"{Name} {LastName}"; + } +} + +internal static class SharedTestExtensions +{ + [EqlMethod("regex")] + public static bool Regex(this string input, string pattern) + { + if (input == null) + return false; + return System.Text.RegularExpressions.Regex.IsMatch(input, pattern); + } + + [EqlMethod("matchesLength")] + public static bool MatchesLength(this string input, int length) + { + return input?.Length == length; + } +} 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 d01573bd..132248f2 100644 --- a/src/tests/EntityGraphQL.Tests/IntrospectionTests/IntrospectionTests.cs +++ b/src/tests/EntityGraphQL.Tests/IntrospectionTests/IntrospectionTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using EntityGraphQL.Schema; @@ -7,6 +8,45 @@ namespace EntityGraphQL.Tests; public class IntrospectionTests { + [Fact] + public void IncludeEnumInputField_Introspection() + { + var schema = SchemaBuilder.FromObject(); + + schema.AddInputType("EnumInputArgs", "args with enums").AddAllFields(); + + var gql = new QueryRequest + { + Query = + @"query { + __type(name: ""EnumInputArgs"") { + name + inputFields { + name + type { kind name ofType { kind name } } + } + } + }", + }; + + var result = schema.ExecuteRequestWithContext(gql, new TestDataContext(), null, null); + Assert.Null(result.Errors); + + var fields = (IEnumerable)((dynamic)result.Data!["__type"]!).inputFields; + Assert.Contains(fields, f => f.name == "unit"); + + var unitField = fields.First(f => f.name == "unit"); + Assert.Equal("NON_NULL", unitField.type.kind); + Assert.Equal("ENUM", unitField.type.ofType.kind); + Assert.Equal("HeightUnit", unitField.type.ofType.name); + } + + private class EnumInputArgs + { + public HeightUnit Unit { get; set; } + public DayOfWeek Day { get; set; } + } + [Fact] public void TestGraphiQLIntrospection() { @@ -250,7 +290,7 @@ public void TestScalarDescription() { Query = @"query { - __type(name: ""Date"") { + __type(name: ""DateTime"") { name description } @@ -264,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 c502ae0c..7b6df888 100644 --- a/src/tests/EntityGraphQL.Tests/MutationTests/MutationTests.cs +++ b/src/tests/EntityGraphQL.Tests/MutationTests/MutationTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading; using EntityGraphQL.Schema; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -35,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 = @@ -182,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] @@ -362,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(); @@ -541,9 +542,8 @@ public void TestUnnamedMutationOp() { Query = @"mutation { - doGreatThing - } - ", + doGreatThing + }", }; var testSchema = new TestDataContext(); @@ -617,9 +617,8 @@ public void TestRequiredGuid() { Query = @"mutation { - needsGuid - } - ", + needsGuid + }", }; var testSchema = new TestDataContext(); @@ -967,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] @@ -1076,7 +1075,7 @@ public void TestAddFromMultipleClassesImplementingInterface() var schemaProvider = SchemaBuilder.FromObject(); schemaProvider.Mutation().AddFrom(new SchemaBuilderOptions { AutoCreateInputTypes = true }); - Assert.Equal(33, schemaProvider.Mutation().SchemaType.GetFields().Count()); + Assert.Equal(35, schemaProvider.Mutation().SchemaType.GetFields().Count()); } public class NonAttributeMarkedMethod @@ -1153,7 +1152,50 @@ 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); } + + [Fact] + public async System.Threading.Tasks.Task TestMutationCancellationTokenSupport() + { + var schema = SchemaBuilder.FromObject(); + schema.AddMutationsFrom(new SchemaBuilderOptions() { AutoCreateInputTypes = true }); + + var gql = new QueryRequest + { + Query = + @"mutation AddPersonWithDelay($name: String) { + addPersonWithDelayAsync(name: $name) { + id + name + lastName + } + }", + Variables = new QueryVariables { { "name", "TestPerson" } }, + }; + + var context = new TestDataContext(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(context); + + // Test 1: Normal execution (should work) + var result1 = await schema.ExecuteRequestAsync(gql, serviceCollection.BuildServiceProvider(), null); + Assert.Null(result1.Errors); + Assert.NotNull(result1.Data); + dynamic addPersonResult = result1.Data!["addPersonWithDelayAsync"]!; + Assert.Equal(999, addPersonResult.id); + Assert.Equal("TestPerson", addPersonResult.name); + Assert.Equal("Delayed", addPersonResult.lastName); + + // Test 2: With cancelled token should throw OperationCanceledException + var cts = new CancellationTokenSource(); + cts.Cancel(); // Cancel immediately + + var result2 = await schema.ExecuteRequestAsync(gql, serviceCollection.BuildServiceProvider(), null, null, cts.Token); + Assert.NotNull(result2.Errors); + Assert.Single(result2.Errors); + Assert.Equal("The operation was canceled.", result2.Errors[0].Message); + Assert.Null(result2.Data); + } } 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 854f7a4f..60b81d51 100644 --- a/src/tests/EntityGraphQL.Tests/MutationTests/PeopleMutations.cs +++ b/src/tests/EntityGraphQL.Tests/MutationTests/PeopleMutations.cs @@ -205,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] @@ -312,6 +318,23 @@ public static int DescriptionArgs(DescriptionArgs args) { return args.X; } + + [GraphQLMutation] + public async Task AddPersonWithDelayAsync(TestDataContext db, PeopleMutationsArgs args) + { + // 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", + }; + + db.People.Add(person); + return person; + } } public class NestedInputObject 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/AsyncAdvancedTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/AsyncAdvancedTests.cs new file mode 100644 index 00000000..645348e1 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/QueryTests/AsyncAdvancedTests.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EntityGraphQL.Schema; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class AsyncAdvancedTests +{ + [Fact] + public void DeeplyNested_Async_Service_Field_Is_Awaited_In_Dynamic_Projection() + { + // Schema with async service field on Person + var schema = SchemaBuilder.FromObject(); + schema.Type().AddField("ageAsync", "Async age").ResolveAsync((p, srv) => srv.GetAgeAsync(p.Birthday)); + + // Also add another async service field to exercise multiple Task in the same anonymous/dynamic object + schema.Type().AddField("nicknameAsync", "Async nickname").ResolveAsync((p, srv) => srv.GetNicknameAsync(p.Name)); + + // Build nested data: Project -> Task -> Assignee(Person) + var context = new TestDataContext + { + Projects = new List + { + new() + { + Id = 1, + Tasks = new List + { + new() + { + Assignee = new Person { Name = "Alyssa", Birthday = DateTime.Now.AddYears(-25) }, + }, + }, + }, + }, + }; + + var services = new ServiceCollection(); + services.AddSingleton(new AgeService()); + services.AddSingleton(new NickService()); + var sp = services.BuildServiceProvider(); + + var gql = new QueryRequest + { + Query = + @"query { + projects { + tasks { + assignee { + ageAsync + nicknameAsync + } + } + } + }", + }; + + var res = schema.ExecuteRequestWithContext(gql, context, sp, null); + Assert.Null(res.Errors); + + // Validate the async results are resolved (no Task left) and values make sense + dynamic projects = res.Data!["projects"]!; + dynamic firstTask = projects[0].tasks[0]; + dynamic assignee = firstTask.assignee; + + // Types should be value types (int/string), not Task + Assert.IsType(assignee.ageAsync); + Assert.IsType(assignee.nicknameAsync); + Assert.Equal("Alyssa_nick", (string)assignee.nicknameAsync); + Assert.Equal(25, (int)assignee.ageAsync); + } + + [Fact] + public void Large_List_Async_Scalar_Fields_Are_Resolved_Once_Each() + { + const int N = 200; + var schema = SchemaBuilder.FromObject(); + + // Counter async service to track calls + var counter = new CounterService(); + schema.Type().AddField("counter", "Async counter test").ResolveAsync((p, srv) => srv.GetValueAsync(p.Id)); + + // Populate data + var ctx = new TestDataContext { People = Enumerable.Range(1, N).Select(i => new Person { Id = i }).ToList() }; + + var services = new ServiceCollection(); + services.AddSingleton(counter); + var sp = services.BuildServiceProvider(); + + var gql = new QueryRequest { Query = @"query { people { counter } }" }; + + var res = schema.ExecuteRequestWithContext(gql, ctx, sp, null); + Assert.Null(res.Errors); + + dynamic people = res.Data!["people"]!; + Assert.Equal(N, ((IEnumerable)people).Count()); + Assert.IsType(people[0].counter); + // Ensure each item resolved exactly once (no double invocation) + Assert.Equal(N, counter.CallCount); + } +} + +internal class NickService +{ + public System.Threading.Tasks.Task GetNicknameAsync(string? name) + { + return System.Threading.Tasks.Task.FromResult((name ?? "").Trim() + "_nick"); + } +} + +internal class CounterService +{ + public int CallCount { get; private set; } + + public async System.Threading.Tasks.Task GetValueAsync(int id) + { + CallCount += 1; + // Simulate async boundary + await System.Threading.Tasks.Task.Yield(); + return id; + } +} diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/AsyncShapesTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/AsyncShapesTests.cs new file mode 100644 index 00000000..abe30327 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/QueryTests/AsyncShapesTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EntityGraphQL.Schema; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class AsyncShapesTests +{ + [Fact] + public void ValueTask_Generic_Field_Is_Resolved() + { + var schema = SchemaBuilder.FromObject(); + schema.Type().AddField("ageVt", "Age via ValueTask").ResolveAsync((p, s) => s.GetAgeAsync(p.Birthday)); + + var ctx = new TestDataContext { People = new List { new Person { Birthday = DateTime.UtcNow.AddYears(-3) } } }; + var services = new ServiceCollection().AddSingleton(new VtAgeService()).BuildServiceProvider(); + + var res = schema.ExecuteRequestWithContext(new QueryRequest { Query = "{ people { ageVt } }" }, ctx, services, null); + Assert.Null(res.Errors); + dynamic people = res.Data!["people"]!; + Assert.IsType(people[0].ageVt); + Assert.InRange((int)people[0].ageVt, 1, 200); + } + + [Fact] + public void IAsyncEnumerable_Field_Is_Buffered_To_List() + { + var schema = SchemaBuilder.FromObject(); + // Add the field that returns IAsyncEnumerable directly (no service dependency) + schema.Type().AddField("tickets", "Async stream of ints").ResolveAsync((p, s) => s.GetStreamAsync(p.Id)); + + var ctx = new TestDataContext { People = new List { new() { Id = 5 } } }; + var services = new ServiceCollection().AddSingleton(new StreamService()).BuildServiceProvider(); + + var res = schema.ExecuteRequestWithContext(new QueryRequest { Query = "{ people { id tickets } }" }, ctx, services, null); + + Assert.Null(res.Errors); + Assert.NotNull(res.Data); + dynamic people = res.Data!["people"]!; + var list = (IEnumerable)people[0].tickets; + Assert.Equal(3, list.Count()); + Assert.Equal(5, list.ElementAt(0)); + Assert.Equal(6, list.ElementAt(1)); + Assert.Equal(7, list.ElementAt(2)); + } +} + +internal class VtAgeService +{ + public async ValueTask GetAgeAsync(DateTime? birthday) + { + await System.Threading.Tasks.Task.Yield(); + return birthday.HasValue ? (int)((DateTime.UtcNow - birthday.Value).TotalDays / 365) : 0; + } +} + +internal class StreamService +{ + public async IAsyncEnumerable GetStreamAsync(int id) + { + yield return id; + await System.Threading.Tasks.Task.Delay(0); + yield return id + 1; + await System.Threading.Tasks.Task.Delay(0); + yield return id + 2; + } +} diff --git a/src/tests/EntityGraphQL.Tests/QueryTests/AsyncTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/AsyncTests.cs index 091670b5..67d5daab 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/AsyncTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/AsyncTests.cs @@ -1,5 +1,8 @@ using System; -using EntityGraphQL.Compiler; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using EntityGraphQL.Schema; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -12,9 +15,7 @@ public class AsyncTests public void TestAsyncServiceField() { var schema = SchemaBuilder.FromObject(); - // Expression have no concept of async/await as it is a compiler feature so you need to use - // .GetAwaiter().GetResult() on your async methods - schema.Type().AddField("age", "Returns persons age").Resolve((ctx, srv) => srv.GetAgeAsync(ctx.Birthday).GetAwaiter().GetResult()); + schema.Type().AddField("age", "Returns persons age").ResolveAsync((ctx, srv) => srv.GetAgeAsync(ctx.Birthday)); var gql = new QueryRequest { @@ -37,15 +38,19 @@ public void TestAsyncServiceField() var res = schema.ExecuteRequestWithContext(gql, context, serviceCollection.BuildServiceProvider(), null); Assert.NotNull(res.Data); - Assert.Equal(2, ((dynamic)res.Data!["people"]!)[0].age); + var age = ((dynamic)res.Data!["people"]!)[0].age; + Assert.Equal(2, age); } [Fact] - public void TestNonResolvedAsyncServiceFieldErrors() + public void TestAsyncServiceFieldNowSupported() { var schema = SchemaBuilder.FromObject(); - // Error as we return a Task<> - Assert.Throws(() => schema.Type().AddField("age", "Returns persons age").Resolve((ctx, srv) => srv.GetAgeAsync(ctx.Birthday))); + // Task<> returns are now supported with automatic async resolution + var field = schema.Type().AddField("age", "Returns persons age").ResolveAsync((ctx, srv) => srv.GetAgeAsync(ctx.Birthday)); + Assert.NotNull(field); + Assert.Equal("age", field.Name); + Assert.Equal(typeof(int), field.ReturnType.TypeDotnet); } [Fact] @@ -57,8 +62,229 @@ public void TestReturnsTaskButNotAsync() Assert.Equal(typeof(Person), schema.Mutation().SchemaType.GetField("testAddPersonAsync", null).ReturnType.TypeDotnet); } + [Fact] + public void TestFieldRequiresGenericTask() + { + var schema = SchemaBuilder.FromObject(); + // Task<> returns are now supported with automatic async resolution + Assert.Throws(() => + { + schema.Type().AddField("age", "Returns persons age").ResolveAsync((ctx, srv) => srv.GetAgeAsyncNoResult(ctx.Birthday)); + }); + } + private System.Threading.Tasks.Task TestAddPersonAsync() { return System.Threading.Tasks.Task.FromResult(new Person()); } + + [Fact] + public async System.Threading.Tasks.Task TestCancellationTokenSupport() + { + var schema = SchemaBuilder.FromObject(); + + // Add a field that accepts CancellationToken - use the regular async overload for now + schema + .Type() + .AddField("delayedAge", "Returns age after delay with cancellation support") + .ResolveAsync((ctx, srv, ct) => srv.GetAgeWithDelayAsync(ctx.Birthday, ct)); + + var gql = new QueryRequest + { + Query = + @"query { + people { + delayedAge + } + }", + }; + + var context = new TestDataContext(); + context.People.Clear(); + context.People.Add(new Person { Birthday = DateTime.Now.AddYears(-25) }); + + var serviceCollection = new ServiceCollection(); + var service = new CancellationTestService(); + serviceCollection.AddSingleton(service); + serviceCollection.AddSingleton(context); + + // Test 1: Normal execution (should work) + var result1 = await schema.ExecuteRequestAsync(gql, serviceCollection.BuildServiceProvider(), null); + Assert.Null(result1.Errors); + Assert.NotNull(result1.Data); + var age1 = ((dynamic)result1.Data!["people"]!)[0].delayedAge; + Assert.Equal(25, age1); + + // Test 2: With cancelled token should work for now since we're using sync method + var cts = new CancellationTokenSource(); + cts.Cancel(); // Cancel immediately + + // For now, just test that the feature compiles and works + var result2 = await schema.ExecuteRequestAsync(gql, serviceCollection.BuildServiceProvider(), null, null, cts.Token); + Assert.NotNull(result2.Errors); + Assert.Single(result2.Errors); + 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/BulkAsyncTests.cs b/src/tests/EntityGraphQL.Tests/QueryTests/BulkAsyncTests.cs new file mode 100644 index 00000000..cc623764 --- /dev/null +++ b/src/tests/EntityGraphQL.Tests/QueryTests/BulkAsyncTests.cs @@ -0,0 +1,343 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EntityGraphQL.Schema; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace EntityGraphQL.Tests; + +public class BulkAsyncTests +{ + [Fact] + public void TestAsyncBulkResolverFullObject() + { + var schema = SchemaBuilder.FromObject(); + schema.UpdateType(type => + { + type.ReplaceField("createdBy", "Get user that created it") + .ResolveAsync((proj, users) => users.GetUserByIdAsync(proj.CreatedBy)) + .ResolveBulkAsync(proj => proj.CreatedBy, (ids, srv) => srv.GetAllUsersAsync(ids)); + }); + + var gql = new QueryRequest + { + Query = + @"{ + projects { + name createdBy { id field2 } + } + }", + }; + + var context = new TestDataContext + { + Projects = + [ + new Project + { + Id = 1, + CreatedBy = 1, + Name = "Project 1", + }, + new Project + { + Id = 2, + CreatedBy = 2, + Name = "Project 2", + }, + ], + }; + var serviceCollection = new ServiceCollection(); + AsyncUserService userService = new(); + serviceCollection.AddSingleton(userService); + serviceCollection.AddSingleton(context); + var sp = serviceCollection.BuildServiceProvider(); + + var res = schema.ExecuteRequest(gql, sp, null); + Assert.Null(res.Errors); + // called once not for each project due to bulk resolving + Assert.Equal(1, userService.CallCount); + // verify we have correct data for all projects + dynamic projects = res.Data!["projects"]!; + Assert.Equal(2, projects.Count); + for (int i = 0; i < 2; i++) + { + var project = projects[i]; + Assert.Equal(i + 1, project.createdBy.id); + Assert.Equal("Hello", project.createdBy.field2); + } + } + + [Fact] + public void TestAsyncBulkResolverWithConcurrencyLimit() + { + var schema = SchemaBuilder.FromObject(); + + // Add multiple fields that each use bulk resolvers to force concurrent execution + schema.UpdateType(type => + { + type.ReplaceField("createdBy", "Get user that created it") + .ResolveAsync((proj, users) => users.GetUserByIdAsync(proj.CreatedBy)) + .ResolveBulkAsync(proj => proj.CreatedBy, (ids, srv) => srv.GetAllUsersAsync(ids), maxConcurrency: 2); + + type.AddField("assignedUser", "Get assigned user") + .ResolveAsync((proj, users) => users.GetUserByIdAsync(proj.CreatedBy + 10)) + .ResolveBulkAsync(proj => proj.CreatedBy + 10, (ids, srv) => srv.GetAllUsersAsync(ids), maxConcurrency: 2); + }); + + var gql = new QueryRequest + { + Query = + @"{ + projects { + name + createdBy { id field2 } + assignedUser { id field2 } + } + }", + }; + + var context = new TestDataContext + { + Projects = Enumerable + .Range(1, 4) + .Select(i => new Project + { + Id = i, + CreatedBy = i, + Name = $"Project {i}", + }) + .ToList(), + }; + + var serviceCollection = new ServiceCollection(); + ConcurrencyTrackingAsyncUserService userService = new(); + serviceCollection.AddSingleton(userService); + serviceCollection.AddSingleton(context); + var sp = serviceCollection.BuildServiceProvider(); + + var res = schema.ExecuteRequest(gql, sp, null); + Assert.Null(res.Errors); + + // We should have made 2 bulk calls (one for createdBy, one for assignedUser) + Assert.True(userService.CallCount >= 2, $"Expected at least 2 calls, but only had {userService.CallCount}"); + + // Verify concurrency was limited to the specified max + Assert.True(userService.MaxConcurrentOperations == 2, $"Expected max concurrency of 2, but actual max was {userService.MaxConcurrentOperations}"); + + // verify we have correct data for all projects + dynamic projects = res.Data!["projects"]!; + Assert.Equal(4, projects.Count); + for (int i = 0; i < 4; i++) + { + var project = projects[i]; + Assert.Equal(i + 1, project.createdBy.id); + Assert.Equal("Hello", project.createdBy.field2); + Assert.Equal(i + 11, project.assignedUser.id); + Assert.Equal("Hello", project.assignedUser.field2); + } + } + + [Fact] + public void TestAsyncBulkResolverScalarResult() + { + var schema = SchemaBuilder.FromObject(); + schema.UpdateType(type => + { + type.AddField("createdByName", "Get user name that created it") + .ResolveAsync((proj, users) => users.GetAllUserNamesAsync(new[] { proj.CreatedBy }).ContinueWith(task => task.Result[proj.CreatedBy])) + .ResolveBulkAsync(proj => proj.CreatedBy, (ids, srv) => srv.GetAllUserNamesAsync(ids)); + }); + + var gql = new QueryRequest + { + Query = + @"{ + projects { + name createdByName + } + }", + }; + + var context = new TestDataContext + { + Projects = + [ + new Project + { + Id = 1, + CreatedBy = 1, + Name = "Project 1", + }, + new Project + { + Id = 2, + CreatedBy = 2, + Name = "Project 2", + }, + ], + }; + var serviceCollection = new ServiceCollection(); + AsyncUserService userService = new(); + serviceCollection.AddSingleton(userService); + serviceCollection.AddSingleton(context); + var sp = serviceCollection.BuildServiceProvider(); + + var res = schema.ExecuteRequest(gql, sp, null); + Assert.Null(res.Errors); + // called once not for each project + Assert.Equal(1, userService.CallCount); + dynamic projects = res.Data!["projects"]!; + Assert.Equal(2, projects.Count); + Assert.Equal("Name_1", projects[0].createdByName); + Assert.Equal("Name_2", projects[1].createdByName); + } + + [Fact] + public void TestAsyncBulkResolverWithArguments() + { + var schema = SchemaBuilder.FromObject(); + schema.UpdateType(type => + { + type.AddField("user", new { id = ArgumentHelper.Required() }, "Get user by id") + .Resolve((proj, args, users) => users.GetUserByIdForProjectIdAsync(proj.Id, args.id).GetAwaiter().GetResult()!) + .ResolveBulkAsync(proj => proj.Id, (ids, args, srv) => srv.GetUserByIdForProjectIdAsync(ids, args.id)); + }); + + var gql = new QueryRequest + { + Query = + @"{ + projects { + name + user(id: 1) { id field2 } + } + }", + }; + + var context = new TestDataContext + { + Projects = + [ + new Project + { + Id = 1, + CreatedBy = 1, + Name = "Project 1", + }, + new Project + { + Id = 2, + CreatedBy = 2, + Name = "Project 2", + }, + ], + }; + var serviceCollection = new ServiceCollection(); + AsyncUserService userService = new(); + serviceCollection.AddSingleton(userService); + serviceCollection.AddSingleton(context); + var sp = serviceCollection.BuildServiceProvider(); + + var res = schema.ExecuteRequest(gql, sp, null); + Assert.Null(res.Errors); + // called once not for each project + Assert.Equal(1, userService.CallCount); + dynamic projects = res.Data!["projects"]!; + Assert.Equal(2, projects.Count); + Assert.Equal(1, projects[0].user.id); + Assert.Equal("Hello", projects[0].user.field2); + Assert.Null(projects[1].user); // id 2 != 1, so should be null + } +} + +public class AsyncUserService +{ + public int CallCount { get; private set; } + public List Calls { get; private set; } = []; + + public async Task GetUserByIdAsync(int id) + { + CallCount += 1; + Calls.Add(nameof(GetUserByIdAsync)); + await System.Threading.Tasks.Task.Yield(); // Simulate async operation + return new User { Id = id, Field2 = "SingleCall" }; + } + + public async Task> GetAllUsersAsync(IEnumerable data) + { + CallCount += 1; + Calls.Add(nameof(GetAllUsersAsync)); + await System.Threading.Tasks.Task.Delay(50); // Simulate async operation + return data.Distinct() + .Select(id => new User + { + Id = id, + Field2 = "Hello", + Name = $"Name_{id}", + }) + .ToDictionary(u => u.Id, u => u); + } + + public async Task> GetAllUserNamesAsync(IEnumerable data) + { + CallCount += 1; + Calls.Add(nameof(GetAllUserNamesAsync)); + await System.Threading.Tasks.Task.Delay(50); // Simulate async operation + return data.Distinct().ToDictionary(id => id, id => $"Name_{id}"); + } + + public async Task GetUserByIdForProjectIdAsync(int projectId, int userId) + { + CallCount += 1; + Calls.Add(nameof(GetUserByIdForProjectIdAsync)); + await System.Threading.Tasks.Task.Yield(); // Simulate async operation + return projectId != userId ? null : new User { Id = userId, Field2 = "Hello" }; + } + + public async Task> GetUserByIdForProjectIdAsync(IEnumerable projectIds, int userId) + { + CallCount += 1; + Calls.Add(nameof(GetUserByIdForProjectIdAsync)); + await System.Threading.Tasks.Task.Delay(50); // Simulate async operation + return projectIds.ToDictionary(projectId => projectId, projectId => projectId != userId ? null : new User { Id = userId, Field2 = "Hello" }); + } +} + +public class ConcurrencyTrackingAsyncUserService +{ + private int currentConcurrency = 0; + private int maxConcurrency = 0; + private int callCount = 0; + + public int MaxConcurrentOperations => maxConcurrency; + public int CallCount => callCount; + + public async Task> GetAllUsersAsync(IEnumerable ids) + { + Interlocked.Increment(ref callCount); + var current = Interlocked.Increment(ref currentConcurrency); + var max = Math.Max(maxConcurrency, current); + Interlocked.Exchange(ref maxConcurrency, max); + + try + { + await System.Threading.Tasks.Task.Delay(100); // Simulate async work + + return ids.ToDictionary(id => id, id => new User { Id = id, Field2 = "Hello" }); + } + finally + { + Interlocked.Decrement(ref currentConcurrency); + } + } + + public async Task GetUserByIdAsync(int id) + { + await System.Threading.Tasks.Task.Delay(50); + return new User { Id = id, Field2 = "Hello" }; + } +} 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 de46bbc2..38cf8670 100644 --- a/src/tests/EntityGraphQL.Tests/QueryTests/ServiceFieldTests.cs +++ b/src/tests/EntityGraphQL.Tests/QueryTests/ServiceFieldTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Threading; using System.Threading.Tasks; using EntityGraphQL.Compiler; using EntityGraphQL.Extensions; @@ -1048,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 @@ -1073,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] @@ -1129,7 +1130,7 @@ public void TestCollectionToSingle_ToObjectRelation_WithAsyncServiceArrayField_U .Type() .AddField("arrayField", "Get project config") // p.Updated.HasValue is the important bit here - .Resolve((p, x) => x.GetArrayFieldAsync(p.Id, p.Updated.HasValue).GetAwaiter().GetResult()) + .ResolveAsync((p, x) => x.GetArrayFieldAsync(p.Id, p.Updated.HasValue)) .IsNullable(false); var serviceCollection = new ServiceCollection(); @@ -1173,7 +1174,7 @@ public void TestCollectionToSingle_WithAsyncService_UsingContextFieldEnumerable( .Type() .AddField("serviceField", "Get project config") // p.Updated.HasValue is the important bit here - .Resolve((p, x) => x.GetFieldAsync(p.Id, p.Tasks.Where(t => t.Name != "Task").Select(t => t.Id)).GetAwaiter().GetResult()); + .ResolveAsync((p, x) => x.GetFieldAsync(p.Id, p.Tasks.Where(t => t.Name != "Task").Select(t => t.Id))); var serviceCollection = new ServiceCollection(); serviceCollection.AddScoped(); @@ -1401,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, @@ -1602,7 +1602,7 @@ public void TestServiceFieldWithQueryable() new Project { Id = 0, - Tasks = new List { new Task { Id = 1 } }, + Tasks = new List { new() { Id = 1 } }, }, ], }; @@ -1936,7 +1936,7 @@ public void TestGeneratedNamesDoNotCollide() schema.UpdateType(p => { // Here the expression project is extracted and the expression is used for a field name, which is a duplicate of the project field - p.AddField("getProjectId", "Something").Resolve((project, us) => us.GetProjectId(project).GetAwaiter().GetResult()); + p.AddField("getProjectId", "Something").ResolveAsync((project, us) => us.GetProjectId(project)); }); var gql = new QueryRequest @@ -1977,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() @@ -2274,6 +2405,11 @@ public async Task GetAgeAsync(DateTime? birthday) return await System.Threading.Tasks.Task.Run(() => birthday.HasValue ? (int)(DateTime.Now - birthday.Value).TotalDays / 365 : 0); } + public async System.Threading.Tasks.Task GetAgeAsyncNoResult(DateTime? birthday) + { + await System.Threading.Tasks.Task.Run(() => birthday.HasValue ? (int)(DateTime.Now - birthday.Value).TotalDays / 365 : 0); + } + public int GetAge(DateTime? birthday) { CallCount += 1; @@ -2282,8 +2418,31 @@ public int GetAge(DateTime? birthday) } } +public class CancellationTestService +{ + public async Task GetAgeWithDelayAsync(DateTime? birthday, CancellationToken cancellationToken) + { + // Simulate some async work + await System.Threading.Tasks.Task.Delay(10, cancellationToken); + + // Check for cancellation + cancellationToken.ThrowIfCancellationRequested(); + + return birthday.HasValue ? (int)(DateTime.Now - birthday.Value).TotalDays / 365 : 0; + } +} + 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/Util/ExpressionExtractorTests.cs b/src/tests/EntityGraphQL.Tests/Util/ExpressionExtractorTests.cs index 9b33cc81..1887feab 100644 --- a/src/tests/EntityGraphQL.Tests/Util/ExpressionExtractorTests.cs +++ b/src/tests/EntityGraphQL.Tests/Util/ExpressionExtractorTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Linq.Expressions; +using System.Threading.Tasks; using EntityGraphQL.Compiler.Util; using Xunit; using static EntityGraphQL.Tests.ServiceFieldTests; @@ -53,13 +54,13 @@ public void ExtractLongMemberExpressionSameNameInMethod() public void ExtractExpressionInAsync() { // Calling a service using EF fields - Expression> expression = (ctx, srv) => srv.GetAgeAsync(ctx.Birthday).GetAwaiter().GetResult(); + Expression>> expression = (ctx, srv) => srv.GetAgeAsync(ctx.Birthday); var extractor = new ExpressionExtractor(); var extracted = extractor.Extract(expression.Body, expression.Parameters[0], false); Assert.NotNull(extracted); Assert.Single(extracted); Assert.Equal("egql__ctx_Birthday", extracted.First().Key); - Assert.Equal(((MethodCallExpression)((MethodCallExpression)((MethodCallExpression)expression.Body).Object!).Object!).Arguments[0], extracted.First().Value.First()); + Assert.Equal(((MethodCallExpression)expression.Body).Arguments[0], extracted.First().Value.First()); } [Fact] diff --git a/src/tests/EntityGraphQL.Tests/Util/NullableReferenceTypeTests.cs b/src/tests/EntityGraphQL.Tests/Util/NullableReferenceTypeTests.cs index 07f799d5..86ca5886 100644 --- a/src/tests/EntityGraphQL.Tests/Util/NullableReferenceTypeTests.cs +++ b/src/tests/EntityGraphQL.Tests/Util/NullableReferenceTypeTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using EntityGraphQL.Schema; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Xunit; namespace EntityGraphQL.Tests.Util; @@ -9,6 +10,14 @@ public class NullableReferenceTypeTests { public class Test { } + // Helper method to compare JSON objects semantically (ignoring property order) + private static void AssertJsonEqual(string expectedJson, object actual) + { + var expectedToken = JToken.Parse(expectedJson); + var actualToken = JToken.Parse(JsonConvert.SerializeObject(actual)); + Assert.True(JToken.DeepEquals(expectedToken, actualToken), $"Expected: {expectedToken}\nActual: {actualToken}"); + } + public class WithoutNullableRefEnabled { public int NonNullableInt { get; set; } @@ -112,20 +121,14 @@ public void TestNullableWithNullableRefEnabled() var type = (dynamic)res.Data!["__type"]!; - Assert.Equal( - @"{""name"":""nonNullableInt"",""type"":{""name"":null,""kind"":""NON_NULL"",""ofType"":{""name"":""Int"",""kind"":""SCALAR""}},""args"":[]}", - JsonConvert.SerializeObject(type.fields[0]) - ); - Assert.Equal(@"{""name"":""nullableInt"",""type"":{""name"":""Int"",""kind"":""SCALAR"",""ofType"":null},""args"":[]}", JsonConvert.SerializeObject(type.fields[1])); - Assert.Equal(@"{""name"":""nullable"",""type"":{""name"":""String"",""kind"":""SCALAR"",""ofType"":null},""args"":[]}", JsonConvert.SerializeObject(type.fields[3])); - Assert.Equal( - @"{""name"":""nonNullable"",""type"":{""name"":null,""kind"":""NON_NULL"",""ofType"":{""name"":""String"",""kind"":""SCALAR""}},""args"":[]}", - JsonConvert.SerializeObject(type.fields[2]) - ); - - Assert.Equal(@"{""name"":""tests"",""type"":{""name"":null,""kind"":""NON_NULL"",""ofType"":{""name"":null,""kind"":""LIST""}},""args"":[]}", JsonConvert.SerializeObject(type.fields[4])); - Assert.Equal(@"{""name"":""tests2"",""type"":{""name"":null,""kind"":""LIST"",""ofType"":{""name"":""Test"",""kind"":""OBJECT""}},""args"":[]}", JsonConvert.SerializeObject(type.fields[5])); - Assert.Equal(@"{""name"":""tests3"",""type"":{""name"":null,""kind"":""NON_NULL"",""ofType"":{""name"":null,""kind"":""LIST""}},""args"":[]}", JsonConvert.SerializeObject(type.fields[6])); - Assert.Equal(@"{""name"":""tests4"",""type"":{""name"":null,""kind"":""LIST"",""ofType"":{""name"":""Test"",""kind"":""OBJECT""}},""args"":[]}", JsonConvert.SerializeObject(type.fields[7])); + AssertJsonEqual(@"{""name"":""nonNullableInt"",""type"":{""name"":null,""kind"":""NON_NULL"",""ofType"":{""name"":""Int"",""kind"":""SCALAR""}},""args"":[]}", type.fields[0]); + AssertJsonEqual(@"{""name"":""nullableInt"",""type"":{""name"":""Int"",""kind"":""SCALAR"",""ofType"":null},""args"":[]}", type.fields[1]); + AssertJsonEqual(@"{""name"":""nullable"",""type"":{""name"":""String"",""kind"":""SCALAR"",""ofType"":null},""args"":[]}", type.fields[3]); + AssertJsonEqual(@"{""name"":""nonNullable"",""type"":{""name"":null,""kind"":""NON_NULL"",""ofType"":{""name"":""String"",""kind"":""SCALAR""}},""args"":[]}", type.fields[2]); + + AssertJsonEqual(@"{""name"":""tests"",""type"":{""name"":null,""kind"":""NON_NULL"",""ofType"":{""name"":null,""kind"":""LIST""}},""args"":[]}", type.fields[4]); + AssertJsonEqual(@"{""name"":""tests2"",""type"":{""name"":null,""kind"":""LIST"",""ofType"":{""name"":""Test"",""kind"":""OBJECT""}},""args"":[]}", type.fields[5]); + AssertJsonEqual(@"{""name"":""tests3"",""type"":{""name"":null,""kind"":""NON_NULL"",""ofType"":{""name"":null,""kind"":""LIST""}},""args"":[]}", type.fields[6]); + AssertJsonEqual(@"{""name"":""tests4"",""type"":{""name"":null,""kind"":""LIST"",""ofType"":{""name"":""Test"",""kind"":""OBJECT""}},""args"":[]}", type.fields[7]); } } 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 - -