diff --git a/README.md b/README.md index 3ffdf9f6f..40fc515b6 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Then navigate to `http://127.0.0.1:8000` in your browser. * OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf * Listen on a single or multiple IP(v4/v6) address/hostnames * Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S) -* Host REST APIs, Web Pages, and Static Content (with caching) +* Host REST APIs,async REST APIs, Web Pages, and Static Content (with caching) * Support for custom error pages * Request and Response compression using GZip/Deflate * Multi-thread support for incoming requests @@ -82,7 +82,7 @@ Then navigate to `http://127.0.0.1:8000` in your browser. * In-memory caching, with optional support for external providers (such as Redis) * (Windows) Open the hosted server as a desktop application * FileBrowsing support -* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, and Chinese +* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, Dutch, and Chinese. ## šŸ“¦ Install diff --git a/docs/Tutorials/Configuration.md b/docs/Tutorials/Configuration.md index f6a8720fa..423ee4eea 100644 --- a/docs/Tutorials/Configuration.md +++ b/docs/Tutorials/Configuration.md @@ -92,6 +92,8 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file: | Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) | | Server.ReceiveTimeout | Define the amount of time a Receive method call will block waiting for data | [link](../Endpoints/Basic/StaticContent/#server-timeout) | | Server.DefaultFolders | Set the Default Folders paths | [link](../Routes/Utilities/StaticContent/#changing-the-default-folders) | +| Server.Tasks.HouseKeeping | Set the House Keeping retension and frequency for the Tasks | [link](../Tasks) | +| Server.AsyncRoutes.HouseKeeping | Set the House Keeping retension and frequency for the AsyncRoutes | [link](../Routes/Async/Utilities/HouseKeeping) | | Server.Console | Set the Console settings | [link](../Getting-Started/Console) | | Web.OpenApi.DefaultDefinitionTag | Define the primary tag name for OpenAPI ( `default` is the default) | [link](../OpenAPI/Overview) | | Web.OpenApi.UsePodeYamlInternal | Force the use of the internal YAML converter (`False` is the default) | | diff --git a/docs/Tutorials/Routes/Async/Features/AsyncIdGenerator.md b/docs/Tutorials/Routes/Async/Features/AsyncIdGenerator.md new file mode 100644 index 000000000..14514b4f1 --- /dev/null +++ b/docs/Tutorials/Routes/Async/Features/AsyncIdGenerator.md @@ -0,0 +1,19 @@ +# IdGenerator + +The `IdGenerator` parameter specifies the function used to generate unique IDs for asynchronous tasks. This allows you to customize the way IDs are generated for each async route task, ensuring they meet your application's requirements. + +- **Default Value**: The default function used is `New-PodeGuid`, which generates a unique GUID for each task. + +#### Customizing Async ID Generation + +You can define your own custom function to generate IDs by specifying it in the `IdGenerator` parameter. This can be useful if you need to follow a specific format or include particular information in the IDs. + +**Example Usage** + +```powershell +Add-PodeRoute -PassThru -Method Post -Path '/customAsyncId' -ScriptBlock { + return @{ Message = "Custom Async ID" } +} | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -IdGenerator {return [guid]::NewGuid().ToString() + "-custom" } +``` + +In this example, the `New-CustomAsyncId` function generates a GUID with a custom suffix, ensuring each async route task has a unique and identifiable ID. \ No newline at end of file diff --git a/docs/Tutorials/Routes/Async/Features/Callback.md b/docs/Tutorials/Routes/Async/Features/Callback.md new file mode 100644 index 000000000..775285a93 --- /dev/null +++ b/docs/Tutorials/Routes/Async/Features/Callback.md @@ -0,0 +1,89 @@ + +# Callback + +The `Set-PodeAsyncRoute` function supports including callback functionality for routes. This allows you to define a URL that will be called when the asynchronous task is completed. You can specify the callback URL, content type, HTTP method, and header fields. + +#### Callback Parameters + +- **Callback URL**: Specifies the URL field for the callback. Default is `'$request.body#/callbackUrl'`. + - Can accept the following meta values: + - `$request.query.param-name`: query-param-value + - `$request.header.header-name`: application/json + - `$request.body#/field-name`: callbackUrl + - Can accept runtime expressions based on the [OpenAPI specification](https://swagger.io/docs/specification/callbacks/). + - Acceptable static values (examples): + - 'http://example.com/callback' + - 'https://api.example.com/callback' + +- **Callback Content Type**: Specifies the content type for the callback. The default is `'application/json'`. + - Can accept the following meta values: + - `$request.query.param-name`: query-param-value + - `$request.header.header-name`: application/json + - `$request.body#/field-name`: callbackUrl + - Can accept runtime expressions based on the [OpenAPI specification](https://swagger.io/docs/specification/callbacks/). + - Acceptable static values (examples): + - 'application/json' + - 'application/xml' + - 'text/plain' + +- **Callback Method**: Specifies the HTTP method for the callback. The default is `'Post'`. + - Can accept the following meta values: + - `$request.query.param-name`: query-param-value + - `$request.header.header-name`: application/json + - `$request.body#/field-name`: callbackUrl + - Can accept runtime expressions based on the [OpenAPI specification](https://swagger.io/docs/specification/callbacks/). + - Acceptable static values (examples): + - `GET` + - `POST` + - `PUT` + - `DELETE` + +- **Callback Header Fields**: Specifies the header fields for the callback as a hashtable. The key can be a string representing the header key or one of the meta values. The value is the header value if it's a standard key or the default value if the meta value is not resolvable. + - Can accept the following meta values as keys: + - `$request.query.param-name`: query-param-value + - `$request.header.header-name`: application/json + - `$request.body#/field-name`: callbackUrl + - Can accept runtime expressions based on the [OpenAPI specification](https://swagger.io/docs/specification/callbacks/). + - Acceptable static values (examples): + - `@{ 'Content-Type' = 'application/json' }` + - `@{ 'Authorization' = 'Bearer token' }` + - `@{ 'Custom-Header' = 'value' }` + +- **Send Result**: If specified, sends the result of the callback. + - Type Boolean. + +- **Event Name**: Specifies the event name for the callback. + - Type String. + + +#### Example Usage + +```powershell +Add-PodeRoute -PassThru -Method Post -Path '/asyncWithCallback' -ScriptBlock { + return @{ Message = "Async Route with Callback" } +} | Set-PodeAsyncRoute ` + -ResponseContentType 'application/json', 'application/yaml' ` + -Callback ` + -CallbackUrl 'http://example.com/callbacks/{$request.body#/callbackPath}' ` + -CallbackContentType 'application/json' ` + -CallbackMethod '$request.body#/callbackMethod' ` + -CallbackHeaderFields @{ 'Custom-Header' = '$request.header.CustomHeader' } ` + -SendResult ` + -EventName 'AsyncCompleted' +``` + +#### Explanation + +1. **Route Definition**: The `Add-PodeRoute` defines a route at `/asyncWithCallback` that processes a request and returns a message indicating it's an async route with a callback. + +2. **Setting Async Route with Callback**: The `Set-PodeAsyncRoute` processes the route to make it asynchronous and sets up the callback. + - `-ResponseContentType` specifies the response formats as JSON and YAML. + - `-Callback` enables the callback functionality. + - `-CallbackUrl` sets the URL that will be called when the async route task is completed, using a runtime expression based on the request body. + - `-CallbackContentType` specifies the content type for the callback request. + - `-CallbackMethod` sets the HTTP method for the callback request, using a runtime expression based on the request body. + - `-CallbackHeaderFields` includes custom header fields in the callback request, using a runtime expression based on the request headers. + - `-SendResult` ensures that the result of the async route task is sent in the callback request. + - `-EventName` specifies the event name for the callback. + +This setup ensures that when the asynchronous task completes, a request will be made to the specified callback URL with the defined settings, including the result of the async route task, using runtime expressions to dynamically set the callback parameters. \ No newline at end of file diff --git a/docs/Tutorials/Routes/Async/Features/OpenAPI.md b/docs/Tutorials/Routes/Async/Features/OpenAPI.md new file mode 100644 index 000000000..967023b8f --- /dev/null +++ b/docs/Tutorials/Routes/Async/Features/OpenAPI.md @@ -0,0 +1,94 @@ + +# OpenAPI Integration with Async Routes + +Async routes defined using the `Set-PodeAsyncRoute` function can seamlessly integrate with OpenAPI documentation. This feature automatically generates detailed documentation, including response types and callback information, enhancing the ease of sharing and maintaining your API specifications. + +## Key Features + +### Automatic Documentation Generation + +When an async route is configured using `Set-PodeAsyncRoute`, the corresponding OpenAPI documentation is automatically generated. This documentation includes: +- **Route Details**: Information about the HTTP method, path, and operation summary. +- **Response Types**: Details of the possible response content types (`application/json`, `application/yaml`, etc.) and their associated schemas. +- **Callback Details**: If the route includes callbacks, these are also documented in the OpenAPI definition. + +### Customization Options + +You can tailor the generated OpenAPI documentation to fit your specific needs: +- **OpenApi Schemas**: Customize the schema name for the async route task using the `OATypeName` parameter, or other relevant parameters like `$TaskIdName`, `$QueryRequestName`, and `$QueryParameterName` using `Set-PodeAsyncRouteOASchemaName`. +- **Route Information**: Further customize the OpenAPI route definition using Pode’s OpenAPI functions, such as `Set-PodeOARouteInfo` and any othe OpenApi functions available for route definition. + +### Piping for Documentation + +To generate OpenAPI documentation for an async route, you must pipe the route definition through `Set-PodeOARouteInfo`, as shown in the example below. This requirement also applies to the following async routes: +- `Add-PodeAsyncRouteQuery` +- `Add-PodeAsyncRouteStop` +- `Add-PodeAsyncRouteGet` + +## Example Usage + +The following example demonstrates how to define an async route and customize its OpenAPI documentation: + +```powershell +# Set a custom schema name for the async route task +Set-PodeAsyncRouteOASchemaName -OATypeName 'MyTask' + +# Define an async route and customize its OpenAPI information +Add-PodeRoute -PassThru -Method Post -Path '/asyncExample' -ScriptBlock { + return @{ Message = "Async Route" } +} | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -PassThru | + Set-PodeOARouteInfo -Summary 'My Async Route Task' -Description 'This is a description' +``` + +### Resulting OpenAPI Documentation + +The generated OpenAPI documentation might look as follows: + +```yaml +/asyncExample: + post: + summary: My Async Route Task + description: This is a description + responses: + 200: + description: Successful operation + content: + application/yaml: + schema: + $ref: '#/components/schemas/MyTask' + application/json: + schema: + $ref: '#/components/schemas/MyTask' + +components: + schemas: + MyTask: + type: object + properties: + User: + type: string + description: The async route task owner. + CompletedTime: + type: string + description: The async route task completion time. + example: 2024-07-02T20:59:23.2174712Z + format: date-time + State: + type: string + description: The async route task status. + example: Running + enum: + - NotStarted + - Running + - Failed + - Completed + Result: + type: object + description: The result of the async route task. + properties: + InnerValue: + type: string + description: The inner value returned by the operation. +``` + +**Note**: The `MyTask` schema definition provided above is a partial example. You can expand this definition with additional properties according to your specific use case. \ No newline at end of file diff --git a/docs/Tutorials/Routes/Async/Features/Security.md b/docs/Tutorials/Routes/Async/Features/Security.md new file mode 100644 index 000000000..caf84496f --- /dev/null +++ b/docs/Tutorials/Routes/Async/Features/Security.md @@ -0,0 +1,103 @@ + +# Security + +All async route operations are subject to Pode security, ensuring that any task operation complies with defined authentication and authorization rules. + +> **⚠ Important:** +> All security checks are performed using the user identifier field specified by the `Set-PodeAsyncRouteUserIdentifierField` function. If this field is not explicitly set, the default field `Id` is used. + +#### Permissions + You can specify read and write permissions for each route. This can include specific users, groups, roles, and scopes. + - **Read Access**: Define which users, groups, roles, and scopes have read access. This means that the authenticated user that fits the permission can query the task status. + - **Write Access**: Define which users, groups, roles, and scopes have write access. This means that the authenticated user that fits the permission can stop the task. + +#### Permission Object Structure + +The permission object defines who can perform read or write operations on an async route. The object `Permission` has this structure: + +```powershell +@{ + Read = @{ + Groups = @() + Roles = @() + Scopes = @() + Users = @() + } + Write = @{ + Groups = @() + Roles = @() + Scopes = @() + Users = @() + } +} +``` + +- **Read**: Controls who can query the status of the async route task. +- **Write**: Controls who can stop the async route task. + +An async route task generated by a route without any specified permissions will have read and write permissions granted to anyone, including anonymous users. + +By default, the owner has read and write privileges on the async route task. + +#### Example Usage + +```powershell +New-PodeAuthScheme -Basic -Realm 'Pode Example Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($username, $password) + + # here you'd check a real user storage, this is just for example + if ($username -eq 'morty' -and $password -eq 'pickle') { + return @{ + User = @{ + Username = 'morty' + ID = 'M0R7Y302' + Name = 'Morty' + Type = 'Human' + Groups = @('Support') + } + } + } + elseif ($username -eq 'mindy' -and $password -eq 'pickle') { + return @{ + User = @{ + Username = 'mindy' + ID = 'MINY321' + Name = 'Mindy' + Type = 'Alien' + Groups = @('Developer') + } + } + + return @{ Message = 'Invalid details supplied' } + } +} + +Add-PodeRoute -PassThru -Method Put -Path '/asyncState' -Authentication 'Validate' -Group 'Support' -ScriptBlock { + $data = Get-PodeState -Name 'data' + Write-PodeHost 'data:' + Write-PodeHost $data -Explode -ShowType + Start-Sleep $data.sleepTime + return @{ InnerValue = $data.Message } +} | Set-PodeAsyncRoute -PassThru \` + -ResponseContentType 'application/json', 'application/yaml' -Timeout 300 | + Set-PodeAsyncRoutePermission -Type Read -Groups 'Developer' +``` + +#### Explanation + +1. **Authentication Scheme**: The `New-PodeAuthScheme` creates a basic authentication scheme, and `Add-PodeAuth` adds the authentication named `Validate` with a script block that validates the user credentials. + - If the credentials match, the user information is returned. + - If the credentials do not match, an error message is returned. + +2. **Route Definition**: The `Add-PodeRoute` defines a route at `/asyncState` that requires authentication using the `Validate` scheme and is restricted to users in the `Support` group. + - The route retrieves some state data and writes it to the host, simulates some work by sleeping, and then returns the inner value of the state data. + +3. **Setting Async Route**: The `Set-PodeAsyncRoute` processes the route to make it asynchronous. + - `-ResponseContentType` specifies the response formats as JSON and YAML. + - `-Timeout 300` sets a timeout of 300 seconds for the async route task. +4. **Setting Async Route Task Permissions**: The `Set-PodeAsyncRoutePermission` sets the read permission for users in the `Developer` group. + + - By default only users in the `Developer` group can query the status of the task, and only users with write access can stop the task. + - The owner has read and write privileges on the async route task. + +This setup ensures that the route is secured with authentication, and permissions are properly managed to control who can query or stop the async route task. \ No newline at end of file diff --git a/docs/Tutorials/Routes/Async/Overview.md b/docs/Tutorials/Routes/Async/Overview.md new file mode 100644 index 000000000..5dc0f6ae2 --- /dev/null +++ b/docs/Tutorials/Routes/Async/Overview.md @@ -0,0 +1,295 @@ + +# Async Routes Documentation + +## 1. Overview + +Pode now supports asynchronous routes, allowing you to handle requests asynchronously. This feature is designed to enhance the responsiveness, scalability, security, and flexibility of your Pode applications. With Async Routes, you can manage multiple requests concurrently, handle complex tasks efficiently, and ensure secure operations. + +### Benefits: +- **Improved Responsiveness**: Handle multiple requests concurrently, reducing response times and improving overall system responsiveness. +- **Scalability**: Efficiently manage resources and scale your application to handle increased loads or complex tasks by creating independent runspace pools. +- **Enhanced Security**: Ensure that only authorized users have access to sensitive information and operations with integrated Pode security features. +- **Flexible Task Management**: Easily create, stop, query, or callback on running tasks using a unified interface for managing asynchronous tasks. + + +## 2. Usage + +### Creating an Async Route + +The `Set-PodeAsyncRoute` function enables you to define routes in Pode that execute asynchronously, leveraging runspace management for non-blocking operation. This function allows you to specify response types (JSON, XML, YAML) and manage asynchronous task parameters such as timeout and unique ID generation. It supports the use of arguments, `$using` variables, and state variables. + +#### How It Works + +Creating an async route in Pode is almost like creating a standard route with a few key differences: +1. `Set-PodeAsyncRoute` has to process the output of `Add-PodeRoute`. +2. The route's script block cannot use any response functions like `Write-PodeJsonResponse`. +3. The route's script block must return the result, if any. + +### Example 1: Using ArgumentList + +```powershell +Add-PodeRoute -PassThru -Method Put -Path '/asyncParam' -ScriptBlock { + param($sleepTime2, $Message) + Write-PodeHost "sleepTime2=$sleepTime2" + Write-PodeHost "Message=$Message" + for ($i = 0; $i -lt 20; $i++) { + Start-Sleep $sleepTime2 + } + return @{ InnerValue = $Message } +} -ArgumentList @{sleepTime2 = 2; Message = 'coming as argument' } | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/xml' +``` + +### Example 2: Using `$using` Variables + +```powershell +$uSleepTime = 5 +$uMessage = 'coming from using' + +Add-PodeRoute -PassThru -Method Put -Path '/asyncUsing' -ScriptBlock { + Write-PodeHost "sleepTime=$($using:uSleepTime)" + Write-PodeHost "Message=$($using:uMessage)" + Start-Sleep $using:uSleepTime + return @{ InnerValue = $using:uMessage } +} | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' +``` + +### Example 3: Using `$state` Variables + +```powershell +Set-PodeState -Name 'data' -Value @{ + sleepTime = 5 + Message = 'coming from a PodeState' +} + +Add-PodeRoute -PassThru -Method Put -Path '/asyncState' -ScriptBlock { + Write-PodeHost "state:sleepTime=$($state:data.sleepTime)" + Write-PodeHost "state:MessageTest=$($state:data.Message)" + Start-Sleep $state:data.sleepTime + return @{ InnerValue = $state:data.Message } +} | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' +``` + +### Route Response + +When a route is invoked, it automatically creates a runspace to execute the scriptblock associated with the route. It then returns an `AsyncRouteTask` object that includes information related to the task just sent for execution. + +#### `AsyncRouteTask` Object Definition + +| Name | Type | Description | +|---------------------------------|---------|----------------------------------------------------------------------------------------------| +| **User** | string | The async route task owner. | +| **CompletedTime** | date | The async route task completion time. | +| **State*** | string | The async route task status. Possible values: `NotStarted`, `Running`, `Failed`, `Completed` | +| **CallbackInfo** | object | The Callback operation result. | +| **CallbackInfo.State** | string | Operation status. Possible values: `NotStarted`, `Running`, `Failed`, `Completed` | +| **CallbackInfo.Tentative** | integer | Number of tentatives. | +| **CallbackInfo.Url** | string | The callback URL. | +| **StartingTime** | date | The async route task starting time. | +| **Cancellable*** | boolean | The async route task can be forcefully terminated. | +| **CreationTime*** | string | The async route task creation time. | +| **Id*** | string | The async route task unique identifier. | +| **Permission** | object | The permission governing the async route task. | +| **Permission.Write** | object | Write permission. | +| **Permission.Write.Users** | array | Users with write permission. | +| **Permission.Write.Groups** | array | Groups with write permission. | +| **Permission.Write.Roles** | array | Roles with write permission. | +| **Permission.Write.Scopes** | array | Scopes with write permission. | +| **Permission.Read** | object | Read permission. | +| **Permission.Read.Users** | array | Users with read permission. | +| **Permission.Read.Groups** | array | Groups with read permission. | +| **Permission.Read.Roles** | array | Roles with read permission. | +| **Permission.Read.Scopes** | array | Scopes with read permission. | +| **Error** | string | The error message, if any. | +| **CallbackSettings** | object | Callback Configuration. | +| **CallbackSettings.UrlField** | string | The URL Field. | +| **CallbackSettings.Method** | string | HTTP Method. Possible values: `Post`, `Put` | +| **CallbackSettings.SendResult** | boolean | Send the result. | +| **Result** | string | The result of the async route task. | +| **AsyncRouteId*** | string | The async route Id. | +| **Progress** | number | Represents the task activity progress. | +| **IsCompleted** | boolean | True when the task is completed. | + +**Note**: Properties marked with `*` are always available. + + +## Features + +- **Timeout**: By default, a timeout of 600 minutes (10 hours) is set for asynchronous tasks, but this can be customized to suit your needs. To remove any timeout, set the value to -1. + +- **Response Types**: You can specify multiple response types for the route. Valid values include `application/json`, `application/xml`, and `application/yaml`. The default is `application/json`. + +- **Runspace Management**: Each async route creates an independent runspace pool that is configurable with a minimum and maximum number of simultaneous runspaces, allowing for efficient resource management and scalability. + - **MaxRunspaces**: The maximum number of Runspaces that can exist in this route. The default is 2. + - **MinRunspaces**: The minimum number of Runspaces that exist in this route. The default is 1. + +- **Callbacks**: Supports including callback functionality for routes. You can specify the callback URL, content type, HTTP method, and header fields. Callbacks can also send the result of the asynchronous operation. + - **Callback URL**: You can define the URL to which the callback should be sent. The default is `'$request.body#/callbackUrl'`. + - **Callback Content Type**: Specify the content type of the callback. The default is `'application/json'`. + - **Callback Method**: Define the HTTP method for the callback. The default is `'Post'`. + - **Callback Header Fields**: Include custom header fields for the callback in the form of a hashtable. + +- **Security**: All async route operations are subject to Pode security, ensuring that any task operation complies with defined authentication and authorization rules. + - **Permissions**: You can specify read and write permissions for each route. This can include specific users, groups, roles, and scopes. + - **Read Access**: Define which users, groups, roles, and scopes have read access. + - **Write Access**: Define which users, groups, roles, and scopes have write access. + +- **Server-Sent Events (SSE)**: Enables real-time updates and seamless async communication through SSE support. + - **Enable SSE**: You can enable SSE for async routes to provide real-time updates. + - **SSE Group**: Optionally group SSE connections to broadcast events to all connections in a specified group. + +- **NotCancellable**: If specified, the async route task cannot be forcefully terminated. This ensures that critical tasks are not interrupted. + +- **IdGenerator**: A custom ScriptBlock to generate a random unique IDs for asynchronous tasks. The default is `{ return (New-PodeGuid) }`. + +- **Automatic OpenAPI Definition**: Routes defined with `Set-PodeAsyncRoute` can automatically generate OpenAPI documentation. This includes response types and callback details, making it easier to document and share your API. + +## Functions for Managing Async Route Tasks + +### Add-PodeAsyncRouteGet + +The `Add-PodeAsyncRouteGet` function creates a route in Pode that allows retrieving the status and details of an asynchronous task. This function supports different methods for task ID retrieval (Cookie, Header, Path, Query) and various response types (JSON, XML, YAML). It integrates with OpenAPI documentation, providing detailed route information and response schemas. + +The task ID name can be changed using the `TaskIdName` parameter. The default name is `taskId`. + +This function accepts almost any parameter applicable to a standard Pode Route. + +#### Example + +```powershell +Add-PodeRoute -PassThru -Method Put -Path '/asyncWait' -ScriptBlock { + Start-Sleep 20 +} | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -Timeout 300 + +Add-PodeAsyncRouteGet -Path '/task' -ResponseContentType 'application/json', 'application/yaml' -In Path | +Set-PodeOARouteInfo -Summary 'Query an Async Route Task' # Set-PodeOARouteInfo is required to get the OpenApi documentation +``` + +#### Usage as a User + +```powershell +$response_asyncWait = Invoke-RestMethod -Uri 'http://localhost:8080/asyncWait' -Method Put + +Invoke-RestMethod -Uri "http://localhost:8080/task?taskId=$($response_asyncWait.Id)" -Method Get +``` +### Add-PodeAsyncRouteStop + +The `Add-PodeAsyncRouteStop` function creates a route in Pode that allows stopping an asynchronous task. This function supports different methods for task ID retrieval (Cookie, Header, Path, Query) and various response types (JSON, XML, YAML). It integrates with OpenAPI documentation, providing detailed route information and response schemas. + +The task ID can be passed as a cookie, header, path, or query, and the name itself can be changed using `Set-PodeAsyncRouteOASchemaName` and the `TaskIdName` parameter. The default name is `id`. + +This function accepts almost any parameter applicable to a standard Pode Route. + +Stopping an asynchronous task sets its state to 'Aborted' and disposes of the associated runspace. + +#### Example + +```powershell +Add-PodeRoute -PassThru -Method Put -Path '/asyncWait' -ScriptBlock { + Start-Sleep 20 +} | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -Timeout 300 + +Add-PodeAsyncRouteStop -Path '/task' -ResponseContentType 'application/json', 'application/yaml' -In Path -PassThru | +Set-PodeOARouteInfo -Summary 'Stop an Async Route Task' # Set-PodeOARouteInfo is required to get the OpenApi documentation +``` + +#### Usage as a User + +```powershell +$response_asyncWait = Invoke-RestMethod -Uri 'http://localhost:8080/asyncWait' -Method Put + +Invoke-RestMethod -Uri "http://localhost:8080/task?taskId=$($response_asyncWait.Id)" -Method Delete +``` + + +### Add-PodeAsyncRouteQuery + +The `Add-PodeAsyncRouteQuery` function creates a route in Pode for querying task information based on specified parameters. This function supports multiple content types for both requests and responses, and can generate OpenAPI documentation. + +This function accepts almost any parameter applicable to a standard Pode Route. + +#### Properties for Query + +The following properties can be used for the query: +- `Id` +- `AsyncRouteId` +- `Output` +- `StartingTime` +- `CreationTime` +- `CompletedTime` +- `ExpireTime` +- `State` +- `Error` +- `CallbackSettings` +- `Cancellable` +- `EnableSse` +- `SseGroup` +- `Timeout` +- `User` +- `Url` +- `Method` +- `Progress` + +#### Valid Operators + +The following operators are valid for use in queries: +- `GT` (Greater Than) +- `LT` (Less Than) +- `GE` (Greater Than or Equal To) +- `LE` (Less Than or Equal To) +- `EQ` (Equal To) +- `NE` (Not Equal To) +- `LIKE` +- `NOTLIKE` + +All conditions in the query are joined together by a logical AND. + +Users can only query objects they are entitled to read. + +#### Example + +```powershell +Add-PodeRoute -PassThru -Method Put -Path '/asyncWait' -ScriptBlock { + Start-Sleep 20 +} | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -Timeout 300 + +Add-PodeAsyncRouteQuery -Path '/tasks/query' -ResponseContentType 'application/json', 'application/yaml' -In Body| +Set-PodeOARouteInfo -Summary 'Query an Async Route Task' # Set-PodeOARouteInfo is required to get the OpenApi documentation +``` + +#### Usage as a User + +##### Example PowerShell Usage + +```powershell +$response_asyncWait = Invoke-RestMethod -Uri 'http://localhost:8080/asyncWait' -Method Put + +$queryBody = @{ + Id = @{ + value = $response_asyncWait.Id + op = "EQ" + } + State = @{ + value = "Completed" + op = "EQ" + } + CreationTime = @{ + value = "7/5/2024 1:20:00 PM" + op = "LE" + } + StartingTime = @{ + value = "7/5/2024 1:20:00 PM" + op = "GT" + } + AsyncRouteId = @{ + value = "Get" + op = "LIKE" + } + Cancellable = @{ + value = $true + op = "EQ" + } +} + +Invoke-RestMethod -Uri "http://localhost:8080/tasks/query" -Method Post -Body ($queryBody | ConvertTo-Json) -ContentType "application/json" +``` + diff --git a/docs/Tutorials/Routes/Async/Utilities/HouseKeeping.md b/docs/Tutorials/Routes/Async/Utilities/HouseKeeping.md new file mode 100644 index 000000000..380c56dec --- /dev/null +++ b/docs/Tutorials/Routes/Async/Utilities/HouseKeeping.md @@ -0,0 +1,45 @@ + +## Housekeeping for Async Route Tasks + +Housekeeping for asynchronous routes in Pode is responsible for maintaining the health and efficiency of asynchronous tasks. This process sets up a timer that periodically cleans up expired or completed asynchronous tasks, ensuring that resources are properly managed and stale tasks are removed from the context. + +### Overview + +The housekeeping process runs a timer, named `__pode_asyncroutes_housekeeper__`, which executes every 30 seconds by default. The primary purpose of this housekeeper is to check and handle expired or completed asynchronous routes. + +### Key Features + +- **Periodic Cleanup**: The housekeeper runs at a configurable interval (default is 30 seconds) to check for and clean up expired or completed tasks. +- **Automatic Disposal**: It ensures that runspaces associated with completed or expired tasks are properly disposed of, freeing up resources. +- **State Management**: Updates the state of tasks to 'Aborted' if they have expired without completing, marking them with a 'Timeout' error. +- **Retention Policy**: Completed tasks are removed based on a retention period specified in minutes. By default, tasks are retained for a specific period before being cleaned up. + +### Configuration + +The configuration can be done using the `server.psd1` configuration file: + +```powershell +@{ + Server = @{ + AsyncRoutes = @{ + HouseKeeping = @{ + TimerInterval = 20 # seconds + RetentionMinutes = 5 # minutes + } + } + } +} +``` + +The default values are: +- `TimerInterval = 30`: The interval in seconds at which the housekeeper runs to perform cleanup tasks. +- `RetentionMinutes = 10`: The duration in minutes for which completed tasks are retained before being removed. + +Usually, no configuration is necessary, as the default settings are sufficient for most use cases. + +**Note**: The `TimerInterval` configuration can be changed but will not be enforced until the server is restarted. + +### Notes + +- The housekeeper function is an internal mechanism and may change in future releases of Pode. +- The function ensures that the system remains efficient by regularly cleaning up unnecessary or stale asynchronous task data. \ No newline at end of file diff --git a/docs/Tutorials/Routes/Async/Utilities/Management.md b/docs/Tutorials/Routes/Async/Utilities/Management.md new file mode 100644 index 000000000..de1d147d0 --- /dev/null +++ b/docs/Tutorials/Routes/Async/Utilities/Management.md @@ -0,0 +1,81 @@ + +## Management Functions + +The management functions in Pode allow you to control and query the status of asynchronous tasks. These functions provide an interface to search, fetch, stop, and check the existence of asynchronous operations within your Pode application. These functions are primarily intended for internal use and are not subject to any permissions or restrictions. + +### Get-PodeAsyncRouteOperationByFilter + +The ` Get-PodeAsyncRouteOperationByFilter` function acts as a public interface for searching asynchronous Pode route operations based on specified query conditions. It allows you to query the status and details of multiple asynchronous tasks based on various parameters. + +` Get-PodeAsyncRouteOperationByFilter` is similar in intent to `Add-PodeAsyncRouteQuery`. The main difference is that this function is used inside the Pode code to manage Async Route Tasks and is not subject to any permissions or restrictions. + +#### Example Usage + +```powershell +$queryConditions = @{ + State = @{ + value = "Running" + op = "EQ" + } + Name = @{ + value = "TaskName" + op = "LIKE" + } +} + +$results = Get-PodeAsyncRouteOperationByFilter -Filter $queryConditions +``` + +#### Explanation + +- **Filter**: A hashtable specifying the query conditions. The keys are the properties of the asynchronous tasks, and the values are hashtables specifying the `value` and `op` (operator) for the query. + +--- + +### Get-PodeAsyncRouteOperation + +The ` Get-PodeAsyncRouteOperation` function fetches details of an asynchronous Pode route operation by its ID. It allows you to retrieve the status, results, and other information about a specific asynchronous task. + +#### Example Usage + +```powershell +$operationDetails = Get-PodeAsyncRouteOperation -Id 'b143660f-ebeb-49d9-9f92-cd21f3ff559c' +``` + +#### Explanation + +- **Id**: The unique identifier of the asynchronous task whose details are to be fetched. + +--- + +### Stop-PodeAsyncRouteOperation + +The `Stop-PodeAsyncRouteOperation` function aborts a specific asynchronous Pode route operation by its ID. It sets the task's state to 'Aborted' and disposes of the associated runspace. Returns a hashtable representing the detailed information of the aborted asynchronous route operation. + +`Stop-PodeAsyncRouteOperation` is similar in intent to `Add-PodeAsyncRouteStop`. The main difference is that this function is used inside the Pode code to manage Async Route Tasks and is not subject to any permissions or restrictions. + +#### Example Usage + +```powershell +$abortedOperationDetails = Stop-PodeAsyncRouteOperation -Id 'b143660f-ebeb-49d9-9f92-cd21f3ff559c' +``` + +#### Explanation + +- **Id**: The unique identifier of the asynchronous task to be aborted. + +--- + +### Test-PodeAsyncRouteOperation + +The `Test-PodeAsyncRouteOperation` function checks if a specific asynchronous Pode route operation exists by its ID, returning a boolean value. + +#### Example Usage + +```powershell +$exists = Test-PodeAsyncRouteOperation -Id 'b143660f-ebeb-49d9-9f92-cd21f3ff559c' +``` + +#### Explanation + +- **Id**: The unique identifier of the asynchronous task to be checked. \ No newline at end of file diff --git a/docs/Tutorials/Routes/Async/Utilities/Progress.md b/docs/Tutorials/Routes/Async/Utilities/Progress.md new file mode 100644 index 000000000..6ce30ef6f --- /dev/null +++ b/docs/Tutorials/Routes/Async/Utilities/Progress.md @@ -0,0 +1,125 @@ + +## Progress + +The Progress functions in Pode allow you to manage and retrieve the progress of asynchronous tasks within your routes. These functions provide real-time feedback on the status of your tasks, making it easier to track and monitor long-running operations. + +**Note**: These functions can only be used inside an AsyncRoute scriptblock. Using them outside of that context will generate an exception. + +### Set-PodeAsyncRouteProgress + +The `Set-PodeAsyncRouteProgress` function manages the progress of an asynchronous task within Pode routes. It allows you to update the progress of a running asynchronous task in various ways, providing real-time feedback on the task's status. + +#### Key Features + +- **Start and End Progress with Ticks**: Define a starting and ending progress value, with optional steps to increment progress. Use ticks to advance the progress in this scenario. +- **Time-based Progress**: Automatically increment progress over a specified duration with interval-based ticks. +- **Set Specific Progress Value**: Directly set the progress to a specific value. +- **SSE Integration**: If `Server-Sent Events (Sse)` is configured for the route, a 'pode.progress' event is automatically sent to the web client, providing real-time updates on the task's progress. + + +#### Example Usage + + + +##### Start and End Progress with Ticks + +```powershell +Add-PodeRoute -PassThru -Method Post -Path '/process-data' -ScriptBlock { + $data = Get-PodeBody + Set-PodeAsyncRouteProgress -Start 0 -End 100 -Steps 5 + + # Simulate long-running task + for ($i = 0; $i -le 100; $i += 5) { + Set-PodeAsyncRouteProgress -Tick + Start-Sleep 1 + } + + return @{ message = "Processing completed" } +} | Set-PodeAsyncRoute +``` + +In this example: +- An async route processes data and sends progress updates using `Set-PodeAsyncRouteProgress`. + +##### Multiple Start and End Progress with Ticks + +```powershell +Add-PodeRoute -PassThru -Method Get -Path '/SumOfSquareRoot' -ScriptBlock { + $start = [int](Get-PodeHeader -Name 'Start') + $end = [int](Get-PodeHeader -Name 'End') + Write-PodeHost "Start=$start End=$end" + Set-PodeAsyncRouteProgress -Start $start -End $end -UseDecimalProgress -MaxProgress 80 + [double]$sum = 0.0 + for ($i = $start; $i -le $end; $i++) { + $sum += [math]::Sqrt($i) + Set-PodeAsyncRouteProgress -Tick + } + Write-PodeHost (Get-PodeAsyncRouteProgress) + Set-PodeAsyncRouteProgress -Start $start -End $end -Steps 4 + for ($i = $start; $i -le $end; $i += 4) { + $sum += [math]::Sqrt($i) + Set-PodeAsyncRouteProgress -Tick + } + Write-PodeHost (Get-PodeAsyncRouteProgress) + Write-PodeHost "Result of Start=$start End=$end is $sum" + return $sum +} | Set-PodeAsyncRoute +``` + +In this example: +- The first progress runs from 0 to 80 with a default step of 1, representing progress as a decimal number. +- The second progress runs from 80 to 100 with a step of 4, also representing progress as a decimal number. + + + +##### Time-based Progress + +```powershell +Add-PodeRoute -PassThru -Method Put -Path 'asyncProgressByTimer' -ScriptBlock { + Set-PodeAsyncRouteProgress -DurationSeconds 30 -IntervalSeconds 1 + for ($i = 0 ; $i -lt 30 ; $i++) { + Start-Sleep 1 + } +} | Set-PodeAsyncRoute +``` + +In this example: +- The progress is automatically incremented over a duration of 30 seconds, with updates every second. + +##### Set Specific Progress Value + +```powershell +Set-PodeAsyncRouteProgress -Value 75 +``` + +#### Parameters + +- **Start**: The starting progress value. +- **End**: The ending progress value. +- **Steps**: The increments between the start and end values. +- **Tick**: Advance progress in a Start-End scenario. +- **DurationSeconds**: The total duration over which progress should be updated. +- **IntervalSeconds**: The interval at which progress should be incremented. +- **MaxProgress**: The maximum progress value. +- **UseDecimalProgress**: Use decimal values for progress. +- **Value**: Directly set the progress to a specific value. + +--- + +### Get-PodeAsyncRouteProgress + +The `Get-PodeAsyncRouteProgress` function retrieves the current progress of an asynchronous route in Pode. It allows you to check the progress of a running asynchronous task. + +**Note**: This function can only be used inside an AsyncRoute scriptblock. Using it outside of that context will generate an exception. + +#### Example Usage + +```powershell +Add-PodeRoute -PassThru -Method Get '/process' { + # Perform some work and update progress + Set-PodeAsyncRouteProgress -Value 40 + # Retrieve the current progress + $progress = Get-PodeAsyncRouteProgress + Write-PodeHost "Current Progress: $progress" +} | Set-PodeAsyncRoute -ResponseContentType 'application/json' +``` \ No newline at end of file diff --git a/docs/Tutorials/Routes/Async/Utilities/SSE.md b/docs/Tutorials/Routes/Async/Utilities/SSE.md new file mode 100644 index 000000000..18c1cd933 --- /dev/null +++ b/docs/Tutorials/Routes/Async/Utilities/SSE.md @@ -0,0 +1,91 @@ +## Server-Sent Events (SSE) + +Server-Sent Events (SSE) can be integrated with asynchronous routes to enable real-time updates to the client. This feature is especially useful for long-running tasks, where clients can receive continuous feedback without needing to poll the server. + +### Key Features + +- **Real-time Server Push**: Sends updates from the server to the client in real-time, keeping the client informed about the progress or completion of asynchronous tasks. +- **Progress and Task Completion Events**: Automatically trigger events like `pode.taskCompleted` to notify clients when a task finishes, with the option to include result data or error details. +- **Customizable SSE Groups**: SSE connections can be grouped, either by the route’s path or by a custom group name, for better organization and management. +- **Send Task Results**: Optionally include the result of the asynchronous task in the SSE event when the task completes. +- **Seamless Async Route Integration**: SSE routes work alongside async routes, enabling instant feedback without additional complexity. + +### Example Use Cases + +#### Real-time Progress Updates + +When processing long-running tasks such as file uploads, data processing, or background calculations, SSE provides a way to continuously send updates to the client. As the task progresses, events are pushed to inform the client of the current state, eliminating the need for polling. + +##### Example: Adding SSE to a Progressing Task + +```powershell +Add-PodeRoute -PassThru -Method Post -Path '/process-data' -ScriptBlock { + $data = Get-PodeBody + Set-PodeAsyncRouteProgress -Start 0 -End 100 -Steps 5 + + # Simulate long-running task + for ($i = 0; $i -le 100; $i += 5) { + Set-PodeAsyncRouteProgress -Tick + Start-Sleep 1 + } + + return @{ message = "Processing completed" } +} | Set-PodeAsyncRoute -MaxRunspaces 2 -PassThru | + Add-PodeAsyncRouteSse -SseGroup 'DataProcess' +``` + +In this example: +- An async route processes data and sends progress updates using `Set-PodeAsyncRouteProgress`. +- The route is enabled for SSE, pushing real-time progress events to clients grouped under 'DataProcess'. + +#### Task Completion and Result Delivery + +Once an async task completes, an SSE event can automatically be sent to the client with the task's result or an error if it failed. This ensures that clients are immediately informed when a task has finished, without requiring them to repeatedly check the task’s status. + +##### Example: Task Completion with Result Transmission + +```powershell +Add-PodeRoute -PassThru -Method Get -Path '/generate-report' -ScriptBlock { + # Simulate long-running report generation + Start-Sleep 10 + return @{ report = "Report generation completed" } +} | Set-PodeAsyncRoute -MaxRunspaces 3 -PassThru | + Add-PodeAsyncRouteSse -SendResult +``` + +In this example: +- A GET route generates a report asynchronously. +- The SSE sends the final report result to the client upon task completion. + +#### Example: Sending Custom SSE Events with Delays + +```powershell +Add-PodeRoute -PassThru -Method Get -Path '/sse' -ScriptBlock { + + # First SSE event with a message about the current time + $msg = "Start - Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" + Send-PodeSseEvent -Data $msg -FromEvent + + # Simulate a delay between messages (10 seconds) + Start-Sleep -Seconds 10 + + # Final SSE event after all operations are done + $msg = "End - Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" + Send-PodeSseEvent -Data $msg -FromEvent + + # Return a JSON response to the client indicating the operation is complete + return @{ message = 'Done' } +} | Set-PodeAsyncRoute -ResponseContentType 'application/json' -MaxRunspaces 3 -PassThru | + Add-PodeAsyncRouteSse -SseGroup 'Test events' -SendResult +``` + +In this example: +- Custom SSE events are sent to the client at specific intervals with messages that include the current time. +- After a delay, a final SSE event is sent, and the client receives a JSON response once the task is complete. + +### Parameters + +- **Route**: A hashtable array representing the async route(s) to which SSE functionality will be added. This parameter is mandatory and accepts pipeline input. +- **PassThru**: If specified, the modified route object is returned after adding the SSE route. +- **SseGroup**: The group name for the SSE connection. If not provided, the route’s path will be used as the group name. +- **SendResult**: If specified, the result of the async operation will be sent to the client when the task completes. \ No newline at end of file diff --git a/docs/Tutorials/Routes/Overview.md b/docs/Tutorials/Routes/Overview.md index a9acd7efe..33923abae 100644 --- a/docs/Tutorials/Routes/Overview.md +++ b/docs/Tutorials/Routes/Overview.md @@ -281,6 +281,9 @@ The following is the structure of the Route object internally, as well as the ob | OpenApi | hashtable[] | The OpenAPI definition/settings for the route | | Path | string | The path of the route - this path will have regex in place of route parameters | | TransferEncoding | string | The transfer encoding to use when parsing the payload in the request | +| IsAsync | bool | The route is Async | +| Async | hashtable | Contains the information for the route when is Async mode | + Static routes have a slightly different format: diff --git a/docs/Tutorials/Routes/Utilities/ContentTypes.md b/docs/Tutorials/Routes/Utilities/ContentTypes.md index 57816670b..cd3ba56e1 100644 --- a/docs/Tutorials/Routes/Utilities/ContentTypes.md +++ b/docs/Tutorials/Routes/Utilities/ContentTypes.md @@ -18,11 +18,11 @@ Start-PodeServer { Write-PodeJsonResponse -Value @{} } - Add-PodeRoute -Method Get -Path '/api/xml' -ContentType 'text/xml' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/api/xml' -ContentType 'application/xml' -ScriptBlock { Write-PodeXmlResponse -Value @{} } - Add-PodeRoute -Method Get -Path '/api/yaml' -ContentType 'text/yaml' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/api/yaml' -ContentType 'application/yaml' -ScriptBlock { Write-PodeYamlResponse -Value @{} } } diff --git a/docs/index.md b/docs/index.md index 25d0d9dd9..9ad26ffed 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,7 +25,7 @@ Pode is a Cross-Platform framework to create web servers that host REST APIs, We * OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf * Listen on a single or multiple IP(v4/v6) addresses/hostnames * Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S) -* Host REST APIs, Web Pages, and Static Content (with caching) +* Host REST APIs,Async REST APIs Web Pages, and Static Content (with caching) * Support for custom error pages * Request and Response compression using GZip/Deflate * Multi-thread support for incoming requests @@ -47,7 +47,7 @@ Pode is a Cross-Platform framework to create web servers that host REST APIs, We * In-memory caching, with optional support for external providers (such as Redis) * (Windows) Open the hosted server as a desktop application * FileBrowsing support -* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, and Chinese +* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, Dutch, and Chinese ## šŸ¢ Companies using Pode diff --git a/examples/AsyncRoute/index.html b/examples/AsyncRoute/index.html new file mode 100644 index 000000000..0f729dabd --- /dev/null +++ b/examples/AsyncRoute/index.html @@ -0,0 +1,45 @@ + + + + + + + SSE Test Launcher + + + + +

SSE Test Launcher

+

Click the button below to launch the SSE test page.

+ + + + + + + diff --git a/examples/AsyncRoute/sse_test.html b/examples/AsyncRoute/sse_test.html new file mode 100644 index 000000000..0cb43a72b --- /dev/null +++ b/examples/AsyncRoute/sse_test.html @@ -0,0 +1,183 @@ + + + + + + + EventSource Example + + + +

EventSource Demo

+

Listening for events...

+
+ + + + + \ No newline at end of file diff --git a/examples/OpenApi-TuttiFrutti.ps1 b/examples/OpenApi-TuttiFrutti.ps1 index 5b70abed6..d6659568b 100644 --- a/examples/OpenApi-TuttiFrutti.ps1 +++ b/examples/OpenApi-TuttiFrutti.ps1 @@ -320,9 +320,9 @@ Some useful links: New-PodeOAStringProperty -Format Date-Time -Description 'date in UTC when token expires' | Add-PodeOAComponentHeader -Name 'X-Expires-After' #define '#/components/responses/' - Add-PodeOAComponentResponse -Name 'UserOpSuccess' -Description 'Successful operation' -ContentSchemas (@{'application/json' = 'User' ; 'application/xml' = 'User' }) + Add-PodeOAComponentResponse -Name 'UserOpSuccess' -Description 'Successful operation' -ContentSchemas ([ordered]@{'application/json' = 'User' ; 'application/xml' = 'User' }) - Add-PodeOAComponentRequestBody -Name 'PetBodySchema' -Required -Description 'Pet in the store' -ContentSchemas (@{ 'application/json' = 'Pets'; 'application/xml' = 'Pets'; 'application/x-www-form-urlencoded' = 'Pets' }) + Add-PodeOAComponentRequestBody -Name 'PetBodySchema' -Required -Description 'Pet in the store' -ContentSchemas ([ordered]@{'application/json' = 'Pets'; 'application/xml' = 'Pets'; 'application/x-www-form-urlencoded' = 'Pets' }) #define '#/components/parameters/' @@ -435,11 +435,11 @@ Some useful links: Set-PodeOARequest -Parameters @( (New-PodeOAStringProperty -Name 'petId' -Description 'ID of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Path -Required) ) -RequestBody ( - New-PodeOARequestBody -Description 'user to add to the system' -Content @{ 'application/json' = 'User'; 'application/xml' = 'User' } -Examples $ex + New-PodeOARequestBody -Description 'user to add to the system' -Content ([ordered]@{'application/json' = 'User'; 'application/xml' = 'User' }) -Examples $ex ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) -PassThru | - Add-PodeOAResponse -StatusCode 405 -Description 'Method Not Allowed' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) + Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content ([ordered]@{'application/json' = '' ; 'application/xml' = '' }) -PassThru | + Add-PodeOAResponse -StatusCode 405 -Description 'Method Not Allowed' -Content ([ordered]@{'application/json' = '' ; 'application/xml' = '' }) @@ -466,8 +466,8 @@ Some useful links: ) }) ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) -PassThru | - Add-PodeOAResponse -StatusCode 405 -Description 'Method Not Allowed' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) + Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content ([ordered]@{'application/json' = '' ; 'application/xml' = '' }) -PassThru | + Add-PodeOAResponse -StatusCode 405 -Description 'Method Not Allowed' -Content ([ordered]@{'application/json' = '' ; 'application/xml' = '' }) #Start 3.1 @@ -484,7 +484,7 @@ Some useful links: Add-PodeOAWebhook -Name 'newPet' -Method Post -PassThru | Set-PodeOARouteInfo -Description 'Information about a new pet in the system' -PassThru | Set-PodeOARequest -PassThru -RequestBody ( - New-PodeOARequestBody -Content @{ 'application/json' = 'Pets' } + New-PodeOARequestBody -Content @{'application/json' = 'Pets' } ) | Add-PodeOAResponse -StatusCode 200 -Description 'Return a 200 status to indicate that the data was received successfully' } @@ -501,8 +501,8 @@ Some useful links: Set-PodeOARequest -Parameters @( (New-PodeOAStringProperty -Name 'petId' -Description 'ID of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Path -Required) ) -RequestBody (New-PodeOARequestBody -Description 'user to add to the system' -Content @{ 'text/plain' = New-PodeOAStringProperty -array } ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) -PassThru | - Add-PodeOAResponse -StatusCode 405 -Description 'Method Not Allowed' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) + Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content ([ordered]@{'application/json' = '' ; 'application/xml' = '' }) -PassThru | + Add-PodeOAResponse -StatusCode 405 -Description 'Method Not Allowed' -Content ([ordered]@{'application/json' = '' ; 'application/xml' = '' }) @@ -525,14 +525,14 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Updates a pet in the store with form data' -Tags 'pet' -OperationId 'updatepaet3' -PassThru | Set-PodeOARequest -Parameters @( (New-PodeOAStringProperty -Name 'petId' -Description 'ID of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Path -Required) - ) -RequestBody (New-PodeOARequestBody -Description 'user to add to the system' -Content @{ 'application/json' = 'NewCat' } -Examples ( + ) -RequestBody (New-PodeOARequestBody -Description 'user to add to the system' -Content @{'application/json' = 'NewCat' } -Examples ( New-PodeOAExample -ContentType 'application/json' -Name 'cat' -Summary 'An example of a cat' -Value @{name = 'Fluffy'; petType = 'Cat'; color = 'White'; gender = 'male'; breed = 'Persian' } | New-PodeOAExample -ContentType 'application/json' -Name 'dog' -Summary "An example of a dog with a cat's name" -Value @{name = 'Puma'; petType = 'Dog'; color = 'Black'; gender = 'Female'; breed = 'Mixed' } | New-PodeOAExample -ContentType 'application/json' -Reference 'frog-example' ) ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) -PassThru | - Add-PodeOAResponse -StatusCode 4XX -Description 'Method Not Allowed' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) + Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content ([ordered]@{'application/json' = '' ; 'application/xml' = '' }) -PassThru | + Add-PodeOAResponse -StatusCode 4XX -Description 'Method Not Allowed' -Content ([ordered]@{'application/json' = '' ; 'application/xml' = '' }) Add-PodeRoute -PassThru -Method Put -Path '/paet4/:petId' -ScriptBlock { @@ -546,7 +546,7 @@ Some useful links: } | Set-PodeOARouteInfo -Summary 'Updates a pet in the store with form data' -Tags 'pet' -OperationId 'updatepaet4' -PassThru | Set-PodeOARequest -Parameters @( (New-PodeOAStringProperty -Name 'petId' -Description 'ID of pet that needs to be updated' | ConvertTo-PodeOAParameter -In Path -Required -ContentType 'application/json') - ) -RequestBody (New-PodeOARequestBody -Description 'user to add to the system' -Content @{ 'application/json' = 'Pet' } -Examples ( + ) -RequestBody (New-PodeOARequestBody -Description 'user to add to the system' -Content @{'application/json' = 'Pet' } -Examples ( New-PodeOAExample -ContentType 'application/json' -Name 'cat' -Summary 'An example of a cat' -Value @{name = 'Fluffy'; petType = 'Cat'; color = 'White'; gender = 'male'; breed = 'Persian' } | New-PodeOAExample -ContentType 'application/json' -Name 'dog' -Summary "An example of a dog with a cat's name" -Value @{name = 'Puma'; petType = 'Dog'; color = 'Black'; gender = 'Female'; breed = 'Mixed' } | New-PodeOAExample -ContentType 'application/json' -Reference 'frog-example' @@ -554,7 +554,7 @@ Some useful links: ) -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Pet updated.' -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml' -Content '') -PassThru | - Add-PodeOAResponse -StatusCode 405 -Description 'Method Not Allowed' -Content (@{ 'application/json' = '' ; 'application/xml' = '' }) + Add-PodeOAResponse -StatusCode 405 -Description 'Method Not Allowed' -Content ([ordered]@{'application/json' = '' ; 'application/xml' = '' }) } } @@ -702,7 +702,7 @@ Some useful links: Write-PodeJsonResponse -Value 'done' -StatusCode 200 } | Set-PodeOARouteInfo -Summary 'Find pet by ID' -Description 'Returns a single pet.' -Tags 'pet' -OperationId 'getPetById' -PassThru | Set-PodeOARequest -PassThru -Parameters @( ConvertTo-PodeOAParameter -Reference 'PetIdParam' ) | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (@{ 'application/json' = 'Pet' ; 'application/xml' = 'Pet' }) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content ([ordered]@{'application/json' = 'Pet' ; 'application/xml' = 'Pet' }) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'Pet not found' @@ -831,14 +831,14 @@ Some useful links: Add-PodeRoute -PassThru -Method Get -Path '/store/inventory' -ScriptBlock { Write-PodeJsonResponse -Value 'done' -StatusCode 200 } | Set-PodeOARouteInfo -Summary 'Returns pet inventories by status' -Description 'Returns a map of status codes to quantities' -Tags 'store' -OperationId 'getInventory' -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = New-PodeOAObjectProperty -Properties @(New-PodeOAStringProperty -Name 'none' ) } #missing additionalProperties + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = New-PodeOAObjectProperty -Properties @(New-PodeOAStringProperty -Name 'none' ) } #missing additionalProperties Add-PodeRoute -PassThru -Method post -Path '/store/order' -ScriptBlock { Write-PodeJsonResponse -Value 'done' -StatusCode 200 } | Set-PodeOARouteInfo -Deprecated -Summary 'Place an order for a pet' -Description 'Place a new order in the store' -Tags 'store' -OperationId 'placeOrder' -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Required -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' )) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (@{ 'application/json' = 'Order' ; 'application/xml' = 'Order' }) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content ([ordered]@{'application/json' = 'Order' ; 'application/xml' = 'Order' }) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' Add-PodeRoute -PassThru -Method Get -Path '/store/order/:orderId' -ScriptBlock { diff --git a/examples/PetStore/Petstore-OpenApi.ps1 b/examples/PetStore/Petstore-OpenApi.ps1 index 983df0ff8..42222184e 100644 --- a/examples/PetStore/Petstore-OpenApi.ps1 +++ b/examples/PetStore/Petstore-OpenApi.ps1 @@ -572,7 +572,7 @@ Some useful links: Write-PodeJsonResponse -Value $result -StatusCode 200 } | Set-PodeOARouteInfo -Summary 'Returns pet inventories by status' -Description 'Returns a map of status codes to quantities' -Tags 'store' -OperationId 'getInventory' -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = New-PodeOAObjectProperty -AdditionalProperties (New-PodeOAIntProperty -Format Int32 ) } + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = New-PodeOAObjectProperty -AdditionalProperties (New-PodeOAIntProperty -Format Int32 ) } <# @@ -607,7 +607,7 @@ Some useful links: } } | Set-PodeOARouteInfo -Summary 'Place an order for a pet' -Description 'Place a new order in the store' -Tags 'store' -OperationId 'placeOrder' -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' )) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (@{ 'application/json' = 'Order' }) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (@{'application/json' = 'Order' }) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' <# diff --git a/examples/PetStore/Petstore-OpenApiMultiTag.ps1 b/examples/PetStore/Petstore-OpenApiMultiTag.ps1 index 3e5c6ca91..2b9d27f6a 100644 --- a/examples/PetStore/Petstore-OpenApiMultiTag.ps1 +++ b/examples/PetStore/Petstore-OpenApiMultiTag.ps1 @@ -569,7 +569,7 @@ Some useful links: Write-PodeJsonResponse -Value $result -StatusCode 200 } | Set-PodeOARouteInfo -Summary 'Returns pet inventories by status' -Description 'Returns a map of status codes to quantities' -Tags 'store' -OperationId 'getInventory' -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = New-PodeOAObjectProperty -AdditionalProperties (New-PodeOAIntProperty -Format Int32 ) } + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = New-PodeOAObjectProperty -AdditionalProperties (New-PodeOAIntProperty -Format Int32 ) } <# @@ -604,7 +604,7 @@ Some useful links: } } | Set-PodeOARouteInfo -Summary 'Place an order for a pet' -Description 'Place a new order in the store' -Tags 'store' -OperationId 'placeOrder' -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Content (New-PodeOAContentMediaType -ContentType 'application/json', 'application/xml', 'application/x-www-form-urlencoded' -Content 'Order' )) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (@{ 'application/json' = 'Order' }) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (@{'application/json' = 'Order' }) -PassThru | Add-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' <# diff --git a/examples/Web-AsyncRoute.ps1 b/examples/Web-AsyncRoute.ps1 new file mode 100644 index 000000000..c1373b400 --- /dev/null +++ b/examples/Web-AsyncRoute.ps1 @@ -0,0 +1,398 @@ +<# +.SYNOPSIS + A script to either run a Pode server with various endpoints or to send multiple REST requests to the server. + +.DESCRIPTION + This script sets up a Pode server with multiple endpoints demonstrating asynchronous operations and authorization. + It also includes examples of how to send REST requests to the server. + +.PARAMETER Port + The port on which the Pode server will listen. Default is 8080. + +.PARAMETER Quiet + Suppresses output when the server is running. + +.PARAMETER DisableTermination + Prevents the server from being terminated. + +.EXAMPLE + .\Web-AsyncRoute.ps1 -Port 9090 -Quiet -DisableTermination + +.EXAMPLE + # Example of using the endpoints with Invoke-RestMethod + $mortyCommonHeaders = @{ + 'accept' = 'application/json' + 'X-API-KEY' = 'test-api-key' + 'Authorization' = 'Basic bW9ydHk6cGlja2xl' + } + + $mindyCommonHeaders = @{ + 'accept' = 'application/json' + 'X-API-KEY' = 'test2-api-key' + 'Authorization' = 'Basic bWluZHk6cGlja2xl' + } + + $response_asyncUsingNotCancellable = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncUsingNotCancellable' -Method Put -Headers $mortyCommonHeaders + $response_asyncUsingCancellable = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncUsingCancellable' -Method Put -Headers $mortyCommonHeaders + + $body = @{ + callbackUrl = 'http://localhost:8080/receive/callback' + } | ConvertTo-Json + + $headersWithContentType = $mortyCommonHeaders.Clone() + $headersWithContentType['Content-Type'] = 'application/json' + + $response_asyncUsing = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncUsing' -Method Put -Headers $headersWithContentType -Body $body + + $response_asyncState = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncState' -Method Put -Headers $mortyCommonHeaders + + $response_asyncParam = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncParam' -Method Put -Headers $mortyCommonHeaders + + $response_asyncWaitForeverTimeout = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncInfiniteLoopTimeout' -Method Put -Headers $mortyCommonHeaders + + $response = Invoke-RestMethod -Uri 'http://localhost:8080/tasks' -Method Post -Body '{}' -Headers $mortyCommonHeaders + + + + $response_Mindy_asyncWaitForever = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncInfiniteLoop' -Method Put -Headers $mindyCommonHeaders + + $response_Mindy_asyncUsingNotCancellable = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncUsingNotCancellable' -Method Put -Headers $mindyCommonHeaders + $response_Mindy_asyncUsingCancellable = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncUsingCancellable' -Method Put -Headers $mindyCommonHeaders + $response_Mindy_asyncStateNoColumn = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncStateNoColumn' -Method Put -Headers $mindyCommonHeaders + + $headersWithContentType = $mindyCommonHeaders.Clone() + $headersWithContentType['Content-Type'] = 'application/json' + $response_Mindy_asyncUsing = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncUsing' -Method Put -Headers $headersWithContentType -Body $body + + $response_Mindy_asyncState = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncState' -Method Put -Headers $mindyCommonHeaders + + $response_Mindy_asyncParam = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncParam' -Method Put -Headers $mindyCommonHeaders + + $response_Mindy_asyncWaitForeverTimeout = Invoke-RestMethod -Uri 'http://localhost:8080/auth/asyncInfiniteLoopTimeout' -Method Put -Headers $mindyCommonHeaders + + $response = Invoke-RestMethod -Uri 'http://localhost:8080/tasks' -Method Post -Body '{}' -Headers $mindyCommonHeaders + + $response_Mindy_asyncWaitForever = Invoke-RestMethod -Uri "http://localhost:8080/task?Id=$($response_Mindy_asyncWaitForever.Id)" -Method Delete -Headers $mindyCommonHeaders + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AsyncRoute.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +param( + [Parameter()] + [int] + $Port = 8080, + [switch] + $Quiet, + [switch] + $DisableTermination +) + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +<# +# Demostrates Lockables, Mutexes, and Semaphores +#> + +Start-PodeServer -Threads 1 -Quiet:$Quiet -DisableTermination:$DisableTermination { + + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http -DualMode + New-PodeLoggingMethod -name 'async' -File -Path "$ScriptPath/logs" | Enable-PodeErrorLogging + + # request logging + # New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging + + + Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -EnableSchemaValidation:$($PSVersionTable.PSVersion -ge [version]'6.1.0') -DisableMinimalDefinitions -NoDefaultResponses + Enable-PodeOpenApi -Path '/docs/openapi/v3.1' -OpenApiVersion '3.1.0' -EnableSchemaValidation:$($PSVersionTable.PSVersion -ge [version]'6.1.0') -DefinitionTag 'v3.1' -DisableMinimalDefinitions -NoDefaultResponses + + Add-PodeOAInfo -Title 'Async test - OpenAPI 3.0' -Version 0.0.2 + Add-PodeOAInfo -Title 'Async test - OpenAPI 3.1' -Version 0.0.2 -DefinitionTag 'v3.1' + + Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' + Enable-PodeOAViewer -Type Swagger -Path '/docs3.1/swagger' -DefinitionTag 'v3.1' + + Enable-PodeOAViewer -Editor -Path '/docs/swagger-editor' + Enable-PodeOAViewer -Bookmarks -Path '/docs' + Enable-PodeOAViewer -Bookmarks -Path '/docs3.1' -DefinitionTag 'v3.1' + $uSleepTime = 4 + $uMessage = 'coming from using' + + # $global:gMessage = 'coming from global' + # $global:gSleepTime = 3 + Set-PodeState -Name 'data' -Value @{ + sleepTime = 5 + Message = 'coming from a PodeState' + } + + + # setup access + New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'Rbac' + New-PodeAccessScheme -Type Group | Add-PodeAccess -Name 'Gbac' + + # setup a merged access + Merge-PodeAccess -Name 'MergedAccess' -Access 'Rbac', 'Gbac' -Valid All + + $testApiKeyUsers = @{ + 'M0R7Y302' = @{ + Id = 'M0R7Y302' + Name = 'Morty' + Type = 'Human' + Roles = @('Manager') + Groups = @('Software') + } + 'MINDY021' = @{ + Id = 'MINDY021' + Name = 'Mindy' + Type = 'AI' + Roles = @('Developer') + Groups = @('Support') + } + } + + + $testBasicUsers = @{ + 'M0R7Y302' = @{ + Id = 'M0R7Y302' + Name = 'Morty' + Type = 'Human' + Roles = @('Developer') + Groups = @('Platform') + } + 'MINDY021' = @{ + Id = 'MINDY021' + Name = 'Mindy' + Type = 'AI' + Roles = @('Developer') + Groups = @('Software') + } + } + + + + # setup apikey auth + New-PodeAuthScheme -ApiKey -Location Header | Add-PodeAuth -Name 'ApiKey' -Sessionless -ScriptBlock { + param($key) + + # here you'd check a real user storage, this is just for example + if ($key -ieq 'test-api-key') { + return @{ + User = ($using:testApiKeyUsers).M0R7Y302 + } + } + if ($key -ieq 'test2-api-key') { + return @{ + User = ($using:testApiKeyUsers).MINDY021 + } + } + + return $null + } + + # setup basic auth (base64> username:password in header) + New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Basic' -Sessionless -ScriptBlock { + param($username, $password) + + # here you'd check a real user storage, this is just for example + if ($username -eq 'morty' -and $password -eq 'pickle') { + return @{ + User = ($using:testBasicUsers).M0R7Y302 + } + } + + if ($username -eq 'mindy' -and $password -eq 'pickle') { + return @{ + User = ($using:testBasicUsers).MINDY021 + } + } + + return @{ Message = 'Invalid details supplied' } + } + + # merge the auths together + Merge-PodeAuth -Name 'MergedAuth' -Authentication 'ApiKey', 'Basic' -Valid All -ScriptBlock { + param($results) + + $apiUser = $results['ApiKey'].User + $basicUser = $results['Basic'].User + + return @{ + User = @{ + Id = $apiUser.Id + Name = $apiUser.Name + Type = $apiUser.Type + Roles = @($apiUser.Roles + $basicUser.Roles) | Sort-Object -Unique + Groups = @($apiUser.Groups + $basicUser.Groups) | Sort-Object -Unique + } + } + } + + Add-PodeRoute -Method 'Post' -Path '/close' -ScriptBlock { + Close-PodeServer + } -PassThru | Set-PodeOARouteInfo -Summary 'Shutdown the server' -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' + + Add-PodeRoute -PassThru -Method Put -Path '/auth/asyncUsing' -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Software' -ScriptBlock { + Write-PodeHost '/auth/asyncUsing' + Write-PodeHost "sleepTime=$($using:uSleepTime)" + Write-PodeHost "Message=$($using:uMessage)" + Start-Sleep $using:uSleepTime + return @{ InnerValue = $using:uMessage } + } | Set-PodeOARouteInfo -Summary 'Async with callback with Using variable' -OperationId 'asyncUsingCallback' -DefinitionTag 'Default', 'v3.1' -PassThru | + Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -Timeout 300 -PassThru | + Add-PodeAsyncRouteCallback -PassThru -SendResult | Set-PodeOARequest -RequestBody ( + New-PodeOARequestBody -Content @{'application/json' = (New-PodeOAStringProperty -Name 'callbackUrl' -Format Uri -Object -Example 'http://localhost:8080/receive/callback') } + ) + + + Add-PodeRoute -PassThru -Method Put -Path '/auth/asyncState' -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Software' -ScriptBlock { + Write-PodeHost '/auth/asyncState' + Write-PodeHost "state:sleepTime=$($state:data.sleepTime)" + Write-PodeHost "state:MessageTest=$($state:data.Message)" + for ($i = 0; $i -lt 10; $i++) { + Start-Sleep $state:data.sleepTime + } + return @{ InnerValue = $state:data.Message } + } | Set-PodeOARouteInfo -Summary 'Async with State variable' -OperationId 'asyncState' -PassThru | + Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -Timeout 300 + + + + Add-PodeRoute -PassThru -Method Put -Path '/auth/asyncStateNoColumn' -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Support' -ScriptBlock { + Write-PodeHost '/auth/asyncStateNoColumn' + $data = Get-PodeState -Name 'data' + Write-PodeHost 'data:' + Write-PodeHost $data -Explode -ShowType + for ($i = 0; $i -lt 10; $i++) { + Start-Sleep $data.sleepTime + } + return @{ InnerValue = $data.Message } + } | Set-PodeOARouteInfo -Summary 'Async with State variable NoColumn' -OperationId 'asyncStateNoColumn' -PassThru | + Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -Timeout 300 + + + + + Add-PodeRoute -PassThru -Method Put -Path '/auth/asyncParam' -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Software' -ScriptBlock { + param($sleepTime2, $Message) + Write-PodeHost '/auth/asyncParam' + Write-PodeHost "sleepTime2=$sleepTime2" + Write-PodeHost "Message=$Message" + + for ($i = 0; $i -lt 10; $i++) { + Start-Sleep $sleepTime2 + } + return @{ InnerValue = $Message } + } -ArgumentList @{sleepTime2 = 2; Message = 'comming as argument' } | + Set-PodeOARouteInfo -Summary 'Async with Parameters' -OperationId 'asyncParameters' -PassThru | + Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -Timeout 300 + + + Add-PodeRoute -PassThru -Method Put -Path '/auth/asyncUsingNotCancellable' -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Software' -ScriptBlock { + Write-PodeHost '/auth/asyncUsingNotCancellable' + Write-PodeHost "sleepTime=$($using:uSleepTime * 5)" + Write-PodeHost "Message=$($using:uMessage)" + #write-podehost $WebEvent.auth.User -Explode + Start-Sleep ($using:uSleepTime * 10) + return @{ InnerValue = $using:uMessage } + } | Set-PodeOARouteInfo -Summary 'Async with Using variable Not Cancellable' -OperationId 'asyncUsingNotCancellable' -PassThru | + Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -NotCancellable -Timeout 300 + + Add-PodeRoute -PassThru -Method Put -Path '/auth/asyncUsingCancellable' -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Software' -ScriptBlock { + Write-PodeHost '/auth/asyncUsingCancellable' + Write-PodeHost "sleepTime=$($using:uSleepTime * 5)" + Write-PodeHost "Message=$($using:uMessage)" + #write-podehost $WebEvent.auth.User -Explode + Start-Sleep ($using:uSleepTime * 10) + return @{ InnerValue = $using:uMessage } + } | Set-PodeOARouteInfo -Summary 'Async with Using variable Cancellable' -OperationId 'asyncUsingCancellable' -PassThru | + Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' + + + Add-PodeRoute -PassThru -Method Put -Path '/auth/asyncInfiniteLoop' -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Software' -ScriptBlock { + while ($true) { + Start-Sleep 2 + } + } | Set-PodeOARouteInfo -Summary 'Async infinite loop' -OperationId 'asyncInfiniteLoop' -PassThru | + Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -Timeout 300 + + + + Add-PodeRoute -PassThru -Method Put -Path '/auth/asyncInfiniteLoopTimeout' -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Software' -ScriptBlock { + while ($true) { + Start-Sleep 2 + } + } | Set-PodeOARouteInfo -Summary 'Async infinite loop with Timeout' -OperationId 'asyncInfiniteLoopTimeout' -PassThru | + Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -Timeout 40 -NotCancellable + + + Add-PodeRoute -PassThru -Method Put -Path '/auth/asyncProgressByTimer' -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Software' -ScriptBlock { + Set-PodeAsyncRouteProgress -DurationSeconds 30 -IntervalSeconds 1 + for ($i = 0 ; $i -lt 30 ; $i++) { + Start-Sleep 1 + } + } | Set-PodeOARouteInfo -Summary 'Async with Progress By Timer' -OperationId 'asyncProgressByTimer' -PassThru | + Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -Timeout 300 -MaxRunspaces 10 + + Add-PodeRoute -PassThru -Method Get -path '/SumOfSquareRoot' -ScriptBlock { + $start = [int]( Get-PodeHeader -Name 'Start') + $end = [int]( Get-PodeHeader -Name 'End') + Write-PodeHost "Start=$start End=$end" + Set-PodeAsyncRouteProgress -Start $start -End $End -UseDecimalProgress -MaxProgress 80 + [double]$sum = 0.0 + for ($i = $Start; $i -le $End; $i++) { + $sum += [math]::Sqrt($i ) + Set-PodeAsyncRouteProgress -Tick + } + Write-PodeHost (Get-PodeAsyncRouteProgress) + Set-PodeAsyncRouteProgress -Start $start -End $End -Steps 4 + for ($i = $Start; $i -le $End; $i += 4) { + $sum += [math]::Sqrt($i ) + Set-PodeAsyncRouteProgress -Tick + } + + Write-PodeHost (Get-PodeAsyncRouteProgress) + Write-PodeHost "Result of Start=$start End=$end is $sum" + return $sum + } | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -MaxRunspaces 10 -MinRunspaces 5 -PassThru | Set-PodeOARouteInfo -Summary 'Calculate sum of square roots' -PassThru | + Set-PodeOARequest -PassThru -Parameters ( + ( New-PodeOANumberProperty -Name 'Start' -Format Double -Description 'Start' -Required | ConvertTo-PodeOAParameter -In Header), + ( New-PodeOANumberProperty -Name 'End' -Format Double -Description 'End' -Required | ConvertTo-PodeOAParameter -In Header) + ) | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = New-PodeOANumberProperty -Name 'Result' -Format Double -Description 'Result' -Required -Object } + + + Add-PodeAsyncRouteGet -Path '/task' -ResponseContentType 'application/json', 'application/yaml' -In Path -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Software' -PassThru | Set-PodeOARouteInfo -Summary 'Get Async Route Task Info' + + Add-PodeAsyncRouteStop -Path '/task' -ResponseContentType 'application/json', 'application/yaml' -In Query -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Software' -OADefinitionTag 'Default', 'v3.1' -PassThru | Set-PodeOARouteInfo -Summary 'Stop Async Route Task' + + Add-PodeAsyncRouteQuery -path '/tasks' -ResponseContentType 'application/json', 'application/yaml' -Payload Body -QueryContentType 'application/json', 'application/yaml' -Authentication 'MergedAuth' -Access 'MergedAccess' -Group 'Software' -PassThru | Set-PodeOARouteInfo -Summary 'Query Async Route Task Info' + + Add-PodeRoute -PassThru -Method Post -path '/receive/callback' -ScriptBlock { + write-podehost 'Callback received' + write-podehost $WebEvent.Data -Explode + } + + + Add-PodeRoute -Method 'Get' -Path '/hello' -ScriptBlock { + Write-PodeJsonResponse -Value @{'message' = 'Hello!' } -StatusCode 200 + } -PassThru | Set-PodeOARouteInfo -Summary 'Hello from the server' -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' + +} \ No newline at end of file diff --git a/examples/Web-AsyncRouteBenchmark.ps1 b/examples/Web-AsyncRouteBenchmark.ps1 new file mode 100644 index 000000000..bf2d87f58 --- /dev/null +++ b/examples/Web-AsyncRouteBenchmark.ps1 @@ -0,0 +1,358 @@ +<# +.SYNOPSIS + A script to either run a Pode server with various endpoints or to run a client that makes requests to the server. + +.DESCRIPTION + This script can be executed in two modes: Server mode and Client mode. + - In Server mode, it sets up a Pode server with multiple endpoints to calculate the sum of squares using different methods. + - In Client mode, it makes parallel requests to the server endpoints to calculate the sum of squares. + +.PARAMETER Port + The port on which the Pode server will listen. Default is 8080. + +.PARAMETER Quiet + Suppresses output when the server is running. Used only in Server mode. + +.PARAMETER DisableTermination + Prevents the server from being terminated. Used only in Server mode. + +.PARAMETER MaxRunspaces + The maximum number of Runspaces that can exist in this route. The default is 50. + +.PARAMETER Client + Switch to run the script in Client mode. + +.PARAMETER StepSize + The size of each step for the calculations in Client mode. Default is 10,000,000. + +.PARAMETER ThrottleLimit + The maximum number of parallel requests in Client mode. Default is 20. + +.PARAMETER Endpoint + The endpoint to be used for requests in Client mode. Default is 'SumOfSquaresInCSharp'. + +.EXAMPLE + .\Web-AsyncRouteBenchmark.ps1 -Client -StepSize 1000000 -ThrottleLimit 10 -Endpoint 'SumOfSquaresNoLoop' + +.EXAMPLE + .\Web-AsyncRouteBenchmark.ps1 -Port 9090 -Quiet -DisableTermination + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AsyncRouteBenchmark.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> +[CmdletBinding(DefaultParameterSetName = 'Server')] +param( + [Parameter()] + [int] + $Port = 8080, + + [Parameter(Mandatory = $false, ParameterSetName = 'Server')] + [switch] + $Quiet, + + [Parameter(Mandatory = $false, ParameterSetName = 'Server')] + [switch] + $DisableTermination, + + [Parameter()] + [ValidateRange(1, 100)] + [int] + $MaxRunspaces = 50, + + [Parameter(Mandatory = $true, ParameterSetName = 'Client')] + [switch] + $Client, + + [Parameter(Mandatory = $false, ParameterSetName = 'Client')] + [ValidateRange(1, [int]::MaxValue)] + [int] + $StepSize = 10000000, + + [Parameter(Mandatory = $false, ParameterSetName = 'Client')] + [ValidateRange(1, 100)] + [int] + $ThrottleLimit = 20, + + [Parameter(Mandatory = $false, ParameterSetName = 'Client')] + [ValidateSet('SumOfSquares', 'SumOfSquaresInCSharp', 'SumOfSquaresDotSourcing', 'SumOfSquaresNoLoop', 'SumOfSquaresPSM1')] + [string]$Endpoint = 'SumOfSquaresInCSharp' +) + +if ($Client) { + $totalSteps = [math]::Floor([int]::MaxValue / ($StepSize )) + Write-Progress -Id 1 -ParentId 0 -Activity 'Overall Progress' -Status "Invoking Rest" -PercentComplete 0 + $jobs = 0..$totalSteps | ForEach-Object -Parallel { + + $i = ($_ ) * ($using:StepSize ) + $squareHeader = @{ + Start = $i + End = ($i + $using:StepSize) + + } + if ($squareHeader.End -le [int]::MaxValue) { + Write-Information "[$_]/using:totalSteps) [$using:StepSize+$i]" + $result = Invoke-RestMethod -Uri "http://localhost:$($using:Port)/$($using:Endpoint)" -Method Get -Headers $squareHeader + return $result + } + + } -ThrottleLimit $ThrottleLimit + + + + # Wait for all jobs to complete with a progress bar + $jobCount = $jobs.Count + $allJobsCompleted = $false + Write-Progress -Id 1 -ParentId 0 -Activity 'Overall Progress' -Status "Waiting for Jobs to complete" -PercentComplete 0 + while (! $allJobsCompleted) { + $allJobsCompleted = $true + $completedJobs = 0 + foreach ($job in $jobs) { + $jobStatus = Invoke-RestMethod -Uri "http://localhost:$Port/task/$($job.Id)" -Method Get + if ( $jobStatus.IsCompleted) { + $completedJobs++ + } + Write-Progress -Id 1 -ParentId 0 -Activity 'Overall Progress' -Status "Waiting for Jobs to complete" -PercentComplete (($completedJobs / $jobCount) * 100) + } + } + + # Clear the progress bars + Write-Progress -Id 1 -ParentId 0 -Activity 'Overall Progress' -Status 'All jobs completed.' -Completed + + return $jobs +} +else { + try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } + } + catch { throw } + + # or just: + # Import-Module Pode + + # Get the temporary directory path + $tempDir = [System.IO.Path]::GetTempPath() + + # Define the file path + $filePath = Join-Path -Path $tempDir -ChildPath 'SumOfSquares.ps1' + # Define the function content + $functionContent = @' +function SumOfSquares { + param ( + [int]$Start, + [int]$End + + ) + [double] $sum = 0 + for ($i = $Start; $i -le $End; $i++) { + $sum += [math]::Pow($i, 2) + } + return $sum +} +'@ + + # Write the function content to the file + Set-Content -Path $filePath -Value $functionContent + + + $SumOfSquaresModulefilePath = Join-Path -Path $tempDir -ChildPath 'SumOfSquares.psm1' + # Define the function content + $functionContent = @' +function SumOfSquaresModule { + param ( + [int]$Start, + [int]$End + + ) + [double] $sum = 0 + for ($i = $Start; $i -le $End; $i++) { + $sum += [math]::Pow($i, 2) + } + return $sum +} +Export-ModuleMember -Function SumOfSquaresModule +'@ + + # Write the function content to the file + Set-Content -Path $SumOfSquaresModulefilePath -Value $functionContent + + + Start-PodeServer -Threads 30 -Quiet:$Quiet -DisableTermination:$DisableTermination { + Import-PodeModule -Path $SumOfSquaresModulefilePath + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http -DualMode + # request logging + New-PodeLoggingMethod -name 'async_computing_error' -File -Path "$ScriptPath/logs" | Enable-PodeErrorLogging + + New-PodeLoggingMethod -name 'async_computing_request' -File -Path "$ScriptPath/logs" | Enable-PodeRequestLogging + + Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -DisableMinimalDefinitions -NoDefaultResponses + + Add-PodeOAInfo -Title 'Async Computing - OpenAPI 3.0' -Version 0.0.1 + + Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' + + Enable-PodeOAViewer -Editor -Path '/docs/swagger-editor' + Enable-PodeOAViewer -Bookmarks -Path '/docs' + + Add-PodeRoute -PassThru -Method Get -path '/SumOfSquares' -ScriptBlock { + function SumOfSquares { + param ( + [int]$Start, + [int]$End + + ) + [double] $sum = 0 + for ($i = $Start; $i -le $End; $i++) { + $sum += [math]::Pow($i, 2) + } + return $sum + } + + $start = [int]( Get-PodeHeader -Name 'Start') + $end = [int]( Get-PodeHeader -Name 'End') + Write-PodeHost "Start=$start End=$end" + [double] $sum = SumOfSquares -Start $Start -End $End + Write-PodeHost "Result of Start=$start End=$end is $sum" + return $sum + } | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -MaxRunspaces $MaxRunspaces -MinRunspaces 5 -PassThru | Set-PodeOARouteInfo -Summary 'Caluclate sum of squares' -PassThru | + Set-PodeOARequest -PassThru -Parameters ( + ( New-PodeOANumberProperty -Name 'Start' -Format Double -Description 'Start' -Required | ConvertTo-PodeOAParameter -In Header), + ( New-PodeOANumberProperty -Name 'End' -Format Double -Description 'End' -Required | ConvertTo-PodeOAParameter -In Header) + ) | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = New-PodeOANumberProperty -Name 'Result' -Format Double -Description 'Result' -Required -Object } + + + + Add-PodeRoute -PassThru -Method Get -path '/SumOfSquaresPSM1' -ScriptBlock { + + $start = [int]( Get-PodeHeader -Name 'Start') + $end = [int]( Get-PodeHeader -Name 'End') + Write-PodeHost "Start=$start End=$end" + [double] $sum = SumOfSquaresModule -Start $Start -End $End + Write-PodeHost "Result of Start=$start End=$end is $sum" + return $sum + } | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -MaxRunspaces $MaxRunspaces -MinRunspaces 5 -PassThru | Set-PodeOARouteInfo -Summary 'Caluclate sum of squares' -PassThru | + Set-PodeOARequest -PassThru -Parameters ( + ( New-PodeOANumberProperty -Name 'Start' -Format Double -Description 'Start' -Required | ConvertTo-PodeOAParameter -In Header), + ( New-PodeOANumberProperty -Name 'End' -Format Double -Description 'End' -Required | ConvertTo-PodeOAParameter -In Header) + ) | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = New-PodeOANumberProperty -Name 'Result' -Format Double -Description 'Result' -Required -Object } + + + + Add-PodeRoute -PassThru -Method Get -path '/SumOfSquaresDotSourcing' -ScriptBlock { + + . (Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'SumOfSquares.ps1') + $start = [int]( Get-PodeHeader -Name 'Start') + $end = [int]( Get-PodeHeader -Name 'End') + Write-PodeHost "Start=$start End=$end" + [double] $sum = SumOfSquares -Start $Start -End $End + Write-PodeHost "Result of Start=$start End=$end is $sum" + return $sum + } | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -MaxRunspaces $MaxRunspaces -MinRunspaces 2 -PassThru | Set-PodeOARouteInfo -Summary 'Caluclate sum of squares' -PassThru | + Set-PodeOARequest -PassThru -Parameters ( + ( New-PodeOANumberProperty -Name 'Start' -Format Double -Description 'Start' -Required | ConvertTo-PodeOAParameter -In Header), + ( New-PodeOANumberProperty -Name 'End' -Format Double -Description 'End' -Required | ConvertTo-PodeOAParameter -In Header) + ) | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = New-PodeOANumberProperty -Name 'Result' -Format Double -Description 'Result' -Required -Object } + + + + Add-PodeRoute -PassThru -Method Get -path '/SumOfSquaresNoLoop' -ScriptBlock { + $start = [int]( Get-PodeHeader -Name 'Start') + $end = [int]( Get-PodeHeader -Name 'End') + Write-PodeHost "Start=$start End=$end" + + # Calculate the sum of squares from 1 to $End + $n = $End + [double]$sumEnd = ($n * ($n + 1) * (2 * $n + 1)) / 6 + + # Calculate the sum of squares from 1 to $Start-1 + $m = $Start - 1 + [double]$sumStart = ($m * ($m + 1) * (2 * $m + 1)) / 6 + + # The sum of squares from $Start to $End + [double]$sum = $sumEnd - $sumStart + Write-PodeHost "Result of Start=$start End=$end is $sum" + return $sum + } | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -MaxRunspaces $MaxRunspaces -MinRunspaces 5 -PassThru | Set-PodeOARouteInfo -Summary 'Caluclate sum of squares' -PassThru | + Set-PodeOARequest -PassThru -Parameters ( + ( New-PodeOANumberProperty -Name 'Start' -Format Double -Description 'Start' -Required | ConvertTo-PodeOAParameter -In Header), + ( New-PodeOANumberProperty -Name 'End' -Format Double -Description 'End' -Required | ConvertTo-PodeOAParameter -In Header) + ) | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = New-PodeOANumberProperty -Name 'Result' -Format Double -Description 'Result' -Required -Object } + + + + + + Add-PodeRoute -PassThru -Method Get -path '/SumOfSquaresInCSharp' -ScriptBlock { + Add-Type -TypeDefinition @' +public class MathOperations +{ + public static double SumOfSquares(int start, int end) + { + double sum = 0; + for (int i = start; i <= end; i++) + { + sum += (long)System.Math.Pow(i, 2); + } + return sum; + } +} +'@ + $start = [int]( Get-PodeHeader -Name 'Start') + $end = [int]( Get-PodeHeader -Name 'End') + Write-PodeHost "C# code - Start=$start End=$end" + $sum = [MathOperations]::SumOfSquares($Start, $End) + Write-PodeHost "Result of Start=$start End=$end is $sum" + return $sum + } | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -MaxRunspaces $MaxRunspaces -MinRunspaces 5 -PassThru | Set-PodeOARouteInfo -Summary 'Caluclate sum of squares' -PassThru | + Set-PodeOARequest -PassThru -Parameters ( + ( New-PodeOANumberProperty -Name 'Start' -Format Double -Description 'Start' -Required | ConvertTo-PodeOAParameter -In Header), + ( New-PodeOANumberProperty -Name 'End' -Format Double -Description 'End' -Required | ConvertTo-PodeOAParameter -In Header) + ) | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = New-PodeOANumberProperty -Name 'Result' -Format Double -Description 'Result' -Required -Object } + + + Add-PodeRoute -PassThru -Method Get -path '/SumOfSquareRoot' -ScriptBlock { + $start = [int]( Get-PodeHeader -Name 'Start') + $end = [int]( Get-PodeHeader -Name 'End') + Write-PodeHost "Start=$start End=$end" + [double]$sum = 0.0 + for ($i = $Start; $i -le $End; $i++) { + $sum += [math]::Sqrt($i ) + } + Write-PodeHost "Result of Start=$start End=$end is $sum" + return $sum + } | Set-PodeAsyncRoute -ResponseContentType 'application/json', 'application/yaml' -MaxRunspaces $MaxRunspaces -MinRunspaces 5 -PassThru | Set-PodeOARouteInfo -Summary 'Caluclate sum of square roots' -PassThru | + Set-PodeOARequest -PassThru -Parameters ( + ( New-PodeOANumberProperty -Name 'Start' -Format Double -Description 'Start' -Required | ConvertTo-PodeOAParameter -In Header), + ( New-PodeOANumberProperty -Name 'End' -Format Double -Description 'End' -Required | ConvertTo-PodeOAParameter -In Header) + ) | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = New-PodeOANumberProperty -Name 'Result' -Format Double -Description 'Result' -Required -Object } + + + Add-PodeAsyncRouteGet -Path '/task' -ResponseContentType 'application/json', 'application/yaml' -In Path + Add-PodeAsyncRouteStop -Path '/task' -ResponseContentType 'application/json', 'application/yaml' -In Query + + Add-PodeAsyncRouteQuery -path '/tasks' -ResponseContentType 'application/json', 'application/yaml' -Payload Body -QueryContentType 'application/json', 'application/yaml' + + + Add-PodeRoute -Method 'Post' -Path '/close' -ScriptBlock { + Close-PodeServer + } -PassThru | Set-PodeOARouteInfo -Summary 'Shutdown the server' + + Add-PodeRoute -Method 'Get' -Path '/hello' -ScriptBlock { + Write-PodeJsonResponse -Value @{'message' = 'Hello!' } -StatusCode 200 + } -PassThru | Set-PodeOARouteInfo -Summary 'Hello from the server' + + } +} \ No newline at end of file diff --git a/examples/Web-AsyncRouteSse.ps1 b/examples/Web-AsyncRouteSse.ps1 new file mode 100644 index 000000000..1dc6a9d46 --- /dev/null +++ b/examples/Web-AsyncRouteSse.ps1 @@ -0,0 +1,92 @@ +<# +.SYNOPSIS + A script that either runs a Pode server with asynchronous SSE endpoints or sends multiple REST requests to the server. + +.DESCRIPTION + This script demonstrates how to set up a Pode server with endpoints that include asynchronous operations and + Server-Sent Events (SSE). It provides examples of handling asynchronous requests and sending REST calls to + interact with the server's SSE-based routes. + + The Html pages used by the sample are: + - /AsyncRoutes/index.html + - /AsyncRoutes/sse_test.html + +.EXAMPLE + .\Web-AsyncRouteSse.ps1 + + Starts the Pode server with asynchronous routes and SSE events enabled. + +.EXAMPLE + Open a web browser that supports the SSE protocol (e.g., Google Chrome) and navigate to + http://localhost:8081/ to see the SSE demo in action. + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AsyncRouteSse.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# or just: +# Import-Module Pode + +# Start the Pode server with 6 threads +Start-PodeServer -Threads 6 { + + # Add an HTTP endpoint on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -DualMode + # Enable logging for asynchronous SSE events, logs stored in the script's path under /logs + New-PodeLoggingMethod -name 'asyncSse' -File -Path "$ScriptPath/logs" | Enable-PodeErrorLogging + + # Define an asynchronous SSE route at the '/sse' path + Add-PodeRoute -PassThru -Method Get -Path '/sse' -ScriptBlock { + # Set progress for the asynchronous route, simulating progress updates over 23 seconds + Set-PodeAsyncRouteProgress -IntervalSeconds 2 -DurationSeconds 23 -MaxProgress 100 + + # First SSE event with a message about the current time + $msg = "Start - Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" + Send-PodeSseEvent -Data $msg -FromEvent + + # Simulate a delay between messages (10 seconds) + for ($i = 0; $i -lt 10; $i++) { + Start-Sleep -Seconds 1 + } + + # Second SSE event with a new message after the delay + $msg = "InTheMiddle - Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" + Send-PodeSseEvent -Data $msg -FromEvent + + # Another delay between the second and final messages + for ($i = 0; $i -lt 10; $i++) { + Start-Sleep -Seconds 1 + } + + # Final SSE event after all operations are done + $msg = "End - Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" + Send-PodeSseEvent -Data $msg -FromEvent + + # Return a JSON response to the client indicating the operation is complete + return @{'message' = 'Done' } + } | Set-PodeAsyncRoute -ResponseContentType 'application/json' -MaxRunspaces 4 -PassThru | + Add-PodeAsyncRouteSse -SseGroup 'Test events' -SendResult + + # Add a static route to serve files located in the /AsyncRoute directory + Add-PodeStaticRoute -Path '/' -File "$ScriptPath/AsyncRoute" + +} \ No newline at end of file diff --git a/examples/server.psd1 b/examples/server.psd1 index ec5b15edf..0697dc512 100644 --- a/examples/server.psd1 +++ b/examples/server.psd1 @@ -19,6 +19,7 @@ Default = 'application/html' Routes = @{ '/john' = 'application/json' + '/auth' = 'application/json' } } Compression = @{ @@ -64,6 +65,19 @@ Enable = $true } } + AsyncRoutes = @{ + UserFieldIdentifier = 'Id' + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + Tasks = @{ + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 1 + } + } AllowedActions = @{ Suspend = $true # Enable or disable the suspend operation Restart = $true # Enable or disable the restart operation diff --git a/src/Listener/PodeResponse.cs b/src/Listener/PodeResponse.cs index bd4e54e13..79175ff33 100644 --- a/src/Listener/PodeResponse.cs +++ b/src/Listener/PodeResponse.cs @@ -86,6 +86,34 @@ public PodeResponse(PodeContext context) OutputStream = new MemoryStream(); Context = context; } + + //Clone constructor + public PodeResponse(PodeResponse other) + { + // Copy the status code and other scalar values + StatusCode = other.StatusCode; + SendChunked = other.SendChunked; + IsDisposed = other.IsDisposed; + SseScope = other.SseScope; + SentHeaders = other.SentHeaders; + SentBody = other.SentBody; + _statusDesc = other._statusDesc; + + // Create a new memory stream and copy the content of the other stream + OutputStream = new MemoryStream(); + other.OutputStream.CopyTo(OutputStream); + + // Copy the headers (assuming PodeResponseHeaders supports cloning or deep copy) + Headers = new PodeResponseHeaders(); + foreach (var key in other.Headers.Keys) + { + Headers.Set(key, other.Headers[key]); + } + + // Copy the context and request, or create new instances if necessary (context should probably be reused) + Context = other.Context; + } + public async Task Send() { @@ -197,7 +225,7 @@ public async Task Flush() } } - public async Task SetSseConnection(PodeSseScope scope, string clientId, string name, string group, int retry, bool allowAllOrigins) + public async Task SetSseConnection(PodeSseScope scope, string clientId, string name, string group, int retry, bool allowAllOrigins, string asyncRouteTaskId = null) { // do nothing for no scope if (scope == PodeSseScope.None) @@ -237,7 +265,11 @@ public async Task SetSseConnection(PodeSseScope scope, string clientId, // send headers, and open event await Send().ConfigureAwait(false); await SendSseRetry(retry).ConfigureAwait(false); - await SendSseEvent("pode.open", $"{{\"clientId\":\"{clientId}\",\"group\":\"{group}\",\"name\":\"{name}\"}}").ConfigureAwait(false); + string sseEvent = (string.IsNullOrEmpty(asyncRouteTaskId)) ? + $"{{\"clientId\":\"{clientId}\",\"group\":\"{group}\",\"name\":\"{name}\"}}" : + $"{{\"clientId\":\"{clientId}\",\"group\":\"{group}\",\"name\":\"{name}\",\"asyncRouteTaskId\":\"{asyncRouteTaskId}\"}}"; + + await SendSseEvent("pode.open", sseEvent).ConfigureAwait(false); // if global, cache connection in listener if (scope == PodeSseScope.Global) diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index d9617f469..884fe0222 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -211,7 +211,7 @@ viewsFolderNameAlreadyExistsExceptionMessage = 'اسم مجلد العرض Ł…ŁˆŲ¬ŁˆŲÆ بالفعل: {0}' noNameForWebSocketResetExceptionMessage = 'لا يوجد اسم ل؄عادة ŲŖŲ¹ŁŠŁŠŁ† WebSocket من Ų§Ł„Ł…Ų²ŁˆŲÆ.' mergeDefaultAuthNotInListExceptionMessage = "المصادقة MergeDefault '{0}' غير Ł…ŁˆŲ¬ŁˆŲÆŲ© في قائمة المصادقة المقدمة." - descriptionRequiredExceptionMessage = 'Ł…Ų·Ł„ŁˆŲØ وصف للمسار: {0} الاستجابة: {1}' + descriptionRequiredExceptionMessage = 'Ų§Ł„ŁˆŲµŁ Ł…Ų·Ł„ŁˆŲØ.' pageNameShouldBeAlphaNumericExceptionMessage = 'يجب أن ŁŠŁƒŁˆŁ† اسم الصفحة Ł‚ŁŠŁ…Ų© أبجدية Ų±Ł‚Ł…ŁŠŲ© صالحة: {0}' defaultValueNotBooleanOrEnumExceptionMessage = 'Ų§Ł„Ł‚ŁŠŁ…Ų© Ų§Ł„Ų§ŁŲŖŲ±Ų§Ų¶ŁŠŲ© Ł„ŁŠŲ³ŲŖ من Ł†ŁˆŲ¹ boolean ŁˆŁ„ŁŠŲ³ŲŖ جزًؔا من التعداد.' openApiComponentSchemaDoesNotExistExceptionMessage = 'Ł…Ų®Ų·Ų· Ł…ŁƒŁˆŁ† OpenApi {0} غير Ł…ŁˆŲ¬ŁˆŲÆ.' @@ -283,11 +283,21 @@ adModuleWindowsOnlyExceptionMessage = 'وحدة Active Directory Ł…ŲŖŲ§Ų­Ų© فقط على نظام Windows.' requestLoggingAlreadyEnabledExceptionMessage = 'ŲŖŁ… ŲŖŁ…ŁƒŁŠŁ† ŲŖŲ³Ų¬ŁŠŁ„ الطلبات بالفعل.' invalidAccessControlMaxAgeDurationExceptionMessage = 'Ł…ŲÆŲ© Access-Control-Max-Age غير صالحة المقدمة: {0}. يجب أن ŲŖŁƒŁˆŁ† أكبر من 0.' + invalidQueryFormatExceptionMessage = 'الاستعلام المقدم له ŲŖŁ†Ų³ŁŠŁ‚ غير صالح.' openApiDefinitionAlreadyExistsExceptionMessage = 'تعريف OpenAPI باسم {0} Ł…ŁˆŲ¬ŁˆŲÆ بالفعل.' renamePodeOADefinitionTagExceptionMessage = "لا ŁŠŁ…ŁƒŁ† Ų§Ų³ŲŖŲ®ŲÆŲ§Ł… Rename-PodeOADefinitionTag داخل Select-PodeOADefinition 'ScriptBlock'." + asyncIdDoesNotExistExceptionMessage = 'Async {0} غير Ł…ŁˆŲ¬ŁˆŲÆ.' + asyncRouteOperationDoesNotExistExceptionMessage = 'لا توجد Ų¹Ł…Ł„ŁŠŲ© Ł…Ų³Ų§Ų± غير متزامن بالمعرف {0}.' + scriptContainsDisallowedCommandExceptionMessage = "لا ŁŠŁŲ³Ł…Ų­ للبرنامج Ų§Ł„Ł†ŲµŁŠ باحتواؔ الأمر '{0}'." + invalidQueryElementExceptionMessage = 'الاستعلام المقدم غير صالح. {0} Ł„ŁŠŲ³ عنصرًا صالحًا للاستعلام.' + setPodeAsyncProgressExceptionMessage = 'ŁŠŁ…ŁƒŁ† Ų§Ų³ŲŖŲ®ŲÆŲ§Ł… Set-PodeAsyncRouteProgress فقط داخل ŁƒŲŖŁ„Ų© Ł†ŲµŁŠŲ© لمسار غير متزامن.' + progressLimitLowerThanCurrentExceptionMessage = 'لا ŁŠŁ…ŁƒŁ† أن ŁŠŁƒŁˆŁ† Ų­ŲÆ التقدم أقل من التقدم Ų§Ł„Ų­Ų§Ł„ŁŠ.' taskProcessDoesNotExistExceptionMessage = 'Ų¹Ł…Ł„ŁŠŲ© المهمة غير Ł…ŁˆŲ¬ŁˆŲÆŲ©: {0}' scheduleProcessDoesNotExistExceptionMessage = 'Ų¹Ł…Ł„ŁŠŲ© Ų§Ł„Ų¬ŲÆŁˆŁ„ Ų§Ł„Ų²Ł…Ł†ŁŠ غير Ł…ŁˆŲ¬ŁˆŲÆŲ©: {0}' definitionTagChangeNotAllowedExceptionMessage = 'لا ŁŠŁ…ŁƒŁ† تغيير علامة Ų§Ł„ŲŖŲ¹Ų±ŁŠŁ لمسار.' + openApiDefinitionsMismatchExceptionMessage = '{0} ŁŠŲ®ŲŖŁ„Ł ŲØŁŠŁ† تعريفات OpenAPI المختلفة.' + routeNotMarkedAsAsyncExceptionMessage = "المسار '{0}' لم ŁŠŲŖŁ… وضع علامة Ų¹Ł„ŁŠŁ‡ ŁƒŁ…Ų³Ų§Ų± غير متزامن." + functionCannotBeInvokedMultipleTimesExceptionMessage = "لا ŁŠŁ…ŁƒŁ† Ų§Ų³ŲŖŲÆŲ¹Ų§Ų” الدالة '{0}' Ų¹ŲÆŲ© Ł…Ų±Ų§ŲŖ لنفس المسار '{1}'." getRequestBodyNotAllowedExceptionMessage = "'{0}' لا ŁŠŁ…ŁƒŁ† أن يحتوي على جسم الطلب. Ų§Ų³ŲŖŲ®ŲÆŁ… -AllowNonStandardBody Ł„ŲŖŲ¬Ų§ŁˆŲ² هذا Ų§Ł„ŲŖŁ‚ŁŠŁŠŲÆ." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "الدالة '{0}' لا تقبل Ł…ŲµŁŁˆŁŲ© ŁƒŁ…ŲÆŲ®Ł„ Ł„Ų£Ł†ŲØŁˆŲØ Ų§Ł„ŲØŁŠŲ§Ł†Ų§ŲŖ." unsupportedStreamCompressionEncodingExceptionMessage = 'تؓفير الضغط غير Ł…ŲÆŲ¹ŁˆŁ… Ł„Ł„ŲŖŲ“ŁŁŠŲ± {0}' @@ -326,4 +336,4 @@ rateLimitRuleDoesNotExistExceptionMessage = 'قاعدة الحد الأقصى للمعدل غير Ł…ŁˆŲ¬ŁˆŲÆŲ©: {0}' accessLimitRuleAlreadyExistsExceptionMessage = 'ŲŖŁ… تعريف قاعدة الحد الأقصى Ł„Ł„ŁˆŲµŁˆŁ„ بالفعل: {0}' accessLimitRuleDoesNotExistExceptionMessage = 'قاعدة الحد الأقصى Ł„Ł„ŁˆŲµŁˆŁ„ غير Ł…ŁˆŲ¬ŁˆŲÆŲ©: {0}' -} +} \ No newline at end of file diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index d85929f70..4e3ed38bc 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -211,7 +211,7 @@ viewsFolderNameAlreadyExistsExceptionMessage = 'Der Name des Ansichtsordners existiert bereits: {0}' noNameForWebSocketResetExceptionMessage = 'Kein Name für das Zurücksetzen des WebSocket angegeben.' mergeDefaultAuthNotInListExceptionMessage = "Die MergeDefault-Authentifizierung '{0}' befindet sich nicht in der angegebenen Authentifizierungsliste." - descriptionRequiredExceptionMessage = 'Eine Beschreibung ist erforderlich für Pfad:{0} Antwort:{1}' + descriptionRequiredExceptionMessage = 'Eine Beschreibung ist erforderlich.' pageNameShouldBeAlphaNumericExceptionMessage = 'Der Seitenname sollte einen gültigen alphanumerischen Wert haben: {0}' defaultValueNotBooleanOrEnumExceptionMessage = 'Der Standardwert ist kein Boolean und gehƶrt nicht zum Enum.' openApiComponentSchemaDoesNotExistExceptionMessage = 'Das OpenApi-Komponentenschema {0} existiert nicht.' @@ -285,9 +285,19 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Ungültige Access-Control-Max-Age-Dauer angegeben: {0}. Sollte größer als 0 sein.' openApiDefinitionAlreadyExistsExceptionMessage = 'Die OpenAPI-Definition mit dem Namen {0} existiert bereits.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag kann nicht innerhalb eines 'ScriptBlock' von Select-PodeOADefinition verwendet werden." + invalidQueryFormatExceptionMessage = 'Die angegebene Abfrage hat ein ungültiges Format.' + asyncIdDoesNotExistExceptionMessage = 'Async {0} existiert nicht.' + asyncRouteOperationDoesNotExistExceptionMessage = 'Keine Async-Route-Operation mit der Id {0} existiert.' + scriptContainsDisallowedCommandExceptionMessage = "Das Skript darf den Befehl '{0}' nicht enthalten." + invalidQueryElementExceptionMessage = 'Die bereitgestellte Abfrage ist ungültig. {0} ist kein gültiges Element für eine Abfrage.' + setPodeAsyncProgressExceptionMessage = 'Set-PodeAsyncRouteProgress kann nur innerhalb eines Async-Route-Skriptblocks verwendet werden.' + progressLimitLowerThanCurrentExceptionMessage = 'Ein Fortschrittslimit darf nicht niedriger als der aktuelle Fortschritt sein.' taskProcessDoesNotExistExceptionMessage = "Der Aufgabenprozess '{0}' existiert nicht." scheduleProcessDoesNotExistExceptionMessage = "Der Aufgabenplanerprozess '{0}' existiert nicht." definitionTagChangeNotAllowedExceptionMessage = 'Definitionstag für eine Route kann nicht geƤndert werden.' + openApiDefinitionsMismatchExceptionMessage = '{0} variiert zwischen verschiedenen OpenAPI-Definitionen.' + routeNotMarkedAsAsyncExceptionMessage = "Die Route '{0}' ist nicht als asynchrone Route markiert." + functionCannotBeInvokedMultipleTimesExceptionMessage = "Die Funktion '{0}' kann nicht mehrmals für dieselbe Route '{1}' aufgerufen werden." getRequestBodyNotAllowedExceptionMessage = "'{0}' Operationen dürfen keinen Anfragekƶrper haben. Verwenden Sie -AllowNonStandardBody, um diese EinschrƤnkung zu umgehen." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Die Funktion '{0}' akzeptiert kein Array als Pipeline-Eingabe." unsupportedStreamCompressionEncodingExceptionMessage = 'Die Stream-Komprimierungskodierung wird nicht unterstützt: {0}' diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index c2ac6d2b0..166cde8ea 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -285,9 +285,19 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Invalid Access-Control-Max-Age duration supplied: {0}. Should be greater than 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI definition named {0} already exists.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag cannot be used inside a Select-PodeOADefinition 'ScriptBlock'." + invalidQueryFormatExceptionMessage = 'The query provided has an invalid format.' + asyncIdDoesNotExistExceptionMessage = "Async {0} doesn't exist." + asyncRouteOperationDoesNotExistExceptionMessage = 'No Async Route operation exists with Id {0}.' + scriptContainsDisallowedCommandExceptionMessage = "Script is not allowed to contain the command '{0}'." + invalidQueryElementExceptionMessage = 'The query provided is invalid. {0} is not a valid element for a query.' + setPodeAsyncProgressExceptionMessage = 'Set-PodeAsyncRouteProgress can only be used inside an Async Route Scriptblock.' + progressLimitLowerThanCurrentExceptionMessage = 'A Progress limit cannot be lower than the current progress.' taskProcessDoesNotExistExceptionMessage = 'Task process does not exist: {0}' scheduleProcessDoesNotExistExceptionMessage = 'Schedule process does not exist: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Definition Tag for a Route cannot be changed.' + openApiDefinitionsMismatchExceptionMessage = '{0} varies between different OpenAPI definitions.' + routeNotMarkedAsAsyncExceptionMessage = "The route '{0}' is not marked as an Async Route." + functionCannotBeInvokedMultipleTimesExceptionMessage = "The function '{0}' cannot be invoked multiple times for the same route '{1}'." getRequestBodyNotAllowedExceptionMessage = "'{0}' operations cannot have a Request Body. Use -AllowNonStandardBody to override this restriction." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index fc04db0d2..83a5356df 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -288,6 +288,16 @@ taskProcessDoesNotExistExceptionMessage = 'Task process does not exist: {0}' scheduleProcessDoesNotExistExceptionMessage = 'Schedule process does not exist: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Definition Tag for a Route cannot be changed.' + InvalidQueryFormatExceptionMessage = 'The query provided has an invalid format.' + asyncIdDoesNotExistExceptionMessage = "Async {0} doesn't exist." + asyncRouteOperationDoesNotExistExceptionMessage = 'No Async Route operation exists with Id {0}.' + scriptContainsDisallowedCommandExceptionMessage = "Script is not allowed to contain the command '{0}'." + invalidQueryElementExceptionMessage = 'The query provided is invalid. {0} is not a valid element for a query.' + setPodeAsyncProgressExceptionMessage = 'Set-PodeAsyncRouteProgress can only be used inside an Async Route Scriptblock.' + progressLimitLowerThanCurrentExceptionMessage = 'A Progress limit cannot be lower than the current progress.' + openApiDefinitionsMismatchExceptionMessage = '{0} varies between different OpenAPI definitions.' + routeNotMarkedAsAsyncExceptionMessage = "The route '{0}' is not marked as an Async Route." + functionCannotBeInvokedMultipleTimesExceptionMessage = "The function '{0}' cannot be invoked multiple times for the same route '{1}'." getRequestBodyNotAllowedExceptionMessage = "'{0}' operations cannot have a Request Body. Use -AllowNonStandardBody to override this restriction." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index d7db64e6c..720748fd4 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -285,9 +285,19 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Duración invĆ”lida para Access-Control-Max-Age proporcionada: {0}. Debe ser mayor que 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'La definición de OpenAPI con el nombre {0} ya existe.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag no se puede usar dentro de un 'ScriptBlock' de Select-PodeOADefinition." + invalidQueryFormatExceptionMessage = 'La consulta proporcionada tiene un formato no vĆ”lido.' + asyncIdDoesNotExistExceptionMessage = 'Async {0} no existe.' + asyncRouteOperationDoesNotExistExceptionMessage = 'No existe ninguna operación de ruta asĆ­ncrona con Id {0}.' + scriptContainsDisallowedCommandExceptionMessage = "El script no puede contener el comando '{0}'." + invalidQueryElementExceptionMessage = 'La consulta proporcionada no es vĆ”lida. {0} no es un elemento vĆ”lido para una consulta.' + setPodeAsyncProgressExceptionMessage = 'Set-PodeAsyncRouteProgress solo se puede usar dentro de un Scriptblock de Ruta AsĆ­ncrona.' + progressLimitLowerThanCurrentExceptionMessage = 'Un lĆ­mite de progreso no puede ser inferior al progreso actual.' taskProcessDoesNotExistExceptionMessage = "El proceso de la tarea '{0}' no existe." scheduleProcessDoesNotExistExceptionMessage = "El proceso del programación '{0}' no existe." definitionTagChangeNotAllowedExceptionMessage = 'La etiqueta de definición para una Route no se puede cambiar.' + openApiDefinitionsMismatchExceptionMessage = '{0} varĆ­a entre diferentes definiciones de OpenAPI.' + routeNotMarkedAsAsyncExceptionMessage = "La ruta '{0}' no estĆ” marcada como una Ruta AsĆ­ncrona." + functionCannotBeInvokedMultipleTimesExceptionMessage = "La función '{0}' no se puede invocar varias veces para la misma ruta '{1}'." getRequestBodyNotAllowedExceptionMessage = "Las operaciones '{0}' no pueden tener un cuerpo de solicitud. Use -AllowNonStandardBody para evitar esta restricción." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La función '{0}' no acepta una matriz como entrada de canalización." unsupportedStreamCompressionEncodingExceptionMessage = 'La codificación de compresión de transmisión no es compatible: {0}' diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index a0d769eab..eea26f955 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -285,9 +285,19 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'DurĆ©e Access-Control-Max-Age invalide fournie : {0}. Doit ĆŖtre supĆ©rieure Ć  0.' openApiDefinitionAlreadyExistsExceptionMessage = 'La dĆ©finition OpenAPI nommĆ©e {0} existe dĆ©jĆ .' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag ne peut pas ĆŖtre utilisĆ© Ć  l'intĆ©rieur d'un 'ScriptBlock' de Select-PodeOADefinition." + invalidQueryFormatExceptionMessage = 'La requĆŖte fournie a un format invalide.' + asyncIdDoesNotExistExceptionMessage = "Async {0} n'existe pas." + asyncRouteOperationDoesNotExistExceptionMessage = "Aucune opĆ©ration de route asynchrone n'existe avec l'Id {0}." + scriptContainsDisallowedCommandExceptionMessage = "Le script ne peut pas contenir la commande '{0}'." + invalidQueryElementExceptionMessage = "La requĆŖte fournie est invalide. {0} n'est pas un Ć©lĆ©ment valide pour une requĆŖte." + setPodeAsyncProgressExceptionMessage = "Set-PodeAsyncRouteProgress ne peut ĆŖtre utilisĆ© qu'Ć  l'intĆ©rieur d'un Scriptblock de Route Asynchrone." + progressLimitLowerThanCurrentExceptionMessage = 'Une limite de progression ne peut pas ĆŖtre infĆ©rieure Ć  la progression actuelle.' taskProcessDoesNotExistExceptionMessage = "Le processus de la tĆ¢che '{0}' n'existe pas." scheduleProcessDoesNotExistExceptionMessage = "Le processus de l'horaire '{0}' n'existe pas." definitionTagChangeNotAllowedExceptionMessage = 'Le tag de dĆ©finition pour une Route ne peut pas ĆŖtre modifiĆ©.' + openApiDefinitionsMismatchExceptionMessage = '{0} varie entre diffĆ©rentes dĆ©finitions OpenAPI.' + routeNotMarkedAsAsyncExceptionMessage = "La route '{0}' n'est pas marquĆ©e comme une route asynchrone." + functionCannotBeInvokedMultipleTimesExceptionMessage = "La fonction '{0}' ne peut pas ĆŖtre invoquĆ©e plusieurs fois pour la mĆŖme route '{1}'." getRequestBodyNotAllowedExceptionMessage = "Les opĆ©rations '{0}' ne peuvent pas avoir de corps de requĆŖte. Utilisez -AllowNonStandardBody pour contourner cette restriction." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La fonction '{0}' n'accepte pas un tableau en tant qu'entrĆ©e de pipeline." unsupportedStreamCompressionEncodingExceptionMessage = "La compression de flux {0} n'est pas prise en charge." diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 5dd3c47f4..67d38ce6c 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -285,9 +285,19 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Durata non valida fornita per Access-Control-Max-Age: {0}. Deve essere maggiore di 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'La definizione OpenAPI denominata {0} esiste giĆ .' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag non può essere utilizzato all'interno di un 'ScriptBlock' di Select-PodeOADefinition." + invalidQueryFormatExceptionMessage = 'La query fornita ha un formato non valido.' + asyncIdDoesNotExistExceptionMessage = 'Async {0} non esiste.' + asyncRouteOperationDoesNotExistExceptionMessage = "Nessuna operazione di percorso asincrono esiste con l'Id {0}." + scriptContainsDisallowedCommandExceptionMessage = "Lo script non può contenere il comando '{0}'." + invalidQueryElementExceptionMessage = 'La query fornita non ĆØ valida. {0} non ĆØ un elemento valido per una query.' + setPodeAsyncProgressExceptionMessage = "Set-PodeAsyncRouteProgress può essere utilizzato solo all'interno di uno Scriptblock di un percorso asincrono." + progressLimitLowerThanCurrentExceptionMessage = "Un limite di progresso non può essere inferiore all'attuale progresso." taskProcessDoesNotExistExceptionMessage = "Il processo dell'attivitĆ  '{0}' non esiste." scheduleProcessDoesNotExistExceptionMessage = "Il processo della programma '{0}' non esiste." definitionTagChangeNotAllowedExceptionMessage = 'Il tag di definizione per una Route non può essere cambiato.' + openApiDefinitionsMismatchExceptionMessage = '{0} varia tra diverse definizioni OpenAPI.' + routeNotMarkedAsAsyncExceptionMessage = "Il percorso '{0}' non ĆØ asincrono." + functionCannotBeInvokedMultipleTimesExceptionMessage = "La funzione '{0}' non può essere invocata più volte per lo stesso percorso '{1}'." getRequestBodyNotAllowedExceptionMessage = "Le operazioni '{0}' non possono avere un corpo della richiesta. Utilizzare -AllowNonStandardBody per aggirare questa restrizione." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La funzione '{0}' non accetta una matrice come input della pipeline." unsupportedStreamCompressionEncodingExceptionMessage = 'La compressione dello stream non ĆØ supportata per la codifica {0}' diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index 43d8243bc..dbefa4881 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -288,6 +288,16 @@ taskProcessDoesNotExistExceptionMessage = 'ć‚æć‚¹ć‚Æćƒ—ćƒ­ć‚»ć‚¹ćŒå­˜åœØć—ć¾ć›ć‚“: {0}' scheduleProcessDoesNotExistExceptionMessage = 'ć‚¹ć‚±ć‚øćƒ„ćƒ¼ćƒ«ćƒ—ćƒ­ć‚»ć‚¹ćŒå­˜åœØć—ć¾ć›ć‚“: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Routeć®å®šē¾©ć‚æć‚°ćÆå¤‰ę›“ć§ćć¾ć›ć‚“ć€‚' + invalidQueryFormatExceptionMessage = 'ęä¾›ć•ć‚ŒćŸć‚Æć‚ØćƒŖć«ćÆē„”åŠ¹ćŖå½¢å¼ćŒć‚ć‚Šć¾ć™ć€‚' + asyncIdDoesNotExistExceptionMessage = 'Async {0} ćÆå­˜åœØć—ć¾ć›ć‚“.' + asyncRouteOperationDoesNotExistExceptionMessage = 'Id {0} ć®éžåŒęœŸćƒ«ćƒ¼ćƒˆę“ä½œćÆå­˜åœØć—ć¾ć›ć‚“.' + scriptContainsDisallowedCommandExceptionMessage = "ć‚¹ć‚ÆćƒŖćƒ—ćƒˆć«ć‚³ćƒžćƒ³ćƒ‰ '{0}' ć‚’å«ć‚ć‚‹ć“ćØćÆć§ćć¾ć›ć‚“ć€‚" + invalidQueryElementExceptionMessage = 'ęä¾›ć•ć‚ŒćŸć‚Æć‚ØćƒŖćÆē„”åŠ¹ć§ć™ć€‚ {0} ćÆć‚Æć‚ØćƒŖć®ęœ‰åŠ¹ćŖč¦ē“ ć§ćÆć‚ć‚Šć¾ć›ć‚“ć€‚' + setPodeAsyncProgressExceptionMessage = 'Set-PodeAsyncProgressćÆć€éžåŒęœŸćƒ«ćƒ¼ćƒˆć‚¹ć‚ÆćƒŖćƒ—ćƒˆćƒ–ćƒ­ćƒƒć‚Æå†…ć§ć®ćæä½æē”Øć§ćć¾ć™ć€‚' + progressLimitLowerThanCurrentExceptionMessage = 'é€²č”ŒēŠ¶ę³ć®åˆ¶é™ćÆć€ē¾åœØć®é€²č”ŒēŠ¶ę³ć‚ˆć‚Šä½Žćć™ć‚‹ć“ćØćÆć§ćć¾ć›ć‚“ć€‚' + openApiDefinitionsMismatchExceptionMessage = '{0} は異なる OpenAPI å®šē¾©é–“ć§ē•°ćŖć‚Šć¾ć™ć€‚' + routeNotMarkedAsAsyncExceptionMessage = "ルート '{0}' ćÆéžåŒęœŸćƒ«ćƒ¼ćƒˆćØć—ć¦ćƒžćƒ¼ć‚Æć•ć‚Œć¦ć„ć¾ć›ć‚“ć€‚" + functionCannotBeInvokedMultipleTimesExceptionMessage = "関数 '{0}' ć‚’åŒć˜ćƒ«ćƒ¼ćƒˆ '{1}' ć«åÆ¾ć—ć¦č¤‡ę•°å›žå‘¼ć³å‡ŗć™ć“ćØćÆć§ćć¾ć›ć‚“ć€‚" getRequestBodyNotAllowedExceptionMessage = "'{0}' ę“ä½œć«ćÆćƒŖć‚Æć‚Øć‚¹ćƒˆćƒœćƒ‡ć‚£ć‚’å«ć‚ć‚‹ć“ćØćÆć§ćć¾ć›ć‚“ć€‚-AllowNonStandardBody ć‚’ä½æē”Øć—ć¦ć“ć®åˆ¶é™ć‚’å›žéæć—ć¦ćć ć•ć„ć€‚" fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "関数 '{0}' ćÆé…åˆ—ć‚’ćƒ‘ć‚¤ćƒ—ćƒ©ć‚¤ćƒ³å…„åŠ›ćØć—ć¦å—ć‘ä»˜ć‘ć¾ć›ć‚“ć€‚" unsupportedStreamCompressionEncodingExceptionMessage = 'ć‚µćƒćƒ¼ćƒˆć•ć‚Œć¦ć„ćŖć„ć‚¹ćƒˆćƒŖćƒ¼ćƒ åœ§ēø®ć‚Øćƒ³ć‚³ćƒ¼ćƒ‡ć‚£ćƒ³ć‚°ćŒęä¾›ć•ć‚Œć¾ć—ćŸ: {0}' diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index a0d882588..fd70cdf5c 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -288,6 +288,16 @@ taskProcessDoesNotExistExceptionMessage = 'ģž‘ģ—… ķ”„ė”œģ„øģŠ¤ź°€ ģ”“ģž¬ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤: {0}' scheduleProcessDoesNotExistExceptionMessage = 'ģŠ¤ģ¼€ģ¤„ ķ”„ė”œģ„øģŠ¤ź°€ ģ”“ģž¬ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Route에 ėŒ€ķ•œ ģ •ģ˜ ķƒœź·øėŠ” 변경할 수 ģ—†ģŠµė‹ˆė‹¤.' + invalidQueryFormatExceptionMessage = '제공된 ģæ¼ė¦¬ģ˜ ķ˜•ģ‹ģ“ ģž˜ėŖ»ė˜ģ—ˆģŠµė‹ˆė‹¤.' + asyncIdDoesNotExistExceptionMessage = 'Async {0} ģ”“ģž¬ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤.' + asyncRouteOperationDoesNotExistExceptionMessage = 'Id {0}ģ˜ ė¹„ė™źø° 경딜 ģž‘ģ—…ģ“ ģ”“ģž¬ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤.' + scriptContainsDisallowedCommandExceptionMessage = "ģŠ¤ķ¬ė¦½ķŠøģ— '{0}' ėŖ…ė ¹ģ„ ķ¬ķ•Øķ•  수 ģ—†ģŠµė‹ˆė‹¤." + invalidQueryElementExceptionMessage = '제공된 쿼리가 ģž˜ėŖ»ė˜ģ—ˆģŠµė‹ˆė‹¤. {0} ėŠ” 쿼리에 ėŒ€ķ•œ ģœ ķšØķ•œ ģš”ģ†Œź°€ ģ•„ė‹™ė‹ˆė‹¤.' + setPodeAsyncProgressExceptionMessage = 'Set-PodeAsyncProgressėŠ” ė¹„ė™źø° 경딜 스크립트 ėø”ė” ė‚“ģ—ģ„œė§Œ ģ‚¬ģš©ķ•  수 ģžˆģŠµė‹ˆė‹¤.' + progressLimitLowerThanCurrentExceptionMessage = 'ģ§„ķ–‰ ķ•œė„ėŠ” ķ˜„ģž¬ 진행볓다 ė‚®ģ„ 수 ģ—†ģŠµė‹ˆė‹¤.' + openApiDefinitionsMismatchExceptionMessage = '{0} ėŠ” ģ„œė”œ 다넸 OpenAPI ģ •ģ˜ 간에 ė‹¤ė¦…ė‹ˆė‹¤.' + routeNotMarkedAsAsyncExceptionMessage = "경딜 '{0}' ģ“(ź°€) ė¹„ė™źø° 경딜딜 ķ‘œģ‹œė˜ģ§€ ģ•Šģ•˜ģŠµė‹ˆė‹¤." + functionCannotBeInvokedMultipleTimesExceptionMessage = "ķ•Øģˆ˜ '{0}' 넼 ė™ģ¼ķ•œ 경딜 '{1}' 에 ėŒ€ķ•“ ģ—¬ėŸ¬ 번 ķ˜øģ¶œķ•  수 ģ—†ģŠµė‹ˆė‹¤." getRequestBodyNotAllowedExceptionMessage = "'{0}' ģž‘ģ—…ģ€ ģš”ģ²­ ė³øė¬øģ„ ź°€ģ§ˆ 수 ģ—†ģŠµė‹ˆė‹¤. ģ“ ģ œķ•œģ„ ė¬“ģ‹œķ•˜ė ¤ė©“ -AllowNonStandardBody넼 ģ‚¬ģš©ķ•˜ģ„øģš”." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "ķ•Øģˆ˜ '{0}'ģ€(ėŠ”) ė°°ģ—“ģ„ ķŒŒģ“ķ”„ė¼ģø ģž…ė „ģœ¼ė”œ 받지 ģ•ŠģŠµė‹ˆė‹¤." unsupportedStreamCompressionEncodingExceptionMessage = 'ģ§€ģ›ė˜ģ§€ ģ•ŠėŠ” 스트림 ģ••ģ¶• ģøģ½”ė”©: {0}' diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index 6aad6ebc4..af066d051 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -285,9 +285,19 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Ongeldige Access-Control-Max-Age duur opgegeven: {0}. Moet groter zijn dan 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI-definitie met de naam {0} bestaat al.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag kan niet worden gebruikt binnen een Select-PodeOADefinition 'ScriptBlock'." + invalidQueryFormatExceptionMessage = 'De opgegeven query heeft een ongeldig formaat.' + asyncIdDoesNotExistExceptionMessage = 'Async {0} bestaat niet.' + asyncRouteOperationDoesNotExistExceptionMessage = 'Er bestaat geen Async Route-operatie met Id {0}.' + scriptContainsDisallowedCommandExceptionMessage = "Script mag het commando '{0}' niet bevatten." + invalidQueryElementExceptionMessage = 'De opgegeven query is ongeldig. {0} is geen geldig element voor een query.' + setPodeAsyncProgressExceptionMessage = 'Set-PodeAsyncRouteProgress kan alleen worden gebruikt binnen een Async Route Scriptblock.' + progressLimitLowerThanCurrentExceptionMessage = 'Een voortgangslimiet kan niet lager zijn dan de huidige voortgang.' taskProcessDoesNotExistExceptionMessage = "Taakproces '{0}' bestaat niet." scheduleProcessDoesNotExistExceptionMessage = "Schema-proces '{0}' bestaat niet." definitionTagChangeNotAllowedExceptionMessage = 'Definitietag voor een route kan niet worden gewijzigd.' + openApiDefinitionsMismatchExceptionMessage = '{0} verschilt tussen verschillende OpenAPI-definities.' + routeNotMarkedAsAsyncExceptionMessage = "De route '{0}' is niet gemarkeerd als een asynchrone route." + functionCannotBeInvokedMultipleTimesExceptionMessage = "De functie '{0}' kan niet meerdere keren worden aangeroepen voor dezelfde route '{1}'." getRequestBodyNotAllowedExceptionMessage = "'{0}' operaties kunnen geen aanvraagbody hebben. Gebruik -AllowNonStandardBody om deze beperking te omzeilen." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "De functie '{0}' accepteert geen array als pipeline-invoer." unsupportedStreamCompressionEncodingExceptionMessage = 'Niet-ondersteunde streamcompressie-encodering: {0}' diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index cf5dd507b..16e2654f3 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -288,6 +288,16 @@ taskProcessDoesNotExistExceptionMessage = "Proces zadania '{0}' nie istnieje." scheduleProcessDoesNotExistExceptionMessage = "Proces harmonogramu '{0}' nie istnieje." definitionTagChangeNotAllowedExceptionMessage = 'Tag definicji dla Route nie może zostać zmieniony.' + invalidQueryFormatExceptionMessage = 'Podane zapytanie ma nieprawidłowy format.' + asyncIdDoesNotExistExceptionMessage = 'Async {0} nie istnieje.' + asyncRouteOperationDoesNotExistExceptionMessage = 'Operacja Async Route z Id {0} nie istnieje.' + scriptContainsDisallowedCommandExceptionMessage = "Skrypt nie może zawierać polecenia '{0}'." + invalidQueryElementExceptionMessage = 'Podane zapytanie jest nieprawidłowe. {0} nie jest prawidłowym elementem zapytania.' + setPodeAsyncProgressExceptionMessage = 'Set-PodeAsyncRouteProgress można używać tylko wewnątrz bloku skryptowego Async Route.' + progressLimitLowerThanCurrentExceptionMessage = 'Limit postępu nie może być niższy niż obecny postęp.' + openApiDefinitionsMismatchExceptionMessage = '{0} różni się między różnymi definicjami OpenAPI.' + routeNotMarkedAsAsyncExceptionMessage = "Trasa '{0}' nie jest oznaczona jako trasa asynchroniczna." + functionCannotBeInvokedMultipleTimesExceptionMessage = "Funkcja '{0}' nie może być wywoływana wielokrotnie dla tej samej trasy '{1}'." getRequestBodyNotAllowedExceptionMessage = "Operacje '{0}' nie mogą zawierać treści żądania. Użyj -AllowNonStandardBody, aby obejść to ograniczenie." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Funkcja '{0}' nie akceptuje tablicy jako wejścia potoku." unsupportedStreamCompressionEncodingExceptionMessage = 'Kodowanie kompresji strumienia nie jest obsługiwane: {0}' diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index b8e1e4cd1..37b8f6fa6 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -288,6 +288,16 @@ taskProcessDoesNotExistExceptionMessage = "O processo da tarefa '{0}' nĆ£o existe." scheduleProcessDoesNotExistExceptionMessage = "O processo do cronograma '{0}' nĆ£o existe." definitionTagChangeNotAllowedExceptionMessage = 'A Tag de definição para uma Route nĆ£o pode ser alterada.' + invalidQueryFormatExceptionMessage = 'A consulta fornecida tem um formato invĆ”lido.' + asyncIdDoesNotExistExceptionMessage = 'Async {0} nĆ£o existe.' + asyncRouteOperationDoesNotExistExceptionMessage = 'Nenhuma operação de Rota AssĆ­ncrona existe com o Id {0}.' + scriptContainsDisallowedCommandExceptionMessage = "O script nĆ£o pode conter o comando '{0}'." + invalidQueryElementExceptionMessage = 'A consulta fornecida Ć© invĆ”lida. {0} nĆ£o Ć© um elemento vĆ”lido para uma consulta.' + setPodeAsyncProgressExceptionMessage = 'Set-PodeAsyncRouteProgress só pode ser usado dentro de um Scriptblock de Rota AssĆ­ncrona.' + progressLimitLowerThanCurrentExceptionMessage = 'Um limite de progresso nĆ£o pode ser inferior ao progresso atual.' + openApiDefinitionsMismatchExceptionMessage = '{0} varia entre diferentes definiƧƵes OpenAPI.' + routeNotMarkedAsAsyncExceptionMessage = "A rota '{0}' nĆ£o estĆ” marcada como uma Rota AssĆ­ncrona." + functionCannotBeInvokedMultipleTimesExceptionMessage = "A função '{0}' nĆ£o pode ser invocada vĆ”rias vezes para a mesma rota '{1}'." getRequestBodyNotAllowedExceptionMessage = "As operaƧƵes '{0}' nĆ£o podem ter um corpo de solicitação. Use -AllowNonStandardBody para contornar essa restrição." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "A função '{0}' nĆ£o aceita uma matriz como entrada de pipeline." unsupportedStreamCompressionEncodingExceptionMessage = 'A codificação de compressĆ£o de fluxo nĆ£o Ć© suportada.' diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 3653dc151..4228a7f9d 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -288,6 +288,16 @@ taskProcessDoesNotExistExceptionMessage = "ä»»åŠ”čæ›ēØ‹ '{0}' äøå­˜åœØć€‚" scheduleProcessDoesNotExistExceptionMessage = "č®”åˆ’čæ›ēØ‹ '{0}' äøå­˜åœØć€‚" definitionTagChangeNotAllowedExceptionMessage = 'Routeēš„å®šä¹‰ę ‡ē­¾ę— ę³•ę›“ę”¹ć€‚' + invalidQueryFormatExceptionMessage = 'ęä¾›ēš„ęŸ„čÆ¢ę ¼å¼ę— ę•ˆć€‚' + asyncIdDoesNotExistExceptionMessage = '异歄 {0} äøå­˜åœØć€‚' + asyncRouteOperationDoesNotExistExceptionMessage = 'äøå­˜åœØ ID äøŗ {0} ēš„å¼‚ę­„č·Æē”±ę“ä½œć€‚' + scriptContainsDisallowedCommandExceptionMessage = "č„šęœ¬äøå…č®øåŒ…å«å‘½ä»¤ '{0}'怂" + invalidQueryElementExceptionMessage = 'ęä¾›ēš„ęŸ„čÆ¢ę— ę•ˆć€‚{0} äøę˜Æęœ‰ę•ˆēš„ęŸ„čÆ¢å…ƒē“ ć€‚' + setPodeAsyncProgressExceptionMessage = 'Set-PodeAsyncRouteProgress åŖčƒ½åœØå¼‚ę­„č·Æē”±č„šęœ¬å—äø­ä½æē”Øć€‚' + progressLimitLowerThanCurrentExceptionMessage = 'čæ›åŗ¦é™åˆ¶äøčƒ½ä½ŽäŗŽå½“å‰čæ›åŗ¦ć€‚' + openApiDefinitionsMismatchExceptionMessage = '{0} åœØäøåŒēš„ OpenAPI å®šä¹‰ä¹‹é—“ęœ‰ę‰€äøåŒć€‚' + routeNotMarkedAsAsyncExceptionMessage = "č·Æē”± '{0}' ęœŖę ‡č®°äøŗå¼‚ę­„č·Æē”±ć€‚" + functionCannotBeInvokedMultipleTimesExceptionMessage = "函数 '{0}' äøčƒ½åœØåŒäø€č·Æē”± '{1}' äøŠå¤šę¬”č°ƒē”Øć€‚" getRequestBodyNotAllowedExceptionMessage = "'{0}' ę“ä½œę— ę³•åŒ…å«čÆ·ę±‚ä½“ć€‚ä½æē”Ø -AllowNonStandardBody ä»„č§£é™¤ę­¤é™åˆ¶ć€‚" fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "函数 '{0}' äøęŽ„å—ę•°ē»„ä½œäøŗē®”é“č¾“å…„ć€‚" unsupportedStreamCompressionEncodingExceptionMessage = 'äøę”ÆęŒēš„ęµåŽ‹ē¼©ē¼–ē : {0}' diff --git a/src/Pode.psd1 b/src/Pode.psd1 index aff2bfcb7..c0a527fa9 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -170,6 +170,24 @@ 'Test-PodeStaticRoute', 'Test-PodeSignalRoute', + #Async + 'Set-PodeAsyncRoute', + 'Add-PodeAsyncRouteStop', + 'Add-PodeAsyncRouteGet', + 'Add-PodeAsyncRouteQuery', + 'Stop-PodeAsyncRouteOperation', + 'Get-PodeAsyncRouteOperationByFilter', + 'Test-PodeAsyncRouteOperation', + 'Set-PodeAsyncRouteProgress', + 'Get-PodeAsyncRouteProgress', + 'Set-PodeAsyncRouteOASchemaName', + 'Set-PodeAsyncRoutePermission', + 'Get-PodeAsyncRouteOperation', + 'Add-PodeAsyncRouteCallback', + 'Add-PodeAsyncRouteSse', + 'Get-PodeAsyncRouteUserIdentifierField', + 'Set-PodeAsyncRouteUserIdentifierField', + # handlers 'Add-PodeHandler', 'Remove-PodeHandler', diff --git a/src/Private/AsyncRoute.ps1 b/src/Private/AsyncRoute.ps1 new file mode 100644 index 000000000..4d7703a70 --- /dev/null +++ b/src/Private/AsyncRoute.ps1 @@ -0,0 +1,1533 @@ + +<# +.SYNOPSIS + Converts a provided script block into an enhanced script block for asynchronous execution in Pode. + +.DESCRIPTION + The `Get-PodeAsyncRouteScriptblock` function takes a given script block and wraps it with additional code + to manage asynchronous execution within the Pode framework. It handles setting up the execution state, + logging errors, and invoking callback URLs with results. + +.PARAMETER ScriptBlock + The original script block to be converted into an enhanced script block. + +.OUTPUTS + [ScriptBlock] - Returns the enhanced script block suitable for asynchronous execution in Pode. + +.EXAMPLE + $originalScriptBlock = { + param($param1, $param2) + # Script block code here + } + + $enhancedScriptBlock = Get-PodeAsyncRouteScriptblock -ScriptBlock $originalScriptBlock + + # Now you can use $enhancedScriptBlock for asynchronous execution in Pode. + +.NOTES + - The enhanced script block manages state transitions, error logging, and optional callback invocations. + - It supports additional parameters for WebEvent and Async Id. + - This is an internal function and may change in future releases of Pode. +#> + +function Get-PodeAsyncRouteScriptblock { + param ( + [Parameter(Mandatory = $true)] + [ScriptBlock] + $ScriptBlock + ) + + # Template for the enhanced script block + $enhancedScriptBlockTemplate = { + <# Param #> + # Sometimes the key is not available when the process starts. Workaround: wait 2 seconds + if (!$PodeContext.AsyncRoutes.Processes.ContainsKey($___async___id___)) { + Start-Sleep 2 + } + if (!$PodeContext.AsyncRoutes.Processes.ContainsKey($___async___id___)) { + try { + throw ($PodeLocale.asyncIdDoesNotExistExceptionMessage -f $___async___id___) + } + catch { + # Log the error + $_ | Write-PodeErrorLog + } + } + + $asyncProcess = $PodeContext.AsyncRoutes.Processes[$___async___id___] + + try { + $asyncProcess['StartingTime'] = [datetime]::UtcNow + + # Set the state to 'Running' + $asyncProcess['State'] = 'Running' + + $___result___ = & { # Original ScriptBlock Start + <# ScriptBlock #> + # Original ScriptBlock End + } + if ($___result___) { + $asyncProcess['Result'] = $___result___ + } + # Set the completed time + $asyncProcess['CompletedTime'] = [datetime]::UtcNow + } + catch { + if (! $asyncProcess.ContainsKey('CompletedTime')) { + $asyncProcess['CompletedTime'] = [datetime]::UtcNow + } + # Set the state to 'Failed' in case of error + $asyncProcess['State'] = 'Failed' + + # Log the error + $_ | Write-PodeErrorLog + + # Store the error in the AsyncRoutes Processes + $asyncProcess['Error'] = $_.ToString() + + } + finally { + Complete-PodeAsyncRouteOperation -AsyncProcess $asyncProcess + } + } + + # Convert the provided script block to a string + $sc = $ScriptBlock.ToString() + + # Split the string into lines + $lines = $sc -split "`n" + + # Initialize variables + $paramLineIndex = $null + $parameters = '' + + # Find the line containing 'param' and extract parameters + for ($i = 0; $i -lt $lines.Length; $i++) { + if ($lines[$i] -match '^\s*param\((.*)\)\s*$') { + $parameters = $matches[1].Trim() + $paramLineIndex = $i + break + } + } + + # Remove the line containing 'param' + if ($null -ne $paramLineIndex) { + if ($paramLineIndex -eq 0) { + $remainingLines = $lines[1..($lines.Length - 1)] + } + else { + # include comments or empty lines + $remainingLines = $lines[0..($paramLineIndex - 1)] + $lines[($paramLineIndex + 1)..($lines.Length - 1)] + } + + $remainingString = $remainingLines -join "`n" + $param = 'param({0}, $WebEvent, $___async___id___)' -f $parameters + } + else { + $remainingString = $sc + $param = 'param($WebEvent, $___async___id___)' + } + + # Replace placeholders in the template with actual script block content and parameters + $enhancedScriptBlockContent = $enhancedScriptBlockTemplate.ToString().Replace('<# ScriptBlock #>', $remainingString.ToString()).Replace('<# Param #>', $param) + + # Return the enhanced script block + return [ScriptBlock]::Create($enhancedScriptBlockContent) +} + +<# +.SYNOPSIS + Validates a ScriptBlock to ensure it does not contain disallowed Pode response commands. + +.DESCRIPTION + The Test-PodeAsyncRouteScriptblockInvalidCommand function checks a given ScriptBlock + to ensure that it does not contain any disallowed Pode response commands, such as + 'Write-Pode...Response'. If such a command is found, the function throws an exception + with a relevant error message. + +.PARAMETER ScriptBlock + The ScriptBlock that you want to validate. This parameter is mandatory. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeAsyncRouteScriptblockInvalidCommand { + param( + [Parameter(Mandatory = $true)] + [ScriptBlock] + $ScriptBlock + ) + + # Convert the ScriptBlock to a string and check if it contains disallowed commands + if ($ScriptBlock.ToString() -imatch 'Write\-Pode.+Response') { + # If a disallowed command is found, throw an exception with a relevant message + throw ($PodeLocale.scriptContainsDisallowedCommandExceptionMessage -f $Matches[0].Trim()) + } +} + +<# +.SYNOPSIS + Closes an asynchronous script execution, setting its state to 'Completed' and handling callback invocations. + +.DESCRIPTION + The `Complete-PodeAsyncRouteOperation` function finalizes an asynchronous script's execution by setting its state to 'Completed' if it is still running and logs the completion time. It also manages callbacks by sending requests to a specified callback URL with appropriate headers and content types. If Server-Sent Events (SSE) are enabled, the function will send events based on the execution state. + +.PARAMETER AsyncProcess + A [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] that contains the results and state information of the asynchronous script. + +.EXAMPLE + $asyncProcess = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $webEvent = @{ + Request = @{ + Url = 'http://example.com/request' + } + Method = 'GET' + } + Complete-PodeAsyncRouteOperation -AsyncProcess $asyncProcess -WebEvent $webEvent + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Complete-PodeAsyncRouteOperation { + param ( + [Parameter(Mandatory = $true)] + [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] + $AsyncProcess + ) + # Set the completed time if not already set + if (! $AsyncProcess.ContainsKey('CompletedTime') -or ($null -eq $AsyncProcess['CompletedTime'])) { + $AsyncProcess['CompletedTime'] = [datetime]::UtcNow + } + + # Ensure state is set to 'Completed' if it was still 'Running' + if ($AsyncProcess['State'] -eq 'Running') { + $AsyncProcess['State'] = 'Completed' + } + + + if ($AsyncProcess['Timer']) { + # Closes and disposes of the timer + Close-PodeAsyncRouteTimer -Operation $AsyncProcess + } + + # Ensure Progress is set to 100 if in use + if ($AsyncProcess.ContainsKey('Progress')) { + $AsyncProcess['Progress'] = 100 + } + + try { + if ($AsyncProcess['CallbackSettings']) { + + # Resolve the callback URL, method, content type, and headers + $callbackUrl = (Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $AsyncProcess['CallbackSettings'].UrlField).Value + $method = (Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $AsyncProcess['CallbackSettings'].Method -DefaultValue 'Post').Value + $contentType = (Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $AsyncProcess['CallbackSettings'].ContentType).Value + $headers = @{} + foreach ($key in $AsyncProcess['CallbackSettings'].HeaderFields.Keys) { + $value = Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $key -DefaultValue $AsyncProcess['HeaderFields'][$key] + if ($value) { + $headers[$value.Key] = $value.Value + } + } + + # Prepare the body for the callback + $body = @{ + Url = $AsyncProcess['Url'] + Method = $AsyncProcess['Method'] + EventName = $AsyncProcess['CallbackSettings'].EventName + State = $AsyncProcess['State'] + } + switch ($AsyncProcess['State']) { + 'Failed' { + $body.Error = $AsyncProcess['Error'] + } + 'Completed' { + if ($AsyncProcess['CallbackSettings'].SendResult -and $AsyncProcess['Result']) { + $body.Result = $AsyncProcess['Result'] + } + } + 'Aborted' { + $body.Error = $AsyncProcess['Error'] + } + } + + # Convert the body to the appropriate content type + switch ($contentType) { + 'application/json' { $cBody = ($body | ConvertTo-Json -Depth 10) } + 'application/xml' { $cBody = ($body | ConvertTo-Xml -NoTypeInformation) } + 'application/yaml' { $cBody = ($body | ConvertTo-PodeYaml -Depth 10) } + } + + # Store callback information in the async result + $AsyncProcess['CallbackUrl'] = $callbackUrl + $AsyncProcess['CallbackInfoState'] = 'Running' + $AsyncProcess['CallbackTentative'] = 0 + + # Attempt to invoke the callback up to 3 times + for ($i = 0; $i -le 3; $i++) { + try { + $AsyncProcess['CallbackTentative'] = $AsyncProcess['CallbackTentative'] + 1 + $null = Invoke-RestMethod -Uri $callbackUrl -Method $method -Headers $headers -Body $cBody -ContentType $contentType -ErrorAction Stop + $AsyncProcess['CallbackInfoState'] = 'Completed' + break + } + catch { + $_ | Write-PodeErrorLog + $AsyncProcess['CallbackInfoState'] = 'Failed' + Start-Sleep -Seconds 2 + } + } + } + } + catch { + # Log any errors encountered during the callback process + $_ | Write-PodeErrorLog + $AsyncProcess['CallbackInfoState'] = 'Failed' + } + +} + +<# +.SYNOPSIS + Starts the housekeeper for Pode asynchronous routes. + +.DESCRIPTION + The `Start-PodeAsyncRoutesHousekeeper` function sets up a timer that periodically cleans up expired or completed asynchronous routes + in Pode. It ensures that any expired or completed routes are properly handled and removed from the context. + +.NOTES + - The timer is named '__pode_asyncroutes_housekeeper__' and runs at an HousekeepingInterval of 30 seconds. + - The timer checks for forced expiry, completion, and completion expiry of asynchronous routes. + + This is an internal function and may change in future releases of Pode. +#> +function Start-PodeAsyncRoutesHousekeeper { + + # Check if the timer already exists + if (Test-PodeTimer -Name '__pode_asyncroutes_housekeeper__') { + return + } + + # Add a new timer with the specified $Context.Server.AsyncRoute.TimerInterval and script block + Add-PodeTimer -Name '__pode_asyncroutes_housekeeper__' -Interval $PodeContext.AsyncRoutes.HouseKeeping.TimerInterval -ScriptBlock { + # Return if there are no async route Processes + if ($PodeContext.AsyncRoutes.Processes.Count -eq 0) { + return + } + + $now = [datetime]::UtcNow + $RetentionMinutes = $PodeContext.AsyncRoutes.HouseKeeping.RetentionMinutes + # Iterate over the keys of the async route results + foreach ($key in $PodeContext.AsyncRoutes.Processes.Keys.Clone()) { + $result = $PodeContext.AsyncRoutes.Processes[$key] + + if ($result) { + # Check if the task is completed + if ($result['Runspace'].Handler.IsCompleted) { + try { + # Remove the task if it is past the removal time + if ($result['CompletedTime'] -and $result['CompletedTime'].AddMinutes($RetentionMinutes) -le $now) { + $result['Runspace'].Pipeline.Dispose() + $v = 0 + $removed = $PodeContext.AsyncRoutes.Processes.TryRemove($key, [ref]$v) + Write-Verbose "Key $key Removed: $removed" + } + } + catch { + $_ | Write-PodeErrorLog + } + } + # Check if the task has force expired + elseif ($result['ExpireTime'] -lt $now) { + try { + $result['CompletedTime'] = $now + $result['State'] = 'Aborted' + $result['Error'] = 'Timeout' + $result['Runspace'].Pipeline.Dispose() + Complete-PodeAsyncRouteOperation -AsyncProcess $result + } + catch { + $_ | Write-PodeErrorLog + } + } + } + } + + # Clear the result variable + $result = $null + } +} + +<# +.SYNOPSIS + Searches for asynchronous route Pode tasks based on specified query conditions. + +.DESCRIPTION + The Search-PodeAsyncRouteTask function searches the Pode context for asynchronous route tasks that match the specified query conditions. + It supports comparison operators such as greater than (GT), less than (LT), greater than or equal (GE), less than or equal (LE), + equal (EQ), not equal (NE), like (LIKE), and not like (NOTLIKE). Additionally, it can check user permissions if specified. + +.PARAMETER Query + A hashtable containing the query conditions. Each key in the hashtable represents a field to search on, + and the value is another hashtable containing 'op' (operator) and 'value' (comparison value). + +.PARAMETER User + An optional hashtable representing the user details. This is used when checking permissions on tasks. + +.PARAMETER CheckPermission + A switch to indicate whether to check permissions on tasks. If specified, the function will filter tasks based on the user's permissions. + +.EXAMPLE + $query = @{ + 'State' = @{ 'op' = 'EQ'; 'value' = 'Running' } + 'CreationTime' = @{ 'op' = 'GT'; 'value' = (Get-Date).AddHours(-1) } + } + $results = Search-PodeAsyncRouteTask -Query $query + + This example searches for tasks that are in the 'Running' state and were created within the last hour. + +.EXAMPLE + $user = @{ + 'Name' = 'AdminUser' + 'Roles' = @('Admin', 'User') + } + $query = @{ + 'State' = @{ 'op' = 'EQ'; 'value' = 'Completed' } + } + $results = Search-PodeAsyncRouteTask -Query $query -User $user -CheckPermission + + This example searches for tasks that are in the 'Completed' state and checks if the specified user has permission to view them. + +.OUTPUTS + Returns an array of hashtables representing the matched tasks. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Search-PodeAsyncRouteTask { + param ( + [Parameter(Mandatory = $true)] + [hashtable] + $Query, + + [Parameter()] + [hashtable] + $User, + + [switch] + $CheckPermission + ) + + # Initialize an array to store the matched elements + $matchedElements = @() + # Check if there are any async route Processes to search + if ($PodeContext.AsyncRoutes.Processes.count -gt 0) { + # Clone the keys of the Processes to iterate over them + foreach ($rkey in $PodeContext.AsyncRoutes.Processes.keys.Clone()) { + $result = $PodeContext.AsyncRoutes.Processes[$rkey] + + # If permission checking is enabled, validate the user's permissions + if ($CheckPermission.IsPresent) { + if ($result.User -and ($null -eq $User)) { + continue + } + if ($result.Permission -and (! (Test-PodeAsyncRoutePermission -Permission $result.Permission.Read -User $User))) { + continue + } + } + + $match = $true + + # Iterate through each query condition + foreach ($key in $Query.Keys) { + # Check the variable name + if (! (('Id', 'AsyncRouteId', 'StartingTime', 'CreationTime', 'CompletedTime', 'ExpireTime', 'State', 'Error', 'CallbackSettings', 'Cancellable', 'User', 'Url', 'Method', 'Progress') -contains $key)) { + # The query provided is invalid.{0} is not a valid element for a query. + throw ($PodeLocale.invalidQueryElementExceptionMessage -f $key) + } + $queryCondition = $Query[$key] + + # Ensure the query condition has both 'op' and 'value' keys + if ($queryCondition.ContainsKey('op') -and $queryCondition.ContainsKey('value')) { + # Check if the result contains the key and it is not null + if ($result.ContainsKey($key) -and ($null -ne $result[$key])) { + + $operator = $queryCondition['op'] + $value = $queryCondition['value'] + + # Evaluate the condition based on the specified operator + switch ($operator) { + 'GT' { + $match = $match -and ($result[$key] -gt $value) + break + } + 'LT' { + $match = $match -and ($result[$key] -lt $value) + break + } + 'GE' { + $match = $match -and ($result[$key] -ge $value) + break + } + 'LE' { + $match = $match -and ($result[$key] -le $value) + break + } + 'EQ' { + $match = $match -and ($result[$key] -eq $value) + break + } + 'NE' { + $match = $match -and ($result[$key] -ne $value) + break + } + 'NOTLIKE' { + $match = $match -and ($result[$key] -notlike "*$value*") + break + } + 'LIKE' { + $match = $match -and ($result[$key] -like "*$value*") + break + } + Default { + $match = $match -and $false + break + } + } + } + else { + $match = $match -and $false + } + } + else { + # The query provided has an invalid format. + throw $PodeLocale.invalidQueryFormatExceptionMessage + } + } + + # If the result matches all conditions, add it to the matched elements + if ($match) { + $matchedElements += $result + } + } + } + + # Return the array of matched elements + return $matchedElements +} + +<# +.SYNOPSIS + Converts runtime expressions for Pode callback variables. + +.DESCRIPTION + The `Convert-PodeAsyncRouteCallBackRuntimeExpression` function processes runtime expressions + for Pode callback variables. It interprets variables in headers, query parameters, + and body fields from the web event request, providing a default value if the variable + is not resolvable. This function is used in the context of OpenAPI callback specifications + to dynamically resolve values at runtime. + +.PARAMETER Variable + The variable expression to be converted. This can be a header, query parameter, or body field. + Valid formats include: + - $request.header.header-name + - $request.query.param-name + - $request.body#/field-name + +.PARAMETER DefaultValue + The default value to be used if the variable cannot be resolved from the request. + +.INPUTS + [string], [string] + +.OUTPUTS + [hashtable] + The output is a hashtable containing the resolved key and value. + +.EXAMPLE + # Convert a header variable with a default value + $result = Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable '$request.header.Content-Type' -DefaultValue 'application/json' + Write-Output $result + +.EXAMPLE + # Convert a query parameter variable with a default value + $result = Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable '$request.query.userId' -DefaultValue 'unknown' + Write-Output $result + +.EXAMPLE + # Convert a body field variable with a default value + $result = Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable '$request.body#/user/name' -DefaultValue 'anonymous' + Write-Output $result + +.NOTES + This function is used in the context of OpenAPI callback specifications to dynamically resolve + values at runtime. The parameters can accept the following meta values: + - $request.query.param-name : query-param-value + - $request.header.header-name: application/json + - $request.body#/field-name : callbackUrl + + If the variable cannot be resolved from the request, the provided default value is used. + If no default value is provided and the variable cannot be resolved, the variable itself is returned as the value. +#> +function Convert-PodeAsyncRouteCallBackRuntimeExpression { + param( + [string]$Variable, + [string]$DefaultValue + ) + + # Check if the variable starts with '$request.header' + if ($Variable.StartsWith('$request.header')) { + # Match the header key + if ($Variable -match '^[^.]*\.[^.]*\.(.*)') { + $Value = $WebEvent.Request.Headers[$Matches[1]] + if ($Value) { + return @{Key = $Matches[1]; Value = $Value } + } + else { + return @{Key = $Matches[1]; Value = $DefaultValue } + } + } + } + # Check if the variable starts with '$request.query' + elseif ($Variable.StartsWith('$request.query')) { + # Match the query parameter key + if ($Variable -match '^[^.]*\.[^.]*\.(.*)') { + $Value = $WebEvent.Query[$Matches[1]] + if ($Value) { + return @{Key = $Matches[1]; Value = $Value } + } + else { + return @{Key = $Matches[1]; Value = $DefaultValue } + } + } + } + # Check if the variable starts with '$request.body' + elseif ($Variable.StartsWith('$request.body')) { + # Match the body data key + if ($Variable -match '^[^.]*\.[^.]*#/(.*)') { + $Value = $WebEvent.data.$($Matches[1]) + if ($Value) { + return @{Key = $Matches[1]; Value = $Value } + } + else { + return @{Key = $Matches[1]; Value = $DefaultValue } + } + } + } + + # Return the default value if no match was found and default value is not null or empty + if (![string]::IsNullOrEmpty($DefaultValue)) { + return @{Key = $Variable; Value = $DefaultValue } + } + + # Return the variable itself as the value if no match was found and no default value is provided + return @{Key = $Variable; Value = $Variable } +} + + +<# +.SYNOPSIS + Tests if a user has the required asynchronous permissions based on provided permissions hashtable. + +.DESCRIPTION + The `Test-PodeAsyncRoutePermission` function checks if a user has the required permissions specified in the provided hashtable. + It iterates through the keys in the permission hashtable and checks if the user has the necessary permissions. + +.PARAMETER Permission + A hashtable containing the permissions to be checked. + +.PARAMETER User + A hashtable containing the user information and their permissions. + +.OUTPUTS + [Boolean] - Returns $true if the user has the required permissions, otherwise $false. + +.EXAMPLE + + $user = @{ + Id = 'user002' + Groups = @('group3') + Roles = @{'taskadmin'} + } + + $permissions = @{ + Read = @{ + Groups = @('group1','group2') + Roles = @('reviewer','taskadmin') + Scopes = @() + Users = @('user001') + } + Write = @{ + Groups = @() + Roles = @('taskadmin') + Scopes = @() + Users = @('user001') + } + } + + $result = Test-PodeAsyncRoutePermission -Permission $permissions -User $user + Write-Output $result + +.NOTES + This is an internal function and may change in future releases of Pode. +#> + +function Test-PodeAsyncRoutePermission { + param( + [hashtable] + $Permission, + [hashtable] + $User + ) + # If the user information is provided + if ($User) { + # Iterate through each key in the Permission hashtable + foreach ($key in $Permission.Keys) { + + # Check if the user's attributes contain the current permission key + if ($User.ContainsKey($key)) { + # Check if there is a common element between the user's attributes and the required permissions + if (Test-PodeArraysHaveCommonElement -ReferenceArray $Permission[$key] -DifferenceArray $User[$key]) { + return $true + } + } + # Special case for 'Users' key, checking if the user's Id is in the permission list + elseif ($key -eq 'Users') { + if (Test-PodeArraysHaveCommonElement -ReferenceArray $Permission[$key] -DifferenceArray $User.Id) { + return $true + } + } + } + # Return false if no common elements are found for any permission key + return $false + } + # If no user information is provided, assume permission is granted + return $true +} + + +<# +.SYNOPSIS + Retrieves a script block for handling asynchronous route operations in Pode. + +.DESCRIPTION + This function returns a script block designed to handle asynchronous route operations in a Pode web server. + It generates an Id for the async route task, invokes the internal async route task, and prepares the response based on the Accept header. + The response includes details such as creation time, Id, state, name, and cancellable status. If the task involves a user, + it adds default read and write permissions for the user. + +.EXAMPLE + $scriptBlock = Get-PodeAsyncRouteSetScriptBlock + # Use the returned script block in an async route in Pode + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeAsyncRouteSetScriptBlock { + # This function returns a script block that handles async route operations + return [scriptblock] { + try { + # Get the 'Accept' header from the request to determine the response format + $responseMediaType = Get-PodeHeader -Name 'Accept' + + # Retrieve the task to be executed asynchronously + $asyncRouteTask = $WebEvent.Route.Async + + # Generate an Id for the async route task, using the provided IdGenerator or a new GUID + $id = Invoke-PodeScriptBlock -ScriptBlock $asyncRouteTask.AsyncRouteTaskIdGenerator -Return + + # Make a deepcopy of webEvent + $webEventClone = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + + foreach ($key in $webEvent.Keys) { + if (!('OnEnd', 'Route', 'Sse', 'Response', 'Request', 'Lockable' -contains $key)) { + if ($null -eq $webEvent[$key]) { + $webEventClone[$key] = $null + } + elseif ($webEvent[$key] -is [ValueType] -or $Variable -is [string]) { + $webEventClone[$key] = $webEvent[$key] + } + else { + $webEventClone[$key] = Copy-PodeObjectDeepClone -InputObject $webEvent[$key] + } + } + } + $webEventClone['Response'] = [Pode.PodeResponse]::new($webEvent.Response) + $webEventClone['Async'] = $true + $webEventClone['Route'] = $webEvent.Route + $webEventClone['Request'] = [Pode.PodeRequest]::new($webEvent.Request) + $webEventClone['Lockable'] = $webEvent.Lockable + + # Setup event parameters + $parameters = @{ + Event = @{ + Lockable = $PodeContext.Threading.Lockables.Global + Sender = $asyncRouteTask + Metadata = @{} + } + WebEvent = $webEventClone + ___async___id___ = $id + } + # Add any task arguments + foreach ($key in $asyncRouteTask.Arguments.Keys) { + $parameters[$key] = $asyncRouteTask.Arguments[$key] + } + + # Add any using variables + if ($null -ne $asyncRouteTask.UsingVariables) { + foreach ($usingVar in $asyncRouteTask.UsingVariables) { + $parameters[$usingVar.NewName] = $usingVar.Value + } + } + + # Set the creation time + $creationTime = [datetime]::UtcNow + + # Initialize the result and runspace for the async route task + $result = [System.Management.Automation.PSDataCollection[psobject]]::new() + $runspace = Add-PodeRunspace -Type $asyncRouteTask.AsyncRouteId -ScriptBlock (($asyncRouteTask.Script).GetNewClosure()) -Parameters $parameters -OutputStream $result -PassThru -Name $asyncRouteTask.AsyncRouteId + + # Set the expiration time based on the timeout value + if ($asyncRouteTask.Timeout -ge 0) { + $expireTime = [datetime]::UtcNow.AddSeconds($asyncRouteTask.Timeout) + } + else { + $expireTime = [datetime]::MaxValue + } + + # Initialize the result hashtable + $asyncOperation = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $asyncOperation['Id'] = $Id + $asyncOperation['AsyncRouteId'] = $asyncRouteTask.AsyncRouteId + $asyncOperation['Runspace'] = $runspace + $asyncOperation['Output'] = $result + $asyncOperation['StartingTime'] = $null + $asyncOperation['CreationTime'] = $creationTime + $asyncOperation['CompletedTime'] = $null + $asyncOperation['ExpireTime'] = $expireTime + $asyncOperation['State'] = 'NotStarted' + $asyncOperation['Error'] = $null + $asyncOperation['CallbackSettings'] = $asyncRouteTask.CallbackSettings + $asyncOperation['Cancellable'] = $asyncRouteTask.Cancellable + $asyncOperation['Timeout'] = $asyncRouteTask.Timeout + $asyncOperation['WebEvent'] = $parameters.WebEvent + + if ($asyncRouteTask.ContainsKey('Sse')) { + $sseObject = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $sseObject['Name'] = $asyncRouteTask['Sse'].Name + $sseObject['Group'] = $asyncRouteTask['Sse'].Group + $sseObject['Url'] = "$($asyncRouteTask['Sse'].Name)?Id=$Id" + $sseObject['State'] = 'NotStarted' + $asyncOperation['Sse'] = $sseObject + } + + # Add user information if available + if ($WebEvent.Auth.User) { + $asyncOperation['User'] = $WebEvent.Auth.User[$PodeContext.AsyncRoutes.UserFieldIdentifier] + # Make a deepcopy of the permission object + $asyncOperation['Permission'] = Copy-PodeObjectDeepClone -InputObject $asyncRouteTask.Permission + } + + # Add the request URL and method + $asyncOperation['Url'] = $WebEvent.Request.Url + $asyncOperation['Method'] = $WebEvent.Method + + # If the task involves a user, include user information and add default permissions + if ($asyncOperation['User']) { + + # Iterate over the permission types: 'Read' and 'Write' + 'Read', 'Write' | ForEach-Object { + # Check if the Permission hashtable contains the current permission type (e.g., 'Read' or 'Write') + if (! $asyncOperation['Permission'].ContainsKey($_)) { + # If not, initialize it as an empty hashtable + $asyncOperation['Permission'][$_] = @{} + } + + # Check if the 'Users' array exists within the current permission type + if (! $asyncOperation['Permission'][$_].ContainsKey('Users')) { + # If not, initialize it as an empty array + $asyncOperation['Permission'][$_].Users = @() + } + + # Add the user to the 'Users' array if they are not already present + if (! ($asyncOperation['Permission'][$_].Users -icontains $asyncOperation.User)) { + $asyncOperation['Permission'][$_].Users += $asyncOperation.User + } + } + } + # Store the result in the Pode context + $PodeContext.AsyncRoutes.Processes[$Id] = $asyncOperation + + # Return the result of the asynchronous operation + $res = Export-PodeAsyncRouteInfo -Async $asyncOperation + # Send the response based on the requested media type + switch ($responseMediaType) { + 'application/xml' { Write-PodeXmlResponse -Value $res -StatusCode 200; break } + 'application/json' { Write-PodeJsonResponse -Value $res -StatusCode 200 ; break } + 'application/yaml' { Write-PodeYamlResponse -Value $res -StatusCode 200 ; break } + default { Write-PodeJsonResponse -Value $res -StatusCode 200 } + } + } + catch { + $_ | Write-PodeErrorLog + } + } +} + +<# +.SYNOPSIS + Retrieves a script block for handling asynchronous GET requests in Pode. + +.DESCRIPTION + This function returns a script block designed to process asynchronous GET requests in a Pode web server. + The script block checks for task identifiers in different parts of the request (cookies, headers, path parameters, query parameters) + and retrieves the corresponding async route result. It handles authorization, formats the response based on the Accept header, + and returns the appropriate response. + + PARAMETER In + The source of the task identifier, such as 'Cookie', 'Header', 'Path', or 'Query'. + + PARAMETER TaskIdName + The name of the task identifier to be retrieved from the specified source. + +.EXAMPLE + $scriptBlock = Get-PodeAsyncGetScriptBlock + # Use the returned script block in an async GET route in Pode + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeAsyncGetScriptBlock { + # This function returns a script block that handles async route operations + return [scriptblock] { + param($In, $TaskIdName) + + # Determine which type of input we have (Cookie, Header, Path or Query) + switch ($In) { + 'Cookie' { $id = Get-PodeCookie -Name $TaskIdName; break } + 'Header' { $id = Get-PodeHeader -Name $TaskIdName; break } + 'Path' { $id = $WebEvent.Parameters[$TaskIdName]; break } + 'Query' { $id = $WebEvent.Query[$TaskIdName]; break } + } + + # Get the Accept header to determine the response format + $responseMediaType = Get-PodeHeader -Name 'Accept' + + # Check if we have a result for this async route operation + if ($PodeContext.AsyncRoutes.Processes.ContainsKey($id)) { + $async = $PodeContext.AsyncRoutes.Processes[$id] + # Check if the user is authorized to perform this operation + if ($async['User']) { + if ($WebEvent.Auth.User) { + $authorized = Test-PodeAsyncRoutePermission -Permission $async['Permission'].Read -User $WebEvent.Auth.User + } + else { + $authorized = $false + } + } + else { + $authorized = $true + } + + # If authorized, export the task info and return a response + if ($authorized) { + # Create a summary of the task for export + $export = Export-PodeAsyncRouteInfo -Async $async + + switch ($responseMediaType) { + 'application/xml' { Write-PodeXmlResponse -Value $export -StatusCode 200; break } + 'application/json' { Write-PodeJsonResponse -Value $export -StatusCode 200 ; break } + 'application/yaml' { Write-PodeYamlResponse -Value $export -StatusCode 200 ; break } + default { Write-PodeJsonResponse -Value $export -StatusCode 200 } + } + return + } + else { + # If not authorized, return an error response + $errorMsg = @{Id = $id ; Error = 'User not entitled to view the Async Route operation' } + $statusCode = 401 #'Unauthorized' + } + } + else { + # If no async route operation is found, return a not found error response + $errorMsg = @{Id = $id ; Error = 'No Async Route operation Found' } + $statusCode = 404 #'Not Found' + } + switch ($responseMediaType) { + 'application/xml' { Write-PodeXmlResponse -Value $errorMsg -StatusCode $statusCode; break } + 'application/json' { Write-PodeJsonResponse -Value $errorMsg -StatusCode $statusCode ; break } + 'application/yaml' { Write-PodeYamlResponse -Value $errorMsg -StatusCode $statusCode ; break } + default { Write-PodeJsonResponse -Value $errorMsg -StatusCode $statusCode } + } + } +} + + +<# +.SYNOPSIS + Retrieves a script block for handling the stopping of asynchronous route tasks in Pode. + +.DESCRIPTION + This function returns a script block designed to stop asynchronous route tasks in a Pode web server. + The script block checks for task identifiers in different parts of the request (cookies, headers, path parameters, query parameters) + and retrieves the corresponding async route result. It handles authorization, cancels the task if it is cancellable and not completed, + and formats the response based on the Accept header. + + PARAMETER In + The source of the task identifier, such as 'Cookie', 'Header', 'Path', or 'Query'. + + PARAMETER TaskIdName + The name of the task identifier to be retrieved from the specified source. + +.EXAMPLE + $scriptBlock = Get-PodeAsyncRouteStopScriptBlock + # Use the returned script block in an async stop route in Pode + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeAsyncRouteStopScriptBlock { + # This function returns a script block that handles async route operations + return [scriptblock] { + param($In, $TaskIdName) + + # Determine the source of the task Id based on the input parameter + switch ($In) { + 'Cookie' { $id = Get-PodeCookie -Name $TaskIdName; break } + 'Header' { $id = Get-PodeHeader -Name $TaskIdName; break } + 'Path' { $id = $WebEvent.Parameters[$TaskIdName]; break } + 'Query' { $id = $WebEvent.Query[$TaskIdName]; break } + } + + # Get the 'Accept' header from the request to determine response format + $responseMediaType = Get-PodeHeader -Name 'Accept' + + # Check if the task Id exists in the async routes results + if ($PodeContext.AsyncRoutes.Processes.ContainsKey($id)) { + $async = $PodeContext.AsyncRoutes.Processes[$id] + + # If the task is not completed + if (!$async['Runspace'].Handler.IsCompleted) { + # If the task is cancellable + if ($async['Cancellable']) { + + if ($async['User'] -and ($null -eq $WebEvent.Auth.User)) { + # If the task is not cancellable, set an error message + $errorMsg = @{Id = $id ; Error = 'Async Route operation requires authentication.' } + $statusCode = 401 # Unauthorized + } + else { + if ((Test-PodeAsyncRoutePermission -Permission $async['Permission'].Write -User $WebEvent.Auth.User)) { + # Set the task state to 'Aborted' and log the error and completion time + $async['State'] = 'Aborted' + $async['Error'] = 'Aborted by the user' + $async['CompletedTime'] = [datetime]::UtcNow + $async['Runspace'].Pipeline.Dispose() + Complete-PodeAsyncRouteOperation -AsyncProcess $async + + # Create a summary of the task + $export = Export-PodeAsyncRouteInfo -Async $async + + # Respond with the task summary in the appropriate format + switch ($responseMediaType) { + 'application/xml' { Write-PodeXmlResponse -Value $export -StatusCode 200; break } + 'application/json' { Write-PodeJsonResponse -Value $export -StatusCode 200 ; break } + 'application/yaml' { Write-PodeYamlResponse -Value $export -StatusCode 200 ; break } + default { Write-PodeJsonResponse -Value $export -StatusCode 200 } + } + return + } + else { + $errorMsg = @{Id = $id ; Error = 'User not entitled to stop the Async Route operation' } + $statusCode = 401 #'Unauthorized' + } + } + } + else { + # If the task is not cancellable, set an error message + $errorMsg = @{Id = $id ; Error = "The task has the 'NonCancellable' flag." } + $statusCode = 423 #'Locked + } + } + else { + # If the task is already completed, set an error message + $errorMsg = @{Id = $id ; Error = 'The Task is already completed.' } + $statusCode = 410 #'Gone' + } + } + else { + # If no task is found, set an error message + $errorMsg = @{Id = $id ; Error = 'No Task Found.' } + $statusCode = 404 #'Not Found' + } + + # Respond with the error message in the appropriate format + if ($errorMsg) { + switch ($responseMediaType) { + 'application/xml' { Write-PodeXmlResponse -Value $errorMsg -StatusCode $statusCode ; break } + 'application/json' { Write-PodeJsonResponse -Value $errorMsg -StatusCode $statusCode ; break } + 'application/yaml' { Write-PodeYamlResponse -Value $errorMsg -StatusCode $statusCode ; break } + default { Write-PodeJsonResponse -Value $errorMsg -StatusCode $statusCode } + } + } + } +} + +<# +.SYNOPSIS + Exports the detailed information of an asynchronous operation to a hashtable. + +.DESCRIPTION + The `Export-PodeAsyncRouteInfo` function extracts and formats information from an asynchronous operation encapsulated in a [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] object. It includes details such as Id, creation time, state, user, permissions, and callback settings, among others. The function returns a hashtable with this information, suitable for logging or further processing. + +.PARAMETER Async + A [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] containing the asynchronous operation's details. This parameter is mandatory. + +.PARAMETER Raw + If specified, returns the raw [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] without any formatting. + +.EXAMPLE + $asyncInfo = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $exportedInfo = Export-PodeAsyncRouteInfo -Async $asyncInfo + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Export-PodeAsyncRouteInfo { + param( + [Parameter(Mandatory = $true )] + [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] + $Async, + + [switch] + $Raw + ) + if ($Raw.IsPresent) { + return $Async + } + + # Initialize a hashtable to store the exported information + $export = @{ + Id = $Async['Id'] + Cancellable = $Async['Cancellable'] + # Format creation time in ISO 8601 UTC format + CreationTime = Format-PodeDateToIso8601 -Date $Async['CreationTime'] + ExpireTime = Format-PodeDateToIso8601 -Date $Async['ExpireTime'] + AsyncRouteId = $Async['AsyncRouteId'] + State = $Async['State'] + } + + # Include permission if it exists + if ($Async.ContainsKey('Permission')) { + $export.Permission = $Async['Permission'] + } + + # Include starting time if it exists + if ($Async['StartingTime']) { + $export.StartingTime = Format-PodeDateToIso8601 -Date $Async['StartingTime'] + } + + # Include callback settings if they exist + if ($Async['CallbackSettings']) { + $export.CallbackSettings = $Async['CallbackSettings'] + } + + # Include user if it exists + if ($Async.ContainsKey('User')) { + $export.User = $Async['User'] + } + + # Include SSE setting if it exists + if ($Async.ContainsKey('Sse')) { + $export.Sse = @{ + Name = $Async['Sse'].Name + State = $Async['Sse'].State + } + if ($Async['Sse'].ContainsKey('Group')) { + $export.Sse['Group'] = $Async['Sse'].Group + } + if ($Async['Sse'].ContainsKey('Url')) { + $export.Sse['Url'] = $Async['Sse'].Url + } + } + + # Include Progress setting if it exists + if ($Async.ContainsKey('Progress')) { + $export.Progress = [math]::Round($Async['Progress'], 2) + } + + $export.IsCompleted = $Async['Runspace'].Handler.IsCompleted + + # If the task is completed, include the result or error based on the state + if ($export.IsCompleted) { + switch ($Async['State'] ) { + 'Failed' { + $export.Error = $Async['Error'] + break + } + 'Completed' { + if ($Async['Result']) { + $export.Result = $Async['Result'] + } + break + } + 'Aborted' { + $export.Error = $Async['Error'] + break + } + } + + # Include callback information if it exists + if ($Async.ContainsKey('CallbackTentative') -and $Async['CallbackTentative'] -gt 0) { + $export.CallbackInfo = @{ + Tentative = $Async['CallbackTentative'] + State = $Async['CallbackInfoState'] + Url = $Async['CallbackUrl'] + } + } + + # Ensure completed time is set, retrying after a short delay if necessary + if (! $Async.ContainsKey('CompletedTime')) { + Start-Sleep 1 + } + if ($Async.ContainsKey('CompletedTime')) { + # Format completed time in ISO 8601 UTC format + $export.CompletedTime = Format-PodeDateToIso8601 -Date $Async['CompletedTime'] + } + + } + + # Return the exported information + return $export +} + +<# +.SYNOPSIS + Retrieves a script block for querying asynchronous route tasks in Pode. + +.DESCRIPTION + This function returns a script block designed to query asynchronous route tasks in a Pode web server. + The script block processes the query from different parts of the request (body, query parameters, headers), + searches for Async Route Tasks based on the query, checks permissions, and formats the response based on the Accept header. + +.PARAMETER Payload + The source of the query, such as 'Body', 'Query', or 'Header'. + +.EXAMPLE + $scriptBlock = Get-PodeAsyncRouteQueryScriptBlock + # Use the returned script block in an async query route in Pode + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeAsyncRouteQueryScriptBlock { + return [scriptblock] { + param($Payload, $DefinitionTag) + + # Determine the source of the query based on the payload parameter + switch ($Payload) { + 'Body' { $query = $WebEvent.Data } # Retrieve the query from the body + 'Query' { $query = $WebEvent.Query['query'] } # Retrieve the query from query parameters + 'Header' { $query = $WebEvent.Request.Headers['query'] } # Retrieve the query from headers + } + + # Get the 'Accept' header from the request to determine the response format + $responseMediaType = Get-PodeHeader -Name 'Accept' + $response = @() # Initialize an empty array to hold the response + try { + if ($PodeContext.Server.OpenAPI.Definitions[$DefinitionTag].hiddenComponents.schemaValidation) { + $validation = Test-PodeOAJsonSchemaCompliance -Json $query -SchemaReference ` + $PodeContext.Server.OpenApi.Definitions[$DefinitionTag].hiddenComponents.AsyncRoute.QueryRequestName + $validated = $validation.result + } + else { + $validated = $true + } + if ($validated) { + # Search for Async Route Tasks based on the query and user, checking permissions + $results = Search-PodeAsyncRouteTask -Query $query -User $WebEvent.Auth.User -CheckPermission + + # If results are found, export async route task information for each result + if ($results) { + foreach ($async in $results) { + $response += Export-PodeAsyncRouteInfo -Async $async + } + } + + # Respond with the results in the appropriate format + switch ($responseMediaType) { + 'application/xml' { Write-PodeXmlResponse -Value $response -StatusCode 200; break } + 'application/json' { Write-PodeJsonResponse -Value $response -StatusCode 200 ; break } + 'application/yaml' { Write-PodeYamlResponse -Value $response -StatusCode 200 ; break } + default { Write-PodeJsonResponse -Value $response -StatusCode 200 } + } + } + else { + $response = @{'Error' = $validation.message } + switch ($responseMediaType) { + 'application/xml' { Write-PodeXmlResponse -Value $response -StatusCode 400; break } + 'application/json' { Write-PodeJsonResponse -Value $response -StatusCode 400 ; break } + 'application/yaml' { Write-PodeYamlResponse -Value $response -StatusCode 400 ; break } + default { Write-PodeJsonResponse -Value $response -StatusCode 400 } + } + } + } + catch { + $response = @{'Error' = $_.tostring() } + switch ($responseMediaType) { + 'application/xml' { Write-PodeXmlResponse -Value $response -StatusCode 500; break } + 'application/json' { Write-PodeJsonResponse -Value $response -StatusCode 500 ; break } + 'application/yaml' { Write-PodeYamlResponse -Value $response -StatusCode 500 ; break } + default { Write-PodeJsonResponse -Value $response -StatusCode 500 } + } + } + } +} + + +<# +.SYNOPSIS + Retrieves the asynchronous route OpenAPI schema names. + +.DESCRIPTION + The Get-PodeAsyncRouteOAName function is used to fetch the schema names for asynchronous Pode route operations from the OpenAPI definitions. + It checks for consistency across multiple OpenAPI definition tags and throws an exception if there are mismatches in the schema names. + +.PARAMETER Tag + An array of OpenAPI definition tags to be checked. + +.THROWS + An exception if there are mismatches in the schema names across different OpenAPI definitions. +#> +function Get-PodeAsyncRouteOAName { + param ( + [string[]] + $Tag, + + [switch] + $ForEachOADefinition + ) + $DefinitionTag = Test-PodeOADefinitionTag -Tag $Tag + + if ($ForEachOADefinition.IsPresent) { + $result = @{} + if ( $DefinitionTag -is [string]) { + $DefinitionTag = [string[]]@($DefinitionTag) + } + for ($i = 0; $i -lt $DefinitionTag.Count; $i++) { + $result[$DefinitionTag[$i]] = $PodeContext.Server.OpenApi.Definitions[$DefinitionTag[$i]].hiddenComponents.AsyncRoute + } + return $result + } + if ($DefinitionTag.Count -gt 1) { + for ( $i = 1 ; $i -lt $DefinitionTag.Count ; $i++) { + + if ($PodeContext.Server.OpenApi.Definitions[$DefinitionTag[0]].hiddenComponents.AsyncRoute.OATypeName -ne $PodeContext.Server.OpenApi.Definitions[$DefinitionTag[$i]].hiddenComponents.AsyncRoute.OATypeName) { + # varies between different OpenAPI definitions. + throw ($PodeLocale.openApiDefinitionsMismatchExceptionMessage -f 'OATypeName') + } + + if ($PodeContext.Server.OpenApi.Definitions[$DefinitionTag[0]].hiddenComponents.AsyncRoute.QueryParameter -ne $PodeContext.Server.OpenApi.Definitions[$DefinitionTag[$i]].hiddenComponents.AsyncRoute.QueryParameter) { + # varies between different OpenAPI definitions. + throw ($PodeLocale.openApiDefinitionsMismatchExceptionMessage -f 'QueryParameter') + } + + if ($PodeContext.Server.OpenApi.Definitions[$DefinitionTag[0]].hiddenComponents.AsyncRoute.QueryRequestName -ne $PodeContext.Server.OpenApi.Definitions[$DefinitionTag[$i]].hiddenComponents.AsyncRoute.QueryRequestName) { + # varies between different OpenAPI definitions. + throw ($PodeLocale.openApiDefinitionsMismatchExceptionMessage -f 'QueryRequestName') + } + + if ($PodeContext.Server.OpenApi.Definitions[$DefinitionTag[0]].hiddenComponents.AsyncRoute.TaskIdName -ne $PodeContext.Server.OpenApi.Definitions[$DefinitionTag[$i]].hiddenComponents.AsyncRoute.TaskIdName) { + # varies between different OpenAPI definitions. + throw ($PodeLocale.openApiDefinitionsMismatchExceptionMessage -f 'TaskIdName') + } + + } + + return $PodeContext.Server.OpenApi.Definitions[$DefinitionTag[0]].hiddenComponents.AsyncRoute + } + else { + return $PodeContext.Server.OpenApi.Definitions[$DefinitionTag].hiddenComponents.AsyncRoute + } +} + + + +<# +.SYNOPSIS + Retrieves the schema names for asynchronous Pode route operations. + +.DESCRIPTION + The Get-PodeAsyncRouteOASchemaNameInternal function is designed to return a hashtable containing schema names for asynchronous Pode route operations. + It includes the type names and parameter names that are used for OpenAPI documentation. + +.PARAMETER OATypeName + The type name for OpenAPI documentation. The default is 'AsyncRouteTask'. + +.PARAMETER TaskIdName + The name of the parameter that contains the task Id. The default is 'id'. + +.PARAMETER QueryRequestName + The name of the Pode task query request in the OpenAPI schema. Defaults to 'AsyncRouteTaskQuery'. + +.PARAMETER QueryParameterName + The name of the query parameter in the OpenAPI schema. Defaults to 'AsyncRouteTaskQueryParameter'. +#> +function Get-PodeAsyncRouteOASchemaNameInternal { + param ( + [string] + $OATypeName = 'AsyncRouteTask', + + [Parameter()] + [string] + $TaskIdName = 'id', + + [Parameter()] + [string] + $QueryRequestName = 'AsyncRouteTaskQuery', + + [Parameter()] + [string] + $QueryParameterName = 'AsyncRouteTaskQueryParameter' + ) + return @{ + # Store the OATypeName name + OATypeName = $OATypeName + # Store the TaskIdName name + TaskIdName = $TaskIdName + # Store the QueryRequestName name + QueryRequestName = $QueryRequestName + # Store the QueryParameterName name + QueryParameterName = $QueryParameterName + } +} + +<# +.SYNOPSIS + Closes and disposes of the timer associated with a Pode asynchronous route operation. + +.DESCRIPTION + The `Close-PodeAsyncRouteTimer` function stops and disposes of a timer that is part of a + Pode asynchronous route operation. It also unregisters any event associated with the timer + and removes the timer from the operation's hashtable. + +.PARAMETER Operation + A hashtable representing the operation that contains the timer and event information. The + function expects the hashtable to have a 'Timer' key and an 'eventName' key. + +.EXAMPLE + $operation = @{ + Timer = New-Object System.Timers.Timer + eventName = 'AsyncRouteTimerEvent' + } + Close-PodeAsyncRouteTimer -Operation $operation + + This example stops and disposes of the timer in the `$operation` hashtable, unregistering the + associated event and removing the timer from the hashtable. + +.NOTES + Ensure that the 'Timer' key and 'eventName' key are present in the hashtable passed to the + function. If the 'Timer' key is not found, the function will return without performing any actions. + +#> +function Close-PodeAsyncRouteTimer { + param( + [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] + $Operation + ) + try { + if (!$Operation['Timer']) { + return + } + + $Operation['Timer'].Stop() + $Operation['Timer'].Dispose() + Unregister-Event -SourceIdentifier $Operation['eventName'] -Force + $null = $Operation.Remove('Timer') + } + catch { + $_ | Write-PodeErrorLog + } +} + + +<# +.SYNOPSIS + Adds an OpenAPI component schema for Pode asynchronous route tasks. + +.DESCRIPTION + The Add-PodeAsyncRouteComponentSchema function creates an OpenAPI component schema for Pode asynchronous route tasks if it does not already exist. + This schema includes properties such as Id, CreationTime, StartingTime, Result, CompletedTime, State, Error, and Task. + +.PARAMETER Name + The name of the OpenAPI component schema. Defaults to 'AsyncRouteTask'. + +.EXAMPLE + Add-PodeAsyncRouteComponentSchema -Name 'CustomTask' + + This example creates an OpenAPI component schema named 'CustomTask' with the specified properties if it does not already exist. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Add-PodeAsyncRouteComponentSchema { + param ( + [string] + $Name = 'AsyncRouteTask', + + [string[]] + $DefinitionTag + ) + + # Test and normalize the definition tag + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + + # Check if the component schema already exists + if (!(Test-PodeOAComponent -Field schemas -Name $Name -DefinitionTag $DefinitionTag)) { + + # Define permission content + $permissionContent = New-PodeOAStringProperty -Name 'Groups' -Array -Example 'group1', 'group2' | + New-PodeOAStringProperty -Name 'Roles' -Array -Example 'reviewer', 'taskadmin' | + New-PodeOAStringProperty -Name 'Scopes' -Array -Example 'scope1', 'scope2', 'scope3' | + New-PodeOAStringProperty -Name 'Users' -Array -Example 'id0001', 'id0005', 'id0231' + + # Create the component schema + New-PodeOAStringProperty -Name 'Id' -Format Uuid -Description 'The async route task unique identifier.' -Required | + New-PodeOAStringProperty -Name 'User' -Description 'The async route task owner.' | + New-PodeOAStringProperty -Name 'CreationTime' -Format Date-Time -Description 'The async route task creation time.' -Example '2024-07-02T20:58:15.2014422Z' -Required | + New-PodeOAStringProperty -Name 'ExpireTime' -Format Date-Time -Description 'The async route task expiration.' -Example '2024-07-02T23:58:15.2014422Z' -Required | + New-PodeOAStringProperty -Name 'StartingTime' -Format Date-Time -Description 'The async route task starting time.' -Example '2024-07-02T20:58:15.2014422Z' | + New-PodeOAStringProperty -Name 'Result' -Example '{result ="Anything is good" , numOfIteration = 3 }' | + New-PodeOAStringProperty -Name 'CompletedTime' -Format Date-Time -Description 'The async route task completion time.' -Example '2024-07-02T20:59:23.2174712Z' | + New-PodeOAStringProperty -Name 'State' -Description 'The async route task status' -Required -Example 'Running' -Enum @('NotStarted', 'Running', 'Failed', 'Completed', 'Aborted') | + New-PodeOAStringProperty -Name 'Error' -Description 'The error message if any.' | + New-PodeOAStringProperty -Name 'AsyncRouteId' -Example '__Get_path_endpoint1_' -Description 'The async route Id.' -Required | + New-PodeOABoolProperty -Name 'Cancellable' -Description 'The async route task can be forcefully terminated' -Required | + New-PodeOABoolProperty -Name 'IsCompleted' -Description 'The async route task is completed' -Required | + New-PodeOAObjectProperty -Name 'Sse' -Description 'The async route task Sse details.' -Properties ( + New-PodeOAStringProperty -Name 'Name' -Description 'The name of the Sse connection.' -Required | + New-PodeOAStringProperty -Name 'State' -Description 'The state of the Sse connection.' -Required -Enum @('NotStarted', 'Running', 'Failed', 'Completed', 'Aborted') | + New-PodeOAStringProperty -Name 'Group' -Description 'The group name for this Sse connection.' | + New-PodeOAStringProperty -Name 'Url' -Description 'The Sse url.' + ) | + New-PodeOANumberProperty -Name 'Progress' -Description 'The async route task percentage progress' -Minimum 0 -Maximum 100 | + New-PodeOAObjectProperty -Name 'Permission' -Description 'The permission governing the async route task.' -Properties ( + ($permissionContent | New-PodeOAObjectProperty -Name 'Read'), + ($permissionContent | New-PodeOAObjectProperty -Name 'Write') + ) | + New-PodeOAObjectProperty -Name 'CallbackInfo' -Description 'The Callback operation result' -Properties ( + New-PodeOAStringProperty -Name 'State' -Description 'Operation status' -Example 'Completed' -Enum @('NotStarted', 'Running', 'Failed', 'Completed') | + New-PodeOAIntProperty -Name 'Tentative' -Description 'Number of tentatives' | + New-PodeOAStringProperty -Name 'Url' -Format Uri -Description 'The callback URL' -Example 'Completed' + ) | + New-PodeOAObjectProperty -Name 'CallbackSettings' -Description 'Callback Configuration' -Properties ( + New-PodeOAStringProperty -Name 'UrlField' -Description 'The URL Field.' -Example '$request.body#/callbackUrl' -Required | + New-PodeOABoolProperty -Name 'SendResult' -Description 'Send the result.' -Required | + New-PodeOAStringProperty -Name 'Method' -Description 'HTTP Method.' -Enum @('Post', 'Put') -Required | + New-PodeOAStringProperty -Name 'ContentType' -Description 'ContentType.' -Enum @('application/json' , 'application/xml', 'application/yaml') -Required | + New-PodeOAObjectProperty -Name 'HeaderFields' -AdditionalProperties (New-PodeOAStringProperty) -NoProperties + ) | + New-PodeOAObjectProperty | Add-PodeOAComponentSchema -Name $Name -DefinitionTag $DefinitionTag + } + +} \ No newline at end of file diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 19d4c1004..9037fb259 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -73,24 +73,24 @@ function New-PodeContext { } # basic context object - $ctx = [PSCustomObject]@{ - Threads = @{} - Timers = @{} - Schedules = @{} - Tasks = @{} - RunspacePools = $null - Runspaces = $null - RunspaceState = $null - Tokens = @{} - LogsToProcess = $null - Threading = @{} - Server = @{} - Metrics = @{} - Listeners = @() - Receivers = @() - Watchers = @() - Fim = @{} - } + $ctx = [PSCustomObject]::new() | + Add-Member -MemberType NoteProperty -Name Threads -Value @{} -PassThru | + Add-Member -MemberType NoteProperty -Name Timers -Value @{} -PassThru | + Add-Member -MemberType NoteProperty -Name Schedules -Value @{} -PassThru | + Add-Member -MemberType NoteProperty -Name Tasks -Value @{} -PassThru | + Add-Member -MemberType NoteProperty -Name AsyncRoutes -Value @{} -PassThru | + Add-Member -MemberType NoteProperty -Name RunspacePools -Value $null -PassThru | + Add-Member -MemberType NoteProperty -Name Runspaces -Value $null -PassThru | + Add-Member -MemberType NoteProperty -Name RunspaceState -Value $null -PassThru | + Add-Member -MemberType NoteProperty -Name Tokens -Value @{} -PassThru | + Add-Member -MemberType NoteProperty -Name LogsToProcess -Value $null -PassThru | + Add-Member -MemberType NoteProperty -Name Threading -Value @{} -PassThru | + Add-Member -MemberType NoteProperty -Name Server -Value @{} -PassThru | + Add-Member -MemberType NoteProperty -Name Metrics -Value @{} -PassThru | + Add-Member -MemberType NoteProperty -Name Listeners -Value @() -PassThru | + Add-Member -MemberType NoteProperty -Name Receivers -Value @() -PassThru | + Add-Member -MemberType NoteProperty -Name Watchers -Value @() -PassThru | + Add-Member -MemberType NoteProperty -Name Fim -Value @{} -PassThru # set the server name, logic and root, and other basic properties $ctx.Server.Name = $Name @@ -122,9 +122,23 @@ function New-PodeContext { } $ctx.Tasks = @{ - Enabled = ($EnablePool -icontains 'tasks') - Items = @{} - Processes = @{} + Enabled = ($EnablePool -icontains 'tasks') + Items = @{} + Processes = @{} + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 1 + } + } + + $ctx.AsyncRoutes = @{ + Enabled = $true + Processes = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + UserFieldIdentifier = 'Id' } $ctx.Fim = @{ @@ -143,12 +157,13 @@ function New-PodeContext { # set thread counts $ctx.Threads = @{ - General = $Threads - Schedules = 10 - Files = 1 - Tasks = 2 - WebSockets = 2 - Timers = 1 + General = $Threads + Schedules = 10 + Files = 1 + Tasks = 2 + WebSockets = 2 + AsyncRoutes = 0 + Timers = 1 } # set socket details for pode server @@ -490,18 +505,17 @@ function New-PodeContext { $ctx.Server.Endware = @() # runspace pools - $ctx.RunspacePools = @{ - Main = $null - Web = $null - Smtp = $null - Tcp = $null - Signals = $null - Schedules = $null - Gui = $null - Tasks = $null - Files = $null - Timers = $null - } + $ctx.RunspacePools = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + $ctx.RunspacePools['Main'] = $null + $ctx.RunspacePools['Web'] = $null + $ctx.RunspacePools['Smtp'] = $null + $ctx.RunspacePools['Tcp'] = $null + $ctx.RunspacePools['Signals'] = $null + $ctx.RunspacePools['Schedules'] = $null + $ctx.RunspacePools['Gui'] = $null + $ctx.RunspacePools['Tasks'] = $null + $ctx.RunspacePools['Files'] = $null + $ctx.RunspacePools['Timers'] = $null # threading locks, etc. $ctx.Threading.Lockables = @{ @@ -552,29 +566,6 @@ function New-PodeContext { return $ctx } -function New-PodeRunspaceState { - # create the state, and add the pode modules - $state = [initialsessionstate]::CreateDefault() - $state.ImportPSModule($PodeContext.Server.PodeModule.DataPath) - $state.ImportPSModule($PodeContext.Server.PodeModule.InternalPath) - - # load the vars into the share state - $session = New-PodeStateContext -Context $PodeContext - - $variables = @( - [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('PodeLocale', $PodeLocale, $null), - [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('PodeContext', $session, $null), - [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('Console', $Host, $null), - [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('PODE_SCOPE_RUNSPACE', $true, $null) - ) - - foreach ($var in $variables) { - $state.Variables.Add($var) - } - - $PodeContext.RunspaceState = $state -} - <# .SYNOPSIS Creates and initializes runspace pools for various Pode components. @@ -609,7 +600,7 @@ function New-PodeRunspacePool { # main runspace - for timers, schedules, etc $totalThreadCount = ($threadsCounts.Values | Measure-Object -Sum).Sum $PodeContext.RunspacePools.Main = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, $totalThreadCount, $PodeContext.RunspaceState, $Host) + Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces $totalThreadCount -RunspaceState $PodeContext.RunspaceState State = 'Waiting' LastId = 0 } @@ -626,7 +617,7 @@ function New-PodeRunspacePool { # smtp runspace - if we have any smtp endpoints if (Test-PodeEndpointByProtocolType -Type Smtp) { $PodeContext.RunspacePools.Smtp = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) + Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces ($PodeContext.Threads.General + 1) -RunspaceState $PodeContext.RunspaceState State = 'Waiting' LastId = 0 } @@ -635,7 +626,7 @@ function New-PodeRunspacePool { # tcp runspace - if we have any tcp endpoints if (Test-PodeEndpointByProtocolType -Type Tcp) { $PodeContext.RunspacePools.Tcp = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) + Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces ($PodeContext.Threads.General + 1) -RunspaceState $PodeContext.RunspaceState State = 'Waiting' LastId = 0 } @@ -644,7 +635,7 @@ function New-PodeRunspacePool { # signals runspace - if we have any ws/s endpoints if (Test-PodeEndpointByProtocolType -Type Ws) { $PodeContext.RunspacePools.Signals = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 2), $PodeContext.RunspaceState, $Host) + Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces ($PodeContext.Threads.General + 2) -RunspaceState $PodeContext.RunspaceState State = 'Waiting' LastId = 0 } @@ -653,7 +644,7 @@ function New-PodeRunspacePool { # web socket connections runspace - for receiving data for external sockets if (Test-PodeWebSocketsExist) { $PodeContext.RunspacePools.WebSockets = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.WebSockets + 1, $PodeContext.RunspaceState, $Host) + Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces ($PodeContext.Threads.WebSockets + 1) -RunspaceState $PodeContext.RunspaceState State = 'Waiting' LastId = 0 } @@ -673,7 +664,7 @@ function New-PodeRunspacePool { # setup schedule runspace pool -if we have any schedules if (Test-PodeSchedulesExist) { $PodeContext.RunspacePools.Schedules = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Schedules, $PodeContext.RunspaceState, $Host) + Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces $PodeContext.Threads.Schedules -RunspaceState $PodeContext.RunspaceState State = 'Waiting' LastId = 0 } @@ -682,7 +673,7 @@ function New-PodeRunspacePool { # setup tasks runspace pool -if we have any tasks if (Test-PodeTasksExist) { $PodeContext.RunspacePools.Tasks = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Tasks, $PodeContext.RunspaceState, $Host) + Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces $PodeContext.Threads.Tasks -RunspaceState $PodeContext.RunspaceState State = 'Waiting' LastId = 0 } @@ -691,7 +682,7 @@ function New-PodeRunspacePool { # setup files runspace pool -if we have any file watchers if (Test-PodeFileWatchersExist) { $PodeContext.RunspacePools.Files = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Files + 1, $PodeContext.RunspaceState, $Host) + Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces ($PodeContext.Threads.Files + 1) -RunspaceState $PodeContext.RunspaceState State = 'Waiting' LastId = 0 } @@ -700,7 +691,7 @@ function New-PodeRunspacePool { # setup gui runspace pool (only for non-ps-core) - if gui enabled if (Test-PodeGuiEnabled) { $PodeContext.RunspacePools.Gui = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, 1, $PodeContext.RunspaceState, $Host) + Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces 1 -RunspaceState $PodeContext.RunspaceState State = 'Waiting' LastId = 0 } @@ -711,149 +702,18 @@ function New-PodeRunspacePool { <# .SYNOPSIS - Opens and initializes runspace pools for various Pode components. + Creates a new Pode state context object from an existing context. .DESCRIPTION - This function opens and initializes runspace pools for different Pode components, such as timers, schedules, web endpoints, web sockets, SMTP, TCP, and more. It asynchronously opens the pools and waits for them to be in the 'Opened' state. If any pool fails to open, it reports an error. - -.OUTPUTS - Opens and initializes runspace pools for various Pode components. -#> -function Open-PodeRunspacePool { - if ($PodeContext.Server.IsServerless) { - return - } - - $start = [datetime]::Now - Write-Verbose 'Opening RunspacePools' - - # open pools async - foreach ($key in $PodeContext.RunspacePools.Keys) { - $item = $PodeContext.RunspacePools[$key] - if ($null -eq $item) { - continue - } - - $item.Pool.ThreadOptions = [System.Management.Automation.Runspaces.PSThreadOptions]::ReuseThread - $item.Pool.CleanupInterval = [timespan]::FromMinutes(5) - $item.Result = $item.Pool.BeginOpen($null, $null) - } - - # wait for them all to open - $queue = @($PodeContext.RunspacePools.Keys) - - while ($queue.Length -gt 0) { - foreach ($key in $queue) { - $item = $PodeContext.RunspacePools[$key] - if ($null -eq $item) { - $queue = ($queue | Where-Object { $_ -ine $key }) - continue - } - - if ($item.Pool.RunspacePoolStateInfo.State -iin @('Opened', 'Broken')) { - $queue = ($queue | Where-Object { $_ -ine $key }) - Write-Verbose "RunspacePool for $($key): $($item.Pool.RunspacePoolStateInfo.State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]" - } - } - - if ($queue.Length -gt 0) { - Start-Sleep -Milliseconds 100 - } - } + The New-PodeStateContext function constructs a new PSCustomObject that represents the current state of various Pode components. It extracts specific properties from the provided context object and organizes them into a structured format for easier management and monitoring. - # report errors for failed pools - foreach ($key in $PodeContext.RunspacePools.Keys) { - $item = $PodeContext.RunspacePools[$key] - if ($null -eq $item) { - continue - } - - if ($item.Pool.RunspacePoolStateInfo.State -ieq 'broken') { - $item.Pool.EndOpen($item.Result) | Out-Default - throw ($PodeLocale.failedToOpenRunspacePoolExceptionMessage -f $key) #"Failed to open RunspacePool: $($key)" - } - } - - Write-Verbose "RunspacePools opened [duration: $(([datetime]::Now - $start).TotalSeconds)s]" -} - -<# -.SYNOPSIS - Closes and disposes runspace pools for various Pode components. - -.DESCRIPTION - This function closes and disposes runspace pools for different Pode components, such as timers, schedules, web endpoints, web sockets, SMTP, TCP, and more. It asynchronously closes the pools and waits for them to be in the 'Closed' state. If any pool fails to close, it reports an error. +.PARAMETER Context + The existing context object containing Pode state information. This parameter is mandatory and must not be null. -.OUTPUTS - Closes and disposes runspace pools for various Pode components. +.EXAMPLE + # Load the vars into the share state + $session = New-PodeStateContext -Context $PodeContext #> -function Close-PodeRunspacePool { - if ($PodeContext.Server.IsServerless -or ($null -eq $PodeContext.RunspacePools)) { - return - } - - $start = [datetime]::Now - Write-Verbose 'Closing RunspacePools' - - # close pools async - foreach ($key in $PodeContext.RunspacePools.Keys) { - $item = $PodeContext.RunspacePools[$key] - if (($null -eq $item) -or ($item.Pool.IsDisposed)) { - continue - } - - $item.Result = $item.Pool.BeginClose($null, $null) - } - - # wait for them all to close - $queue = @($PodeContext.RunspacePools.Keys) - - while ($queue.Length -gt 0) { - foreach ($key in $queue) { - $item = $PodeContext.RunspacePools[$key] - if ($null -eq $item) { - $queue = ($queue | Where-Object { $_ -ine $key }) - continue - } - - if ($item.Pool.RunspacePoolStateInfo.State -iin @('Closed', 'Broken')) { - $queue = ($queue | Where-Object { $_ -ine $key }) - Write-Verbose "RunspacePool for $($key): $($item.Pool.RunspacePoolStateInfo.State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]" - } - } - - if ($queue.Length -gt 0) { - Start-Sleep -Milliseconds 100 - } - } - - # report errors for failed pools - foreach ($key in $PodeContext.RunspacePools.Keys) { - $item = $PodeContext.RunspacePools[$key] - if ($null -eq $item) { - continue - } - - if ($item.Pool.RunspacePoolStateInfo.State -ieq 'broken') { - $item.Pool.EndClose($item.Result) | Out-Default - # Failed to close RunspacePool - throw ($PodeLocale.failedToCloseRunspacePoolExceptionMessage -f $key) - } - } - - # dispose pools - foreach ($key in $PodeContext.RunspacePools.Keys) { - $item = $PodeContext.RunspacePools[$key] - if (($null -eq $item) -or ($item.Pool.IsDisposed)) { - continue - } - - Close-PodeDisposable -Disposable $item.Pool - } - - Write-Verbose "RunspacePools closed [duration: $(([datetime]::Now - $start).TotalSeconds)s]" -} - function New-PodeStateContext { param( [Parameter(Mandatory = $true)] @@ -861,20 +721,22 @@ function New-PodeStateContext { $Context ) - return [PSCustomObject]@{ - Threads = $Context.Threads - Timers = $Context.Timers - Schedules = $Context.Schedules - Tasks = $Context.Tasks - Fim = $Context.Fim - RunspacePools = $Context.RunspacePools - Tokens = $Context.Tokens - Metrics = $Context.Metrics - LogsToProcess = $Context.LogsToProcess - Threading = $Context.Threading - Server = $Context.Server - } + return ([PSCustomObject]::new() | + Add-Member -MemberType NoteProperty -Name Threads -Value $Context.Threads -PassThru | + Add-Member -MemberType NoteProperty -Name Timers -Value $Context.Timers -PassThru | + Add-Member -MemberType NoteProperty -Name Schedules -Value $Context.Schedules -PassThru | + Add-Member -MemberType NoteProperty -Name Tasks -Value $Context.Tasks -PassThru | + Add-Member -MemberType NoteProperty -Name Fim -Value $Context.Fim -PassThru | + Add-Member -MemberType NoteProperty -Name RunspacePools -Value $Context.RunspacePools -PassThru | + Add-Member -MemberType NoteProperty -Name Tokens -Value $Context.Tokens -PassThru | + Add-Member -MemberType NoteProperty -Name Metrics -Value $Context.Metrics -PassThru | + Add-Member -MemberType NoteProperty -Name LogsToProcess -Value $Context.LogsToProcess -PassThru | + Add-Member -MemberType NoteProperty -Name Threading -Value $Context.Threading -PassThru | + Add-Member -MemberType NoteProperty -Name Server -Value $Context.Server -PassThru | + Add-Member -MemberType NoteProperty -Name AsyncRoutes -Value $Context.AsyncRoutes -PassThru + ) } + <# .SYNOPSIS Opens and processes the Pode server configuration. @@ -1018,6 +880,18 @@ function Set-PodeServerConfiguration { } } + $Context.AsyncRoutes.HouseKeeping = @{ + TimerInterval = Protect-PodeValue -Value $Configuration.AsyncRoutes.HouseKeeping.TimerInterval -Default $Context.AsyncRoutes.HouseKeeping.TimerInterval + RetentionMinutes = Protect-PodeValue -Value $Configuration.AsyncRoutes.HouseKeeping.RetentionMinutes -Default $Context.AsyncRoutes.HouseKeeping.RetentionMinutes + } + + $Context.AsyncRoutes.UserFieldIdentifier = Protect-PodeValue -Value $Configuration.AsyncRoutes.UserFieldIdentifier -Default $Context.AsyncRoutes.UserFieldIdentifier + + $Context.Tasks.HouseKeeping = @{ + TimerInterval = Protect-PodeValue -Value $Configuration.Tasks.HouseKeeping.TimerInterval -Default $Context.Tasks.HouseKeeping.TimerInterval + RetentionMinutes = Protect-PodeValue -Value $Configuration.Tasks.HouseKeeping.RetentionMinutes -Default $Context.Tasks.HouseKeeping.RetentionMinutes + } + $Context.Server.AllowedActions = @{ Suspend = [bool](Protect-PodeValue -Value $Configuration.AllowedActions.Suspend -Default $Context.Server.AllowedActions.Suspend) Restart = [bool](Protect-PodeValue -Value $Configuration.AllowedActions.Restart -Default $Context.Server.AllowedActions.Restart) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index beefebb89..2d1cb2910 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -1569,7 +1569,7 @@ function ConvertFrom-PodeRequestContent { $Result.Data = ($Content | ConvertFrom-Json -AsHashtable) } else { - $Result.Data = ($Content | ConvertFrom-Json) + $Result.Data = ConvertTo-PodeHashtable -PSObject ($Content | ConvertFrom-Json) } } @@ -4009,6 +4009,169 @@ function Test-PodeIsISEHost { return ((Test-PodeIsWindows) -and ('Windows PowerShell ISE Host' -eq $Host.Name)) } + + + +<# +.SYNOPSIS + Checks if two arrays have any common elements. + +.DESCRIPTION + This function takes two arrays as input parameters and checks if they share any common elements. + It returns $true if there is at least one common element, and $false otherwise. + +.PARAMETER ReferenceArray + The first array to compare. + +.PARAMETER DifferenceArray + The second array to compare. + +.EXAMPLE + $array1 = @('a', 'b', 'c') + $array2 = @('c', 'd', 'e') + Test-PodeArraysHaveCommonElement -ReferenceArray $array1 -DifferenceArray $array2 + # Output: True + +.EXAMPLE + $array1 = @('a', 'b', 'c') + $array2 = @('d', 'e', 'f') + Test-PodeArraysHaveCommonElement -ReferenceArray $array1 -DifferenceArray $array2 + # Output: False + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeArraysHaveCommonElement { + param ( + [array]$ReferenceArray, # The first array to compare + [array]$DifferenceArray # The second array to compare + ) + + # Iterate through each item in the DifferenceArray + foreach ($item in $DifferenceArray) { + # Check if the item exists in the ReferenceArray + if ($ReferenceArray -contains $item) { + # Return true if a common element is found + return $true + } + } + # Return false if no common elements are found + return $false +} + +<# +.SYNOPSIS + Converts a PSCustomObject to a hashtable recursively. + +.DESCRIPTION + The ConvertTo-PodeHashtable function takes a PSCustomObject as input and recursively converts it into a hashtable. + This is useful for transforming structured data from JSON or other sources into a native PowerShell hashtable. + +.PARAMETER PSObject + The PSCustomObject to convert to a hashtable. This parameter is mandatory. + +.EXAMPLE + $psObject = [PSCustomObject]@{ + Name = "John Doe" + Age = 30 + Address = [PSCustomObject]@{ + Street = "123 Main St" + City = "Anytown" + State = "CA" + } + PhoneNumbers = @( + [PSCustomObject]@{ Type = "home"; Number = "123-456-7890" }, + [PSCustomObject]@{ Type = "work"; Number = "987-654-3210" } + ) + } + + $hashtable = ConvertTo-PodeHashtable -PSObject $psObject + $hashtable + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function ConvertTo-PodeHashtable { + param ( + [Parameter(Mandatory = $true)] + [PSObject]$PSObject + ) + + # Initialize an empty hashtable + $hashtable = @{} + + # Iterate over each property of the PSObject + foreach ($property in $PSObject.PSObject.Properties) { + + # If the property value is a PSCustomObject, recursively convert it to a hashtable + if ($property.Value -is [PSCustomObject]) { + $hashtable[$property.Name] = ConvertTo-PodeHashtable -PSObject $property.Value + + # If the property value is an enumerable collection (excluding strings) + } + elseif ($property.Value -is [System.Collections.IEnumerable] -and !($property.Value -is [string])) { + + # Initialize an array list to hold the converted items + $arrayList = @() + + # Iterate over each item in the collection + foreach ($item in $property.Value) { + + # If the item is a PSCustomObject, recursively convert it and add to the array list + if ($item -is [PSCustomObject]) { + $arrayList += (ConvertTo-PodeHashtable -PSObject $item) + + # Otherwise, add the item directly to the array list + } + else { + $arrayList += $item + } + } + + # Add the array list to the hashtable under the current property name + $hashtable[$property.Name] = $arrayList + + # If the property value is neither a PSCustomObject nor a collection, add it directly to the hashtable + } + else { + $hashtable[$property.Name] = $property.Value + } + } + + # Return the resulting hashtable + return $hashtable +} + +<# +.SYNOPSIS + Formats a given DateTime object to the ISO 8601 format used in Pode. + +.DESCRIPTION + The `Format-PodeDateToIso8601` function takes a DateTime object and returns + a string formatted as `yyyy-MM-ddTHH:mm:ss.fffffffZ`, which is the ISO 8601 format + with seven fractional seconds, suitable for Pode async route tasks. + +.PARAMETER Date + The DateTime object to format. + +.EXAMPLE + $completedTime = Get-Date + $formattedDate = Format-PodeDateToIso8601 -Date $completedTime + Write-Output $formattedDate + + This example formats the current date and time to the ISO 8601 format. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Format-PodeDateToIso8601 { + param ( + [DateTime]$Date + ) + + return $Date.ToString('yyyy-MM-ddTHH:mm:ss.fffffffZ') +} + <# .SYNOPSIS Determines the MIME type of an image from its binary header. diff --git a/src/Private/OpenApi.ps1 b/src/Private/OpenApi.ps1 index 2b98a0a45..8b9d56d65 100644 --- a/src/Private/OpenApi.ps1 +++ b/src/Private/OpenApi.ps1 @@ -1333,6 +1333,8 @@ function Get-PodeOABaseObject { 'default' = [ordered]@{ description = 'Internal server error' } } operationId = @() + #Async Route OpenAPI names + AsyncRoute = Get-PodeAsyncRouteOASchemaNameInternal } } } diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 25cdfb85f..ad777e3a9 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -189,6 +189,7 @@ function Start-PodeWebServer { Ranges = $null Sse = $null Metadata = @{} + Async = $false } # if iis, and we have an app path, alter it diff --git a/src/Private/Runspaces.ps1 b/src/Private/Runspaces.ps1 index da4bd0b46..17c6577e6 100644 --- a/src/Private/Runspaces.ps1 +++ b/src/Private/Runspaces.ps1 @@ -42,7 +42,6 @@ function Add-PodeRunspace { param( [Parameter(Mandatory = $true)] - [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files', 'Timers')] [string] $Type, @@ -304,6 +303,252 @@ function Close-PodeRunspace { } } +<# +.SYNOPSIS + Opens and initializes runspace pools for various Pode components. + +.DESCRIPTION + This function opens and initializes runspace pools for different Pode components, such as timers, schedules, web endpoints, web sockets, SMTP, TCP, and more. It asynchronously opens the pools and waits for them to be in the 'Opened' state. If any pool fails to open, it reports an error. + +.OUTPUTS + Opens and initializes runspace pools for various Pode components. +#> +function Open-PodeRunspacePool { + if ($PodeContext.Server.IsServerless) { + return + } + + $start = [datetime]::Now + Write-Verbose 'Opening RunspacePools' + + # open pools async + foreach ($key in $PodeContext.RunspacePools.Keys) { + $item = $PodeContext.RunspacePools[$key] + if ($null -eq $item) { + continue + } + + $item.Pool.ThreadOptions = [System.Management.Automation.Runspaces.PSThreadOptions]::ReuseThread + $item.Pool.CleanupInterval = [timespan]::FromMinutes(5) + $item.Result = $item.Pool.BeginOpen($null, $null) + } + + # wait for them all to open + $queue = @($PodeContext.RunspacePools.Keys) + + while ($queue.Length -gt 0) { + foreach ($key in $queue) { + $item = $PodeContext.RunspacePools[$key] + if ($null -eq $item) { + $queue = ($queue | Where-Object { $_ -ine $key }) + continue + } + + if ($item.Pool.RunspacePoolStateInfo.State -iin @('Opened', 'Broken')) { + $queue = ($queue | Where-Object { $_ -ine $key }) + Write-Verbose "RunspacePool for $($key): $($item.Pool.RunspacePoolStateInfo.State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]" + } + } + + if ($queue.Length -gt 0) { + Start-Sleep -Milliseconds 100 + } + } + + # report errors for failed pools + foreach ($key in $PodeContext.RunspacePools.Keys) { + $item = $PodeContext.RunspacePools[$key] + if ($null -eq $item) { + continue + } + + if ($item.Pool.RunspacePoolStateInfo.State -ieq 'broken') { + $item.Pool.EndOpen($item.Result) | Out-Default + throw ($PodeLocale.failedToOpenRunspacePoolExceptionMessage -f $key) #"Failed to open RunspacePool: $($key)" + } + } + + Write-Verbose "RunspacePools opened [duration: $(([datetime]::Now - $start).TotalSeconds)s]" +} + +<# +.SYNOPSIS + Closes and disposes runspace pools for various Pode components. + +.DESCRIPTION + This function closes and disposes runspace pools for different Pode components, such as timers, schedules, web endpoints, web sockets, SMTP, TCP, and more. It asynchronously closes the pools and waits for them to be in the 'Closed' state. If any pool fails to close, it reports an error. + +.OUTPUTS + Closes and disposes runspace pools for various Pode components. +#> +function Close-PodeRunspacePool { + if ($PodeContext.Server.IsServerless -or ($null -eq $PodeContext.RunspacePools)) { + return + } + + $start = [datetime]::Now + Write-Verbose 'Closing RunspacePools' + + # close pools async + foreach ($key in $PodeContext.RunspacePools.Keys) { + $item = $PodeContext.RunspacePools[$key] + if (($null -eq $item) -or ($item.Pool.IsDisposed)) { + continue + } + + $item.Result = $item.Pool.BeginClose($null, $null) + } + + # wait for them all to close + $queue = @($PodeContext.RunspacePools.Keys) + + while ($queue.Length -gt 0) { + foreach ($key in $queue) { + $item = $PodeContext.RunspacePools[$key] + if ($null -eq $item) { + $queue = ($queue | Where-Object { $_ -ine $key }) + continue + } + + if ($item.Pool.RunspacePoolStateInfo.State -iin @('Closed', 'Broken')) { + $queue = ($queue | Where-Object { $_ -ine $key }) + Write-Verbose "RunspacePool for $($key): $($item.Pool.RunspacePoolStateInfo.State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]" + } + } + + if ($queue.Length -gt 0) { + Start-Sleep -Milliseconds 100 + } + } + + # report errors for failed pools + foreach ($key in $PodeContext.RunspacePools.Keys) { + $item = $PodeContext.RunspacePools[$key] + if ($null -eq $item) { + continue + } + + if ($item.Pool.RunspacePoolStateInfo.State -ieq 'broken') { + $item.Pool.EndClose($item.Result) | Out-Default + # Failed to close RunspacePool + throw ($PodeLocale.failedToCloseRunspacePoolExceptionMessage -f $key) + } + } + + # dispose pools + foreach ($key in $PodeContext.RunspacePools.Keys) { + $item = $PodeContext.RunspacePools[$key] + if (($null -eq $item) -or ($item.Pool.IsDisposed)) { + continue + } + + Close-PodeDisposable -Disposable $item.Pool + } + + Write-Verbose "RunspacePools closed [duration: $(([datetime]::Now - $start).TotalSeconds)s]" +} + + + +<# +.SYNOPSIS + Initializes a new Pode runspace state with the necessary modules and shared variables. + +.DESCRIPTION + This function creates an initial PowerShell session state for a Pode runspace. + It imports the Pode modules and sets up a shared state context containing relevant + variables like PodeLocale, PodeContext, and others for proper runspace isolation. + +.PARAMETER None + No parameters are required for this function. + +.NOTES + This function is designed to be used in Pode-based applications where runspaces + are necessary for handling parallel execution with access to the Pode context. + Variables added to the state are accessible inside the runspace. + +.EXAMPLE + New-PodeRunspaceState + Initializes the Pode runspace state with the required modules and variables. +#> +function New-PodeRunspaceState { + # Create the initial session state for the runspace + $state = [initialsessionstate]::CreateDefault() + + # Import Pode modules: DataPath for core functionality and InternalPath for internal operations + $state.ImportPSModule($PodeContext.Server.PodeModule.DataPath) + $state.ImportPSModule($PodeContext.Server.PodeModule.InternalPath) + + # load the vars into the share state + $session = New-PodeStateContext -Context $PodeContext + + $variables = @( + # PodeLocale stores the localization settings for Pode + [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('PodeLocale', $PodeLocale, $null), + + # PodeContext stores the current Pode session context + [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('PodeContext', $session, $null), + + # Console refers to the current host for the PowerShell session + [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('Console', $Host, $null), + + # PODE_SCOPE_RUNSPACE is a flag indicating the runspace scope is in use + [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('PODE_SCOPE_RUNSPACE', $true, $null) + ) + # Add each variable to the session state to make them available in the runspace + foreach ($var in $variables) { + $state.Variables.Add($var) + } + # Store the constructed runspace state in the Pode context + $PodeContext.RunspaceState = $state +} + +<# +.SYNOPSIS + Creates a new runspace pool with specified minimum and maximum runspaces. + +.DESCRIPTION + This function wraps the .NET `[runspacefactory]::CreateRunspacePool` method to create a new runspace pool. + It allows specifying the minimum and maximum number of runspaces, as well as the runspace state. + This function also automatically passes the current host context to the runspace pool. + +.PARAMETER MinRunspaces + The minimum number of runspaces in the pool. This value determines the initial number of runspaces created when the pool is opened. + +.PARAMETER MaxRunspaces + The maximum number of runspaces allowed in the pool. This value limits the total number of concurrent runspaces in the pool. + +.PARAMETER RunspaceState + The state of the runspace, typically determined by the context in which the runspace pool is being created. This parameter is passed directly to the `CreateRunspacePool` method. + +.OUTPUTS + System.Management.Automation.Runspaces.RunspacePool + Returns a `RunspacePool` object representing the created runspace pool. + +.EXAMPLE + $runspacePool = New-PodeRunspacePoolNetWrapper -MinRunspaces 1 -MaxRunspaces 5 -RunspaceState $state + # Creates a new runspace pool with a minimum of 1 runspace, a maximum of 5 runspaces, and a specific runspace state. + +.NOTES + This function is a wrapper around the `[runspacefactory]::CreateRunspacePool` method and is used to simplify the creation of runspace pools in Pode scripts. + This is an internal function and may change in future releases of Pode. + +.LINK + https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.runspacefactory.createrunspacepool +#> +function New-PodeRunspacePoolNetWrapper { + param ( + [Parameter()] + [int]$MinRunspaces = 1, + [Parameter(Mandatory = $true)] + [int]$MaxRunspaces, + [Parameter(Mandatory = $true)] + [System.Management.Automation.Runspaces.InitialSessionState]$RunspaceState + ) + return [runspacefactory]::CreateRunspacePool($MinRunspaces, $MaxRunspaces, $RunspaceState, $Host) +} + + <# .SYNOPSIS Resets the name of the current Pode runspace by modifying its structure. diff --git a/src/Private/Tasks.ps1 b/src/Private/Tasks.ps1 index d240a8515..af5053cb6 100644 --- a/src/Private/Tasks.ps1 +++ b/src/Private/Tasks.ps1 @@ -7,7 +7,7 @@ function Start-PodeTaskHousekeeper { return } - Add-PodeTimer -Name '__pode_task_housekeeper__' -Interval 20 -ScriptBlock { + Add-PodeTimer -Name '__pode_task_housekeeper__' -Interval $PodeContext.Tasks.HouseKeeping.TimerInterval -ScriptBlock { try { # return if no task processes if ($PodeContext.Tasks.Processes.Count -eq 0) { diff --git a/src/Public/AsyncRoute.ps1 b/src/Public/AsyncRoute.ps1 new file mode 100644 index 000000000..192990224 --- /dev/null +++ b/src/Public/AsyncRoute.ps1 @@ -0,0 +1,2103 @@ + +<# +.SYNOPSIS + Adds a route to get the status and details of an asynchronous task in Pode. + +.DESCRIPTION + The `Add-PodeAsyncRouteGet` function creates a route in Pode that allows retrieving the status + and details of an asynchronous task. This function supports different methods for task Id + retrieval (Cookie, Header, Path, Query) and various response types (JSON, XML, YAML). It + integrates with OpenAPI documentation, providing detailed route information and response schemas. + +.PARAMETER Path + The URL path for the route. If the `In` parameter is set to 'Path', the `TaskIdName` will be + appended to this path. + +.PARAMETER Middleware + An array of ScriptBlocks for optional Middleware. + +.PARAMETER EndpointName + The EndpointName of an Endpoint(s) this Route should be bound against. + +.PARAMETER Authentication + The name of an Authentication method which should be used as middleware on this Route. + +.PARAMETER Access + The name of an Access method which should be used as middleware on this Route. + +.PARAMETER ResponseContentType + Specifies the response type(s) for the route. Valid values are 'application/json' , 'application/xml', 'application/yaml'. + You can specify multiple types. The default is 'application/json'. + +.PARAMETER In + Specifies where to retrieve the task Id from. Valid values are 'Cookie', 'Header', 'Path', and + 'Query'. The default is 'Query'. + +.PARAMETER PassThru + If specified, the function returns the route information after processing. + +.PARAMETER Role + One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Group + One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Scope + One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER User + One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER AllowAnon + If supplied, the Route will allow anonymous access for non-authenticated users. + +.PARAMETER IfExists + Specifies what action to take when a Route already exists. (Default: Default) + +.PARAMETER OADefinitionTag + An Array of strings representing the unique tag for the API specification. + This tag helps in distinguishing between different versions or types of API specifications within the application. + You can use this tag to reference the specific API documentation, schema, or version that your function interacts with. + +.OUTPUTS + [hashtable] +#> +function Add-PodeAsyncRouteGet { + [CmdletBinding(DefaultParameterSetName = 'OpenAPI')] + [OutputType([hashtable])] + param ( + [Parameter(Mandatory = $true)] + [string] + $Path, + + [Parameter()] + [object[]] + $Middleware, + + [Parameter( )] + [AllowNull()] + [string[]] + $EndpointName, + + [Parameter()] + [Alias('Auth')] + [string] + $Authentication, + + [Parameter()] + [string] + $Access, + + [string[]] + [ValidateSet('application/json' , 'application/xml', 'application/yaml')] + $ResponseContentType = 'application/json', + + [Parameter()] + [ValidateSet('Cookie', 'Header', 'Path', 'Query')] + [string] + $In = 'Query', + + [switch] + $PassThru, + + [Parameter()] + [string[]] + $Role, + + [Parameter()] + [string[]] + $Group, + + [Parameter()] + [string[]] + $Scope, + + [Parameter()] + [string[]] + $User, + + [switch] + $AllowAnon, + + [Parameter()] + [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')] + [string] + $IfExists = 'Default', + + [Parameter(ParameterSetName = 'OpenAPI')] + [string[]] + $OADefinitionTag + + ) + # Check if a Definition exists + $oaName = Get-PodeAsyncRouteOAName -Tag $OADefinitionTag + + # Remove any trailing '/' + $Path = $Path.TrimEnd('/') + + # Append task Id to path if the task Id is in the path + if ($In -eq 'Path') { + $Path = "$Path/:$($oaName.TaskIdName)" + } + + # Define the parameters for the route + $param = @{ + Method = 'Get' + Path = $Path + ScriptBlock = Get-PodeAsyncGetScriptBlock + ArgumentList = ($In, $oaName.TaskIdName) + ErrorContentType = $ResponseContentType[0] + PassThru = $true + } + + # Add optional parameters to the route + if ($Middleware) { + $param.Middleware = $Middleware + } + if ($EndpointName) { + $param.EndpointName = $EndpointName + } + if ($Authentication) { + $param.Authentication = $Authentication + } + if ($Access) { + $param.Access = $Access + } + if ($Role) { + $param.Role = $Role + } + if ($Group) { + $param.Group = $Group + } + if ($Scope) { + $param.Scope = $Scope + } + if ($User) { + $param.User = $User + } + if ($AllowAnon.IsPresent) { + $param.AllowAnon = $AllowAnon + } + if ($IfExists) { + $param.IfExists = $IfExists + } + + # Add the route to Pode + $route = Add-PodeRoute @param + + # Add OpenAPI documentation postponed script + $route.OpenApi.Postponed = { + param($param) + $r | Set-PodeOARequest -PassThru -Parameters ( + New-PodeOAStringProperty -Name $param.OAName.TaskIdName -Format Uuid -Description 'Task Id' -Required | ConvertTo-PodeOAParameter -In $param.In) | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType $param.ResponseContentType -Content $param.OAName.OATypeName) -PassThru | + Add-PodeOAResponse -StatusCode 4XX -Description 'Client error. The request contains bad syntax or cannot be fulfilled.' -Content ( + New-PodeOAContentMediaType -MediaType $param.ResponseContentType -Content ( + New-PodeOAStringProperty -Name 'Id' -Format Uuid -Required | New-PodeOAStringProperty -Name 'Error' -Required | New-PodeOAObjectProperty -XmlName "$($param.OAName.OATypeName)Error" + )) + } + + $route.OpenApi.PostponedArgumentList = @{ + OAName = $oaName + In = $In + ResponseContentType = $ResponseContentType + } + + # Return the route if PassThru is specified + if ($PassThru) { + return $route + } +} + +<# +.SYNOPSIS + Adds a route to stop an asynchronous task in Pode. + +.DESCRIPTION + The `Add-PodeAsyncRouteStop` function creates a route in Pode that allows the stopping of an + asynchronous task. This function supports different methods for task Id retrieval (Cookie, + Header, Path, Query) and various response types (JSON, XML, YAML). It integrates with OpenAPI + documentation, providing detailed route information and response schemas. + +.PARAMETER Path + The URL path for the route. If the `In` parameter is set to 'Path', the `TaskIdName` will be + appended to this path. + +.PARAMETER Middleware + An array of ScriptBlocks for optional Middleware. + +.PARAMETER EndpointName + The EndpointName of an Endpoint(s) this Route should be bound against. + +.PARAMETER Authentication + The name of an Authentication method which should be used as middleware on this Route. + +.PARAMETER Access + The name of an Access method which should be used as middleware on this Route. + +.PARAMETER ResponseContentType + Specifies the response type(s) for the route. Valid values are 'application/json' , 'application/xml', 'application/yaml'. + You can specify multiple types. The default is 'application/json'. + +.PARAMETER In + Specifies where to retrieve the task Id from. Valid values are 'Cookie', 'Header', 'Path', and + 'Query'. The default is 'Query'. + +.PARAMETER PassThru + If specified, the function returns the route information after processing. + +.PARAMETER Role + One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Group + One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Scope + One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER User + One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER AllowAnon + If supplied, the Route will allow anonymous access for non-authenticated users. + +.PARAMETER IfExists + Specifies what action to take when a Route already exists. (Default: Default) + +.PARAMETER OADefinitionTag + An Array of strings representing the unique tag for the API specification. + This tag helps in distinguishing between different versions or types of API specifications within the application. + You can use this tag to reference the specific API documentation, schema, or version that your function interacts with. + +.OUTPUTS + [hashtable] + +.EXAMPLE + # Adding a route to stop an asynchronous task with the task Id in the query string + Add-PodeAsyncRouteStop -Path '/task/stop' -ResponseType YAML -In Query + +.EXAMPLE + # Adding a route to stop an asynchronous task with the task Id in the URL path + Add-PodeAsyncRouteStop -Path '/task/stop' -ResponseType JSON, YAML -In Path +#> + +function Add-PodeAsyncRouteStop { + [CmdletBinding(DefaultParameterSetName = 'OpenAPI')] + [OutputType([hashtable])] + param ( + [Parameter(Mandatory = $true)] + [string] + $Path, + + [Parameter()] + [object[]] + $Middleware, + + [Parameter()] + [AllowNull()] + [string[]] + $EndpointName, + + [Parameter()] + [Alias('Auth')] + [string] + $Authentication, + + [Parameter()] + [string] + $Access, + + [string[]] + [ValidateSet('application/json', 'application/xml', 'application/yaml')] + $ResponseContentType = 'application/json', + + [Parameter()] + [ValidateSet('Cookie', 'Header', 'Path', 'Query')] + [string] + $In = 'Query', + + [switch] + $PassThru, + + [Parameter()] + [string[]] + $Role, + + [Parameter()] + [string[]] + $Group, + + [Parameter()] + [string[]] + $Scope, + + [Parameter()] + [string[]] + $User, + + [switch] + $AllowAnon, + + [Parameter()] + [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')] + [string] + $IfExists = 'Default', + + [Parameter(ParameterSetName = 'OpenAPI')] + [string[]] + $OADefinitionTag + ) + + # Check if a Definition exists + $oaName = Get-PodeAsyncRouteOAName -Tag $OADefinitionTag + + # Append task Id to path if the task Id is in the path + if ($In -eq 'Path') { + $Path = "$Path/:$($oaName.TaskIdName)" + } + + # Define the parameters for the route + $param = @{ + Method = 'Delete' + Path = $Path + ScriptBlock = Get-PodeAsyncRouteStopScriptBlock + ArgumentList = ($In, $oaName.TaskIdName) + ErrorContentType = $ResponseContentType[0] + PassThru = $true + } + + # Add optional parameters to the route + if ($Middleware) { + $param.Middleware = $Middleware + } + if ($EndpointName) { + $param.EndpointName = $EndpointName + } + if ($Authentication) { + $param.Authentication = $Authentication + } + if ($Access) { + $param.Access = $Access + } + if ($Role) { + $param.Role = $Role + } + if ($Group) { + $param.Group = $Group + } + if ($Scope) { + $param.Scope = $Scope + } + if ($User) { + $param.User = $User + } + if ($AllowAnon.IsPresent) { + $param.AllowAnon = $AllowAnon + } + if ($IfExists.IsPresent) { + $param.IfExists = $IfExists + } + + if ($OADefinitionTag) { + $param.OADefinitionTag = $OADefinitionTag + } + + # Add the route to Pode + $route = Add-PodeRoute @param + + # Add OpenAPI documentation postponed script + $route.OpenApi.Postponed = { + param($param) + $r | Set-PodeOARequest -PassThru -Parameters ( + New-PodeOAStringProperty -Name $param.OAName.TaskIdName -Format Uuid -Description 'Task Id' -Required | ConvertTo-PodeOAParameter -In $param.In) | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType $param.ResponseContentType -Content $param.OAName.OATypeName) -PassThru | + Add-PodeOAResponse -StatusCode 4XX -Description 'Client error. The request contains bad syntax or cannot be fulfilled.' -Content ( + New-PodeOAContentMediaType -MediaType $param.ResponseContentType -Content ( + New-PodeOAStringProperty -Name 'Id' -Format Uuid -Required | New-PodeOAStringProperty -Name 'Error' -Required | New-PodeOAObjectProperty -XmlName "$($param.OAName.OATypeName)Error" + ) + ) + } + $route.OpenApi.PostponedArgumentList = @{ + OAName = $oaName + In = $In + ResponseContentType = $ResponseContentType + } + + # Return the route if PassThru is specified + if ($PassThru) { + return $route + } +} + + +<# +.SYNOPSIS + Adds a Pode route for querying task information. + +.DESCRIPTION + The Add-PodeAsyncRouteQuery function creates a Pode route that allows querying task information based on specified parameters. + The function supports multiple content types for both requests and responses, and can generate OpenAPI documentation if needed. + +.PARAMETER Path + The path for the Pode route. + +.PARAMETER Middleware + An array of ScriptBlocks for optional Middleware. + +.PARAMETER EndpointName + The EndpointName of an Endpoint(s) this Route should be bound against. + +.PARAMETER Authentication + The name of an Authentication method which should be used as middleware on this Route. + +.PARAMETER Access + The name of an Access method which should be used as middleware on this Route. + +.PARAMETER ResponseContentType + Specifies the response type(s) for the route. Valid values are 'application/json' , 'application/xml', 'application/yaml'. + You can specify multiple types. The default is 'application/json'. + +.PARAMETER QueryContentType + Specifies the response type(s) for the query. Valid values are 'application/json' , 'application/xml', 'application/yaml'. + You can specify multiple types. The default is 'application/json'. + +.PARAMETER Payload + Specifies where the payload is located. Acceptable values are 'Body', 'Header', and 'Query'. Defaults to 'Body'. + +.PARAMETER PassThru + If set, the route will be returned from the function. + +.PARAMETER Role + One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Group + One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Scope + One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER User + One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER AllowAnon + If supplied, the Route will allow anonymous access for non-authenticated users. + +.PARAMETER IfExists + Specifies what action to take when a Route already exists. (Default: Default) + +.PARAMETER OADefinitionTag + An Array of strings representing the unique tag for the API specification. + This tag helps in distinguishing between different versions or types of API specifications within the application. + You can use this tag to reference the specific API documentation, schema, or version that your function interacts with. + +.EXAMPLE + Add-PodeAsyncRouteQuery -Path '/tasks/query' -ResponseContentType 'application/json' -QueryContentType 'application/json','application/yaml' -Payload 'Body' + + This example creates a Pode route at '/tasks/query' that processes query requests with JSON content types and expects the payload in the body. + +.OUTPUTS + [hashtable] +#> + +function Add-PodeAsyncRouteQuery { + [CmdletBinding()] + [OutputType([hashtable])] + param ( + [Parameter(Mandatory = $true)] + [string] + $Path, + + [Parameter()] + [object[]] + $Middleware, + + [Parameter( )] + [AllowNull()] + [string[]] + $EndpointName, + + [Parameter()] + [Alias('Auth')] + [string] + $Authentication, + + [Parameter()] + [string] + $Access, + + [string[]] + [ValidateSet('application/json' , 'application/xml', 'application/yaml')] + $ResponseContentType = 'application/json', + + [string[] ] + [ValidateSet('application/json' , 'application/xml', 'application/yaml')] + $QueryContentType = 'application/json', + + [string] + [ValidateSet('Body', 'Header', 'Query' )] + $Payload = 'Body', + + [switch] + $PassThru, + + [Parameter()] + [string[]] + $Role, + + [Parameter()] + [string[]] + $Group, + + [Parameter()] + [string[]] + $Scope, + + [Parameter()] + [string[]] + $User, + + [switch] + $AllowAnon, + + [Parameter()] + [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')] + [string] + $IfExists = 'Default', + + [Parameter()] + [string[]] + $OADefinitionTag + + ) + # Check if a Definition exists + $oaName = Get-PodeAsyncRouteOAName -Tag $OADefinitionTag + + # Define the parameters for the route + $param = @{ + Path = $Path + ScriptBlock = Get-PodeAsyncRouteQueryScriptBlock + ArgumentList = @($Payload, ( Test-PodeOADefinitionTag -Tag $Tag)) + ErrorContentType = $ResponseContentType[0] + ContentType = $QueryContentType[0] + PassThru = $true + } + + # Add optional parameters to the route + if ($Middleware) { + $param.Middleware = $Middleware + } + if ($EndpointName) { + $param.EndpointName = $EndpointName + } + if ($Authentication) { + $param.Authentication = $Authentication + } + if ($Access) { + $param.Access = $Access + } + if ($Role) { + $param.Role = $Role + } + if ($Group) { + $param.Group = $Group + } + if ($Scope) { + $param.Scope = $Scope + } + if ($User) { + $param.User = $User + } + if ($AllowAnon.IsPresent) { + $param.AllowAnon = $AllowAnon + } + if ($IfExists.IsPresent) { + $param.IfExists = $IfExists + } + + # Determine the HTTP method based on the payload location + $param.Method = (@{ + 'Body' = 'Post' + 'Header' = 'Get' + 'Query' = 'Get' + })[$Payload] + + # Add the route to Pode + $route = Add-PodeRoute @param + + # Add OpenAPI documentation postponed script + $route.OpenApi.Postponed = { + param($param ) + if (!(Test-PodeOAComponent -Field schemas -Name $param.OAName.QueryRequestName )) { + + New-PodeOAStringProperty -Name 'op' -Enum 'GT', 'LT', 'GE', 'LE', 'EQ', 'NE', 'LIKE', 'NOTLIKE' -Required | + New-PodeOAStringProperty -Name 'value' -Description 'The value to compare against' -Required | + New-PodeOAObjectProperty | Add-PodeOAComponentSchema -Name "String$($param.OAName.QueryParameterName)" + + + New-PodeOAStringProperty -Name 'op' -Enum 'EQ', 'NE' -Required | + New-PodeOAStringProperty -Name 'value' -Description 'The value to compare against' -Required | + New-PodeOAObjectProperty | Add-PodeOAComponentSchema -Name "Boolean$($param.OAName.QueryParameterName)" + + New-PodeOAStringProperty -Name 'op' -Enum 'GT', 'LT', 'GE', 'LE', 'EQ', 'NE' -Required | + New-PodeOAStringProperty -Name 'value' -format Date-Time -Description 'The value to compare against' -Required | + New-PodeOAObjectProperty | Add-PodeOAComponentSchema -Name "DateTime$($param.OAName.QueryParameterName)" + + + New-PodeOAStringProperty -Name 'op' -Enum 'GT', 'LT', 'GE', 'LE', 'EQ', 'NE' -Required | + New-PodeOANumberProperty -Name 'value' -Description 'The value to compare against' -Required | + New-PodeOAObjectProperty | Add-PodeOAComponentSchema -Name "Number$($param.OAName.QueryParameterName)" + + # Define AsyncTaskQueryRequest using pipelining + New-PodeOASchemaProperty -Name 'Id' -Reference "String$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'AsyncRouteId' -Reference "String$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'StartingTime' -Reference "DateTime$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'CreationTime' -Reference "DateTime$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'CompletedTime' -Reference "DateTime$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'ExpireTime' -Reference "DateTime$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'State' -Reference "String$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'Error' -Reference "String$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'CallbackSettings' -Reference "String$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'Cancellable' -Reference "Boolean$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'SseEnabled' -Reference "Boolean$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'SseGroup' -Reference "String$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'User' -Reference "String$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'Url' -Reference "String$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'Method' -Reference "String$($param.OAName.QueryParameterName)" | + New-PodeOASchemaProperty -Name 'Progress' -Reference "Number$($param.OAName.QueryParameterName)" | + New-PodeOAObjectProperty | + Add-PodeOAComponentSchema -Name $param.OAName.QueryRequestName + } + + # Define an example hashtable for the OpenAPI request + $exampleHashTable = @{ + 'StartingTime' = @{ + op = 'GT' + value = (Get-Date '2024-07-05T20:20:00Z') + } + 'CreationTime' = @{ + op = 'LE' + value = (Get-Date '2024-07-05T20:20:00Z') + } + 'State' = @{ + op = 'EQ' + value = 'Completed' + } + 'AsyncRouteId' = @{ + op = 'LIKE' + value = 'Get' + } + 'Id' = @{ + op = 'EQ' + value = 'b143660f-ebeb-49d9-9f92-cd21f3ff559c' + } + 'Cancellable' = @{ + op = 'EQ' + value = $true + } + } + + # Add OpenAPI route information and responses + $r | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (New-PodeOAContentMediaType -MediaType $param.ResponseContentType -Content $param.OAName.OATypeName -Array) -PassThru | + Add-PodeOAResponse -StatusCode 400 -Description 'Invalid filter supplied' -Content ( + New-PodeOAContentMediaType -MediaType $param.ResponseContentType -Content ( + New-PodeOAStringProperty -Name 'Error' -Required | New-PodeOAObjectProperty -XmlName "$($param.OAName.OATypeName)Error" + ) + ) -PassThru | Add-PodeOAResponse -StatusCode 500 -Content ( + New-PodeOAContentMediaType -MediaType $param.ResponseContentType -Content ( + New-PodeOAStringProperty -Name 'Error' -Required | New-PodeOAObjectProperty -XmlName "$($param.OAName.OATypeName)Error" + ) + ) + + + # Define examples for different media types + $example = [ordered]@{} + foreach ($mt in $param.QueryContentType) { + $example += New-PodeOAExample -MediaType $mt -Name $param.OAName.QueryRequestName -Value $exampleHashTable + } + + # Set the OpenAPI request based on the payload location + switch ($param.Payload.ToLowerInvariant()) { + 'body' { + $r | Set-PodeOARequest -RequestBody ( + New-PodeOARequestBody -Content (New-PodeOAContentMediaType -MediaType $param.QueryContentType -Content $param.OAName.QueryRequestName) -Examples $example + ) + } + 'header' { + $r | Set-PodeOARequest -Parameters (ConvertTo-PodeOAParameter -In Header -Schema $param.OAName.QueryRequestName -ContentType $param.QueryContentType[0] -Example $example[0]) + } + 'query' { + $r | Set-PodeOARequest -Parameters (ConvertTo-PodeOAParameter -In Query -Schema $param.OAName.QueryRequestName -ContentType $param.QueryContentType[0] -Example $example[0]) + } + } + } + + $route.OpenApi.PostponedArgumentList = @{ + OAName = $oaName + In = $In + ResponseContentType = $ResponseContentType + QueryContentType = $QueryContentType + Payload = $Payload + } + + # Return the route if PassThru is specified + if ($PassThru) { + return $route + } +} +<# +.SYNOPSIS + Assigns or removes permissions to/from an asynchronous route in Pode based on specified criteria such as users, groups, roles, and scopes. + +.DESCRIPTION + The `Set-PodeAsyncRoutePermission` function allows you to define and assign or remove specific permissions to/from an async route. + You can control access to the route by specifying which users, groups, roles, or scopes have `Read` or `Write` permissions. + +.PARAMETER Route + A hashtable array representing the async route(s) to which permissions will be assigned or from which they will be removed. This parameter is mandatory. + +.PARAMETER Type + Specifies the type of permission to assign or remove. Acceptable values are 'Read' or 'Write'. This parameter is mandatory. + +.PARAMETER Groups + Specifies the groups that will be granted or removed from the specified permission type. + +.PARAMETER Users + Specifies the users that will be granted or removed from the specified permission type. + +.PARAMETER Roles + Specifies the roles that will be granted or removed from the specified permission type. + +.PARAMETER Scopes + Specifies the scopes that will be granted or removed from the specified permission type. + +.PARAMETER Remove + If specified, the function will remove the specified users, groups, roles, or scopes from the permissions instead of adding them. + +.PARAMETER PassThru + If specified, the function will return the modified route object(s) after assigning or removing permissions. + +.EXAMPLE + Add-PodeRoute -PassThru -Method Put -Path '/asyncState' -Authentication 'Validate' -Group 'Support' ` + -ScriptBlock { + $data = Get-PodeState -Name 'data' + Write-PodeHost 'data:' + Write-PodeHost $data -Explode -ShowType + Start-Sleep $data.sleepTime + return @{ InnerValue = $data.Message } + } | Set-PodeAsyncRoute ` + -ResponseContentType 'application/json', 'application/yaml' -Timeout 300 -PassThru | + Set-PodeAsyncRoutePermission -Type Read -Groups 'Developer' + + This example creates an async route that requires authentication and assigns 'Read' permission to the 'Developer' group. + +.EXAMPLE + # Removing 'Developer' group from Read permissions + Set-PodeAsyncRoutePermission -Route $route -Type Read -Groups 'Developer' -Remove + + This example removes the 'Developer' group from the 'Read' permissions of the specified async route. + +.OUTPUTS + [hashtable] +#> +function Set-PodeAsyncRoutePermission { + param( + [Parameter(Mandatory = $true , ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [hashtable[]] + $Route, + + [ValidateSet('Read', 'Write')] + [string] + $Type, + + [Parameter()] + [string[]] + $Groups, + + [Parameter()] + [string[]] + $Users, + + [Parameter()] + [string[]] + $Roles, + + [Parameter()] + [string[]] + $Scopes, + + [switch] + $Remove, + + [switch] + $PassThru + ) + + Begin { + $pipelineValue = @() + } + + Process { + # Add the current piped-in value to the array + $pipelineValue += $_ + } + + End { + # Helper function to add or remove items from a permission list + function Update-PermissionList { + param ( + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [string[]]$List, + + [string[]]$Items, + + [switch]$Remove + ) + # Initialize $List if it's null + if (! $List) { + $List = @() + } + + if ($Remove) { + return $List | Where-Object { $_ -notin $Items } + } + else { + return $List + $Items + } + } + + # Handle multiple piped-in routes + if ($pipelineValue.Count -gt 1) { + $Route = $pipelineValue + } + + # Validate that the Route parameter is not null + if ($null -eq $Route) { + # The parameter 'Route' cannot be null + throw ($PodeLocale.routeParameterCannotBeNullExceptionMessage) + } + + foreach ($r in $Route) { + # Check if the route is marked as an Async Route + if (! $r.IsAsync) { + # The route '{0}' is not marked as an Async Route. + throw ($PodeLocale.routeNotMarkedAsAsyncExceptionMessage -f $r.Path) + } + + # Initialize the permission type hashtable if not already present + if (! $r.Async.Permission.ContainsKey($Type)) { + $r.Async.Permission[$Type] = @{} + } + + # Assign or remove users from the specified permission type + if ($Users) { + if (!$r.Async.Permission[$Type].ContainsKey('Users')) { + $r.Async.Permission[$Type].Users = @() + } + $r.Async.Permission[$Type].Users = Update-PermissionList -List $r.Async.Permission[$Type].Users -Items $Users -Remove:$Remove + } + + # Assign or remove groups from the specified permission type + if ($Groups) { + if (!$r.Async.Permission[$Type].ContainsKey('Groups')) { + $r.Async.Permission[$Type].Groups = @() + } + $r.Async.Permission[$Type].Groups = Update-PermissionList -List $r.Async.Permission[$Type].Groups -Items $Groups -Remove:$Remove + } + + # Assign or remove roles from the specified permission type + if ($Roles) { + if (!$r.Async.Permission[$Type].ContainsKey('Roles')) { + $r.Async.Permission[$Type].Roles = @() + } + $r.Async.Permission[$Type].Roles = Update-PermissionList -List $r.Async.Permission[$Type].Roles -Items $Roles -Remove:$Remove + } + + # Assign or remove scopes from the specified permission type + if ($Scopes) { + if (!$r.Async.Permission[$Type].ContainsKey('Scopes')) { + $r.Async.Permission[$Type].Scopes = @() + } + $r.Async.Permission[$Type].Scopes = Update-PermissionList -List $r.Async.Permission[$Type].Scopes -Items $Scopes -Remove:$Remove + } + } + + # Return the route object(s) if PassThru is specified + if ($PassThru) { + return $Route + } + } +} + + + +<# +.SYNOPSIS + Adds a callback to an asynchronous route in Pode. + +.DESCRIPTION + The Add-PodeAsyncRouteCallback function allows you to attach a callback to an existing asynchronous route in Pode. + This function takes various parameters to configure the callback URL, method, headers, and more. + +.PARAMETER Route + The route(s) to which the callback should be added. This parameter is mandatory and accepts hashtable arrays. + +.PARAMETER CallbackUrl + Specifies the URL field for the callback. Default is '$request.body#/callbackUrl'. + Can accept the following meta values: + - $request.query.param-name : query-param-value + - $request.header.header-name: application/json + - $request.body#/field-name : callbackUrl + Can accept static values for example: + - 'http://example.com/callback' + - 'https://api.example.com/callback + +.PARAMETER SendResult + If specified, sends the result of the callback. + +.PARAMETER EventName + Specifies the event name for the callback. + +.PARAMETER CallbackContentType + Specifies the content type for the callback. The default is 'application/json'. + Can accept the following meta values: + - $request.query.param-name : query-param-value + - $request.header.header-name: application/json + - $request.body#/field-name : callbackUrl + Can accept static values for example: + - 'application/json' + - 'application/xml' + - 'text/plain' + +.PARAMETER CallbackMethod + Specifies the HTTP method for the callback. The default is 'Post'. + Can accept the following meta values: + - $request.query.param-name : query-param-value + - $request.header.header-name: application/json + - $request.body#/field-name : callbackUrl + Can accept static values for example: + - `GET` + - `POST` + - `PUT` + - `DELETE` +.PARAMETER CallbackHeaderFields + Specifies the header fields for the callback as a hashtable. The key can be a string representing + the header key or one of the meta values. The value is the header value if it's a standard key or + the default value if the meta value is not resolvable. + Can accept the following meta values as keys: + - $request.query.param-name : query-param-value + - $request.header.header-name: application/json + - $request.body#/field-name : callbackUrl + Can accept static values for example: + - `@{ 'Content-Type' = 'application/json' }` + - `@{ 'Authorization' = 'Bearer token' }` + - `@{ 'Custom-Header' = 'value' }` + +.PARAMETER PassThru + If specified, the route information is returned. + +.EXAMPLE + Add-PodeRoute -PassThru -Method Put -Path '/example' | + Add-PodeAsyncRouteCallback -Route $route -CallbackUrl '$request.body#/callbackUrl' + +.NOTES + This function should only be used with routes that have been marked as asynchronous using the Set-PodeAsyncRoute function. + +.NOTES + The parameters CallbackHeaderFields, CallbackMethod, CallbackContentType, and CallbackUrl can accept these meta values: + - $request.query.param-name : query-param-value + - $request.header.header-name: application/json + - $request.body#/field-name : callbackUrl +#> +function Add-PodeAsyncRouteCallback { + param ( + [Parameter(Mandatory = $true , ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [hashtable[]] + $Route, + + [Parameter()] + [string] + $CallbackUrl = '$request.body#/callbackUrl', + + [Parameter()] + [switch] + $SendResult, + + [Parameter()] + [string] + $EventName, + + [Parameter()] + [string] + $CallbackContentType = 'application/json', + + [Parameter()] + [string] + $CallbackMethod = 'Post', + + [Parameter()] + [hashtable] + $CallbackHeaderFields = @{}, + + [switch] + $PassThru + ) + + Begin { + $pipelineValue = @() + $CallbackSettings = @{ + UrlField = $CallbackUrl + ContentType = $CallbackContentType + SendResult = $SendResult.IsPresent + Method = $CallbackMethod + HeaderFields = $CallbackHeaderFields + } + } + + Process { + # Add the current piped-in value to the array + $pipelineValue += $_ + } + + End { + # Handle multiple piped-in routes + if ($pipelineValue.Count -gt 1) { + $Route = $pipelineValue + } + + # Validate that the Route parameter is not null + if ($null -eq $Route) { + # The parameter 'Route' cannot be null + throw ($PodeLocale.routeParameterCannotBeNullExceptionMessage) + } + + foreach ($r in $Route) { + # Check if the route is marked as an Async Route + if (! $r.IsAsync) { + # The route '{0}' is not marked as an Async Route. + throw ($PodeLocale.routeNotMarkedAsAsyncExceptionMessage -f $r.Path) + } + + # Generate or use the provided event name for the callback + if ([string]::IsNullOrEmpty($EventName)) { + $CallbackSettings.EventName = $r.Path.Replace('/', '_') + '_Callback' + } + else { + if ($Route.Count -gt 1) { + $CallbackSettings.EventName = "$EventName_$($r.Path.Replace('/', '_'))" + } + else { + $CallbackSettings.EventName = $EventName + } + } + + # Attach the callback settings to the Async Route + $r.Async.CallbackSettings = $CallbackSettings + + # Add OpenAPI callback documentation if applicable + if ( $r.OpenApi.Swagger) { + $r | + Add-PodeOACallBack -Name $CallbackSettings.EventName -Path $CallbackUrl -Method $CallbackMethod -DefinitionTag $r.OpenApi.DefinitionTag -RequestBody ( + New-PodeOARequestBody -Content @{ $CallbackContentType = ( + New-PodeOAObjectProperty -Name 'Result' | + New-PodeOAStringProperty -Name 'EventName' -Description 'The event name.' -Required | + New-PodeOAStringProperty -Name 'Url' -Format Uri -Example 'http://localhost/callback' -Required | + New-PodeOAStringProperty -Name 'Method' -Example 'Post' -Required | + New-PodeOAStringProperty -Name 'State' -Description 'The parent async route task status' -Required -Example 'Complete' -Enum @('NotStarted', 'Running', 'Failed', 'Completed', 'Aborted') | + New-PodeOAObjectProperty -Name 'Result' -Description 'The parent result' -NoProperties | + New-PodeOAStringProperty -Name 'Error' -Description 'The parent error' | + New-PodeOAObjectProperty + ) + } + ) -Response ( + New-PodeOAResponse -StatusCode 200 -Description 'Successful operation' + ) + } + } + # Return the route information if PassThru is specified + if ($PassThru) { + return $Route + } + } +} + +<# +.SYNOPSIS + Defines an asynchronous route in Pode with runspace management. + +.DESCRIPTION + The `Set-PodeAsyncRoute` function enables you to define routes in Pode that execute asynchronously, + leveraging runspace management for non-blocking operation. This function allows you to specify + response types (JSON, XML, YAML) and manage asynchronous task parameters such as timeout and + unique Id generation. It supports the use of arguments, `$using` variables, and state variables. + +.PARAMETER Route + A hashtable array that contains route definitions. Each hashtable should include + the `Method`, `Path`, and `Logic` keys at a minimum. + +.PARAMETER ResponseContentType + Specifies the response type(s) for the route. Valid values are 'application/json' , 'application/xml', 'application/yaml'. + You can specify multiple types. The default is 'application/json'. + +.PARAMETER Timeout + Defines the timeout period for the asynchronous task in seconds. + The default value is 28800 (8 hours). + -1 indicating no timeout. + +.PARAMETER IdGenerator + A custom ScriptBlock to generate a random unique Ids for asynchronous route tasks. The default + is '{ return New-PodeGuid }'. + +.PARAMETER PassThru + If specified, the function returns the route information after processing. + +.PARAMETER MaxRunspaces + The maximum number of Runspaces that can exist in this route. The default is 2. + +.PARAMETER MinRunspaces + The minimum number of Runspaces that exist in this route. The default is 1. + +.PARAMETER NotCancellable + The async route task cannot be forcefully terminated + +.OUTPUTS + [hashtable[]] + +.EXAMPLE + # Using ArgumentList + Add-PodeRoute -PassThru -Method Put -Path '/asyncParam' -ScriptBlock { + param($sleepTime2, $Message) + Write-PodeHost "sleepTime2=$sleepTime2" + Write-PodeHost "Message=$Message" + for ($i = 0; $i -lt 20; $i++) { + Start-Sleep $sleepTime2 + } + return @{ InnerValue = $Message } + } -ArgumentList @{sleepTime2 = 2; Message = 'coming as argument' } | Set-PodeAsyncRoute -ResponseType JSON, XML + +.EXAMPLE + # Using $using variables + $uSleepTime = 5 + $uMessage = 'coming from using' + + Add-PodeRoute -PassThru -Method Put -Path '/asyncUsing' -ScriptBlock { + Write-PodeHost "sleepTime=$($using:uSleepTime)" + Write-PodeHost "Message=$($using:uMessage)" + Start-Sleep $using:uSleepTime + return @{ InnerValue = $using:uMessage } + } | Set-PodeAsyncRoute + +#> +function Set-PodeAsyncRoute { + [CmdletBinding()] + [OutputType([hashtable[]])] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [hashtable[]] + $Route, + + [Parameter()] + [string[]] + [ValidateSet('application/json' , 'application/xml', 'application/yaml')] + $ResponseContentType = 'application/json', + + [Parameter()] + [int] + $Timeout = 28800, + + [Parameter()] + [scriptblock] + $IdGenerator, + + [Parameter()] + [switch] + $PassThru, + + [Parameter()] + [ValidateRange(1, 100)] + [int] + $MaxRunspaces = 2, + + [Parameter()] + [ValidateRange(1, 100)] + [int] + $MinRunspaces = 1, + + [Parameter()] + [switch] + $NotCancellable + + ) + Begin { + + # Initialize an array to hold piped-in values + $pipelineValue = @() + + # Start the housekeeper for async routes + Start-PodeAsyncRoutesHousekeeper + + } + + process { + # Add the current piped-in value to the array + $pipelineValue += $_ + } + + End { + # Set Route to the array of values if multiple values are piped in + if ($pipelineValue.Count -gt 1) { + $Route = $pipelineValue + } + + if ($null -eq $Route) { + # The parameter 'Route' cannot be null + throw ($PodeLocale.routeParameterCannotBeNullExceptionMessage) + } + + foreach ($r in $Route) { + # Check if the route is already marked as an Async Route + if ( $r.IsAsync) { + # The function cannot be invoked multiple times for the same route + throw ($PodeLocale.functionCannotBeInvokedMultipleTimesExceptionMessage -f $MyInvocation.MyCommand.Name, $r.Path) + } + + # Validates $r.Logic for disallowed Pode commands + Test-PodeAsyncRouteScriptblockInvalidCommand -ScriptBlock $r.Logic + + # Set the Route as Async + $r.IsAsync = $true + + # Assing an unique Id to the async route + if ($r.Endpoint.Name) { $asyncRouteId = "$($r.Endpoint.Name):[$($r.method)]$($r.Path)" } else { $asyncRouteId = "[$($r.method)]$($r.Path)" } + + # Assign the Id generator + if ($IdGenerator) { + $asyncRouteTaskIdGenerator = $IdGenerator + } + else { + $asyncRouteTaskIdGenerator = { return (New-PodeGuid) } + } + + # Store the route's async route task definition in Pode context + $r.Async = @{ + AsyncRouteId = $asyncRouteId + Script = Get-PodeAsyncRouteScriptblock -ScriptBlock $r.Logic + UsingVariables = $r.UsingVariables + Arguments = (Protect-PodeValue -Value $r.Arguments -Default @{}) + CallbackSettings = $null + Cancellable = !($NotCancellable.IsPresent) + MinRunspaces = $MinRunspaces + MaxRunspaces = $MaxRunspaces + Timeout = $Timeout + Permission = @{} + AsyncRouteTaskIdGenerator = $asyncRouteTaskIdGenerator + } + + #Set thread count + $PodeContext.Threads.AsyncRoutes += $MaxRunspaces + if (! $PodeContext.RunspacePools.ContainsKey($asyncRouteId)) { + $PodeContext.RunspacePools[$asyncRouteId] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + + $PodeContext.RunspacePools[$asyncRouteId]['Pool'] = New-PodeRunspacePoolNetWrapper -MinRunspaces $MinRunspaces -MaxRunspaces $MaxRunspaces -RunspaceState $PodeContext.RunspaceState + $PodeContext.RunspacePools[$asyncRouteId]['State'] = 'Waiting' + } + + # Replace the Route logic with this that allow to execute the original logic asynchronously + $r.logic = Get-PodeAsyncRouteSetScriptBlock + + # Set arguments and clear using variables + $r.Arguments = @() + $r.UsingVariables = $null + + # Add OpenAPI documentation if not excluded + if ( $r.OpenApi.Swagger) { + $oaName = Get-PodeAsyncRouteOAName -Tag $r.OpenApi.DefinitionTag -ForEachOADefinition + foreach ($key in $oaName.Keys) { + Add-PodeAsyncRouteComponentSchema -Name $oaName[$key].oATypeName -DefinitionTag $key + $r | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' ` + -DefinitionTag $key ` + -Content (New-PodeOAContentMediaType -MediaType $ResponseContentType -Content $oaName[$key].OATypeName ) + } + } + + } + + # Return the route information if PassThru is specified + if ($PassThru) { + return $Route + } + } +} + +<# +.SYNOPSIS + Adds a Server-Sent Events (SSE) route to an existing Pode async route. + +.DESCRIPTION + The `Add-PodeAsyncRouteSse` function registers a new SSE route associated with an existing Pode async route. + This allows the server to push updates to the client for the specified route. + The function accepts a hashtable array of routes and sets up the SSE route for each. The response content type can be specified, and you can choose to pass through the modified route object with the `-PassThru` switch. + + The function also ensures that the specified routes are marked as async routes. If a route is not marked as async, an exception will be thrown. + +.PARAMETER Route + A hashtable array representing the route(s) to which the SSE route will be added. + This parameter is mandatory and supports pipeline input. Each route must be marked as an async route, or an exception will be thrown. + +.PARAMETER PassThru + If specified, the function will return the route object after adding the SSE route. + +.PARAMETER SseGroup + Specifies the group for the SSE connection. If not provided, the group will be set to the path of the route. + +.PARAMETER SendResult + If specified, sends the result upon completion of the async operation. + +.OUTPUTS + Hashtable[] + +.NOTES + The function creates a new route with the `_events` suffix appended to the original route's path. + The new route handles SSE connections and manages the async results from the original route. + + If the route is not marked as an async route, an exception will be thrown. + +.EXAMPLE + Add-PodeRoute -PassThru -Method Get -Path '/events' -ScriptBlock { + return @{'message' = 'Done' } + } | Set-PodeAsyncRoute -ResponseContentType 'application/json' -MaxRunspaces 2 -PassThru | + Add-PodeAsyncRouteSse -SseGroup 'Test events' + + This example demonstrates creating a new GET route at the path '/events' and setting it as an async route with a maximum of 2 runspaces. The async route is enabled for Server-Sent Events (SSE) and is grouped under 'Test events'. + The `Add-PodeAsyncRouteSse` function is then used to add an SSE route to the async route, ensuring that updates from the server are pushed to the client. +#> +function Add-PodeAsyncRouteSse { + [CmdletBinding()] + [OutputType([hashtable[]])] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [hashtable[]] + $Route, + + [Parameter()] + [switch] + $PassThru, + + [Parameter()] + [string] + $SseGroup, + + [Parameter()] + [switch] + $SendResult + ) + + Begin { + # Initialize an array to store piped-in routes + $pipelineValue = @() + + # Define the SSE script block to handle client-server communication over SSE + $sseScriptBlock = { + param($SseGroup, $SendResult) + $id = $WebEvent.Query['Id'] # Capture the async operation ID from the query string + + # Determine the SSE group to use, fallback to the route path if none specified + if ([string]::IsNullOrEmpty($SseGroup)) { + ConvertTo-PodeSseConnection -Name $webEvent.Route.Path -Scope Local -Group $SseGroup -AsyncRouteTaskId $id + } + else { + ConvertTo-PodeSseConnection -Name $webEvent.Route.Path -Scope Local -AsyncRouteTaskId $id + } + + # Check if the process for the async route exists + if (!$PodeContext.AsyncRoutes.Processes.ContainsKey($id)) { + try { + # Throw an exception if the process is not found + throw ($PodeLocale.asyncIdDoesNotExistExceptionMessage -f $id) + } + catch { + # Log the error and exit + $_ | Write-PodeErrorLog + return + } + } + $process = $PodeContext.AsyncRoutes.Processes[$Id] # Retrieve the async process by ID + + # Initialize an SSE dictionary for the event + $webEventSse = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + foreach ($key in $WebEvent['Sse'].Keys) { + $webEventSse[$key] = $WebEvent.Sse[$key] # Copy SSE data + } + $process.WebEvent['Sse'] = $webEventSse # Store the SSE data in the process object + + # Set the initial state of the SSE process to 'Waiting' + $process['Sse']['State'] = 'Waiting' + + # Wait until the async runspace is completed + while (!$process['Runspace'].Handler.IsCompleted) { + start-sleep 1 + } + + try { + # Handle the completion of the async operation based on its state + switch ($process['State']) { + 'Failed' { + # Send an SSE event if the process failed + $null = Send-PodeSseEvent -FromEvent -Data @{ State = $process['State']; Error = $process['Error'] } -EventType 'pode.taskCompleted' + } + 'Completed' { + # Send the result or a generic completion message if successful + if ($process['Result'] -and $SendResult) { + $null = Send-PodeSseEvent -FromEvent -Data @{ State = $process['State']; Result = $process['Result'] } -EventType 'pode.taskCompleted' + } + else { + $null = Send-PodeSseEvent -FromEvent -Data @{ State = 'Completed' } -EventType 'pode.taskCompleted' + } + } + 'Aborted' { + # Handle aborted async operations + $null = Send-PodeSseEvent -FromEvent -Data @{ State = $process['State']; Error = $process['Error'] } -EventType 'pode.taskCompleted' + } + } + # Mark the SSE process state as completed + $process['Sse']['State'] = 'Completed' + start-sleep 1 + } + catch { + # Log any errors encountered and set the state to 'Failed' + $_ | Write-PodeErrorLog + $process['Sse']['State'] = 'Failed' + } + } + } + + process { + # Collect the piped-in route(s) + $pipelineValue += $_ + } + + End { + # If multiple routes are piped in, assign them to $Route + if ($pipelineValue.Count -gt 1) { + $Route = $pipelineValue + } + + # Throw an error if Route is null + if ($null -eq $Route) { + throw ($PodeLocale.routeParameterCannotBeNullExceptionMessage) + } + + foreach ($r in $Route) { + # Ensure the route is marked as an Async Route + if (! $r.IsAsync) { + throw ($PodeLocale.routeNotMarkedAsAsyncExceptionMessage -f $r.Path) # Throw error if not async + } + + # Create the SSE route with the '_events' suffix + $sseRoute = Add-PodeRoute -PassThru -method Get -Path "$($r.Path)_events" -ArgumentList $SseGroup, $SendResult.IsPresent ` + -ScriptBlock $sseScriptBlock # The new route handles SSE + + # Store the SSE route information in the async route context + $r.Async['Sse'] = @{ + Group = $SseGroup + Name = "$($r.Path)_events" + Route = $sseRoute + } + } + + # Return the route object if PassThru is specified + if ($PassThru) { + return $Route + } + } +} + +<# +.SYNOPSIS + Retrieves asynchronous Pode route operations based on specified query conditions. + +.DESCRIPTION + The Get-PodeAsyncRouteOperationByFilter function acts as a public interface for searching asynchronous Pode route operations. + It utilizes the Search-PodeAsyncRouteTask function to perform the search based on the specified query conditions. + +.PARAMETER Filter + A hashtable containing the query conditions. Each key in the hashtable represents a field to search on, + and the value is another hashtable containing 'op' (operator) and 'value' (comparison value). + +.PARAMETER Raw + If specified, returns the raw [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] without any formatting. + +.EXAMPLE + $filter = @{ + 'State' = @{ 'op' = 'EQ'; 'value' = 'Running' } + 'CreationTime' = @{ 'op' = 'GT'; 'value' = (Get-Date).AddHours(-1) } + } + $results = Get-PodeAsyncRouteOperationByFilter -Filter $filter + + This example retrieves route operations that are in the 'Running' state and were created within the last hour. + +.OUTPUTS + Returns an array of hashtables or [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] representing the matched route operations. +#> +function Get-PodeAsyncRouteOperationByFilter { + param ( + [Parameter(Mandatory = $true)] + [hashtable] + $Filter, + + [switch] + $Raw + ) + $async = Search-PodeAsyncRouteTask -Query $Filter + if ($async -is [System.Object[]]) { + $result = @() + foreach ($item in $async) { + $result += Export-PodeAsyncRouteInfo -Raw:$Raw -Async $item + } + } + else { + $result = Export-PodeAsyncRouteInfo -Raw:$Raw -Async $async + } + return $result +} + +<# +.SYNOPSIS + Retrieves and filters async routes from Pode's async route context. + +.DESCRIPTION + The `Get-PodeAsyncRouteOperation` function allows you to filter Pode async routes based on the `Id` and `AsyncRouteId` properties. + If either `Id` or `AsyncRouteId` is not specified (or `$null`), those fields will not be used for filtering. + The filtered results can be optionally exported in raw format using the `-Raw` switch. + +.PARAMETER Id + The unique identifier of the async route to filter on. + If not specified or `$null`, this parameter is ignored. + +.PARAMETER AsyncRouteId + The name of the async route to filter on. + If not specified or `$null`, this parameter is ignored. + +.PARAMETER Raw + A switch that, if specified, exports the results in raw format. + +.EXAMPLE + Get-PodeAsyncRouteOperation -Id "12345" -Raw + + Retrieves the async route with the Id "12345" and exports it in raw format. + +.EXAMPLE + Get-PodeAsyncRouteOperation -Name "RouteName" + + Retrieves the async routes with the name "RouteName". +#> + +function Get-PodeAsyncRouteOperation { + param ( + [Parameter()] + [string] + $Id, + + [Parameter()] + [string] + $AsyncRouteId, + + [Parameter()] + [switch] + $Raw + ) + + # Filter the async routes based on Id and AsyncRouteId + if (![string]::IsNullOrEmpty($Id)) { + $result = $PodeContext.AsyncRoutes.Processes[$Id] + } + elseif (! [string]::IsNullOrEmpty($AsyncRouteId)) { + foreach ($key in $PodeContext.AsyncRoutes.Processes.Keys) { + if ($PodeContext.AsyncRoutes.Processes[$key]['AsyncRouteId'] -ieq $AsyncRouteId) { + $result = $PodeContext.AsyncRoutes.Processes[$key] + break + } + } + } + else { + $result = $PodeContext.AsyncRoutes.Processes + } + + if ($null -eq $result) { + return $null + } + + # If the -Raw switch is specified, return the filtered results directly + if ($Raw) { + return $result + } + + if ([string]::IsNullOrEmpty($Id) -and [string]::IsNullOrEmpty($AsyncRouteId)) { + # Otherwise, process each item in the filtered results through Export-PodeAsyncRouteInfo + $export = @() + foreach ($item in $result.Values) { + $export += Export-PodeAsyncRouteInfo -Async $item + } + } + else { + $export = Export-PodeAsyncRouteInfo -Async $result + } + # Return the processed export result + return $export +} + + +<# +.SYNOPSIS + Aborts a specific asynchronous Pode route operation by its Id. + +.DESCRIPTION + The Stop-PodeAsyncRouteOperation function stops an asynchronous Pode route operation based on the provided Id. + It sets the operation's state to 'Aborted', records an error message, and marks the completion time. + The function then disposes of the associated runspace pipeline and calls Complete-PodeAsyncRouteOperation to finalize the operation. + If the operation does not exist, it throws an exception with an appropriate error message. + +.PARAMETER Id + A string representing the Id (typically a UUID) of the asynchronous route operation to abort. This parameter is mandatory. + +.PARAMETER Raw + If specified, returns the raw [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] without any formatting. + +.EXAMPLE + $operationId = '123e4567-e89b-12d3-a456-426614174000' + $operationDetails = Stop-PodeAsyncRouteOperation -Id $operationId + + This example aborts the asynchronous route operation with the Id '123e4567-e89b-12d3-a456-426614174000' and retrieves the updated operation details. + +.OUTPUTS + Returns a hashtable representing the detailed information of the aborted asynchronous route operation. +#> +function Stop-PodeAsyncRouteOperation { + param ( + [Parameter(Mandatory = $true)] + [string] + $Id, + + [switch] + $Raw + ) + if ($PodeContext.AsyncRoutes.Processes.ContainsKey($Id )) { + $async = $PodeContext.AsyncRoutes.Processes[$Id] + $async['State'] = 'Aborted' + $async['Error'] = 'Aborted by System' + $async['CompletedTime'] = [datetime]::UtcNow + $async['Runspace'].Pipeline.Dispose() + Complete-PodeAsyncRouteOperation -AsyncProcess $async + return Export-PodeAsyncRouteInfo -Async $async -Raw:$Raw + } + throw ($PodeLocale.asyncRouteOperationDoesNotExistExceptionMessage -f $Id) +} + +<# +.SYNOPSIS + Checks if a specific asynchronous Pode route operation exists by its Id. + +.DESCRIPTION + The Test-PodeAsyncRouteOperation function checks the Pode context to determine if an asynchronous route operation with the specified Id exists. + It returns a boolean value indicating whether the operation is present in the Pode context. + +.PARAMETER Id + A string representing the Id (typically a UUID) of the asynchronous route operation to check. This parameter is mandatory. + +.EXAMPLE + $operationId = '123e4567-e89b-12d3-a456-426614174000' + $exists = Test-PodeAsyncRouteOperation -Id $operationId + + This example checks if the asynchronous route operation with the Id '123e4567-e89b-12d3-a456-426614174000' exists and returns true or false. + +.OUTPUTS + Returns a boolean value: + - $true if the asynchronous route operation exists. + - $false if the asynchronous route operation does not exist. +#> +function Test-PodeAsyncRouteOperation { + param ( + [Parameter(Mandatory = $true)] + [string] + $Id + ) + return ($PodeContext.AsyncRoutes.Processes.ContainsKey($Id )) +} + + +<# +.SYNOPSIS + Manages the progress of an asynchronous task within Pode routes. + +.DESCRIPTION + This function updates the progress of an asynchronous task in Pode. It supports different parameter sets: + - StartEnd: Defines progress between a start and end value. + - Tick: Increments the progress by a predefined tick value. + - TimeBased: Updates progress based on a specified duration and interval. + - SetValue: Allows setting the progress to a specific value. + +.PARAMETER Start + The start value for progress calculation (used in StartEnd parameter set). + +.PARAMETER End + The end value for progress calculation (used in StartEnd parameter set). + +.PARAMETER Steps + The number of steps between the start and end values (used in StartEnd parameter set). + +.PARAMETER MaxProgress + The maximum progress value (default is 100). + +.PARAMETER Tick + A switch to increment the progress by the predefined tick value. + +.PARAMETER UseDecimalProgress + A switch to use decimal values for progress. + +.PARAMETER IntervalSeconds + The interval in seconds for time-based progress updates (default is 5 seconds). + +.PARAMETER DurationSeconds + The total duration in seconds for time-based progress updates. + +.PARAMETER Value + The value to set the progress to (used in SetValue parameter set). + +.EXAMPLE + Set-PodeAsyncRouteProgress -Start 0 -End 100 -Steps 10 -MaxProgress 100 + +.EXAMPLE + Set-PodeAsyncRouteProgress -Tick + +.EXAMPLE + Set-PodeAsyncRouteProgress -IntervalSeconds 5 -DurationSeconds 300 -MaxProgress 100 + +.EXAMPLE + Set-PodeAsyncRouteProgress -Value 50 + +.NOTES + This function can only be used inside an Async Route Scriptblock in Pode. +#> +function Set-PodeAsyncRouteProgress { + [CmdletBinding(DefaultParameterSetName = 'StartEnd')] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'StartEnd')] + [double] $Start, + + [Parameter(Mandatory = $true, ParameterSetName = 'StartEnd')] + [double] $End, + + [Parameter(ParameterSetName = 'StartEnd')] + [double] $Steps = 1, + + [Parameter(ParameterSetName = 'TimeBased')] + [Parameter(ParameterSetName = 'StartEnd')] + [ValidateRange(1, 100)] + [double] $MaxProgress = 100, + + [Parameter(Mandatory = $true, ParameterSetName = 'Tick')] + [switch] $Tick, + + [Parameter(ParameterSetName = 'TimeBased')] + [Parameter(ParameterSetName = 'StartEnd')] + [Parameter(ParameterSetName = 'SetValue')] + [switch] $UseDecimalProgress, + + [Parameter(ParameterSetName = 'TimeBased')] + [int] $IntervalSeconds = 5, + + [Parameter(Mandatory = $true, ParameterSetName = 'TimeBased')] + [int] $DurationSeconds, + + [Parameter(Mandatory = $true, ParameterSetName = 'SetValue')] + [double] $Value + ) + + # Ensure the function is used inside an async route + if (!$___async___id___) { + # Throw an error if not in an async route context + throw $PodeLocale.setPodeAsyncProgressExceptionMessage + } + # Retrieve the async process using the task ID + $process = $PodeContext.AsyncRoutes.Processes[$___async___id___] + + # Initialize progress if not set yet and not using 'Tick' or 'SetValue' modes + if ($PSCmdlet.ParameterSetName -ne 'Tick' -and $PSCmdlet.ParameterSetName -ne 'SetValue') { + if (!$process.ContainsKey('Progress')) { + # Initialize progress to 0, using a decimal or integer based on the presence of the switch + if ($UseDecimalProgress.IsPresent) { + $process['Progress'] = [double] 0 + } + else { + $process['Progress'] = [int] 0 + } + } + + # Throw an error if MaxProgress is less than the current progress + if ($MaxProgress -le $process['Progress']) { + throw $PodeLocale.progressLimitLowerThanCurrentExceptionMessage + } + } + + # Handle progress updates based on the parameter set + switch ($PSCmdlet.ParameterSetName) { + 'StartEnd' { + # Calculate total ticks and tick-to-progress ratio + $totalTicks = [math]::ceiling(($End - $Start) / $Steps) + if ($process['Progress'] -is [double]) { + $process['TickToProgress'] = ($MaxProgress - $process['Progress']) / $totalTicks + } + else { + $process['TickToProgress'] = [Math]::Floor(($MaxProgress - $process['Progress']) / $totalTicks) + } + } + 'Tick' { + # Increment progress by the TickToProgress value + $process['Progress'] = $process['Progress'] + $process['TickToProgress'] + + # Ensure progress does not exceed MaxProgress + if ($process['Progress'] -ge $MaxProgress) { + if ($process['Progress'] -is [double]) { + $process['Progress'] = $MaxProgress - 0.01 + } + else { + $process['Progress'] = $MaxProgress - 1 + } + } + } + 'TimeBased' { + # Calculate the total number of ticks and the progress increment per tick + $totalTicks = [math]::ceiling($DurationSeconds / $IntervalSeconds) + if ($process['Progress'] -is [double]) { + $process['TickToProgress'] = ($MaxProgress - $process['Progress']) / $totalTicks + } + else { + $process['TickToProgress'] = [Math]::Floor(($MaxProgress - $process['Progress']) / $totalTicks) + } + + # Initialize a timer for time-based progress updates + $process['eventName'] = "TimerEvent_$___async___id___" + $process['Timer'] = [System.Timers.Timer]::new() + $process['Timer'].Interval = $IntervalSeconds * 1000 + # Register an event for the timer to handle periodic progress updates + $null = Register-ObjectEvent -InputObject $process['Timer'] -EventName Elapsed -SourceIdentifier $process['eventName'] ` + -MessageData @{AsyncResult = $process; MaxProgress = $MaxProgress } -Action { + $process = $Event.MessageData.AsyncResult + $MaxProgress = $Event.MessageData.MaxProgress + # Increment progress by the TickToProgress value + $process['Progress'] = $process['Progress'] + $process['TickToProgress'] + + # Check if progress exceeds MaxProgress and stop the timer if so + if ($process['Progress'] -gt $MaxProgress) { + # Close and dispose of the timer when max progress is reached + Close-PodeAsyncRouteTimer -Operation $process + + if ($process['Progress'] -is [double]) { + $process['Progress'] = $MaxProgress - 0.01 + } + else { + $process['Progress'] = $MaxProgress - 1 + } + } + + # If SSE is available, send the current progress via SSE + if ($process.ContainsKey('Sse')) { + $null = Send-PodeSseEvent -FromEvent -Data $process['Progress'] -EventType 'pode.progress' + } + } + # Enable the timer to start the progress updates + $process['Timer'].Enabled = $true + } + 'SetValue' { + # Directly set the progress value, using decimal or integer based on the context + if ($UseDecimalProgress.IsPresent -or ($Value % 1 -ne 0)) { + $process['Progress'] = $Value + } + else { + $process['Progress'] = [int]$Value + } + } + } + + # If SSE is enabled, send progress updates via SSE + if ($WebEvent.Sse) { + $null = Send-PodeSseEvent -FromEvent -Data $process['Progress'] -EventType 'pode.progress' + } +} + +<# +.SYNOPSIS + Retrieves the current progress of an asynchronous route in Pode. + +.DESCRIPTION + The `Get-PodeAsyncRouteProgress` function returns the current progress of an asynchronous route in Pode. + It retrieves the progress based on the asynchronous route ID (`$___async___id___`). + If called outside of an asynchronous route script block, an error is thrown. + +.EXAMPLE + # Example usage inside an async route scriptblock + Add-PodeRoute -PassThru -Method Get '/process' { + # Perform some work and update progress + Set-PodeAsyncCounter -Value 40 + # Retrieve the current progress + $progress = Get-PodeAsyncRouteProgress + Write-PodeHost "Current Progress: $progress" + } |Set-PodeAsyncRoute -ResponseContentType 'application/json' + + .NOTES + This function should only be used inside an asynchronous route scriptblock. + +#> +function Get-PodeAsyncRouteProgress { + if ($___async___id___) { + return $PodeContext.AsyncRoutes.Processes[$___async___id___]['Progress'] + } + else { + throw $PodeLocale.setPodeAsyncProgressExceptionMessage + } +} + + +<# +.SYNOPSIS + Sets the schema names for asynchronous Pode route operations. + +.DESCRIPTION + The Set-PodeAsyncRouteOASchemaName function is designed to configure schema names for asynchronous Pode route operations in OpenAPI documentation. + It stores the specified type names and parameter names for OpenAPI documentation in the Pode context server's OpenAPI definitions. + +.PARAMETER OATypeName + The type name for OpenAPI documentation. The default is 'AsyncRouteTask'. This parameter is only used + if the route is included in OpenAPI documentation. + +.PARAMETER TaskIdName + The name of the parameter that contains the task Id. The default is 'id'. + +.PARAMETER QueryRequestName + The name of the Pode task query request in the OpenAPI schema. Defaults to 'AsyncRouteTaskQuery'. + +.PARAMETER QueryParameterName + The name of the query parameter in the OpenAPI schema. Defaults to 'AsyncRouteTaskQueryParameter'. + +.PARAMETER OADefinitionTag + The tags associated with the OpenAPI definitions that need to be updated. +#> +function Set-PodeAsyncRouteOASchemaName { + param( + [string] + $OATypeName, + + [Parameter()] + [string] + $TaskIdName, + + [Parameter()] + [string] + $QueryRequestName, + + [Parameter()] + [string] + $QueryParameterName, + + [Parameter()] + [string[]] + $OADefinitionTag + ) + # Validates the provided OpenAPI definition tags using a custom function. + $DefinitionTag = Test-PodeOADefinitionTag -Tag $OADefinitionTag + + # Iterates over each valid OpenAPI definition tag. + foreach ($tag in $DefinitionTag) { + + # If $OATypeName is not provided, fetch it from the corresponding OpenAPI definition's hidden components. + if (! $OATypeName) { + $OATypeName = $PodeContext.Server.OpenApi.Definitions[$tag].hiddenComponents.AsyncRoute.OATypeName + } + + # If $TaskIdName is not provided, fetch it from the corresponding OpenAPI definition's hidden components. + if (! $TaskIdName) { + $TaskIdName = $PodeContext.Server.OpenApi.Definitions[$tag].hiddenComponents.AsyncRoute.TaskIdName + } + + # If $QueryRequestName is not provided, fetch it from the corresponding OpenAPI definition's hidden components. + if (!$QueryRequestName) { + $QueryRequestName = $PodeContext.Server.OpenApi.Definitions[$tag].hiddenComponents.AsyncRoute.QueryRequestName + } + + # If $QueryParameterName is not provided, fetch it from the corresponding OpenAPI definition's hidden components. + if (!$QueryParameterName) { + $QueryParameterName = $PodeContext.Server.OpenApi.Definitions[$tag].hiddenComponents.AsyncRoute.QueryParameterName + } + + # Update the hiddenComponents.AsyncRoute property of the OpenAPI definition + # with the schema details fetched or provided, by calling Get-PodeAsyncRouteOASchemaNameInternal function. + $PodeContext.Server.OpenApi.Definitions[$tag].hiddenComponents.AsyncRoute = Get-PodeAsyncRouteOASchemaNameInternal ` + -OATypeName $OATypeName -TaskIdName $TaskIdName ` + -QueryRequestName $QueryRequestName -QueryParameterName $QueryParameterName + } +} + +<# +.SYNOPSIS + Sets the field name that uniquely identifies a user for async routes in Pode. + +.DESCRIPTION + The `Set-PodeAsyncRouteUserIdentifierField` function allows you to specify a custom field name + that represents the user identifier in async routes within Pode. This field name is stored in the Pode context + and is used throughout the application to identify users in async operations. + +.PARAMETER UserIdentifierField + The name of the field that uniquely identifies a user. This parameter is mandatory. + By default, the user identifier field is 'Id'. + +.EXAMPLE + Set-PodeAsyncRouteUserIdentifierField -UserIdentifierField 'UserId' + + This example sets the user identifier field to 'UserId', overriding the default 'Id'. + +.NOTES + The user identifier field is stored in `$PodeContext.AsyncRoutes.UserFieldIdentifier`. The default value is 'Id'. +#> +function Set-PodeAsyncRouteUserIdentifierField { + param( + [Parameter(Mandatory = $true)] + [string] + $UserIdentifierField + ) + $PodeContext.AsyncRoutes.UserFieldIdentifier = $UserIdentifierField +} + +<# +.SYNOPSIS + Retrieves the field name that uniquely identifies a user for async routes in Pode. + +.DESCRIPTION + The `Get-PodeAsyncRouteUserIdentifierField` function returns the current field name + used to uniquely identify users in async routes within Pode. This field name is stored in the Pode context. + +.PARAMETER UserIdentifierField + The name of the field that uniquely identifies a user. This parameter is mandatory. + By default, the user identifier field is 'Id'. + +.EXAMPLE + $userField = Get-PodeAsyncRouteUserIdentifierField + + This example retrieves the current user identifier field, which by default is 'Id'. + +.NOTES + The user identifier field is retrieved from `$PodeContext.AsyncRoutes.UserFieldIdentifier`. The default value is 'Id'. +#> +function Get-PodeAsyncRouteUserIdentifierField { + param( + [Parameter(Mandatory = $true)] + [string] + $UserIdentifierField + ) + return $PodeContext.AsyncRoutes.UserFieldIdentifier +} \ No newline at end of file diff --git a/src/Public/OpenApi.ps1 b/src/Public/OpenApi.ps1 index 1f8d5ed77..7cba29d90 100644 --- a/src/Public/OpenApi.ps1 +++ b/src/Public/OpenApi.ps1 @@ -866,7 +866,7 @@ function Set-PodeOARequest { } } - if ($PassThru) { + if ($PassThru) { return $Route } @@ -1735,6 +1735,15 @@ function Set-PodeOARouteInfo { if ($Deprecated.IsPresent) { $r.OpenApi.Deprecated = $Deprecated.IsPresent } + + if ($r.OpenApi.Postponed) { + if ($r.OpenApi.PostponedArgumentList) { + Invoke-Command -ScriptBlock $r.OpenApi.Postponed -ArgumentList $r.OpenApi.PostponedArgumentList + } + else { + Invoke-Command -ScriptBlock $r.OpenApi.Postponed + } + } } if ($PassThru) { diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index af08864c6..8fa7d40c5 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -430,6 +430,8 @@ function Add-PodeRoute { Arguments = $ArgumentList Method = $_method Path = $Path + IsAsync = $false + Async = @{} OpenApi = @{ Path = $OpenApiPath Responses = $DefaultResponse @@ -875,8 +877,8 @@ function Add-PodeStaticRoute { OpenApi = @{ Path = $OpenApiPath Responses = @{} - Parameters = $null - RequestBody = $null + Parameters = [ordered]@{} + RequestBody = [ordered]@{} CallBacks = @{} Authentication = @() Servers = @() @@ -1701,6 +1703,20 @@ function Remove-PodeRoute { }) foreach ($r in $route) { + # remove the runspace + if ($r.IsAsync) { + $asyncRouteId = $r.Async.AsyncRouteId + if ( $asyncRouteId -and $PodeContext.RunspacePools.ContainsKey($asyncRouteId)) { + $PodeContext.Threads.AsyncRoutes -= $r.Async.MaxRunspaces + if ( ! $PodeContext.RunspacePools[$asyncRouteId].Pool.IsDisposed) { + $PodeContext.RunspacePools[$asyncRouteId].Pool.BeginClose($null, $null) + Close-PodeDisposable -Disposable ($PodeContext.RunspacePools[$asyncRouteId].Pool) + } + $v = '' + $null = $PodeContext.RunspacePools.TryRemove($asyncRouteId, [ref]$v) + } + } + # remove the operationId from the openapi operationId list if ($r.OpenAPI) { foreach ( $tag in $r.OpenAPI.DefinitionTag) { diff --git a/src/Public/SSE.ps1 b/src/Public/SSE.ps1 index c1d620139..7213b6f41 100644 --- a/src/Public/SSE.ps1 +++ b/src/Public/SSE.ps1 @@ -27,6 +27,9 @@ An optional ClientId to use for the SSE connection, this value will be signed if .PARAMETER AllowAllOrigins If supplied, then Access-Control-Allow-Origin will be set to * on the response. +.PARAMETER AsyncRouteTaskId +This optional AsyncRouteTaskId is used internally to identify the Asynct Route task that is opening this SSE connection, and then used to send events to these connection. + .PARAMETER Force If supplied, the Accept header of the request will be ignored; attempting to configure an SSE connection even if the header isn't "text/event-stream". @@ -69,11 +72,16 @@ function ConvertTo-PodeSseConnection { [string] $ClientId, + [Parameter()] + [string] + $AsyncRouteTaskId, + [switch] $AllowAllOrigins, [switch] $Force + ) # check Accept header - unless forcing @@ -91,7 +99,7 @@ function ConvertTo-PodeSseConnection { $ClientId = New-PodeSseClientId -ClientId $ClientId # set and send SSE headers - $ClientId = Wait-PodeTask -Task $WebEvent.Response.SetSseConnection($Scope, $ClientId, $Name, $Group, $RetryDuration, $AllowAllOrigins.IsPresent) + $ClientId = Wait-PodeTask -Task $WebEvent.Response.SetSseConnection($Scope, $ClientId, $Name, $Group, $RetryDuration, $AllowAllOrigins.IsPresent, $AsyncRouteTaskId) # create SSE property on WebEvent $WebEvent.Sse = @{ @@ -101,6 +109,10 @@ function ConvertTo-PodeSseConnection { LastEventId = Get-PodeHeader -Name 'Last-Event-ID' IsLocal = ($Scope -ieq 'local') } + + if ($AsyncRouteTaskId) { + $WebEvent.Sse['asyncRouteTaskId'] = $AsyncRouteTaskId + } } <# @@ -227,6 +239,7 @@ function Send-PodeSseEvent { $EventType, [Parameter()] + [ValidateRange(0, 100)] [int] $Depth = 10, @@ -235,13 +248,8 @@ function Send-PodeSseEvent { $FromEvent ) - begin { $pipelineValue = @() - # do nothing if no value - if (($null -eq $Data) -or ([string]::IsNullOrEmpty($Data))) { - return - } } process { @@ -252,13 +260,20 @@ function Send-PodeSseEvent { if ($pipelineValue.Count -gt 1) { $Data = $pipelineValue } - # jsonify the value + + # do nothing if no value + if (($null -eq $Data) -or ([string]::IsNullOrEmpty($Data))) { + return + } + if ($Data -isnot [string]) { - if ($Depth -le 0) { - $Data = (ConvertTo-Json -InputObject $Data -Compress) - } - else { - $Data = (ConvertTo-Json -InputObject $Data -Depth $Depth -Compress) + $Data = (ConvertTo-Json -InputObject $Data -Depth $Depth -Compress) + } + + # if inside an async route wait for ClientId to be syncronized + if ($WebEvent.Async -and $FromEvent) { + while (!($WebEvent.ContainsKey('Sse') -and $WebEvent.Sse.ContainsKey('ClientId'))) { + Start-Sleep -Milliseconds 100 } } @@ -291,6 +306,7 @@ function Send-PodeSseEvent { $PodeContext.Server.Http.Listener.SendSseEvent($Name, $Group, $ClientId, $EventType, $Data, $Id) } } + <# .SYNOPSIS Close one or more SSE connections. diff --git a/tests/integration/AsyncRoute.Tests.ps1 b/tests/integration/AsyncRoute.Tests.ps1 new file mode 100644 index 000000000..f88788343 --- /dev/null +++ b/tests/integration/AsyncRoute.Tests.ps1 @@ -0,0 +1,315 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '')] +param() + +Describe 'ASYNC REST API Requests' { + + BeforeAll { + $mindyCommonHeaders = @{ + 'accept' = 'application/json' + 'X-API-KEY' = 'test2-api-key' + 'Authorization' = 'Basic bWluZHk6cGlja2xl' + } + + $mortyCommonHeaders = @{ + 'accept' = 'application/json' + 'X-API-KEY' = 'test-api-key' + 'Authorization' = 'Basic bW9ydHk6cGlja2xl' + } + $Port = 8080 + $Endpoint = "http://127.0.0.1:$($Port)" + $scriptPath = "$($PSScriptRoot)\..\..\examples\Web-AsyncRoute.ps1" + if ($PSVersionTable.PsVersion -gt [version]'6.0') { + Start-Process 'pwsh' -ArgumentList "-NoProfile -File `"$scriptPath`" -Quiet -Port $Port -DisableTermination" -NoNewWindow + } + else { + Start-Process 'powershell' -ArgumentList "-NoProfile -File `"$scriptPath`" -Quiet -Port $Port -DisableTermination" -NoNewWindow + } + Start-Sleep -Seconds 5 + } + + AfterAll { + Start-Sleep -Seconds 10 + Invoke-RestMethod -Uri "$($Endpoint)/close" -Method Post | Out-Null + + } + + Describe 'Hello Server' { + it 'Hello Server' { + Start-Sleep -Seconds 10 + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/hello" -Method Get + $response.message | Should -Be 'Hello!' + } + } + + Describe 'Create Async Route Task on behalf of Mindy' { + + It 'Create Async Route Task /auth/asyncUsingNotCancellable' { + + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncUsingNotCancellable" -Method Put -Headers $mindyCommonHeaders + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'MINDY021' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncUsingNotCancellable' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $false + } + + It 'Create Async Route Task /auth/asyncUsingCancellable' { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncUsingCancellable" -Method Put -Headers $mindyCommonHeaders + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'MINDY021' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncUsingCancellable' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $true + } + + It 'Create Async Route Task /auth/asyncUsing with JSON body' { + $body = @{ + callbackUrl = "http://localhost:$($Port)/receive/callback" + } | ConvertTo-Json + + $headersWithContentType = $mindyCommonHeaders.Clone() + $headersWithContentType['Content-Type'] = 'application/json' + + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncUsing" -Method Put -Headers $headersWithContentType -Body $body + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'MINDY021' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncUsing' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $true + } + + It 'Create Async Route Task /auth/asyncStateNoColumn' { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncStateNoColumn" -Method Put -Headers $mindyCommonHeaders + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'MINDY021' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncStateNoColumn' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $true + } + + It 'Create Async Route Task /auth/asyncState' { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncState" -Method Put -Headers $mindyCommonHeaders + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'MINDY021' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncState' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $true + } + + It 'Create Async Route Task /auth/asyncParam' { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncParam" -Method Put -Headers $mindyCommonHeaders + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'MINDY021' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncParam' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $true + } + } + + Describe 'Create Async Route Task on behalf of Morty' { + It 'Create Async Route Task /auth/asyncUsingNotCancellable' { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncUsingNotCancellable" -Method Put -Headers $mortyCommonHeaders + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'M0R7Y302' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncUsingNotCancellable' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $false + } + + It 'Create Async Route Task /auth/asyncUsingCancellable' { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncUsingCancellable" -Method Put -Headers $mortyCommonHeaders + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'M0R7Y302' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncUsingCancellable' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $true + } + + It 'Create Async Route Task /auth/asyncUsing with JSON body' { + $body = @{ + callbackUrl = "http://localhost:$($Port)/receive/callback" + } | ConvertTo-Json + + $headersWithContentType = $mortyCommonHeaders.Clone() + $headersWithContentType['Content-Type'] = 'application/json' + + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncUsing" -Method Put -Headers $headersWithContentType -Body $body + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'M0R7Y302' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncUsing' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $true + } + + It 'Throws exception - Create Async Route Task /auth/asyncStateNoColumn' { + { Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncStateNoColumn" -Method Put -Headers $mortyCommonHeaders } | Should -Throw + } + + It 'Create Async Route Task /auth/asyncState' { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncState" -Method Put -Headers $mortyCommonHeaders + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'M0R7Y302' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncState' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $true + } + + It 'Create Async Route Task /auth/asyncParam' { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncParam" -Method Put -Headers $mortyCommonHeaders + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'M0R7Y302' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncParam' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $true + } + + It 'Create Async Route Task /asyncWaitForeverTimeout' { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncInfiniteLoopTimeout" -Method Put -Headers $mortyCommonHeaders + + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'M0R7Y302' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncInfiniteLoopTimeout' + $response.State | Should -BeIn @('NotStarted', 'Running') + $response.Cancellable | Should -Be $false + } + } + + Describe -Name 'Get Async Route Task' { + BeforeAll { + $responseCreateAsync = Invoke-RestMethod -Uri "http://localhost:$($Port)/auth/asyncInfiniteLoop" -Method Put -Headers $mindyCommonHeaders + } + it 'Throws exception - Get Async Route Task as Morty' { + { Invoke-RestMethod -Uri "http://localhost:$($Port)/task/$($responseCreateAsync.ID)" -Method Get -Headers $mortyCommonHeaders } | + Should -Throw #-ExceptionType ([Microsoft.PowerShell.Commands.HttpResponseException]) + } + it 'Throws exception - Terminate Async Route Task as Morty' { + { Invoke-RestMethod -Uri "http://localhost:$($Port)/task?id=$($responseCreateAsync.ID)" -Method Delete -Headers $mortyCommonHeaders } | + Should -Throw #-Exception Type ([Microsoft.PowerShell.Commands.HttpResponseException]) + } + + it 'Get Async Route Task as Mindy' { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/task/$($responseCreateAsync.ID)" -Method Get -Headers $mindyCommonHeaders + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'MINDY021' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncInfiniteLoop' + $response.State | Should -BeIn 'Running' + $response.Cancellable | Should -Be $true + } + + it 'Terminate Async Route Task as Mindy' { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/task?id=$($responseCreateAsync.ID)" -Method Delete -Headers $mindyCommonHeaders + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.User | Should -Be 'MINDY021' + $response.AsyncRouteId | Should -Be '[Put]/auth/asyncInfiniteLoop' + $response.State | Should -BeIn 'Aborted' + $response.Error | Should -BeIn 'Aborted by the user' + $response.Cancellable | Should -Be $true + } + } + + Describe -Name 'Query Async Route Task' { + it 'Get Query Async Route Task as Mindy' { + $body = @{} | ConvertTo-Json + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/tasks" -Method Post -Body $body -Headers $mindyCommonHeaders + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.Count | Should -Be 7 + $response.state.where({ $_ -eq 'Aborted' }).count | Should -Be 1 + } + + it 'Get Query Async Route Task as Morty' { + $body = @{} | ConvertTo-Json + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/tasks" -Method Post -Body $body -Headers $mortyCommonHeaders + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.Count | Should -Be 6 + $response.state.where({ $_ -eq 'Aborted' }).count | Should -Be 0 + } + } + + Describe -Name 'Waiting for results ' { + it 'Wendy results' { + $counter = 0 + do { + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/tasks" -Method Post -Body '{}' -Headers $mindyCommonHeaders + Start-Sleep 2 + + } until (($response.state.where({ $_ -eq 'Running' -or $_ -eq 'NotStarted' }).count -eq 0) -or (++$counter -gt 60)) + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.Count | Should -Be 7 + $response.state.where({ $_ -eq 'Aborted' }).count | Should -Be 1 + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncUsingCancellable' }).Result.InnerValue | Should -Be 'coming from using' + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncUsing' }).Result.InnerValue | Should -Be 'coming from using' + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncUsingNotCancellable' }).Result.InnerValue | Should -Be 'coming from using' + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncInfiniteLoop' }).State | Should -Be 'Aborted' + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncParam' }).Result.InnerValue | Should -Be 'comming as argument' + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncStateNoColumn' }).Result.InnerValue | Should -Be 'coming from a PodeState' + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncState' }).Result.InnerValue | Should -Be 'coming from a PodeState' + } + it 'Morty results' { + $counter = 0 + do { + $body = @{'AsyncRouteId' = @{ + 'value' = '[Put]/auth/asyncInfiniteLoopTimeout' + 'op' = 'NE' + } + } | ConvertTo-Json + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/tasks" -Method Post -Body $body -Headers $mortyCommonHeaders + Start-Sleep 2 + } until (($response.state.where({ $_ -eq 'Running' -or $_ -eq 'NotStarted' }).count -eq 0) -or (++$counter -gt 60)) + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.Count | Should -Be 5 + $response.state.where({ $_ -eq 'Aborted' }).count | Should -Be 0 + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncUsingCancellable' }).Result.InnerValue | Should -Be 'coming from using' + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncUsing' }).Result.InnerValue | Should -Be 'coming from using' + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncUsingNotCancellable' }).Result.InnerValue | Should -Be 'coming from using' + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncParam' }).Result.InnerValue | Should -Be 'comming as argument' + $response.where({ $_.AsyncRouteId -eq '[Put]/auth/asyncState' }).Result.InnerValue | Should -Be 'coming from a PodeState' + } + + it 'Timeout' { + do { + $body = @{'AsyncRouteId' = @{ + 'value' = '[Put]/auth/asyncInfiniteLoopTimeout' + 'op' = 'EQ' + } + } | ConvertTo-Json + $response = Invoke-RestMethod -Uri "http://localhost:$($Port)/tasks" -Method Post -Body $body -Headers $mortyCommonHeaders + } until ($response.state.where({ $_ -eq 'Aborted' }).count -eq 1) + # Assertions to validate the response + $response | Should -Not -BeNullOrEmpty + $response.Count | Should -Be 1 + $response.state.where({ $_ -eq 'Aborted' }).count | Should -Be 1 + } + + } + +} \ No newline at end of file diff --git a/tests/unit/AsyncRoute.Tests.ps1 b/tests/unit/AsyncRoute.Tests.ps1 new file mode 100644 index 000000000..e4327e473 --- /dev/null +++ b/tests/unit/AsyncRoute.Tests.ps1 @@ -0,0 +1,869 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' +} + + +Describe 'Set-PodeAsyncRoutePermission' { + Describe 'Adding Permissions' { + BeforeEach { + # Mock Pode context and async routes + $PodeContext = @{ + AsyncRoutes = @{ + Enabled = $true + Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + Processes = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + } + + # Example route object to test with + $route = @{ + AsyncRouteId = 'testRoute' + IsAsync = $true + Async = @{Permission = @{} } + } + # Sample users, groups, roles, and scopes + $users = @('user1', 'user2') + $groups = @('group1', 'group2') + $roles = @('role1', 'role2') + $scopes = @('scope1', 'scope2') + } + + It 'should add Read permissions for users, groups, roles, and scopes' { + Set-PodeAsyncRoutePermission -Route $route -Type 'Read' -Users $users -Groups $groups -Roles $roles -Scopes $scopes + + $permissions = $route.Async.Permission.Read + + $permissions.Users | Should -Be $users + $permissions.Groups | Should -Be $groups + $permissions.Roles | Should -Be $roles + $permissions.Scopes | Should -Be $scopes + } + + It 'should add Write permissions for users, groups, roles, and scopes' { + Set-PodeAsyncRoutePermission -Route $route -Type 'Write' -Users $users -Groups $groups -Roles $roles -Scopes $scopes + + $permissions = $route.Async.Permission.Write + + $permissions.Users | Should -Be $users + $permissions.Groups | Should -Be $groups + $permissions.Roles | Should -Be $roles + $permissions.Scopes | Should -Be $scopes + } + + It 'should return the route object when PassThru is specified' { + $result = Set-PodeAsyncRoutePermission -Route $route -Type 'Read' -PassThru + + $result | Should -Be $route + } + + It 'should throw an exception when Route is null' { + { Set-PodeAsyncRoutePermission -Route $null -Type 'Read' } | Should -Throw + } + + It 'should handle multiple routes piped in' { + $routes = @( + @{ AsyncRouteId = 'route1' ; IsAsync = $true ; Async = @{ Permission = @{} } }, + @{ AsyncRouteId = 'route2' ; IsAsync = $true ; Async = @{ Permission = @{} } } + ) + + $routes | Set-PodeAsyncRoutePermission -Type 'Read' -Users $users + $routes[0].Async.Permission.Read.Users | Should -Be $users + $routes[1].Async.Permission.Read.Users | Should -Be $users + } + + It 'should initialize the Permission object if not already present' { + $route.Async = @{Permission = @{Read = @{Users = @('user3') } } } + + Set-PodeAsyncRoutePermission -Route $route -Type 'Read' -Users $users + + $route.Async.Permission.Read.Users | Should -Be ( @('user3') + $users ) + } + } + + + Describe 'Remove' { + BeforeEach { + # Mock Pode context and async routes + $PodeContext = @{ + AsyncRoutes = @{ + Enabled = $true + Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + Processes = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + } + + # Example route object to test with + $route = @{ + AsyncRouteId = 'testRoute' + IsAsync = $true + Async = @{ + Permission = @{ + Read = @{ + Users = @('user1', 'user2') + Groups = @('group1', 'group2') + Roles = @('role1', 'role2') + Scopes = @('scope1', 'scope2') + } + Write = @{ + Users = @('user3', 'user4') + Groups = @('group3', 'group4') + Roles = @('role3', 'role4') + Scopes = @('scope3', 'scope4') + } + } + } + } + } + + It 'should remove specified users from Read permissions' { + Set-PodeAsyncRoutePermission -Route $route -Type 'Read' -Users @('user1') -Remove + + $permissions = $route.Async.Permission.Read + + $permissions.Users | Should -Not -Contain 'user1' + $permissions.Users | Should -Contain 'user2' + } + + It 'should remove specified groups from Write permissions' { + Set-PodeAsyncRoutePermission -Route $route -Type 'Write' -Groups @('group3') -Remove + + $permissions = $route.Async.Permission.Write + + $permissions.Groups | Should -Not -Contain 'group3' + $permissions.Groups | Should -Contain 'group4' + } + + It 'should remove specified roles from Read permissions' { + Set-PodeAsyncRoutePermission -Route $route -Type 'Read' -Roles @('role1') -Remove + + $permissions = $route.Async.Permission.Read + + $permissions.Roles | Should -Not -Contain 'role1' + $permissions.Roles | Should -Contain 'role2' + } + + It 'should remove specified scopes from Write permissions' { + Set-PodeAsyncRoutePermission -Route $route -Type 'Write' -Scopes @('scope3') -Remove + + $permissions = $route.Async.Permission.Write + + $permissions.Scopes | Should -Not -Contain 'scope3' + $permissions.Scopes | Should -Contain 'scope4' + } + + It 'should do nothing if the item to remove does not exist' { + Set-PodeAsyncRoutePermission -Route $route -Type 'Read' -Users @('nonexistentuser') -Remove + + $permissions = $route.Async.Permission.Read + + $permissions.Users | Should -Contain 'user1' + $permissions.Users | Should -Contain 'user2' + } + } +} + + +# Assuming the function Export-PodeAsyncRouteInfo is already defined in your session or module + +Describe 'Export-PodeAsyncRouteInfo' { + + BeforeEach { + $testDate = Get-Date + } + Context 'When Async contains full details' { + It 'should export all details into a hashtable' { + + $asyncData = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $asyncData['Id'] = 'async-001' + $asyncData['Cancellable'] = $true + $asyncData['CreationTime'] = $testDate + $asyncData['ExpireTime'] = $testDate.AddMinutes(10) + $asyncData['AsyncRouteId'] = 'TestAsync' + $asyncData['State'] = 'Completed' + $asyncData['Permission'] = 'Admin' + $asyncData['StartingTime'] = $testDate.AddSeconds(30) + $asyncData['CallbackSettings'] = @{ Url = 'http://example.com/callback' } + $asyncData['User'] = 'testuser' + $asyncData['EnableSse'] = $true + $asyncData['Progress'] = 50 + $asyncData['Runspace'] = @{ + Handler = [pscustomobject]@{ IsCompleted = $true } + } + $asyncData['Result'] = 'Success' + $asyncData['CompletedTime'] = $testDate.AddMinutes(5) + $asyncData['IsCompleted'] = $true + + $result = Export-PodeAsyncRouteInfo -Async $asyncData + + $result | Should -BeOfType 'hashtable' + $result.Id | Should -Be 'async-001' + $result.Cancellable | Should -Be $true + $result.CreationTime | Should -Be (Format-PodeDateToIso8601 -Date $testDate) + $result.ExpireTime | Should -Be (Format-PodeDateToIso8601 -Date ($testDate.AddMinutes(10))) + $result.AsyncRouteId | Should -Be 'TestAsync' + $result.State | Should -Be 'Completed' + $result.Permission | Should -Be 'Admin' + $result.StartingTime | Should -Be (Format-PodeDateToIso8601 -Date ($testDate.AddSeconds(30))) + $result.CallbackSettings.Url | Should -Be 'http://example.com/callback' + $result.User | Should -Be 'testuser' + $result.Sse | Should -BeNullOrEmpty + $result.Progress | Should -Be 50 + $result.Result | Should -Be 'Success' + $result.CompletedTime | Should -Be (Format-PodeDateToIso8601 -Date ($testDate.AddMinutes(5))) + $result.IsCompleted | Should -BeTrue + } + } + + Context 'When Raw switch is used' { + It 'should return the raw ConcurrentDictionary' { + $asyncData = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $asyncData['Id'] = 'async-002' + + $result = Export-PodeAsyncRouteInfo -Async $asyncData -Raw + + $result | Should -BeOfType 'System.Collections.Concurrent.ConcurrentDictionary[string, psobject]' + $result['Id'] | Should -Be 'async-002' + } + } + + Context 'When Async contains minimal details' { + It 'should handle missing optional keys gracefully' { + $asyncData = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $asyncData['Id'] = 'async-003' + $asyncData['CreationTime'] = $testDate + $asyncData['ExpireTime'] = $testDate.AddMinutes(10) + $asyncData['State'] = 'Running' + + $result = Export-PodeAsyncRouteInfo -Async $asyncData + + $result | Should -BeOfType 'hashtable' + $result.Id | Should -Be 'async-003' + $result.CreationTime | Should -Be (Format-PodeDateToIso8601 -Date $testDate) + $result.State | Should -Be 'Running' + $result.ContainsKey('Permission') | Should -Be $false + $result.ContainsKey('CallbackSettings') | Should -Be $false + } + } +} + +Describe 'Get-PodeAsyncRouteOperation' { + + BeforeAll { + $PodeContext = @{ + AsyncRoutes = @{ + Enabled = $true + Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + Processes = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + } + + + # Add a sample asynchronous route operation to the mock PodeContext + $operationId1 = '123e4567-e89b-12d3-a456-426614174000' + $asyncOperationDetails = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $asyncOperationDetails['Id'] = $operationId1 + $asyncOperationDetails['State'] = 'Running' + $asyncOperationDetails['Cancellable'] = $true + $asyncOperationDetails['CreationTime'] = Get-Date + $asyncOperationDetails['ExpireTime'] = ($asyncOperationDetails['CreationTime']).AddMinutes(10) + $asyncOperationDetails['AsyncRouteId'] = 'PesterTest1' + $PodeContext.AsyncRoutes.Processes[$operationId1] = $asyncOperationDetails + + + $operationId2 = '123e4567-e89b-12d3-a456-426614174001' + $asyncOperationDetails = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $asyncOperationDetails['Id'] = $operationId2 + $asyncOperationDetails['State'] = 'NotStarted' + $asyncOperationDetails['Cancellable'] = $false + $asyncOperationDetails['CreationTime'] = Get-Date + $asyncOperationDetails['ExpireTime'] = ($asyncOperationDetails['CreationTime']).AddMinutes(10) + $asyncOperationDetails['AsyncRouteId'] = 'PesterTest2' + $PodeContext.AsyncRoutes.Processes[$operationId2] = $asyncOperationDetails + + $operationId3 = '123e4567-e89b-12d3-a456-426614174002' + $asyncOperationDetails = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $asyncOperationDetails['Id'] = $operationId3 + $asyncOperationDetails['State'] = 'Running' + $asyncOperationDetails['Cancellable'] = $true + $asyncOperationDetails['CreationTime'] = Get-Date + $asyncOperationDetails['ExpireTime'] = ($asyncOperationDetails['CreationTime']).AddMinutes(10) + $asyncOperationDetails['AsyncRouteId'] = 'PesterTest3' + $PodeContext.AsyncRoutes.Processes[$operationId3] = $asyncOperationDetails + + } + + It 'should return all routes when Id and AsyncRouteId are null' { + + # Act + $Result = Get-PodeAsyncRouteOperation + + # Assert + $Result.Count | Should -Be 3 + foreach ($r in $Result) { + switch ($r.Id ) { + $operationId1 { + $r.AsyncRouteId | Should -Be 'PesterTest1' + $r.State | Should -Be 'Running' + } + $operationId2 { + $r.AsyncRouteId | Should -Be 'PesterTest2' + $r.State | Should -Be 'NotStarted' + } + $operationId3 { + $r.AsyncRouteId | Should -Be 'PesterTest3' + $r.State | Should -Be 'Running' + } + } + } + } + + It 'should return the route with Id "123e4567-e89b-12d3-a456-426614174002"' { + # Arrange + + # Act + $Result = Get-PodeAsyncRouteOperation -Id $operationId3 + + # Assert + $Result.Id | Should -Be $operationId3 + $Result.State | Should -Be 'Running' + $Result.Cancellable | Should -BeTrue + $Result.AsyncRouteId | Should -Be 'PesterTest3' + } + + It 'should return routes with AsyncRouteId Route1' { + + # Act + $Result = Get-PodeAsyncRouteOperation -AsyncRouteId 'PesterTest2' + + # Assert + $Result.Id | Should -Be $operationId2 + $Result.State | Should -Be 'NotStarted' + $Result.Cancellable | Should -BeFalse + $Result.AsyncRouteId | Should -Be 'PesterTest2' + } + + It 'should return empty when Id does not match' { + # Arrange + $MockResults = @() + + # Act + $Result = Get-PodeAsyncRouteOperation -Id '999' + + # Assert + $Result | Should -BeNullOrEmpty + } + + It 'should pass the Raw switch to Export-PodeAsyncRouteInfo' { + + # Act + $Result = Get-PodeAsyncRouteOperation -Raw + + # Assert + $Result.Count | should -Be 3 + $Result.GetType().tostring() | should -Be 'System.Collections.Concurrent.ConcurrentDictionary`2[System.String,System.Management.Automation.PSObject]' + } +} + + +Describe 'Get-PodeAsyncRouteOperationByFilter' { + BeforeAll { + # Mock data setup + $PodeContext = @{ + AsyncRoutes = @{ + Enabled = $true + Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + Processes = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + } + + # Add mock routes + + # Add a sample asynchronous route operation to the mock PodeContext + $operationId1 = '123e4567-e89b-12d3-a456-426614174000' + $asyncOperationDetails = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $asyncOperationDetails['Id'] = $operationId1 + $asyncOperationDetails['State'] = 'Running' + $asyncOperationDetails['Cancellable'] = $true + $asyncOperationDetails['CreationTime'] = Get-Date + $asyncOperationDetails['ExpireTime'] = ($asyncOperationDetails['CreationTime']).AddMinutes(10) + $asyncOperationDetails['AsyncRouteId'] = 'PesterTest1' + $PodeContext.AsyncRoutes.Processes[$operationId1] = $asyncOperationDetails + + + $operationId2 = '123e4567-e89b-12d3-a456-426614174001' + $asyncOperationDetails = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $asyncOperationDetails['Id'] = $operationId2 + $asyncOperationDetails['State'] = 'NotStarted' + $asyncOperationDetails['Cancellable'] = $false + $asyncOperationDetails['CreationTime'] = Get-Date + $asyncOperationDetails['ExpireTime'] = ($asyncOperationDetails['CreationTime']).AddMinutes(10) + $asyncOperationDetails['AsyncRouteId'] = 'PesterTest2' + $PodeContext.AsyncRoutes.Processes[$operationId2] = $asyncOperationDetails + + $operationId3 = '123e4567-e89b-12d3-a456-426614174002' + $asyncOperationDetails = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $asyncOperationDetails['Id'] = $operationId3 + $asyncOperationDetails['State'] = 'Running' + $asyncOperationDetails['Cancellable'] = $false + $asyncOperationDetails['CreationTime'] = Get-Date + $asyncOperationDetails['ExpireTime'] = ($asyncOperationDetails['CreationTime']).AddMinutes(10) + $asyncOperationDetails['AsyncRouteId'] = 'PesterTest3' + $PodeContext.AsyncRoutes.Processes[$operationId3] = $asyncOperationDetails + + } + + It 'should retrieve the operation details for a valid Id' { + # Act + $result = Get-PodeAsyncRouteOperationByFilter -Filter @{ + 'State' = @{ 'op' = 'EQ'; 'value' = 'Running' } + 'Cancellable' = @{ 'op' = 'EQ'; 'value' = $true } + } + + # Assert + $result['Id'] | Should -Be '123e4567-e89b-12d3-a456-426614174000' + $result['AsyncRouteId'] | Should -Be 'PesterTest1' + } + + It 'should return the raw data if -Raw is specified' { + # Act + $result = Get-PodeAsyncRouteOperationByFilter -Raw -Filter @{ + 'State' = @{ 'op' = 'EQ'; 'value' = 'Running' } + } + + # Assert + $result.Count | should -Be 2 + foreach ($r in $result) { + switch ($r.Id ) { + $operationId1 { + $r | Should -Be $PodeContext.AsyncRoutes.Processes[$operationId1] + } + $operationId3 { + $r | Should -Be $PodeContext.AsyncRoutes.Processes[$operationId3] + } + $operationId2 { + # Fail the test if this case is hit + "Unexpected operation ID '$operationId2' found in results." | Should -Fail + } + default { + # Fail the test if any unexpected operation ID is found + "Unexpected operation ID '$($r.Id)' found in results." | Should -Fail + } + + } + } + } + + It 'should throw an exception if the property does not exist' { + + { Get-PodeAsyncRouteOperationByFilter -Filter @{ + 'notExist' = @{ 'op' = 'EQ'; 'value' = $true } + } } | Should -Throw -ExpectedMessage ($PodeLocale.invalidQueryElementExceptionMessage -f 'notExist') + } +} + + +# Set-PodeAsyncRouteOASchemaName.Tests.ps1 + +Describe 'Set-PodeAsyncRouteOASchemaName' { + # Mocking the dependencies + Mock -CommandName Test-PodeOADefinitionTag -MockWith { return @('default') } + + + # Setting up a mock PodeContext with default values + BeforeEach { + $PodeContext = @{ + Server = @{ + OpenApi = @{ + Definitions = @{ + default = @{ + hiddenComponents = @{ + AsyncRoute = @{ + OATypeName = 'DefaultAsyncRouteTask' + TaskIdName = 'defaultId' + QueryRequestName = 'DefaultAsyncRouteTaskQuery' + QueryParameterName = 'DefaultAsyncRouteTaskQueryParameter' + } + } + } + } + } + } + } + } + + It 'Should set the OpenAPI schema names correctly when all parameters are provided' { + # Arrange + $params = @{ + OATypeName = 'CustomTask' + TaskIdName = 'CustomId' + QueryRequestName = 'CustomQuery' + QueryParameterName = 'CustomQueryParam' + OADefinitionTag = @('default') + } + + # Act + Set-PodeAsyncRouteOASchemaName @params + + # Assert + $definition = $PodeContext.Server.OpenApi.Definitions['default'].hiddenComponents.AsyncRoute + $definition.OATypeName | Should -Be 'CustomTask' + $definition.TaskIdName | Should -Be 'CustomId' + $definition.QueryRequestName | Should -Be 'CustomQuery' + $definition.QueryParameterName | Should -Be 'CustomQueryParam' + } + + It 'Should use default values if parameters are not provided' { + # Arrange + $params = @{ + OADefinitionTag = @('default') + } + + # Act + Set-PodeAsyncRouteOASchemaName @params + + # Assert + $definition = $PodeContext.Server.OpenApi.Definitions['default'].hiddenComponents.AsyncRoute + $definition.OATypeName | Should -Be 'DefaultAsyncRouteTask' + $definition.TaskIdName | Should -Be 'defaultId' + $definition.QueryRequestName | Should -Be 'DefaultAsyncRouteTaskQuery' + $definition.QueryParameterName | Should -Be 'DefaultAsyncRouteTaskQueryParameter' + } +} + + +Describe 'Add-PodeAsyncRouteSse' { + + BeforeAll { + # Mock the required Pode functions and variables + Mock -CommandName 'Add-PodeRoute' -MockWith { + return @{ Path = "$($args[2])_events"; Method = 'Get' } + } + # Mock -CommandName 'ConvertTo-PodeSseConnection' + Mock -CommandName 'Send-PodeSseEvent' + Mock -CommandName 'Write-PodeErrorLog' + + # Mock data setup + $PodeContext = @{ + AsyncRoutes = @{ + Enabled = $true + Processes = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + } + # Add a sample asynchronous route operation to the mock PodeContext + $operationId1 = '123e4567-e89b-12d3-a456-426614174000' + $asyncOperationDetails = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $asyncOperationDetails['Id'] = $operationId1 + $asyncOperationDetails['State'] = 'Completed' + $asyncOperationDetails['Cancellable'] = $true + $asyncOperationDetails['CreationTime'] = Get-Date + $asyncOperationDetails['ExpireTime'] = ($asyncOperationDetails['CreationTime']).AddMinutes(10) + $asyncOperationDetails['AsyncRouteId'] = 'PesterTest1' + $asyncOperationDetails['Result'] = 'Success' + $asyncOperationDetails['Runspace'] = [pscustomobject]@{ Handler = [pscustomobject]@{ IsCompleted = $false } } + $PodeContext.AsyncRoutes.Processes[$operationId1] = $asyncOperationDetails + + + } + + It 'Should throw an exception if the route is not marked as async' { + { + $route = @{ + Path = '/not-async' + AsyncRouteId = 'not-async' + IsAsync = $false + } | Add-PodeAsyncRouteSse + } | Should -Throw -ExpectedMessage ($PodeLocale.routeNotMarkedAsAsyncExceptionMessage -f '/not-async') + } + + It 'Should add SSE route for a valid async route' { + $route = @{ Path = '/events'; AsyncRouteId = 'ExamplePool'; IsAsync = $true; Async = @{} } + + $result = Add-PodeAsyncRouteSse -Route $route -PassThru + + $result | Should -BeOfType 'hashtable' + $result.Path | Should -Be '/events' + $route.Async.Sse.Name | Should -Be '/events_events' + } + + It 'Should handle multiple routes piped in' { + $routes = @( + @{ Path = '/events1'; AsyncRouteId = 'ExamplePool'; IsAsync = $true ; Async = @{} }, + @{ Path = '/events2'; AsyncRouteId = 'ExamplePool'; IsAsync = $true ; Async = @{} } + ) + + $result = $routes | Add-PodeAsyncRouteSse -PassThru + + $result | Should -HaveCount 2 + $routes[0].Async.Sse.Name | Should -Be '/events1_events' + $routes[1].Async.Sse.Name | Should -Be '/events2_events' + } + + It 'Should return the modified route object when PassThru is specified' { + $route = @{ Path = '/events'; AsyncRouteId = 'ExamplePool'; IsAsync = $true; Async = @{} } + + $result = Add-PodeAsyncRouteSse -Route $route -PassThru + + $result | Should -BeOfType 'hashtable' + $result.Path | Should -Be '/events' + } + + +} + +Describe 'Set-PodeAsyncRoute' { + + BeforeEach { + # Mock the required Pode functions and variables + Mock -CommandName 'Start-PodeAsyncRoutesHousekeeper' + Mock -CommandName 'New-PodeGuid' -MockWith { return [guid]::NewGuid().ToString() } + Mock -CommandName 'Test-PodeAsyncRouteScriptblockInvalidCommand' + Mock -CommandName 'Get-PodeAsyncRouteScriptblock' -MockWith { return $args[0] } + Mock -CommandName 'Get-PodeAsyncRouteSetScriptBlock' -MockWith { return $args[0] } + Mock -CommandName 'New-PodeRunspacePoolNetWrapper' -MockWith { return @{} } + + # Mock Pode Context + $PodeContext = @{ + Threads = @{ + AsyncRoutes = 0 + } + RunspacePools = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + RunspaceState = [initialsessionstate]::CreateDefault() + + AsyncRoutes = @{ + Enabled = $true + Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + Processes = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + } + + + } + + + It 'Should correctly mark a route as async and set runspaces' { + $route = @{Method = 'Get'; Path = '/async'; IsAsync = $false; Logic = {} } + Mock -CommandName 'New-PodeRunspacePoolNetWrapper' -MockWith { return @{} } + $result = Set-PodeAsyncRoute -Route $route -MaxRunspaces 3 -MinRunspaces 2 -PassThru + + $result | Should -BeOfType 'hashtable' + $result.IsAsync | Should -Be $true + + $route.Async.MinRunspaces | Should -Be 2 + $route.Async.MaxRunspaces | Should -Be 3 + $PodeContext.Threads.AsyncRoutes | Should -Be 3 + } + + It 'Should throw an exception if attempting to invoke for a route already marked as async' { + $route = @{ Path = '/async'; IsAsync = $true; Logic = {} } + + { + Set-PodeAsyncRoute -Route $route + } | Should -Throw -ExpectedMessage ($PodeLocale.functionCannotBeInvokedMultipleTimesExceptionMessage -f 'Set-PodeAsyncRoute', '/async') + } + + It 'Should handle a custom IdGenerator script block' { + $route = @{ Path = '/async'; IsAsync = $false; Logic = {} } + + $idGenScript = { return 'CustomId' } + $result = Set-PodeAsyncRoute -Route $route -IdGenerator $idGenScript -PassThru + + $route.Async.AsyncRouteTaskIdGenerator.Invoke() | Should -Be 'CustomId' + } + + It 'Should use default IdGenerator if none is provided' { + $route = @{ Path = '/async'; IsAsync = $false; Logic = {} } + + $result = Set-PodeAsyncRoute -Route $route -PassThru + + $id = $route.Async.AsyncRouteTaskIdGenerator.Invoke() + + $id -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' | Should -Be $true # Checks if the generated Id is a valid GUID + } + + It 'Should respect the Timeout parameter' { + $route = @{ Path = '/async'; IsAsync = $false; Logic = {} } + + $result = Set-PodeAsyncRoute -Route $route -Timeout 600 -PassThru + + $result.Async.Timeout | Should -Be 600 + } + +} + + +Describe 'Stop-PodeAsyncRouteOperation' { + # Mocking the dependencies + BeforeAll { + # Mock Pode Context + $PodeContext = @{ + Threads = @{ + AsyncRoutes = 0 + } + RunspacePools = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + RunspaceState = [initialsessionstate]::CreateDefault() + + AsyncRoutes = @{ + Enabled = $true + Processes = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + } + + # Mocking the Complete-PodeAsyncRouteOperation function + Mock -CommandName 'Complete-PodeAsyncRouteOperation' + + # Mocking the Export-PodeAsyncRouteInfo function + Mock -CommandName 'Export-PodeAsyncRouteInfo' -MockWith { + param($Async, [switch]$Raw) + # Return the async operation details, formatted or raw + if ($Raw) { return $Async } else { return @{'Formatted' = $true } } + } + } + + Context 'When operation Id exists' { + BeforeAll { + class TestRunspacePipeline { + [bool]$IsDisposed = $false + [void]Dispose() { + $this.IsDisposed = $true + # Mock the Runspace.Dispose method + } + } + } + BeforeEach { + # Add a mock operation to PodeContext + $mockOperation = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $mockOperation['Id'] = '123e4567-e89b-12d3-a456-426614174000' + $mockOperation['State'] = 'Running' + $mockOperation['Error'] = $null + $mockOperation['CompletedTime'] = $null + $mockOperation['Runspace'] = [pscustomobject]@{ Pipeline = [TestRunspacePipeline]::new() } + + $PodeContext.AsyncRoutes.Processes[$mockOperation.Id] = $mockOperation + } + + It 'Should abort the operation and finalize it' { + $operationId = '123e4567-e89b-12d3-a456-426614174000' + + # Call the function + $result = Stop-PodeAsyncRouteOperation -Id $operationId + + # Assertions + $operation = $PodeContext.AsyncRoutes.Processes[$operationId] + $operation.State | Should -Be 'Aborted' + $operation.Error | Should -Be 'Aborted by System' + $operation.CompletedTime | Should -Not -Be $null + + # Ensure Complete-PodeAsyncRouteOperation was called + $operation.Runspace.Pipeline.IsDisposed | Should -BeTrue + } + + It 'Should return raw operation details when -Raw is specified' { + $operationId = '123e4567-e89b-12d3-a456-426614174000' + + # Call the function with -Raw + $result = Stop-PodeAsyncRouteOperation -Id $operationId -Raw + + # Assertions + $result | Should -Be $PodeContext.AsyncRoutes.Processes[$operationId] + } + } + + Context 'When operation Id does not exist' { + It 'Should throw an exception' { + $operationId = 'nonexistent-id' + + # Assert that the function throws an exception + { Stop-PodeAsyncRouteOperation -Id $operationId } | Should -Throw + } + } +} + + +Describe 'Add-PodeAsyncRouteGet' { + # Mocking the dependencies + BeforeAll { + # Mock the Get-PodeAsyncRouteOAName function + Mock -CommandName Get-PodeAsyncRouteOAName -MockWith { + return @{ + TaskIdName = 'taskId' + OATypeName = 'AsyncTaskType' + } + } + + # Mock the Add-PodeRoute function + Mock -CommandName Add-PodeRoute -MockWith { + return @{ + Path = $Path; AsyncRouteId = "__Get$($Path)__".Replace('/', '_'); IsAsync = $false; Logic = {} ; OpenApi = @{} + } + } + + # Mock the Set-PodeOARequest, Add-PodeOAResponse, New-PodeOAStringProperty, and New-PodeOAObjectProperty functions + Mock -CommandName Set-PodeOARequest -MockWith { return $args[0] } + # Mock -CommandName Add-PodeOAResponse -MockWith { return $args[0] } + Mock -CommandName New-PodeOAStringProperty -MockWith { return @{} } + Mock -CommandName New-PodeOAObjectProperty -MockWith { return @{} }#> + } + + Context 'When Path and OADefinitionTag are specified' { + It 'Should create the route and return it when PassThru is specified' { + $route = Add-PodeAsyncRouteGet -Path '/status' -PassThru + + # Ensure Add-PodeRoute was called with the expected parameters + Assert-MockCalled -CommandName Add-PodeRoute -Exactly 1 -Scope It + + # Verify the returned route + $route.Path | Should -Be '/status' + $route.OpenApi.ContainsKey('Postponed') | Should -Be $true + $route.OpenApi.ContainsKey('PostponedArgumentList') | Should -Be $true + } + + It 'Should correctly modify the Path when In is Path' { + $route = Add-PodeAsyncRouteGet -Path '/status' -In 'Path' -PassThru + + # Ensure the Path was modified to include taskId + $route.Path | Should -Be '/status/:taskId' + } + + It 'Should append the taskId to the Path when In is Path' { + $route = Add-PodeAsyncRouteGet -Path '/status' -In 'Path' -PassThru + + # Verify that the taskId is appended to the path + $route.Path | Should -Be '/status/:taskId' + } + } + +} \ No newline at end of file diff --git a/tests/unit/Routes.Tests.ps1 b/tests/unit/Routes.Tests.ps1 index b5f746448..2fd804171 100644 --- a/tests/unit/Routes.Tests.ps1 +++ b/tests/unit/Routes.Tests.ps1 @@ -109,46 +109,75 @@ Describe 'Add-PodeStaticRoute' { } Describe 'Remove-PodeRoute' { + BeforeAll { + # Mock the Start-PodeAsyncRoutesHousekeeper function + Mock Start-PodeAsyncRoutesHousekeeper {} + # Mock the New-PodeRunspacePoolNetWrapper function + Mock New-PodeRunspacePoolNetWrapper {} + # Mock the Add-PodeAsyncRouteComponentSchema function + Mock Add-PodeAsyncRouteComponentSchema {} + } BeforeEach { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{}; 'Endpoints' = @{}; 'EndpointsMap' = @{} - 'OpenAPI' = @{ - SelectedDefinitionTag = 'default' - Definitions = @{ - default = Get-PodeOABaseObject + $PodeContext = @{ + Server = @{ + 'Routes' = @{ + 'GET' = @{} } + 'FindEndpoints' = @{} + 'Endpoints' = @{} + 'EndpointsMap' = @{} + 'OpenAPI' = @{ + SelectedDefinitionTag = 'default' + Definitions = @{ + default = @{ + hiddenComponents = @{ + operationId = @() + } + } + } + } + } + RunspacePools = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + AsyncRoutes = @{ + Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() } + Threads = @{ + AsyncRoutes = 0 + } + RunspaceState = [initialsessionstate]::CreateDefault() } + $PodeContext.RunspacePools['Items'] } + It 'Adds route with simple url, and then removes it' { Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } - $routes = $PodeContext.Server.Routes['get'] + $routes = $PodeContext.Server.Routes['GET'] $routes | Should -Not -Be $null $routes.ContainsKey('/users') | Should -Be $true $routes['/users'].Length | Should -Be 1 Remove-PodeRoute -Method Get -Path '/users' - $routes = $PodeContext.Server.Routes['get'] + $routes = $PodeContext.Server.Routes['GET'] $routes | Should -Not -Be $null $routes.ContainsKey('/users') | Should -Be $false } It 'Adds two routes with simple url, and then removes one' { - Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Get -Path '/users' -EndpointName user -ScriptBlock { Write-Host 'hello' } - $routes = $PodeContext.Server.Routes['get'] + $routes = $PodeContext.Server.Routes['GET'] $routes | Should -Not -Be $null $routes.ContainsKey('/users') | Should -Be $true $routes['/users'].Length | Should -Be 2 Remove-PodeRoute -Method Get -Path '/users' - $routes = $PodeContext.Server.Routes['get'] + $routes = $PodeContext.Server.Routes['GET'] $routes | Should -Not -Be $null $routes.ContainsKey('/users') | Should -Be $true $routes['/users'].Length | Should -Be 1 @@ -189,8 +218,59 @@ Describe 'Remove-PodeRoute' { $routes['/users'].Length | Should -Be 1 $PodeContext.Server.OpenAPI.Definitions.default.hiddenComponents.operationId | Should -Not -Contain 'getUsers2' } + + It 'Removes async route and cleans up runspace and async route pools' { + $route = Add-PodeRoute -PassThru -Method Get -Path '/async' -ScriptBlock { Write-Host 'hello' } | + Set-PodeAsyncRoute -MaxRunspaces 5 -MinRunspaces 3 -ResponseContentType 'application/json' -Timeout 300 -PassThru + $asyncRouteId = $route.Async.AsyncRouteId + $PodeContext.RunspacePools[$asyncRouteId].Pool = [PSCustomObject]@{ + IsDisposed = $true # to avoid to call BeginClose($null,$null) + } + Remove-PodeRoute -Method Get -Path '/async' + + $PodeContext.RunspacePools.ContainsKey($asyncRouteId) | Should -Be $false + $PodeContext.Threads.AsyncRoutes | Should -Be 0 + } + + It 'Adds two routes and removes one async route and cleans up runspace and async route pools' { + $maxRunspaces=5 + Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user + + $route1 = Add-PodeRoute -PassThru -Method Get -Path '/asyncusers' -ScriptBlock { Write-Host 'hello' } | + Set-PodeAsyncRoute -MaxRunspaces $maxRunspaces -MinRunspaces 3 -ResponseContentType 'application/json' -Timeout 300 -PassThru + + $route2 = Add-PodeRoute -PassThru -Method Get -Path '/asyncusers' -EndpointName user -ScriptBlock { Write-Host 'hello' } | + Set-PodeAsyncRoute -MaxRunspaces $maxRunspaces -MinRunspaces 3 -ResponseContentType 'application/yaml' -Timeout 300 -PassThru + + $PodeContext.RunspacePools[$route1.Async.AsyncRouteId].Pool = [PSCustomObject]@{ + IsDisposed = $true # to avoid to call BeginClose($null,$null) + } + $PodeContext.RunspacePools[$route2.Async.AsyncRouteId].Pool = [PSCustomObject]@{ + IsDisposed = $true # to avoid to call BeginClose($null,$null) + } + $PodeContext.Threads.AsyncRoutes | Should -Be ($maxRunspaces + $maxRunspaces) + $PodeContext.RunspacePools.ContainsKey($route2.Async.AsyncRouteId) | Should -Be $true + + $PodeContext.RunspacePools.ContainsKey($route1.Async.AsyncRouteId) | Should -Be $true + + #remove $route1 + Remove-PodeRoute -Method Get -Path '/asyncusers' + + $PodeContext.RunspacePools.ContainsKey($route2.Async.AsyncRouteId) | Should -Be $true + + $PodeContext.RunspacePools.ContainsKey($route1.Async.AsyncRouteId) | Should -Be $false + + $PodeContext.Threads.AsyncRoutes | Should -Be $maxRunspaces + + $routes = $PodeContext.Server.Routes['GET'] + $routes | Should -Not -Be $null + $routes.ContainsKey('/asyncusers') | Should -Be $true + $routes['/asyncusers'].Length | Should -Be 1 + } + } + Describe 'Remove-PodeStaticRoute' { It 'Adds a static route, and then removes it' { Mock Test-PodePath { return $true } @@ -327,8 +407,8 @@ Describe 'Add-PodeRoute' { It 'Throws error because no scriptblock supplied' { - # ?*[] can be escaped using backtick, ex `*. - $expectedMessage = ($PodeLocale.noLogicPassedForMethodRouteExceptionMessage -f 'GET', '/').Replace('[','`[').Replace(']','`]') + # ?*[] can be escaped using backtick, ex `*. + $expectedMessage = ($PodeLocale.noLogicPassedForMethodRouteExceptionMessage -f 'GET', '/').Replace('[', '`[').Replace(']', '`]') { Add-PodeRoute -Method GET -Path '/' -ScriptBlock {} } | Should -Throw -ExpectedMessage $expectedMessage # '*No logic passed*' # -Throw -ExpectedMessage $expectedMessage # '*No logic passed*' } @@ -343,7 +423,7 @@ Describe 'Add-PodeRoute' { ) } } - $expectedMessage = ($PodeLocale.methodPathAlreadyDefinedExceptionMessage -f 'GET', '/').Replace('[','`[').Replace(']','`]') + $expectedMessage = ($PodeLocale.methodPathAlreadyDefinedExceptionMessage -f 'GET', '/').Replace('[', '`[').Replace(']', '`]') { Add-PodeRoute -Method GET -Path '/' -ScriptBlock { write-host 'hi' } } | Should -Throw -ExpectedMessage $expectedMessage #'*already defined*' }