From c1b54a437bad8c008055d9b99e5f26001bd6d5da Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 7 Sep 2024 16:18:08 -0700 Subject: [PATCH 1/3] Recovered from #1349 --- .gitignore | 3 + README.md | 4 +- docs/Tutorials/Configuration.md | 2 + .../Routes/Async/Features/AsyncIdGenerator.md | 19 + .../Routes/Async/Features/Callback.md | 89 + .../Routes/Async/Features/OpenAPI.md | 94 + .../Routes/Async/Features/Security.md | 103 + docs/Tutorials/Routes/Async/Overview.md | 295 +++ .../Routes/Async/Utilities.md/HouseKeeping.md | 45 + .../Routes/Async/Utilities.md/Management.md | 81 + .../Routes/Async/Utilities.md/Progress.md | 103 + .../Routes/Utilities/ContentTypes.md | 4 +- docs/index.md | 4 +- examples/Web-AsyncRoute.ps1 | 540 +++++ examples/Web-AsyncRouteBenchmark.ps1 | 358 +++ examples/server.psd1 | 14 + src/Locales/ar/Pode.psd1 | 14 +- src/Locales/de/Pode.psd1 | 14 +- src/Locales/en-us/Pode.psd1 | 10 + src/Locales/en/Pode.psd1 | 13 +- src/Locales/es/Pode.psd1 | 10 + src/Locales/fr/Pode.psd1 | 13 +- src/Locales/it/Pode.psd1 | 10 + src/Locales/ja/Pode.psd1 | 13 +- src/Locales/ko/Pode.psd1 | 12 +- src/Locales/nl/Pode.psd1 | 13 +- src/Locales/pl/Pode.psd1 | 13 +- src/Locales/pt/Pode.psd1 | 10 + src/Locales/zh/Pode.psd1 | 10 + src/Pode.psd1 | 18 + src/Private/AsyncRoute.ps1 | 1530 ++++++++++++ src/Private/Context.ps1 | 85 +- src/Private/Helpers.ps1 | 277 ++- src/Private/OpenApi.ps1 | 53 +- src/Private/Tasks.ps1 | 6 +- src/Public/AsyncRoute.ps1 | 2058 +++++++++++++++++ src/Public/OpenApi.ps1 | 79 +- src/Public/Routes.ps1 | 38 +- src/Public/Utilities.ps1 | 12 +- tests/integration/AsyncRoute.Tests.ps1 | 315 +++ tests/unit/AsyncRoute.Tests.ps1 | 898 +++++++ tests/unit/OpenApi.Tests.ps1 | 6 +- tests/unit/Routes.Tests.ps1 | 144 +- 43 files changed, 7317 insertions(+), 115 deletions(-) create mode 100644 docs/Tutorials/Routes/Async/Features/AsyncIdGenerator.md create mode 100644 docs/Tutorials/Routes/Async/Features/Callback.md create mode 100644 docs/Tutorials/Routes/Async/Features/OpenAPI.md create mode 100644 docs/Tutorials/Routes/Async/Features/Security.md create mode 100644 docs/Tutorials/Routes/Async/Overview.md create mode 100644 docs/Tutorials/Routes/Async/Utilities.md/HouseKeeping.md create mode 100644 docs/Tutorials/Routes/Async/Utilities.md/Management.md create mode 100644 docs/Tutorials/Routes/Async/Utilities.md/Progress.md create mode 100644 examples/Web-AsyncRoute.ps1 create mode 100644 examples/Web-AsyncRouteBenchmark.ps1 create mode 100644 src/Private/AsyncRoute.ps1 create mode 100644 src/Public/AsyncRoute.ps1 create mode 100644 tests/integration/AsyncRoute.Tests.ps1 create mode 100644 tests/unit/AsyncRoute.Tests.ps1 diff --git a/.gitignore b/.gitignore index 079cb2f3a..dc2a26104 100644 --- a/.gitignore +++ b/.gitignore @@ -266,3 +266,6 @@ examples/PetStore/data/PetData.json packers/choco/pode.nuspec packers/choco/tools/ChocolateyInstall.ps1 docs/Getting-Started/Samples.md + +#Todo files +*.todo \ No newline at end of file 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 6b18e04fa..e0919bfc7 100644 --- a/docs/Tutorials/Configuration.md +++ b/docs/Tutorials/Configuration.md @@ -79,6 +79,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) | | 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) | | | Web.Static.ValidateLast | Changes the way routes are processed. | [link](../Routes/Utilities/StaticContent) | 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..aed17609d --- /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' } ` + -CallbackSendResult ` + -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. + - `-CallbackSendResult` 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.md/HouseKeeping.md b/docs/Tutorials/Routes/Async/Utilities.md/HouseKeeping.md new file mode 100644 index 000000000..380c56dec --- /dev/null +++ b/docs/Tutorials/Routes/Async/Utilities.md/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.md/Management.md b/docs/Tutorials/Routes/Async/Utilities.md/Management.md new file mode 100644 index 000000000..de1d147d0 --- /dev/null +++ b/docs/Tutorials/Routes/Async/Utilities.md/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.md/Progress.md b/docs/Tutorials/Routes/Async/Utilities.md/Progress.md new file mode 100644 index 000000000..e34fd1412 --- /dev/null +++ b/docs/Tutorials/Routes/Async/Utilities.md/Progress.md @@ -0,0 +1,103 @@ + +## 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. + +#### Example Usage + +##### 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' +``` + +#### Parameters + +This function is intended to be used inside an asynchronous route scriptblock to get the current progress of the task. \ No newline at end of file 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/Web-AsyncRoute.ps1 b/examples/Web-AsyncRoute.ps1 new file mode 100644 index 000000000..b16cf6cf6 --- /dev/null +++ b/examples/Web-AsyncRoute.ps1 @@ -0,0 +1,540 @@ +<# +.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 -CallbackSendResult | 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' + + + Add-PodeRoute -PassThru -Method Get -Path '/events' -ScriptBlock { + # ConvertTo-PodeSseConnection -Name 'Events' -Scope Local -Group 'Test events' + $msg = "Start - Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" + write-podehost $msg + Send-PodeSseEvent -Data $msg -FromEvent #-name 'Events' -Group 'Test events' #-FromEvent + write-podehost 'PodeSseEvent sent' + Start-Sleep -Seconds 10 + $msg = "End -Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" + write-podehost $msg + Send-PodeSseEvent -Data $msg -FromEvent #-name 'Events' -Group 'Test events' #-FromEvent + write-podehost 'PodeSseEvent sent' + return @{'message' = 'Done' } + } | Set-PodeAsyncRoute -ResponseContentType 'application/json' -MaxRunspaces 2 -PassThru | + Add-PodeAsyncRouteSse -SseGroup 'Test events' + + Add-PodeRoute -method Get -Path '/html/events' -ScriptBlock { + Write-PodeHtmlResponse -StatusCode 200 -Value @' + + + + + + EventSource Example + + +

EventSource Demo

+

Listening for events...

+
+ + + + + +'@ + } +} \ 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/server.psd1 b/examples/server.psd1 index d1858842e..2d3ce3883 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,5 +65,18 @@ Enable = $true } } + AsyncRoutes = @{ + UserFieldIdentifier = 'Id' + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + Tasks = @{ + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 1 + } + } } } \ No newline at end of file diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index b6cd8b697..3c9f1beed 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,8 +283,18 @@ 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 = 'لا يمكن أن يكون حد التقدم أقل من التقدم الحالي.' definitionTagChangeNotAllowedExceptionMessage = 'لا يمكن تغيير علامة التعريف لمسار.' + openApiDefinitionsMismatchExceptionMessage = '{0} يختلف بين تعريفات OpenAPI المختلفة.' + routeNotMarkedAsAsyncExceptionMessage = "المسار '{0}' لم يتم وضع علامة عليه كمسار غير متزامن." + functionCannotBeInvokedMultipleTimesExceptionMessage = "لا يمكن استدعاء الدالة '{0}' عدة مرات لنفس المسار '{1}'." getRequestBodyNotAllowedExceptionMessage = 'لا يمكن أن تحتوي عمليات {0} على محتوى الطلب.' -} +} \ No newline at end of file diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index fb7b0c6ad..ec12ee2e0 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,6 +285,16 @@ 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.' 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 können keinen Anforderungstext haben.' -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index 53aba0d1c..286cf031b 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -285,6 +285,16 @@ 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.' 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.' } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index 8f97eead4..12024d2ff 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -286,6 +286,15 @@ openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI definition named {0} already exists.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag cannot be used inside a Select-PodeOADefinition 'ScriptBlock'." 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.' -} - +} \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index 9ca60ee62..67b3912da 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -285,6 +285,16 @@ 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.' 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.' } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index 8b9d047d3..0d7bd54d6 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -285,7 +285,16 @@ 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.' 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.' -} - +} \ No newline at end of file diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index ff9ccfc73..5bc997f2c 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -285,6 +285,16 @@ 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." 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.' } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index ffc193a46..1569cde09 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -286,6 +286,15 @@ openApiDefinitionAlreadyExistsExceptionMessage = '名前が {0} の OpenAPI 定義は既に存在します。' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag は Select-PodeOADefinition 'ScriptBlock' 内で使用できません。" 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}操作にはリクエストボディを含めることはできません。' -} - +} \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index 25e3c3d10..d49918ad9 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -286,5 +286,15 @@ openApiDefinitionAlreadyExistsExceptionMessage = '이름이 {0}인 OpenAPI 정의가 이미 존재합니다.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag은 Select-PodeOADefinition 'ScriptBlock' 내에서 사용할 수 없습니다." 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} 작업에는 요청 본문이 있을 수 없습니다.' -} +} \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index 3f20960a0..05108756c 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -285,7 +285,16 @@ 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.' 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 Request Body hebben.' -} - +} \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index 6c4f2da03..4fa7177e4 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -286,6 +286,15 @@ openApiDefinitionAlreadyExistsExceptionMessage = 'Definicja OpenAPI o nazwie {0} już istnieje.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag nie może być używany wewnątrz 'ScriptBlock' Select-PodeOADefinition." 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ą mieć treści żądania.' -} - +} \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index df0073012..9ed01763d 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -286,5 +286,15 @@ openApiDefinitionAlreadyExistsExceptionMessage = 'A definição OpenAPI com o nome {0} já existe.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag não pode ser usado dentro de um 'ScriptBlock' Select-PodeOADefinition." 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.' } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index daa9ad110..5e07d6afe 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -286,5 +286,15 @@ openApiDefinitionAlreadyExistsExceptionMessage = '名为 {0} 的 OpenAPI 定义已存在。' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag 不能在 Select-PodeOADefinition 'ScriptBlock' 内使用。" 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} 操作不能包含请求体。' } \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index eae7d8c44..6a455c1bb 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -166,6 +166,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..82c761a34 --- /dev/null +++ b/src/Private/AsyncRoute.ps1 @@ -0,0 +1,1530 @@ + +<# +.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.Results.ContainsKey($___async___id___)) { + Start-Sleep 2 + } + if (!$PodeContext.AsyncRoutes.Results.ContainsKey($___async___id___)) { + try { + throw ($PodeLocale.asyncIdDoesNotExistExceptionMessage -f $___async___id___) + } + catch { + # Log the error + $_ | Write-PodeErrorLog + } + } + + $asyncResult = $PodeContext.AsyncRoutes.Results[$___async___id___] + ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace).Name = "$($asyncResult.AsyncRouteId)_$___async___id___" + try { + $asyncResult['StartingTime'] = [datetime]::UtcNow + + # Set the state to 'Running' + $asyncResult['State'] = 'Running' + + $___result___ = & { # Original ScriptBlock Start + <# ScriptBlock #> + # Original ScriptBlock End + } + if ($___result___) { + $asyncResult['Result'] = $___result___ + } + # Set the completed time + $asyncResult['CompletedTime'] = [datetime]::UtcNow + } + catch { + if (! $asyncResult.ContainsKey('CompletedTime')) { + $asyncResult['CompletedTime'] = [datetime]::UtcNow + } + # Set the state to 'Failed' in case of error + $asyncResult['State'] = 'Failed' + + # Log the error + $_ | Write-PodeErrorLog + + # Store the error in the AsyncRoutes results + $asyncResult['Error'] = $_.ToString() + + } + finally { + Complete-PodeAsyncRouteOperation -AsyncResult $asyncResult + } + } + + # 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 AsyncResult + A [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] that contains the results and state information of the asynchronous script. + +.EXAMPLE + $asyncResult = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() + $webEvent = @{ + Request = @{ + Url = 'http://example.com/request' + } + Method = 'GET' + } + Complete-PodeAsyncRouteOperation -AsyncResult $asyncResult -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]] + $AsyncResult + ) + # Set the completed time if not already set + if (! $AsyncResult.ContainsKey('CompletedTime') -or ($null -eq $AsyncResult['CompletedTime'])) { + $AsyncResult['CompletedTime'] = [datetime]::UtcNow + } + + # Ensure state is set to 'Completed' if it was still 'Running' + if ($AsyncResult['State'] -eq 'Running') { + $AsyncResult['State'] = 'Completed' + } + + + if ($AsyncResult['Timer']) { + # Closes and disposes of the timer + Close-PodeAsyncRouteTimer -Operation $AsyncResult + } + + # Ensure Progress is set to 100 if in use + if ($AsyncResult.ContainsKey('Progress')) { + $AsyncResult['Progress'] = 100 + } + + try { + if ($AsyncResult['CallbackSettings']) { + + # Resolve the callback URL, method, content type, and headers + $callbackUrl = (Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $AsyncResult['CallbackSettings'].UrlField).Value + $method = (Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $AsyncResult['CallbackSettings'].Method -DefaultValue 'Post').Value + $contentType = (Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $AsyncResult['CallbackSettings'].ContentType).Value + $headers = @{} + foreach ($key in $AsyncResult['CallbackSettings'].HeaderFields.Keys) { + $value = Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $key -DefaultValue $AsyncResult['HeaderFields'][$key] + if ($value) { + $headers[$value.Key] = $value.Value + } + } + + # Prepare the body for the callback + $body = @{ + Url = $AsyncResult['Url'] + Method = $AsyncResult['Method'] + EventName = $AsyncResult['CallbackSettings'].EventName + State = $AsyncResult['State'] + } + switch ($AsyncResult['State']) { + 'Failed' { + $body.Error = $AsyncResult['Error'] + } + 'Completed' { + if ($AsyncResult['CallbackSettings'].SendResult -and $AsyncResult['Result']) { + $body.Result = $AsyncResult['Result'] + } + } + 'Aborted' { + $body.Error = $AsyncResult['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 + $AsyncResult['CallbackUrl'] = $callbackUrl + $AsyncResult['CallbackInfoState'] = 'Running' + $AsyncResult['CallbackTentative'] = 0 + + # Attempt to invoke the callback up to 3 times + for ($i = 0; $i -le 3; $i++) { + try { + $AsyncResult['CallbackTentative'] = $AsyncResult['CallbackTentative'] + 1 + $null = Invoke-RestMethod -Uri $callbackUrl -Method $method -Headers $headers -Body $cBody -ContentType $contentType -ErrorAction Stop + $AsyncResult['CallbackInfoState'] = 'Completed' + break + } + catch { + $_ | Write-PodeErrorLog + $AsyncResult['CallbackInfoState'] = 'Failed' + Start-Sleep -Seconds 2 + } + } + } + } + catch { + # Log any errors encountered during the callback process + $_ | Write-PodeErrorLog + $AsyncResult['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 { + ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace).Name = '__pode_asyncroutes_housekeeper__' + # Return if there are no async route results + if ($PodeContext.AsyncRoutes.Results.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.Results.Keys.Clone()) { + $result = $PodeContext.AsyncRoutes.Results[$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.Results.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 -AsyncResult $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 results to search + if ($PodeContext.AsyncRoutes.Results.count -gt 0) { + # Clone the keys of the results to iterate over them + foreach ($rkey in $PodeContext.AsyncRoutes.Results.keys.Clone()) { + $result = $PodeContext.AsyncRoutes.Results[$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 = $PodeContext.AsyncRoutes.Items[$WebEvent.Route.AsyncRouteId] + + # Invoke the internal async route task + # $asyncOperation = Invoke-PodeAsyncRoute + # Generate an Id for the async route task, using the provided IdGenerator or a new GUID + $id = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.AsyncRouteTaskIdGenerator -Return + + # Make a deepcopy of webEvent + $webEvent_ToClone = @{Route = @{} } + foreach ($key in $webEvent.Keys) { + if (!('OnEnd', 'Middleware', 'Route', 'Response' -contains $key)) { + $webEvent_ToClone[$key] = $webEvent[$key] + } + } + foreach ($key in $webEvent.Route.Keys) { + if (!( 'AsyncRouteTaskIdGenerator', 'Middleware', 'Logic' -contains $key)) { + $webEvent_ToClone.Route[$key] = $webEvent.Route[$key] + } + } + + $webEvent_ToClone.Response = $webEvent.Response + # Write-PodeHost $webEvent_ToClone.Response -explode -ShowType -Label 'webEvent_ToClone.Response' + # Setup event parameters + $parameters = @{ + Event = @{ + Lockable = $PodeContext.Threading.Lockables.Global + Sender = $asyncRouteTask + Metadata = @{} + } + WebEvent = Copy-PodeDeepClone -InputObject $webEvent_ToClone + ___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 + + # 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 + + 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 + + Write-PodeHost $asyncOperation['Sse'] -explode + } + + # 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'] = ($asyncRouteTask.Permission | Copy-PodeDeepClone) + } + + # 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.Results[$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.Results.ContainsKey($id)) { + $async = $PodeContext.AsyncRoutes.Results[$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.Results.ContainsKey($id)) { + $async = $PodeContext.AsyncRoutes.Results[$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 -AsyncResult $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 3e3395551..bb1869a7f 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -72,6 +72,7 @@ function New-PodeContext { 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 | @@ -116,9 +117,24 @@ function New-PodeContext { } $ctx.Tasks = @{ - Enabled = ($EnablePool -icontains 'tasks') - Items = @{} - Results = @{} + Enabled = ($EnablePool -icontains 'tasks') + Items = @{} + Results = @{} + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 1 + } + } + + $ctx.AsyncRoutes = @{ + Enabled = $true + Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + Results = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + UserFieldIdentifier = 'Id' } $ctx.Fim = @{ @@ -137,11 +153,12 @@ function New-PodeContext { # set thread counts $ctx.Threads = @{ - General = $Threads - Schedules = 10 - Files = 1 - Tasks = 2 - WebSockets = 2 + General = $Threads + Schedules = 10 + Files = 1 + Tasks = 2 + WebSockets = 2 + AsyncRoutes = 0 } # set socket details for pode server @@ -422,17 +439,16 @@ 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 - } + $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 # threading locks, etc. $ctx.Threading.Lockables = @{ @@ -545,7 +561,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' } @@ -560,7 +576,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' } } @@ -568,7 +584,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' } } @@ -576,7 +592,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' } } @@ -584,7 +600,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' } @@ -594,7 +610,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' } } @@ -602,7 +618,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' } } @@ -610,7 +626,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' } } @@ -618,7 +634,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' } @@ -783,6 +799,7 @@ function New-PodeStateContext { 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 AsyncRoutes -Value $Context.AsyncRoutes -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 | @@ -894,6 +911,18 @@ function Set-PodeServerConfiguration { Enabled = [bool]$Configuration.Debug.Breakpoints.Enable } } + + $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 + } } function Set-PodeWebConfiguration { diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index d9d09ded0..93c6420c5 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -554,7 +554,6 @@ function Get-PodeSubnetRange { function Add-PodeRunspace { param( [Parameter(Mandatory = $true)] - [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files')] [string] $Type, @@ -1839,7 +1838,7 @@ function ConvertFrom-PodeRequestContent { $Result.Data = ($Content | ConvertFrom-Json -AsHashtable) } else { - $Result.Data = ($Content | ConvertFrom-Json) + $Result.Data = ConvertTo-PodeHashtable -PSObject ($Content | ConvertFrom-Json) } } @@ -4035,4 +4034,278 @@ function Resolve-PodeObjectArray { # For any other type, convert it to a PowerShell object return New-Object psobject -Property $Property } +} + + + +<# +.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 + 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 + Creates a deep clone of a PSObject by serializing and deserializing the object. + +.DESCRIPTION + The Copy-PodeDeepClone function takes a PSObject as input and creates a deep clone of it. + This is achieved by serializing the object using the PSSerializer class, and then + deserializing it back into a new instance. This method ensures that nested objects, arrays, + and other complex structures are copied fully, without sharing references between the original + and the cloned object. + +.PARAMETER InputObject + The PSObject that you want to deep clone. This object will be serialized and then deserialized + to create a deep copy. + +.PARAMETER Deep + Specifies the depth for the serialization. The depth controls how deeply nested objects + and properties are serialized. The default value is 10. + +.INPUTS + [PSObject] - The function accepts a PSObject to deep clone. + +.OUTPUTS + [PSObject] - The function returns a new PSObject that is a deep clone of the original. + +.EXAMPLE + $originalObject = [PSCustomObject]@{ + Name = 'John Doe' + Age = 30 + Address = [PSCustomObject]@{ + Street = '123 Main St' + City = 'Anytown' + Zip = '12345' + } + } + + $clonedObject = $originalObject | Copy-PodeDeepClone -Deep 15 + + # The $clonedObject is now a deep clone of $originalObject. + # Changes to $clonedObject will not affect $originalObject and vice versa. + +.NOTES + This function uses the System.Management.Automation.PSSerializer class, which is available in + PowerShell 5.1 and later versions. The default depth parameter is set to 10 to handle nested + objects appropriately, but it can be customized via the -Deep parameter. + This is an internal function and may change in future releases of Pode. +#> +function Copy-PodeDeepClone { + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [PSObject]$InputObject, + + [Parameter()] + [int]$Deep = 10 + ) + + process { + # Serialize the object to XML format using PSSerializer + # The depth parameter controls how deeply nested objects are serialized + $xmlSerializer = [System.Management.Automation.PSSerializer]::Serialize($InputObject, $Deep) + + # Deserialize the XML back into a new PSObject, creating a deep clone of the original + return [System.Management.Automation.PSSerializer]::Deserialize($xmlSerializer) + } } \ No newline at end of file diff --git a/src/Private/OpenApi.ps1 b/src/Private/OpenApi.ps1 index 81be1e4e0..e17cfc6fb 100644 --- a/src/Private/OpenApi.ps1 +++ b/src/Private/OpenApi.ps1 @@ -763,8 +763,8 @@ function Set-PodeOpenApiRouteValue { if ($Route.OpenApi.OperationId) { $pm.operationId = $Route.OpenApi.OperationId } - if ($Route.OpenApi.Parameters) { - $pm.parameters = $Route.OpenApi.Parameters + if ($Route.OpenApi.Parameters.$DefinitionTag) { + $pm.parameters = $Route.OpenApi.Parameters.$DefinitionTag } if ($Route.OpenApi.RequestBody.$DefinitionTag) { $pm.requestBody = $Route.OpenApi.RequestBody.$DefinitionTag @@ -1284,6 +1284,8 @@ function Get-PodeOABaseObject { 'default' = @{ description = 'Internal server error' } } operationId = @() + #Async Route OpenAPI names + AsyncRoute = Get-PodeAsyncRouteOASchemaNameInternal } } } @@ -2193,3 +2195,50 @@ function Test-PodeOAComponentInternal { } } } + +function Test-PodeRouteOADefinitionTag { + param( + [Parameter(Mandatory = $true )] + [ValidateNotNullOrEmpty()] + [hashtable ] + $Route, + + [string[]] + $DefinitionTag + ) + # Check if the OpenAPI Definition Tag is already configured + if ($Route.OpenApi.IsDefTagConfigured) { + # If a DefinitionTag is provided + if ($DefinitionTag) { + # Loop through each element in $DefinitionTag + if ($DefinitionTag | ForEach-Object { + + # Check if the current element exists in the already configured DefinitionTag + if (!($Route.OpenApi.DefinitionTag -contains $_)) { + # If any element in $DefinitionTag is not present in the configured DefinitionTag, throw an exception + throw ($PodeLocale.definitionTagChangeNotAllowedExceptionMessage) + } + # Return $true for each element to continue the check + $true + } + ) { + # If all elements in $DefinitionTag are present in the configured DefinitionTag, assign it to $oaDefinitionTag + return $DefinitionTag + } + } + + + return $Route.OpenApi.DefinitionTag + } + # If the OpenAPI Definition Tag is not configured yet + + # Validate the provided DefinitionTag and assign it to $oaDefinitionTag + $oaDefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + # Set the validated DefinitionTag as the OpenAPI DefinitionTag + $Route.OpenApi.DefinitionTag = $oaDefinitionTag + # Mark the OpenAPI DefinitionTag as configured + $Route.OpenApi.IsDefTagConfigured = $true + + + return $oaDefinitionTag +} \ No newline at end of file diff --git a/src/Private/Tasks.ps1 b/src/Private/Tasks.ps1 index 1a4c1f7d2..c246a0693 100644 --- a/src/Private/Tasks.ps1 +++ b/src/Private/Tasks.ps1 @@ -7,11 +7,11 @@ function Start-PodeTaskHousekeeper { return } - Add-PodeTimer -Name '__pode_task_housekeeper__' -Interval 30 -ScriptBlock { + Add-PodeTimer -Name '__pode_task_housekeeper__' -Interval $PodeContext.Tasks.HouseKeeping.TimerInterval -ScriptBlock { if ($PodeContext.Tasks.Results.Count -eq 0) { return } - + $RetentionMinutes = $PodeContext.Tasks.HouseKeeping.RetentionMinutes $now = [datetime]::UtcNow foreach ($key in $PodeContext.Tasks.Results.Keys.Clone()) { @@ -35,7 +35,7 @@ function Start-PodeTaskHousekeeper { } # is it expired by completion? if so, dispose and remove - if ($result.CompletedTime.AddMinutes(1) -lt $now) { + if ($result.CompletedTime.AddMinutes($RetentionMinutes) -lt $now) { Close-PodeTaskInternal -Result $result } } diff --git a/src/Public/AsyncRoute.ps1 b/src/Public/AsyncRoute.ps1 new file mode 100644 index 000000000..c1d2f2887 --- /dev/null +++ b/src/Public/AsyncRoute.ps1 @@ -0,0 +1,2058 @@ + +<# +.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 (! $PodeContext.AsyncRoutes.Items.ContainsKey($r.AsyncRouteId) -or ! $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 (! $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission.ContainsKey($Type)) { + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type] = @{} + } + + # Assign or remove users from the specified permission type + if ($Users) { + if (!$PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].ContainsKey('Users')) { + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Users = @() + } + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Users = Update-PermissionList -List $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Users -Items $Users -Remove:$Remove + } + + # Assign or remove groups from the specified permission type + if ($Groups) { + if (!$PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].ContainsKey('Groups')) { + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Groups = @() + } + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Groups = Update-PermissionList -List $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Groups -Items $Groups -Remove:$Remove + } + + # Assign or remove roles from the specified permission type + if ($Roles) { + if (!$PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].ContainsKey('Roles')) { + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Roles = @() + } + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Roles = Update-PermissionList -List $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Roles -Items $Roles -Remove:$Remove + } + + # Assign or remove scopes from the specified permission type + if ($Scopes) { + if (!$PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].ContainsKey('Scopes')) { + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Scopes = @() + } + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Scopes = Update-PermissionList -List $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].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 CallbackSendResult + 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] + $CallbackSendResult, + + [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 = $CallbackSendResult.ToBool() + 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 (! $PodeContext.AsyncRoutes.Items.ContainsKey($r.AsyncRouteId) -or ! $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 + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].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 ( $PodeContext.AsyncRoutes.Items.ContainsKey($r.AsyncRouteId) -or $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 + + # Assign the Id generator + if ($IdGenerator) { + $r.AsyncRouteTaskIdGenerator = $IdGenerator + } + else { + $r.AsyncRouteTaskIdGenerator = { return (New-PodeGuid) } + } + + # Store the route's async route task definition in Pode context + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId] = @{ + AsyncRouteId = $r.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 = @{} + } + + #Set thread count + $PodeContext.Threads.AsyncRoutes += $MaxRunspaces + if (! $PodeContext.RunspacePools.ContainsKey($r.AsyncRouteId)) { + $PodeContext.RunspacePools[$r.AsyncRouteId] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + + $PodeContext.RunspacePools[$r.AsyncRouteId]['Pool'] = New-PodeRunspacePoolNetWrapper -MinRunspaces $MinRunspaces -MaxRunspaces $MaxRunspaces -RunspaceState $PodeContext.RunspaceState + $PodeContext.RunspacePools[$r.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. + +.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 + ) + + Begin { + # Initialize an array to hold piped-in values + $pipelineValue = @() + + $sseScriptBlock = { + param($SseGroup) + + if ([string]::IsNullOrEmpty($SseGroup)) { + write-podehost "webEvent.Route.Path=$($webEvent.Route.Path)" + ConvertTo-PodeSseConnection -Name $webEvent.Route.Path -Scope Local -Group $SseGroup + } + else { + ConvertTo-PodeSseConnection -Name $webEvent.Route.Path -Scope Local + } + + $id = $WebEvent.Query['Id'] + if (!$PodeContext.AsyncRoutes.Results.ContainsKey($id)) { + try { + throw ($PodeLocale.asyncIdDoesNotExistExceptionMessage -f $id) + } + catch { + # Log the error + $_ | Write-PodeErrorLog + return + } + } + $AsyncResult = $PodeContext.AsyncRoutes.Results[$Id] + + $AsyncResult['Sse']['State'] = 'Waiting' + + while (!$AsyncResult['Runspace'].Handler.IsCompleted) { + start-sleep 1 + } + + try { + switch ($AsyncResult['State']) { + 'Failed' { + $null = Send-PodeSseEvent -FromEvent -Data @{ State = $AsyncResult['State']; Error = $AsyncResult['Error'] } + } + 'Completed' { + if ($AsyncResult['Result']) { + $null = Send-PodeSseEvent -FromEvent -Data @{ State = $AsyncResult['State']; Result = $AsyncResult['Result'] } + } + else { + $null = Send-PodeSseEvent -FromEvent -Data @{ State = 'Completed' } + } + } + 'Aborted' { + $null = Send-PodeSseEvent -FromEvent -Data @{ State = $AsyncResult['State']; Error = $AsyncResult['Error'] } + } + } + $AsyncResult['Sse']['State'] = 'Completed' + } + catch { + # Log any errors encountered during SSE handling + $_ | Write-PodeErrorLog + $AsyncResult['Sse']['State'] = 'Failed' + } + + } + } + + 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 marked as an Async Route + if (! $PodeContext.AsyncRoutes.Items.ContainsKey($r.AsyncRouteId) -or ! $r.IsAsync) { + # The route '{0}' is not marked as an Async Route. + throw ($PodeLocale.routeNotMarkedAsAsyncExceptionMessage -f $r.Path) + } + + $sseRoute = Add-PodeRoute -PassThru -method Get -Path "$($r.Path)_events" -ArgumentList $SseGroup ` + -ScriptBlock $sseScriptBlock + + $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId]['Sse'] = @{ + Group = $SseGroup + Name = "$($r.Path)_events" + Route = $sseRoute + } + } + # Return the route information 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.Results[$Id] + } + elseif (! [string]::IsNullOrEmpty($AsyncRouteId)) { + foreach ($key in $PodeContext.AsyncRoutes.Results.Keys) { + if ($PodeContext.AsyncRoutes.Results[$key]['AsyncRouteId'] -ieq $AsyncRouteId) { + $result = $PodeContext.AsyncRoutes.Results[$key] + break + } + } + } + else { + $result = $PodeContext.AsyncRoutes.Results + } + + 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.Results.ContainsKey($Id )) { + $async = $PodeContext.AsyncRoutes.Results[$Id] + $async['State'] = 'Aborted' + $async['Error'] = 'Aborted by System' + $async['CompletedTime'] = [datetime]::UtcNow + $async['Runspace'].Pipeline.Dispose() + Complete-PodeAsyncRouteOperation -AsyncResult $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.Results.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 this function is used within an async route + if (!$___async___id___) { + # Set-PodeAsyncRouteProgress can only be used inside an Async Route Scriptblock. + throw $PodeLocale.setPodeAsyncProgressExceptionMessage + } + $asyncResult = $PodeContext.AsyncRoutes.Results[$___async___id___] + + # Initialize progress if not already set, for non-tick operations + if ($PSCmdlet.ParameterSetName -ne 'Tick' -and $PSCmdlet.ParameterSetName -ne 'SetValue') { + if (!$asyncResult.ContainsKey('Progress')) { + if ( $UseDecimalProgress.IsPresent) { + $asyncResult['Progress'] = [double] 0 + } + else { + $asyncResult['Progress'] = [int] 0 + } + } + + if ($MaxProgress -le $asyncResult['Progress']) { + # A Progress limit cannot be lower than the current progress. + throw $PodeLocale.progressLimitLowerThanCurrentExceptionMessage + } + } + + switch ($PSCmdlet.ParameterSetName) { + 'StartEnd' { + # Calculate total ticks and tick to progress ratio + $totalTicks = [math]::ceiling(($End - $Start) / $Steps) + if ($asyncResult['Progress'] -is [double]) { + $asyncResult['TickToProgress'] = ($MaxProgress - $asyncResult['Progress']) / $totalTicks + } + else { + $asyncResult['TickToProgress'] = [Math]::Floor(($MaxProgress - $asyncResult['Progress']) / $totalTicks) + } + } + 'Tick' { + # Increment progress by TickToProgress value + $asyncResult['Progress'] = $asyncResult['Progress'] + $asyncResult['TickToProgress'] + + # Ensure Progress does not exceed the specified limit + if ($asyncResult['Progress'] -ge $MaxProgress) { + if ($asyncResult['Progress'] -is [double]) { + $asyncResult['Progress'] = $MaxProgress - 0.01 + } + else { + $asyncResult['Progress'] = $MaxProgress - 1 + } + } + } + 'TimeBased' { + # Calculate tick interval and progress increment per tick + $totalTicks = [math]::ceiling($DurationSeconds / $IntervalSeconds) + if ($asyncResult['Progress'] -is [double]) { + $asyncResult['TickToProgress'] = ($MaxProgress - $asyncResult['Progress']) / $totalTicks + } + else { + $asyncResult['TickToProgress'] = [Math]::Floor(($MaxProgress - $asyncResult['Progress']) / $totalTicks) + } + + # Start the scheduler + $asyncResult['eventName'] = "TimerEvent_$___async___id___" + $asyncResult['Timer'] = [System.Timers.Timer]::new() + $asyncResult['Timer'].Interval = $IntervalSeconds * 1000 + $null = Register-ObjectEvent -InputObject $asyncResult['Timer'] -EventName Elapsed -SourceIdentifier $asyncResult['eventName'] -MessageData @{AsyncResult = $asyncResult; MaxProgress = $MaxProgress } -Action { + $asyncResult = $Event.MessageData.AsyncResult + $MaxProgress = $Event.MessageData.MaxProgress + + # Increment progress by TickToProgress value + $asyncResult['Progress'] = $asyncResult['Progress'] + $asyncResult['TickToProgress'] + + # Check if progress has reached or exceeded MaxProgress + if ($asyncResult['Progress'] -gt $MaxProgress) { + # Closes and disposes of the timer + Close-PodeAsyncRouteTimer -Operation $asyncResult + + if ($asyncResult['Progress'] -is [double]) { + $asyncResult['Progress'] = $MaxProgress - 0.01 + } + else { + $asyncResult['Progress'] = $MaxProgress - 1 + } + } + } + $asyncResult['Timer'].Enabled = $true + } + 'SetValue' { + if ( $UseDecimalProgress.IsPresent -or ($Value % 1 -ne 0) ) { + $asyncResult['Progress'] = $Value + } + else { + $asyncResult['Progress'] = [int]$Value + } + } + } +} + + +<# +.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.Results[$___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 77dd0a4d4..4291290cd 100644 --- a/src/Public/OpenApi.ps1 +++ b/src/Public/OpenApi.ps1 @@ -563,7 +563,6 @@ An Array of strings representing the unique tag for the API specification. This tag helps distinguish 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-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -Content @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) } @@ -632,7 +631,6 @@ function Add-PodeOAResponse { throw ($PodeLocale.routeParameterCannotBeNullExceptionMessage) } - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag # override status code with default if ($Default) { $code = 'default' @@ -643,7 +641,9 @@ function Add-PodeOAResponse { # add the respones to the routes foreach ($r in @($Route)) { - foreach ($tag in $DefinitionTag) { + $oaDefinitionTag = Test-PodeRouteOADefinitionTag -Route $r -DefinitionTag $DefinitionTag + + foreach ($tag in $oaDefinitionTag) { if (! $r.OpenApi.Responses.$tag) { $r.OpenApi.Responses.$tag = @{} } @@ -745,6 +745,11 @@ The Request Body definition the request uses (from New-PodeOARequestBody). .PARAMETER PassThru If supplied, the route passed in will be returned for further chaining. +.PARAMETER DefinitionTag +An Array of strings representing the unique tag for the API specification. +This tag helps distinguish 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-PodeRoute -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Schema 'UserIdBody') #> @@ -764,7 +769,10 @@ function Set-PodeOARequest { $RequestBody, [switch] - $PassThru + $PassThru, + + [string[]] + $DefinitionTag ) if ($null -eq $Route) { @@ -774,23 +782,27 @@ function Set-PodeOARequest { foreach ($r in @($Route)) { - if (($null -ne $Parameters) -and ($Parameters.Length -gt 0)) { - $r.OpenApi.Parameters = @($Parameters) - } + $oaDefinitionTag = Test-PodeRouteOADefinitionTag -Route $r -DefinitionTag $DefinitionTag - if ($null -ne $RequestBody) { - # Only 'POST', 'PUT', 'PATCH' can have a request body - if (('POST', 'PUT', 'PATCH') -inotcontains $r.Method ) { - # {0} operations cannot have a Request Body. - throw ($PodeLocale.getRequestBodyNotAllowedExceptionMessage -f $r.Method) + foreach ($tag in $oaDefinitionTag) { + if (($null -ne $Parameters) -and ($Parameters.Length -gt 0)) { + $r.OpenApi.Parameters[$tag] = @($Parameters) } - $r.OpenApi.RequestBody = $RequestBody - } - } + if ($null -ne $RequestBody) { + # Only 'POST', 'PUT', 'PATCH' can have a request body + if (('POST', 'PUT', 'PATCH') -inotcontains $r.Method ) { + # {0} operations cannot have a Request Body. + throw ($PodeLocale.getRequestBodyNotAllowedExceptionMessage -f $r.Method) + } + $r.OpenApi.RequestBody = $RequestBody + } - if ($PassThru) { - return $Route + } + + if ($PassThru) { + return $Route + } } } @@ -984,7 +996,6 @@ message: any validation issue $UserInfo = Test-PodeOAJsonSchemaCompliance -Json $UserInfo -SchemaReference 'UserIdSchema'} #> - function Test-PodeOAJsonSchemaCompliance { param ( [Parameter(Mandatory = $true)] @@ -1571,19 +1582,8 @@ function Set-PodeOARouteInfo { throw ($PodeLocale.routeParameterCannotBeNullExceptionMessage) } - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag - foreach ($r in @($Route)) { - if ((Compare-Object -ReferenceObject $r.OpenApi.DefinitionTag -DifferenceObject $DefinitionTag).Count -ne 0) { - if ($r.OpenApi.IsDefTagConfigured ) { - # Definition Tag for a Route cannot be changed. - throw ($PodeLocale.definitionTagChangeNotAllowedExceptionMessage) - } - else { - $r.OpenApi.DefinitionTag = $DefinitionTag - $r.OpenApi.IsDefTagConfigured = $true - } - } + $oaDefinitionTag = Test-PodeRouteOADefinitionTag -Route $r -DefinitionTag $DefinitionTag if ($Summary) { $r.OpenApi.Summary = $Summary @@ -1596,7 +1596,7 @@ function Set-PodeOARouteInfo { # OperationID:$OperationId has to be unique and cannot be applied to an array throw ($PodeLocale.operationIdMustBeUniqueForArrayExceptionMessage -f $OperationId) } - foreach ($tag in $DefinitionTag) { + foreach ($tag in $oaDefinitionTag) { if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $OperationId) { # OperationID:$OperationId has to be unique throw ($PodeLocale.operationIdMustBeUniqueExceptionMessage -f $OperationId) @@ -1617,6 +1617,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) { @@ -2612,10 +2621,10 @@ function Add-PodeOACallBack { throw ($PodeLocale.routeParameterCannotBeNullExceptionMessage) } - $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag - foreach ($r in @($Route)) { - foreach ($tag in $DefinitionTag) { + $oaDefinitionTag = Test-PodeRouteOADefinitionTag -Route $r -DefinitionTag $DefinitionTag + + foreach ($tag in $oaDefinitionTag) { if ($Reference) { Test-PodeOAComponentInternal -Field callbacks -DefinitionTag $tag -Name $Reference -PostValidation if (!$Name) { @@ -3543,7 +3552,7 @@ function Test-PodeOADefinitionTag { if ($Tag -and $Tag.Count -gt 0) { foreach ($t in $Tag) { - if (! ($PodeContext.Server.OpenApi.Definitions.Keys -ccontains $t)) { + if (! ($PodeContext.Server.OpenApi.Definitions.Keys -icontains $t)) { # DefinitionTag does not exist. throw ($PodeLocale.definitionTagNotDefinedExceptionMessage -f $t) } diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index ffed8e91f..004625c06 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -430,11 +430,13 @@ function Add-PodeRoute { Arguments = $ArgumentList Method = $_method Path = $Path + IsAsync = $false + AsyncRouteId = "__$($_method)$($Path)_$($_endpoint.Name)_".Replace('/', '_') OpenApi = @{ Path = $OpenApiPath Responses = $DefaultResponse - Parameters = $null - RequestBody = $null + Parameters = @{} + RequestBody = @{} CallBacks = @{} Authentication = @() Servers = @() @@ -1695,14 +1697,34 @@ function Remove-PodeRoute { # select the candidate route for deletion $route = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object { - $_.Endpoint.Name -ine $EndpointName + $_.Endpoint.Name -ieq $EndpointName }) - # remove the operationId from the openapi operationId list - if ($route.OpenAPI) { - foreach ( $tag in $route.OpenAPI.DefinitionTag) { - if ($tag -and ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $route.OpenAPI.OperationId)) { - $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId | Where-Object { $_ -ne $route.OpenAPI.OperationId } + foreach ($r in $route) { + # remove the runspace + if ($r.IsAsync) { + $asyncRouteId = $r.AsyncRouteId + if ( $asyncRouteId -and $PodeContext.RunspacePools.ContainsKey($asyncRouteId)) { + 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) + } + if ( $PodeContext.AsyncRoutes.Items.ContainsKey($asyncRouteId)) { + $PodeContext.Threads.AsyncRoutes -= $PodeContext.AsyncRoutes.Items[$asyncRouteId].MaxRunspaces + $v = '' + $null = $PodeContext.AsyncRoutes.Items.TryRemove( $asyncRouteId, [ref]$v) + } + } + + # remove the operationId from the openapi operationId list + if ($r.OpenAPI) { + foreach ( $tag in $r.OpenAPI.DefinitionTag) { + if ($tag -and ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $route.OpenAPI.OperationId)) { + $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId | Where-Object { $_ -ne $route.OpenAPI.OperationId } + } } } } diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index dde5ff1bb..e4c19149e 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -805,6 +805,9 @@ Show the object content .PARAMETER ShowType Show the Object Type +.PARAMETER Label +Add a label to the object + .EXAMPLE 'Some output' | Write-PodeHost -ForegroundColor Cyan #> @@ -829,7 +832,11 @@ function Write-PodeHost { [Parameter( Mandatory = $false, ParameterSetName = 'object')] [switch] - $ShowType + $ShowType, + + [Parameter( Mandatory = $false, ParameterSetName = 'object')] + [string] + $Label ) if ($PodeContext.Server.Quiet) { @@ -848,6 +855,9 @@ function Write-PodeHost { if ($ShowType) { $Object = "`tTypeName: $type`n$Object" } + if ($Label){ + $Object = "`tName: $Label$Object" + } } } diff --git a/tests/integration/AsyncRoute.Tests.ps1 b/tests/integration/AsyncRoute.Tests.ps1 new file mode 100644 index 000000000..7f36a1b09 --- /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..1ecc343cf --- /dev/null +++ b/tests/unit/AsyncRoute.Tests.ps1 @@ -0,0 +1,898 @@ +[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() + Results = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + } + + # Example route object to test with + $route = @{ + AsyncRouteId = 'testRoute' + IsAsync = $true + } + $PodeContext.AsyncRoutes.Items[$route.AsyncRouteId] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + $PodeContext.AsyncRoutes.Items[$route.AsyncRouteId].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 }, + @{ AsyncRouteId = 'route2' ; IsAsync = $true } + ) + + $PodeContext.AsyncRoutes.Items['route1'] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + $PodeContext.AsyncRoutes.Items['route1'].Permission = @{} + $PodeContext.AsyncRoutes.Items['route2'] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + $PodeContext.AsyncRoutes.Items['route2'].Permission = @{} + $routes | Set-PodeAsyncRoutePermission -Type 'Read' -Users $users + + $PodeContext.AsyncRoutes.Items['route1'].Permission.Read.Users | Should -Be $users + $PodeContext.AsyncRoutes.Items['route2'].Permission.Read.Users | Should -Be $users + } + + It 'should initialize the Permission object if not already present' { + $PodeContext.AsyncRoutes.Items['testRoute'] = @{Permission = @{Read = @{Users = @('user3') } } } + + Set-PodeAsyncRoutePermission -Route $route -Type 'Read' -Users $users + + $PodeContext.AsyncRoutes.Items['testRoute'].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() + Results = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + } + + # Example route object to test with + $route = @{ + AsyncRouteId = 'testRoute' + IsAsync = $true + } + $PodeContext.AsyncRoutes.Items['testRoute'] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + + # Initialize permissions for testing remove functionality + $PodeContext.AsyncRoutes.Items['testRoute'] = @{ + 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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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() + Results = [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.Results[$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.Results[$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.Results[$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() + Results = [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.Results[$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.Results[$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.Results[$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.Results[$operationId1] + } + $operationId3 { + $r | Should -Be $PodeContext.AsyncRoutes.Results[$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 Pode Context + $PodeContext = @{ + AsyncRoutes = @{ + Items = @{ + 'ExamplePool' = @{ + Sse = $null + } + } + Results = @{ + '12345' = @{ + Runspace = [pscustomobject]@{ Handler = [pscustomobject]@{ IsCompleted = $false } } + State = 'Completed' + Result = 'Success' + } + } + } + } + # Mock data setup + $PodeContext = @{ + AsyncRoutes = @{ + Enabled = $true + Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + Results = [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.Results[$operationId1] = $asyncOperationDetails + + $PodeContext.AsyncRoutes.Items['ExamplePool'] = @{ + Sse = $null + } + + } + + It 'Should throw an exception if the route is not marked as async' { + { + $route = @{ + Path = '/not-async' + AsyncRouteId = 'not-async' + IsAsync = $true + } | 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 } + + $result = Add-PodeAsyncRouteSse -Route $route -PassThru + + $result | Should -BeOfType 'hashtable' + $result.Path | Should -Be '/events' + $PodeContext.AsyncRoutes.Items['ExamplePool'].Sse.Name | Should -Be '/events_events' + } + + It 'Should handle multiple routes piped in' { + $routes = @( + @{ Path = '/events1'; AsyncRouteId = 'ExamplePool'; IsAsync = $true }, + @{ Path = '/events2'; AsyncRouteId = 'ExamplePool'; IsAsync = $true } + ) + + $result = $routes | Add-PodeAsyncRouteSse -PassThru + + $result | Should -HaveCount 2 + $PodeContext.AsyncRoutes.Items['ExamplePool'].Sse.Name | Should -Be '/events2_events' + } + + It 'Should return the modified route object when PassThru is specified' { + $route = @{ Path = '/events'; AsyncRouteId = 'ExamplePool'; IsAsync = $true } + + $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() + Results = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + HouseKeeping = @{ + TimerInterval = 30 + RetentionMinutes = 10 + } + } + } + + + } + + + It 'Should correctly mark a route as async and set runspaces' { + $route = @{ Path = '/async'; AsyncRouteId = 'AsyncPool'; 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 + + $PodeContext.AsyncRoutes.Items['AsyncPool'].MinRunspaces | Should -Be 2 + $PodeContext.AsyncRoutes.Items['AsyncPool'].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'; AsyncRouteId = 'AsyncPool'; 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'; AsyncRouteId = 'AsyncPool'; IsAsync = $false; Logic = {} } + + $idGenScript = { return 'CustomId' } + Set-PodeAsyncRoute -Route $route -IdGenerator $idGenScript + + $route.AsyncRouteTaskIdGenerator.Invoke() | Should -Be 'CustomId' + } + + It 'Should use default IdGenerator if none is provided' { + $route = @{ Path = '/async'; AsyncRouteId = 'AsyncPool'; IsAsync = $false; Logic = {} } + + Set-PodeAsyncRoute -Route $route + + $id = $route.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'; AsyncRouteId = 'AsyncPool'; IsAsync = $false; Logic = {} } + + Set-PodeAsyncRoute -Route $route -Timeout 600 + + $PodeContext.AsyncRoutes.Items['AsyncPool'].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 + Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + Results = [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.Results[$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.Results[$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.Results[$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/OpenApi.Tests.ps1 b/tests/unit/OpenApi.Tests.ps1 index aab9ca389..03931b881 100644 --- a/tests/unit/OpenApi.Tests.ps1 +++ b/tests/unit/OpenApi.Tests.ps1 @@ -3112,7 +3112,9 @@ Describe 'OpenApi' { It 'Sets Parameters on the route if provided' { $route = @{ Method = 'GET' - OpenApi = @{} + OpenApi = @{ + Parameters=@{} + } } $parameters = @( @{ Name = 'param1'; In = 'query' } @@ -3120,7 +3122,7 @@ Describe 'OpenApi' { Set-PodeOARequest -Route $route -Parameters $parameters - $route.OpenApi.Parameters | Should -BeExactly $parameters + $route.OpenApi.Parameters['Default'] | Should -BeExactly $parameters } It 'Sets RequestBody on the route if method is POST' { diff --git a/tests/unit/Routes.Tests.ps1 b/tests/unit/Routes.Tests.ps1 index dc5aff5ca..24cc94a44 100644 --- a/tests/unit/Routes.Tests.ps1 +++ b/tests/unit/Routes.Tests.ps1 @@ -109,52 +109,170 @@ 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 + } + + It 'Removes a route and cleans up OpenAPI operationId' { + Add-PodeRoute -PassThru -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } | Set-PodeOARouteInfo -Summary 'Test user' -OperationId 'getUsers' + + $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 | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $false + $PodeContext.Server.OpenAPI.Definitions.default.hiddenComponents.operationId | Should -Not -Contain 'getUsers' + } + + It 'Adds two routes and removes on route and cleans up OpenAPI operationId' { + Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user + + Add-PodeRoute -PassThru -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } | Set-PodeOARouteInfo -Summary 'Test user' -OperationId 'getUsers' + Add-PodeRoute -PassThru -Method Get -Path '/users' -EndpointName user -ScriptBlock { Write-Host 'hello' } | Set-PodeOARouteInfo -Summary 'Test user2' -OperationId 'getUsers2' + + $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' -EndpointName 'user' + + $routes = $PodeContext.Server.Routes['GET'] $routes | Should -Not -Be $null $routes.ContainsKey('/users') | Should -Be $true $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.AsyncRouteId + $PodeContext.RunspacePools[$asyncRouteId].Pool = New-Object PSObject -Property @{ + IsDisposed = $true # to avoid to call BeginClose($null,$null) + } + Remove-PodeRoute -Method Get -Path '/async' + + $PodeContext.RunspacePools.ContainsKey($asyncRouteId) | Should -Be $false + $PodeContext.AsyncRoutes.Items.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.AsyncRouteId].Pool = New-Object PSObject -Property @{ + IsDisposed = $true # to avoid to call BeginClose($null,$null) + } + $PodeContext.Threads.AsyncRoutes | Should -Be ($maxRunspaces + $maxRunspaces) + $PodeContext.RunspacePools.ContainsKey($route2.asyncRouteId) | Should -Be $true + $PodeContext.AsyncRoutes.Items.ContainsKey($route2.asyncRouteId) | Should -Be $true + + $PodeContext.RunspacePools.ContainsKey($route1.asyncRouteId) | Should -Be $true + $PodeContext.AsyncRoutes.Items.ContainsKey($route1.asyncRouteId) | Should -Be $true + + #remove $route1 + Remove-PodeRoute -Method Get -Path '/asyncusers' + + $PodeContext.RunspacePools.ContainsKey($route2.asyncRouteId) | Should -Be $true + $PodeContext.AsyncRoutes.Items.ContainsKey($route2.asyncRouteId) | Should -Be $true + + $PodeContext.RunspacePools.ContainsKey($route1.asyncRouteId) | Should -Be $false + $PodeContext.AsyncRoutes.Items.ContainsKey($route1.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 } @@ -291,8 +409,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*' } @@ -307,7 +425,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*' } From e2b78718b77e56d0432d2deea11c0d6d8a67e773 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 7 Sep 2024 16:18:46 -0700 Subject: [PATCH 2/3] Revert "Recovered from #1349" This reverts commit c1b54a437bad8c008055d9b99e5f26001bd6d5da. --- .gitignore | 3 - README.md | 4 +- docs/Tutorials/Configuration.md | 2 - .../Routes/Async/Features/AsyncIdGenerator.md | 19 - .../Routes/Async/Features/Callback.md | 89 - .../Routes/Async/Features/OpenAPI.md | 94 - .../Routes/Async/Features/Security.md | 103 - docs/Tutorials/Routes/Async/Overview.md | 295 --- .../Routes/Async/Utilities.md/HouseKeeping.md | 45 - .../Routes/Async/Utilities.md/Management.md | 81 - .../Routes/Async/Utilities.md/Progress.md | 103 - .../Routes/Utilities/ContentTypes.md | 4 +- docs/index.md | 4 +- examples/Web-AsyncRoute.ps1 | 540 ----- examples/Web-AsyncRouteBenchmark.ps1 | 358 --- examples/server.psd1 | 14 - src/Locales/ar/Pode.psd1 | 14 +- src/Locales/de/Pode.psd1 | 14 +- src/Locales/en-us/Pode.psd1 | 10 - src/Locales/en/Pode.psd1 | 13 +- src/Locales/es/Pode.psd1 | 10 - src/Locales/fr/Pode.psd1 | 13 +- src/Locales/it/Pode.psd1 | 10 - src/Locales/ja/Pode.psd1 | 13 +- src/Locales/ko/Pode.psd1 | 12 +- src/Locales/nl/Pode.psd1 | 13 +- src/Locales/pl/Pode.psd1 | 13 +- src/Locales/pt/Pode.psd1 | 10 - src/Locales/zh/Pode.psd1 | 10 - src/Pode.psd1 | 18 - src/Private/AsyncRoute.ps1 | 1530 ------------ src/Private/Context.ps1 | 85 +- src/Private/Helpers.ps1 | 277 +-- src/Private/OpenApi.ps1 | 53 +- src/Private/Tasks.ps1 | 6 +- src/Public/AsyncRoute.ps1 | 2058 ----------------- src/Public/OpenApi.ps1 | 79 +- src/Public/Routes.ps1 | 38 +- src/Public/Utilities.ps1 | 12 +- tests/integration/AsyncRoute.Tests.ps1 | 315 --- tests/unit/AsyncRoute.Tests.ps1 | 898 ------- tests/unit/OpenApi.Tests.ps1 | 6 +- tests/unit/Routes.Tests.ps1 | 144 +- 43 files changed, 115 insertions(+), 7317 deletions(-) delete mode 100644 docs/Tutorials/Routes/Async/Features/AsyncIdGenerator.md delete mode 100644 docs/Tutorials/Routes/Async/Features/Callback.md delete mode 100644 docs/Tutorials/Routes/Async/Features/OpenAPI.md delete mode 100644 docs/Tutorials/Routes/Async/Features/Security.md delete mode 100644 docs/Tutorials/Routes/Async/Overview.md delete mode 100644 docs/Tutorials/Routes/Async/Utilities.md/HouseKeeping.md delete mode 100644 docs/Tutorials/Routes/Async/Utilities.md/Management.md delete mode 100644 docs/Tutorials/Routes/Async/Utilities.md/Progress.md delete mode 100644 examples/Web-AsyncRoute.ps1 delete mode 100644 examples/Web-AsyncRouteBenchmark.ps1 delete mode 100644 src/Private/AsyncRoute.ps1 delete mode 100644 src/Public/AsyncRoute.ps1 delete mode 100644 tests/integration/AsyncRoute.Tests.ps1 delete mode 100644 tests/unit/AsyncRoute.Tests.ps1 diff --git a/.gitignore b/.gitignore index dc2a26104..079cb2f3a 100644 --- a/.gitignore +++ b/.gitignore @@ -266,6 +266,3 @@ examples/PetStore/data/PetData.json packers/choco/pode.nuspec packers/choco/tools/ChocolateyInstall.ps1 docs/Getting-Started/Samples.md - -#Todo files -*.todo \ No newline at end of file diff --git a/README.md b/README.md index 40fc515b6..3ffdf9f6f 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,async REST APIs, Web Pages, and Static Content (with caching) +* Host 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, Dutch, and Chinese. +* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, and Chinese ## 📦 Install diff --git a/docs/Tutorials/Configuration.md b/docs/Tutorials/Configuration.md index e0919bfc7..6b18e04fa 100644 --- a/docs/Tutorials/Configuration.md +++ b/docs/Tutorials/Configuration.md @@ -79,8 +79,6 @@ 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) | | 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) | | | Web.Static.ValidateLast | Changes the way routes are processed. | [link](../Routes/Utilities/StaticContent) | diff --git a/docs/Tutorials/Routes/Async/Features/AsyncIdGenerator.md b/docs/Tutorials/Routes/Async/Features/AsyncIdGenerator.md deleted file mode 100644 index 14514b4f1..000000000 --- a/docs/Tutorials/Routes/Async/Features/AsyncIdGenerator.md +++ /dev/null @@ -1,19 +0,0 @@ -# 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 deleted file mode 100644 index aed17609d..000000000 --- a/docs/Tutorials/Routes/Async/Features/Callback.md +++ /dev/null @@ -1,89 +0,0 @@ - -# 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' } ` - -CallbackSendResult ` - -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. - - `-CallbackSendResult` 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 deleted file mode 100644 index 967023b8f..000000000 --- a/docs/Tutorials/Routes/Async/Features/OpenAPI.md +++ /dev/null @@ -1,94 +0,0 @@ - -# 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 deleted file mode 100644 index caf84496f..000000000 --- a/docs/Tutorials/Routes/Async/Features/Security.md +++ /dev/null @@ -1,103 +0,0 @@ - -# 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 deleted file mode 100644 index 5dc0f6ae2..000000000 --- a/docs/Tutorials/Routes/Async/Overview.md +++ /dev/null @@ -1,295 +0,0 @@ - -# 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.md/HouseKeeping.md b/docs/Tutorials/Routes/Async/Utilities.md/HouseKeeping.md deleted file mode 100644 index 380c56dec..000000000 --- a/docs/Tutorials/Routes/Async/Utilities.md/HouseKeeping.md +++ /dev/null @@ -1,45 +0,0 @@ - -## 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.md/Management.md b/docs/Tutorials/Routes/Async/Utilities.md/Management.md deleted file mode 100644 index de1d147d0..000000000 --- a/docs/Tutorials/Routes/Async/Utilities.md/Management.md +++ /dev/null @@ -1,81 +0,0 @@ - -## 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.md/Progress.md b/docs/Tutorials/Routes/Async/Utilities.md/Progress.md deleted file mode 100644 index e34fd1412..000000000 --- a/docs/Tutorials/Routes/Async/Utilities.md/Progress.md +++ /dev/null @@ -1,103 +0,0 @@ - -## 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. - -#### Example Usage - -##### 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' -``` - -#### Parameters - -This function is intended to be used inside an asynchronous route scriptblock to get the current progress of the task. \ No newline at end of file diff --git a/docs/Tutorials/Routes/Utilities/ContentTypes.md b/docs/Tutorials/Routes/Utilities/ContentTypes.md index cd3ba56e1..57816670b 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 'application/xml' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/api/xml' -ContentType 'text/xml' -ScriptBlock { Write-PodeXmlResponse -Value @{} } - Add-PodeRoute -Method Get -Path '/api/yaml' -ContentType 'application/yaml' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/api/yaml' -ContentType 'text/yaml' -ScriptBlock { Write-PodeYamlResponse -Value @{} } } diff --git a/docs/index.md b/docs/index.md index 9ad26ffed..25d0d9dd9 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,Async REST APIs Web Pages, and Static Content (with caching) +* Host 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, Dutch, and Chinese +* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, and Chinese ## 🏢 Companies using Pode diff --git a/examples/Web-AsyncRoute.ps1 b/examples/Web-AsyncRoute.ps1 deleted file mode 100644 index b16cf6cf6..000000000 --- a/examples/Web-AsyncRoute.ps1 +++ /dev/null @@ -1,540 +0,0 @@ -<# -.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 -CallbackSendResult | 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' - - - Add-PodeRoute -PassThru -Method Get -Path '/events' -ScriptBlock { - # ConvertTo-PodeSseConnection -Name 'Events' -Scope Local -Group 'Test events' - $msg = "Start - Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" - write-podehost $msg - Send-PodeSseEvent -Data $msg -FromEvent #-name 'Events' -Group 'Test events' #-FromEvent - write-podehost 'PodeSseEvent sent' - Start-Sleep -Seconds 10 - $msg = "End -Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" - write-podehost $msg - Send-PodeSseEvent -Data $msg -FromEvent #-name 'Events' -Group 'Test events' #-FromEvent - write-podehost 'PodeSseEvent sent' - return @{'message' = 'Done' } - } | Set-PodeAsyncRoute -ResponseContentType 'application/json' -MaxRunspaces 2 -PassThru | - Add-PodeAsyncRouteSse -SseGroup 'Test events' - - Add-PodeRoute -method Get -Path '/html/events' -ScriptBlock { - Write-PodeHtmlResponse -StatusCode 200 -Value @' - - - - - - EventSource Example - - -

EventSource Demo

-

Listening for events...

-
- - - - - -'@ - } -} \ No newline at end of file diff --git a/examples/Web-AsyncRouteBenchmark.ps1 b/examples/Web-AsyncRouteBenchmark.ps1 deleted file mode 100644 index bf2d87f58..000000000 --- a/examples/Web-AsyncRouteBenchmark.ps1 +++ /dev/null @@ -1,358 +0,0 @@ -<# -.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/server.psd1 b/examples/server.psd1 index 2d3ce3883..d1858842e 100644 --- a/examples/server.psd1 +++ b/examples/server.psd1 @@ -19,7 +19,6 @@ Default = 'application/html' Routes = @{ '/john' = 'application/json' - '/auth' = 'application/json' } } Compression = @{ @@ -65,18 +64,5 @@ Enable = $true } } - AsyncRoutes = @{ - UserFieldIdentifier = 'Id' - HouseKeeping = @{ - TimerInterval = 30 - RetentionMinutes = 10 - } - } - Tasks = @{ - HouseKeeping = @{ - TimerInterval = 30 - RetentionMinutes = 1 - } - } } } \ No newline at end of file diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index 3c9f1beed..b6cd8b697 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 = 'الوصف مطلوب.' + descriptionRequiredExceptionMessage = 'مطلوب وصف للمسار: {0} الاستجابة: {1}' pageNameShouldBeAlphaNumericExceptionMessage = 'يجب أن يكون اسم الصفحة قيمة أبجدية رقمية صالحة: {0}' defaultValueNotBooleanOrEnumExceptionMessage = 'القيمة الافتراضية ليست من نوع boolean وليست جزءًا من التعداد.' openApiComponentSchemaDoesNotExistExceptionMessage = 'مخطط مكون OpenApi {0} غير موجود.' @@ -283,18 +283,8 @@ 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 = 'لا يمكن أن يكون حد التقدم أقل من التقدم الحالي.' definitionTagChangeNotAllowedExceptionMessage = 'لا يمكن تغيير علامة التعريف لمسار.' - openApiDefinitionsMismatchExceptionMessage = '{0} يختلف بين تعريفات OpenAPI المختلفة.' - routeNotMarkedAsAsyncExceptionMessage = "المسار '{0}' لم يتم وضع علامة عليه كمسار غير متزامن." - functionCannotBeInvokedMultipleTimesExceptionMessage = "لا يمكن استدعاء الدالة '{0}' عدة مرات لنفس المسار '{1}'." getRequestBodyNotAllowedExceptionMessage = 'لا يمكن أن تحتوي عمليات {0} على محتوى الطلب.' -} \ No newline at end of file +} diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index ec12ee2e0..fb7b0c6ad 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.' + descriptionRequiredExceptionMessage = 'Eine Beschreibung ist erforderlich für Pfad:{0} Antwort:{1}' 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,16 +285,6 @@ 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.' 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 können keinen Anforderungstext haben.' - } \ No newline at end of file +} \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index 286cf031b..53aba0d1c 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -285,16 +285,6 @@ 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.' 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.' } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index 12024d2ff..8f97eead4 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -286,15 +286,6 @@ openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI definition named {0} already exists.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag cannot be used inside a Select-PodeOADefinition 'ScriptBlock'." 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.' -} \ No newline at end of file +} + diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index 67b3912da..9ca60ee62 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -285,16 +285,6 @@ 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.' 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.' } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index 0d7bd54d6..8b9d047d3 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -285,16 +285,7 @@ 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.' 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.' -} \ No newline at end of file +} + diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 5bc997f2c..ff9ccfc73 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -285,16 +285,6 @@ 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." 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.' } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index 1569cde09..ffc193a46 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -286,15 +286,6 @@ openApiDefinitionAlreadyExistsExceptionMessage = '名前が {0} の OpenAPI 定義は既に存在します。' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag は Select-PodeOADefinition 'ScriptBlock' 内で使用できません。" 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}操作にはリクエストボディを含めることはできません。' -} \ No newline at end of file +} + diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index d49918ad9..25e3c3d10 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -286,15 +286,5 @@ openApiDefinitionAlreadyExistsExceptionMessage = '이름이 {0}인 OpenAPI 정의가 이미 존재합니다.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag은 Select-PodeOADefinition 'ScriptBlock' 내에서 사용할 수 없습니다." 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} 작업에는 요청 본문이 있을 수 없습니다.' -} \ No newline at end of file +} diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index 05108756c..3f20960a0 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -285,16 +285,7 @@ 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.' 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 Request Body hebben.' -} \ No newline at end of file +} + diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index 4fa7177e4..6c4f2da03 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -286,15 +286,6 @@ openApiDefinitionAlreadyExistsExceptionMessage = 'Definicja OpenAPI o nazwie {0} już istnieje.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag nie może być używany wewnątrz 'ScriptBlock' Select-PodeOADefinition." 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ą mieć treści żądania.' -} \ No newline at end of file +} + diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index 9ed01763d..df0073012 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -286,15 +286,5 @@ openApiDefinitionAlreadyExistsExceptionMessage = 'A definição OpenAPI com o nome {0} já existe.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag não pode ser usado dentro de um 'ScriptBlock' Select-PodeOADefinition." 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.' } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 5e07d6afe..daa9ad110 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -286,15 +286,5 @@ openApiDefinitionAlreadyExistsExceptionMessage = '名为 {0} 的 OpenAPI 定义已存在。' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag 不能在 Select-PodeOADefinition 'ScriptBlock' 内使用。" 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} 操作不能包含请求体。' } \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 6a455c1bb..eae7d8c44 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -166,24 +166,6 @@ '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 deleted file mode 100644 index 82c761a34..000000000 --- a/src/Private/AsyncRoute.ps1 +++ /dev/null @@ -1,1530 +0,0 @@ - -<# -.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.Results.ContainsKey($___async___id___)) { - Start-Sleep 2 - } - if (!$PodeContext.AsyncRoutes.Results.ContainsKey($___async___id___)) { - try { - throw ($PodeLocale.asyncIdDoesNotExistExceptionMessage -f $___async___id___) - } - catch { - # Log the error - $_ | Write-PodeErrorLog - } - } - - $asyncResult = $PodeContext.AsyncRoutes.Results[$___async___id___] - ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace).Name = "$($asyncResult.AsyncRouteId)_$___async___id___" - try { - $asyncResult['StartingTime'] = [datetime]::UtcNow - - # Set the state to 'Running' - $asyncResult['State'] = 'Running' - - $___result___ = & { # Original ScriptBlock Start - <# ScriptBlock #> - # Original ScriptBlock End - } - if ($___result___) { - $asyncResult['Result'] = $___result___ - } - # Set the completed time - $asyncResult['CompletedTime'] = [datetime]::UtcNow - } - catch { - if (! $asyncResult.ContainsKey('CompletedTime')) { - $asyncResult['CompletedTime'] = [datetime]::UtcNow - } - # Set the state to 'Failed' in case of error - $asyncResult['State'] = 'Failed' - - # Log the error - $_ | Write-PodeErrorLog - - # Store the error in the AsyncRoutes results - $asyncResult['Error'] = $_.ToString() - - } - finally { - Complete-PodeAsyncRouteOperation -AsyncResult $asyncResult - } - } - - # 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 AsyncResult - A [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]] that contains the results and state information of the asynchronous script. - -.EXAMPLE - $asyncResult = [System.Collections.Concurrent.ConcurrentDictionary[string, psobject]]::new() - $webEvent = @{ - Request = @{ - Url = 'http://example.com/request' - } - Method = 'GET' - } - Complete-PodeAsyncRouteOperation -AsyncResult $asyncResult -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]] - $AsyncResult - ) - # Set the completed time if not already set - if (! $AsyncResult.ContainsKey('CompletedTime') -or ($null -eq $AsyncResult['CompletedTime'])) { - $AsyncResult['CompletedTime'] = [datetime]::UtcNow - } - - # Ensure state is set to 'Completed' if it was still 'Running' - if ($AsyncResult['State'] -eq 'Running') { - $AsyncResult['State'] = 'Completed' - } - - - if ($AsyncResult['Timer']) { - # Closes and disposes of the timer - Close-PodeAsyncRouteTimer -Operation $AsyncResult - } - - # Ensure Progress is set to 100 if in use - if ($AsyncResult.ContainsKey('Progress')) { - $AsyncResult['Progress'] = 100 - } - - try { - if ($AsyncResult['CallbackSettings']) { - - # Resolve the callback URL, method, content type, and headers - $callbackUrl = (Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $AsyncResult['CallbackSettings'].UrlField).Value - $method = (Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $AsyncResult['CallbackSettings'].Method -DefaultValue 'Post').Value - $contentType = (Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $AsyncResult['CallbackSettings'].ContentType).Value - $headers = @{} - foreach ($key in $AsyncResult['CallbackSettings'].HeaderFields.Keys) { - $value = Convert-PodeAsyncRouteCallBackRuntimeExpression -Variable $key -DefaultValue $AsyncResult['HeaderFields'][$key] - if ($value) { - $headers[$value.Key] = $value.Value - } - } - - # Prepare the body for the callback - $body = @{ - Url = $AsyncResult['Url'] - Method = $AsyncResult['Method'] - EventName = $AsyncResult['CallbackSettings'].EventName - State = $AsyncResult['State'] - } - switch ($AsyncResult['State']) { - 'Failed' { - $body.Error = $AsyncResult['Error'] - } - 'Completed' { - if ($AsyncResult['CallbackSettings'].SendResult -and $AsyncResult['Result']) { - $body.Result = $AsyncResult['Result'] - } - } - 'Aborted' { - $body.Error = $AsyncResult['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 - $AsyncResult['CallbackUrl'] = $callbackUrl - $AsyncResult['CallbackInfoState'] = 'Running' - $AsyncResult['CallbackTentative'] = 0 - - # Attempt to invoke the callback up to 3 times - for ($i = 0; $i -le 3; $i++) { - try { - $AsyncResult['CallbackTentative'] = $AsyncResult['CallbackTentative'] + 1 - $null = Invoke-RestMethod -Uri $callbackUrl -Method $method -Headers $headers -Body $cBody -ContentType $contentType -ErrorAction Stop - $AsyncResult['CallbackInfoState'] = 'Completed' - break - } - catch { - $_ | Write-PodeErrorLog - $AsyncResult['CallbackInfoState'] = 'Failed' - Start-Sleep -Seconds 2 - } - } - } - } - catch { - # Log any errors encountered during the callback process - $_ | Write-PodeErrorLog - $AsyncResult['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 { - ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace).Name = '__pode_asyncroutes_housekeeper__' - # Return if there are no async route results - if ($PodeContext.AsyncRoutes.Results.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.Results.Keys.Clone()) { - $result = $PodeContext.AsyncRoutes.Results[$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.Results.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 -AsyncResult $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 results to search - if ($PodeContext.AsyncRoutes.Results.count -gt 0) { - # Clone the keys of the results to iterate over them - foreach ($rkey in $PodeContext.AsyncRoutes.Results.keys.Clone()) { - $result = $PodeContext.AsyncRoutes.Results[$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 = $PodeContext.AsyncRoutes.Items[$WebEvent.Route.AsyncRouteId] - - # Invoke the internal async route task - # $asyncOperation = Invoke-PodeAsyncRoute - # Generate an Id for the async route task, using the provided IdGenerator or a new GUID - $id = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.AsyncRouteTaskIdGenerator -Return - - # Make a deepcopy of webEvent - $webEvent_ToClone = @{Route = @{} } - foreach ($key in $webEvent.Keys) { - if (!('OnEnd', 'Middleware', 'Route', 'Response' -contains $key)) { - $webEvent_ToClone[$key] = $webEvent[$key] - } - } - foreach ($key in $webEvent.Route.Keys) { - if (!( 'AsyncRouteTaskIdGenerator', 'Middleware', 'Logic' -contains $key)) { - $webEvent_ToClone.Route[$key] = $webEvent.Route[$key] - } - } - - $webEvent_ToClone.Response = $webEvent.Response - # Write-PodeHost $webEvent_ToClone.Response -explode -ShowType -Label 'webEvent_ToClone.Response' - # Setup event parameters - $parameters = @{ - Event = @{ - Lockable = $PodeContext.Threading.Lockables.Global - Sender = $asyncRouteTask - Metadata = @{} - } - WebEvent = Copy-PodeDeepClone -InputObject $webEvent_ToClone - ___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 - - # 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 - - 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 - - Write-PodeHost $asyncOperation['Sse'] -explode - } - - # 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'] = ($asyncRouteTask.Permission | Copy-PodeDeepClone) - } - - # 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.Results[$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.Results.ContainsKey($id)) { - $async = $PodeContext.AsyncRoutes.Results[$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.Results.ContainsKey($id)) { - $async = $PodeContext.AsyncRoutes.Results[$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 -AsyncResult $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 bb1869a7f..3e3395551 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -72,7 +72,6 @@ function New-PodeContext { 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 | @@ -117,24 +116,9 @@ function New-PodeContext { } $ctx.Tasks = @{ - Enabled = ($EnablePool -icontains 'tasks') - Items = @{} - Results = @{} - HouseKeeping = @{ - TimerInterval = 30 - RetentionMinutes = 1 - } - } - - $ctx.AsyncRoutes = @{ - Enabled = $true - Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - Results = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - HouseKeeping = @{ - TimerInterval = 30 - RetentionMinutes = 10 - } - UserFieldIdentifier = 'Id' + Enabled = ($EnablePool -icontains 'tasks') + Items = @{} + Results = @{} } $ctx.Fim = @{ @@ -153,12 +137,11 @@ function New-PodeContext { # set thread counts $ctx.Threads = @{ - General = $Threads - Schedules = 10 - Files = 1 - Tasks = 2 - WebSockets = 2 - AsyncRoutes = 0 + General = $Threads + Schedules = 10 + Files = 1 + Tasks = 2 + WebSockets = 2 } # set socket details for pode server @@ -439,16 +422,17 @@ function New-PodeContext { $ctx.Server.Endware = @() # runspace pools - $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 = @{ + Main = $null + Web = $null + Smtp = $null + Tcp = $null + Signals = $null + Schedules = $null + Gui = $null + Tasks = $null + Files = $null + } # threading locks, etc. $ctx.Threading.Lockables = @{ @@ -561,7 +545,7 @@ function New-PodeRunspacePool { # main runspace - for timers, schedules, etc $totalThreadCount = ($threadsCounts.Values | Measure-Object -Sum).Sum $PodeContext.RunspacePools.Main = @{ - Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces $totalThreadCount -RunspaceState $PodeContext.RunspaceState + Pool = [runspacefactory]::CreateRunspacePool(1, $totalThreadCount, $PodeContext.RunspaceState, $Host) State = 'Waiting' } @@ -576,7 +560,7 @@ function New-PodeRunspacePool { # smtp runspace - if we have any smtp endpoints if (Test-PodeEndpointByProtocolType -Type Smtp) { $PodeContext.RunspacePools.Smtp = @{ - Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces ($PodeContext.Threads.General + 1) -RunspaceState $PodeContext.RunspaceState + Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) State = 'Waiting' } } @@ -584,7 +568,7 @@ function New-PodeRunspacePool { # tcp runspace - if we have any tcp endpoints if (Test-PodeEndpointByProtocolType -Type Tcp) { $PodeContext.RunspacePools.Tcp = @{ - Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces ($PodeContext.Threads.General + 1) -RunspaceState $PodeContext.RunspaceState + Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) State = 'Waiting' } } @@ -592,7 +576,7 @@ function New-PodeRunspacePool { # signals runspace - if we have any ws/s endpoints if (Test-PodeEndpointByProtocolType -Type Ws) { $PodeContext.RunspacePools.Signals = @{ - Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces ($PodeContext.Threads.General + 2) -RunspaceState $PodeContext.RunspaceState + Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 2), $PodeContext.RunspaceState, $Host) State = 'Waiting' } } @@ -600,7 +584,7 @@ function New-PodeRunspacePool { # web socket connections runspace - for receiving data for external sockets if (Test-PodeWebSocketsExist) { $PodeContext.RunspacePools.WebSockets = @{ - Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces ($PodeContext.Threads.WebSockets + 1) -RunspaceState $PodeContext.RunspaceState + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.WebSockets + 1, $PodeContext.RunspaceState, $Host) State = 'Waiting' } @@ -610,7 +594,7 @@ function New-PodeRunspacePool { # setup schedule runspace pool -if we have any schedules if (Test-PodeSchedulesExist) { $PodeContext.RunspacePools.Schedules = @{ - Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces $PodeContext.Threads.Schedules -RunspaceState $PodeContext.RunspaceState + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Schedules, $PodeContext.RunspaceState, $Host) State = 'Waiting' } } @@ -618,7 +602,7 @@ function New-PodeRunspacePool { # setup tasks runspace pool -if we have any tasks if (Test-PodeTasksExist) { $PodeContext.RunspacePools.Tasks = @{ - Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces $PodeContext.Threads.Tasks -RunspaceState $PodeContext.RunspaceState + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Tasks, $PodeContext.RunspaceState, $Host) State = 'Waiting' } } @@ -626,7 +610,7 @@ function New-PodeRunspacePool { # setup files runspace pool -if we have any file watchers if (Test-PodeFileWatchersExist) { $PodeContext.RunspacePools.Files = @{ - Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces ($PodeContext.Threads.Files + 1) -RunspaceState $PodeContext.RunspaceState + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Files + 1, $PodeContext.RunspaceState, $Host) State = 'Waiting' } } @@ -634,7 +618,7 @@ function New-PodeRunspacePool { # setup gui runspace pool (only for non-ps-core) - if gui enabled if (Test-PodeGuiEnabled) { $PodeContext.RunspacePools.Gui = @{ - Pool = New-PodeRunspacePoolNetWrapper -MaxRunspaces 1 -RunspaceState $PodeContext.RunspaceState + Pool = [runspacefactory]::CreateRunspacePool(1, 1, $PodeContext.RunspaceState, $Host) State = 'Waiting' } @@ -799,7 +783,6 @@ function New-PodeStateContext { 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 AsyncRoutes -Value $Context.AsyncRoutes -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 | @@ -911,18 +894,6 @@ function Set-PodeServerConfiguration { Enabled = [bool]$Configuration.Debug.Breakpoints.Enable } } - - $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 - } } function Set-PodeWebConfiguration { diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 93c6420c5..d9d09ded0 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -554,6 +554,7 @@ function Get-PodeSubnetRange { function Add-PodeRunspace { param( [Parameter(Mandatory = $true)] + [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files')] [string] $Type, @@ -1838,7 +1839,7 @@ function ConvertFrom-PodeRequestContent { $Result.Data = ($Content | ConvertFrom-Json -AsHashtable) } else { - $Result.Data = ConvertTo-PodeHashtable -PSObject ($Content | ConvertFrom-Json) + $Result.Data = ($Content | ConvertFrom-Json) } } @@ -4034,278 +4035,4 @@ function Resolve-PodeObjectArray { # For any other type, convert it to a PowerShell object return New-Object psobject -Property $Property } -} - - - -<# -.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 - 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 - Creates a deep clone of a PSObject by serializing and deserializing the object. - -.DESCRIPTION - The Copy-PodeDeepClone function takes a PSObject as input and creates a deep clone of it. - This is achieved by serializing the object using the PSSerializer class, and then - deserializing it back into a new instance. This method ensures that nested objects, arrays, - and other complex structures are copied fully, without sharing references between the original - and the cloned object. - -.PARAMETER InputObject - The PSObject that you want to deep clone. This object will be serialized and then deserialized - to create a deep copy. - -.PARAMETER Deep - Specifies the depth for the serialization. The depth controls how deeply nested objects - and properties are serialized. The default value is 10. - -.INPUTS - [PSObject] - The function accepts a PSObject to deep clone. - -.OUTPUTS - [PSObject] - The function returns a new PSObject that is a deep clone of the original. - -.EXAMPLE - $originalObject = [PSCustomObject]@{ - Name = 'John Doe' - Age = 30 - Address = [PSCustomObject]@{ - Street = '123 Main St' - City = 'Anytown' - Zip = '12345' - } - } - - $clonedObject = $originalObject | Copy-PodeDeepClone -Deep 15 - - # The $clonedObject is now a deep clone of $originalObject. - # Changes to $clonedObject will not affect $originalObject and vice versa. - -.NOTES - This function uses the System.Management.Automation.PSSerializer class, which is available in - PowerShell 5.1 and later versions. The default depth parameter is set to 10 to handle nested - objects appropriately, but it can be customized via the -Deep parameter. - This is an internal function and may change in future releases of Pode. -#> -function Copy-PodeDeepClone { - param ( - [Parameter(Mandatory, ValueFromPipeline)] - [PSObject]$InputObject, - - [Parameter()] - [int]$Deep = 10 - ) - - process { - # Serialize the object to XML format using PSSerializer - # The depth parameter controls how deeply nested objects are serialized - $xmlSerializer = [System.Management.Automation.PSSerializer]::Serialize($InputObject, $Deep) - - # Deserialize the XML back into a new PSObject, creating a deep clone of the original - return [System.Management.Automation.PSSerializer]::Deserialize($xmlSerializer) - } } \ No newline at end of file diff --git a/src/Private/OpenApi.ps1 b/src/Private/OpenApi.ps1 index e17cfc6fb..81be1e4e0 100644 --- a/src/Private/OpenApi.ps1 +++ b/src/Private/OpenApi.ps1 @@ -763,8 +763,8 @@ function Set-PodeOpenApiRouteValue { if ($Route.OpenApi.OperationId) { $pm.operationId = $Route.OpenApi.OperationId } - if ($Route.OpenApi.Parameters.$DefinitionTag) { - $pm.parameters = $Route.OpenApi.Parameters.$DefinitionTag + if ($Route.OpenApi.Parameters) { + $pm.parameters = $Route.OpenApi.Parameters } if ($Route.OpenApi.RequestBody.$DefinitionTag) { $pm.requestBody = $Route.OpenApi.RequestBody.$DefinitionTag @@ -1284,8 +1284,6 @@ function Get-PodeOABaseObject { 'default' = @{ description = 'Internal server error' } } operationId = @() - #Async Route OpenAPI names - AsyncRoute = Get-PodeAsyncRouteOASchemaNameInternal } } } @@ -2195,50 +2193,3 @@ function Test-PodeOAComponentInternal { } } } - -function Test-PodeRouteOADefinitionTag { - param( - [Parameter(Mandatory = $true )] - [ValidateNotNullOrEmpty()] - [hashtable ] - $Route, - - [string[]] - $DefinitionTag - ) - # Check if the OpenAPI Definition Tag is already configured - if ($Route.OpenApi.IsDefTagConfigured) { - # If a DefinitionTag is provided - if ($DefinitionTag) { - # Loop through each element in $DefinitionTag - if ($DefinitionTag | ForEach-Object { - - # Check if the current element exists in the already configured DefinitionTag - if (!($Route.OpenApi.DefinitionTag -contains $_)) { - # If any element in $DefinitionTag is not present in the configured DefinitionTag, throw an exception - throw ($PodeLocale.definitionTagChangeNotAllowedExceptionMessage) - } - # Return $true for each element to continue the check - $true - } - ) { - # If all elements in $DefinitionTag are present in the configured DefinitionTag, assign it to $oaDefinitionTag - return $DefinitionTag - } - } - - - return $Route.OpenApi.DefinitionTag - } - # If the OpenAPI Definition Tag is not configured yet - - # Validate the provided DefinitionTag and assign it to $oaDefinitionTag - $oaDefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag - # Set the validated DefinitionTag as the OpenAPI DefinitionTag - $Route.OpenApi.DefinitionTag = $oaDefinitionTag - # Mark the OpenAPI DefinitionTag as configured - $Route.OpenApi.IsDefTagConfigured = $true - - - return $oaDefinitionTag -} \ No newline at end of file diff --git a/src/Private/Tasks.ps1 b/src/Private/Tasks.ps1 index c246a0693..1a4c1f7d2 100644 --- a/src/Private/Tasks.ps1 +++ b/src/Private/Tasks.ps1 @@ -7,11 +7,11 @@ function Start-PodeTaskHousekeeper { return } - Add-PodeTimer -Name '__pode_task_housekeeper__' -Interval $PodeContext.Tasks.HouseKeeping.TimerInterval -ScriptBlock { + Add-PodeTimer -Name '__pode_task_housekeeper__' -Interval 30 -ScriptBlock { if ($PodeContext.Tasks.Results.Count -eq 0) { return } - $RetentionMinutes = $PodeContext.Tasks.HouseKeeping.RetentionMinutes + $now = [datetime]::UtcNow foreach ($key in $PodeContext.Tasks.Results.Keys.Clone()) { @@ -35,7 +35,7 @@ function Start-PodeTaskHousekeeper { } # is it expired by completion? if so, dispose and remove - if ($result.CompletedTime.AddMinutes($RetentionMinutes) -lt $now) { + if ($result.CompletedTime.AddMinutes(1) -lt $now) { Close-PodeTaskInternal -Result $result } } diff --git a/src/Public/AsyncRoute.ps1 b/src/Public/AsyncRoute.ps1 deleted file mode 100644 index c1d2f2887..000000000 --- a/src/Public/AsyncRoute.ps1 +++ /dev/null @@ -1,2058 +0,0 @@ - -<# -.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 (! $PodeContext.AsyncRoutes.Items.ContainsKey($r.AsyncRouteId) -or ! $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 (! $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission.ContainsKey($Type)) { - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type] = @{} - } - - # Assign or remove users from the specified permission type - if ($Users) { - if (!$PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].ContainsKey('Users')) { - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Users = @() - } - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Users = Update-PermissionList -List $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Users -Items $Users -Remove:$Remove - } - - # Assign or remove groups from the specified permission type - if ($Groups) { - if (!$PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].ContainsKey('Groups')) { - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Groups = @() - } - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Groups = Update-PermissionList -List $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Groups -Items $Groups -Remove:$Remove - } - - # Assign or remove roles from the specified permission type - if ($Roles) { - if (!$PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].ContainsKey('Roles')) { - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Roles = @() - } - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Roles = Update-PermissionList -List $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Roles -Items $Roles -Remove:$Remove - } - - # Assign or remove scopes from the specified permission type - if ($Scopes) { - if (!$PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].ContainsKey('Scopes')) { - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Scopes = @() - } - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].Permission[$Type].Scopes = Update-PermissionList -List $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].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 CallbackSendResult - 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] - $CallbackSendResult, - - [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 = $CallbackSendResult.ToBool() - 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 (! $PodeContext.AsyncRoutes.Items.ContainsKey($r.AsyncRouteId) -or ! $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 - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId].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 ( $PodeContext.AsyncRoutes.Items.ContainsKey($r.AsyncRouteId) -or $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 - - # Assign the Id generator - if ($IdGenerator) { - $r.AsyncRouteTaskIdGenerator = $IdGenerator - } - else { - $r.AsyncRouteTaskIdGenerator = { return (New-PodeGuid) } - } - - # Store the route's async route task definition in Pode context - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId] = @{ - AsyncRouteId = $r.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 = @{} - } - - #Set thread count - $PodeContext.Threads.AsyncRoutes += $MaxRunspaces - if (! $PodeContext.RunspacePools.ContainsKey($r.AsyncRouteId)) { - $PodeContext.RunspacePools[$r.AsyncRouteId] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - - $PodeContext.RunspacePools[$r.AsyncRouteId]['Pool'] = New-PodeRunspacePoolNetWrapper -MinRunspaces $MinRunspaces -MaxRunspaces $MaxRunspaces -RunspaceState $PodeContext.RunspaceState - $PodeContext.RunspacePools[$r.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. - -.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 - ) - - Begin { - # Initialize an array to hold piped-in values - $pipelineValue = @() - - $sseScriptBlock = { - param($SseGroup) - - if ([string]::IsNullOrEmpty($SseGroup)) { - write-podehost "webEvent.Route.Path=$($webEvent.Route.Path)" - ConvertTo-PodeSseConnection -Name $webEvent.Route.Path -Scope Local -Group $SseGroup - } - else { - ConvertTo-PodeSseConnection -Name $webEvent.Route.Path -Scope Local - } - - $id = $WebEvent.Query['Id'] - if (!$PodeContext.AsyncRoutes.Results.ContainsKey($id)) { - try { - throw ($PodeLocale.asyncIdDoesNotExistExceptionMessage -f $id) - } - catch { - # Log the error - $_ | Write-PodeErrorLog - return - } - } - $AsyncResult = $PodeContext.AsyncRoutes.Results[$Id] - - $AsyncResult['Sse']['State'] = 'Waiting' - - while (!$AsyncResult['Runspace'].Handler.IsCompleted) { - start-sleep 1 - } - - try { - switch ($AsyncResult['State']) { - 'Failed' { - $null = Send-PodeSseEvent -FromEvent -Data @{ State = $AsyncResult['State']; Error = $AsyncResult['Error'] } - } - 'Completed' { - if ($AsyncResult['Result']) { - $null = Send-PodeSseEvent -FromEvent -Data @{ State = $AsyncResult['State']; Result = $AsyncResult['Result'] } - } - else { - $null = Send-PodeSseEvent -FromEvent -Data @{ State = 'Completed' } - } - } - 'Aborted' { - $null = Send-PodeSseEvent -FromEvent -Data @{ State = $AsyncResult['State']; Error = $AsyncResult['Error'] } - } - } - $AsyncResult['Sse']['State'] = 'Completed' - } - catch { - # Log any errors encountered during SSE handling - $_ | Write-PodeErrorLog - $AsyncResult['Sse']['State'] = 'Failed' - } - - } - } - - 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 marked as an Async Route - if (! $PodeContext.AsyncRoutes.Items.ContainsKey($r.AsyncRouteId) -or ! $r.IsAsync) { - # The route '{0}' is not marked as an Async Route. - throw ($PodeLocale.routeNotMarkedAsAsyncExceptionMessage -f $r.Path) - } - - $sseRoute = Add-PodeRoute -PassThru -method Get -Path "$($r.Path)_events" -ArgumentList $SseGroup ` - -ScriptBlock $sseScriptBlock - - $PodeContext.AsyncRoutes.Items[$r.AsyncRouteId]['Sse'] = @{ - Group = $SseGroup - Name = "$($r.Path)_events" - Route = $sseRoute - } - } - # Return the route information 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.Results[$Id] - } - elseif (! [string]::IsNullOrEmpty($AsyncRouteId)) { - foreach ($key in $PodeContext.AsyncRoutes.Results.Keys) { - if ($PodeContext.AsyncRoutes.Results[$key]['AsyncRouteId'] -ieq $AsyncRouteId) { - $result = $PodeContext.AsyncRoutes.Results[$key] - break - } - } - } - else { - $result = $PodeContext.AsyncRoutes.Results - } - - 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.Results.ContainsKey($Id )) { - $async = $PodeContext.AsyncRoutes.Results[$Id] - $async['State'] = 'Aborted' - $async['Error'] = 'Aborted by System' - $async['CompletedTime'] = [datetime]::UtcNow - $async['Runspace'].Pipeline.Dispose() - Complete-PodeAsyncRouteOperation -AsyncResult $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.Results.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 this function is used within an async route - if (!$___async___id___) { - # Set-PodeAsyncRouteProgress can only be used inside an Async Route Scriptblock. - throw $PodeLocale.setPodeAsyncProgressExceptionMessage - } - $asyncResult = $PodeContext.AsyncRoutes.Results[$___async___id___] - - # Initialize progress if not already set, for non-tick operations - if ($PSCmdlet.ParameterSetName -ne 'Tick' -and $PSCmdlet.ParameterSetName -ne 'SetValue') { - if (!$asyncResult.ContainsKey('Progress')) { - if ( $UseDecimalProgress.IsPresent) { - $asyncResult['Progress'] = [double] 0 - } - else { - $asyncResult['Progress'] = [int] 0 - } - } - - if ($MaxProgress -le $asyncResult['Progress']) { - # A Progress limit cannot be lower than the current progress. - throw $PodeLocale.progressLimitLowerThanCurrentExceptionMessage - } - } - - switch ($PSCmdlet.ParameterSetName) { - 'StartEnd' { - # Calculate total ticks and tick to progress ratio - $totalTicks = [math]::ceiling(($End - $Start) / $Steps) - if ($asyncResult['Progress'] -is [double]) { - $asyncResult['TickToProgress'] = ($MaxProgress - $asyncResult['Progress']) / $totalTicks - } - else { - $asyncResult['TickToProgress'] = [Math]::Floor(($MaxProgress - $asyncResult['Progress']) / $totalTicks) - } - } - 'Tick' { - # Increment progress by TickToProgress value - $asyncResult['Progress'] = $asyncResult['Progress'] + $asyncResult['TickToProgress'] - - # Ensure Progress does not exceed the specified limit - if ($asyncResult['Progress'] -ge $MaxProgress) { - if ($asyncResult['Progress'] -is [double]) { - $asyncResult['Progress'] = $MaxProgress - 0.01 - } - else { - $asyncResult['Progress'] = $MaxProgress - 1 - } - } - } - 'TimeBased' { - # Calculate tick interval and progress increment per tick - $totalTicks = [math]::ceiling($DurationSeconds / $IntervalSeconds) - if ($asyncResult['Progress'] -is [double]) { - $asyncResult['TickToProgress'] = ($MaxProgress - $asyncResult['Progress']) / $totalTicks - } - else { - $asyncResult['TickToProgress'] = [Math]::Floor(($MaxProgress - $asyncResult['Progress']) / $totalTicks) - } - - # Start the scheduler - $asyncResult['eventName'] = "TimerEvent_$___async___id___" - $asyncResult['Timer'] = [System.Timers.Timer]::new() - $asyncResult['Timer'].Interval = $IntervalSeconds * 1000 - $null = Register-ObjectEvent -InputObject $asyncResult['Timer'] -EventName Elapsed -SourceIdentifier $asyncResult['eventName'] -MessageData @{AsyncResult = $asyncResult; MaxProgress = $MaxProgress } -Action { - $asyncResult = $Event.MessageData.AsyncResult - $MaxProgress = $Event.MessageData.MaxProgress - - # Increment progress by TickToProgress value - $asyncResult['Progress'] = $asyncResult['Progress'] + $asyncResult['TickToProgress'] - - # Check if progress has reached or exceeded MaxProgress - if ($asyncResult['Progress'] -gt $MaxProgress) { - # Closes and disposes of the timer - Close-PodeAsyncRouteTimer -Operation $asyncResult - - if ($asyncResult['Progress'] -is [double]) { - $asyncResult['Progress'] = $MaxProgress - 0.01 - } - else { - $asyncResult['Progress'] = $MaxProgress - 1 - } - } - } - $asyncResult['Timer'].Enabled = $true - } - 'SetValue' { - if ( $UseDecimalProgress.IsPresent -or ($Value % 1 -ne 0) ) { - $asyncResult['Progress'] = $Value - } - else { - $asyncResult['Progress'] = [int]$Value - } - } - } -} - - -<# -.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.Results[$___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 4291290cd..77dd0a4d4 100644 --- a/src/Public/OpenApi.ps1 +++ b/src/Public/OpenApi.ps1 @@ -563,6 +563,7 @@ An Array of strings representing the unique tag for the API specification. This tag helps distinguish 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-PodeRoute -PassThru | Add-PodeOAResponse -StatusCode 200 -Content @{ 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) } @@ -631,6 +632,7 @@ function Add-PodeOAResponse { throw ($PodeLocale.routeParameterCannotBeNullExceptionMessage) } + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag # override status code with default if ($Default) { $code = 'default' @@ -641,9 +643,7 @@ function Add-PodeOAResponse { # add the respones to the routes foreach ($r in @($Route)) { - $oaDefinitionTag = Test-PodeRouteOADefinitionTag -Route $r -DefinitionTag $DefinitionTag - - foreach ($tag in $oaDefinitionTag) { + foreach ($tag in $DefinitionTag) { if (! $r.OpenApi.Responses.$tag) { $r.OpenApi.Responses.$tag = @{} } @@ -745,11 +745,6 @@ The Request Body definition the request uses (from New-PodeOARequestBody). .PARAMETER PassThru If supplied, the route passed in will be returned for further chaining. -.PARAMETER DefinitionTag -An Array of strings representing the unique tag for the API specification. -This tag helps distinguish 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-PodeRoute -PassThru | Set-PodeOARequest -RequestBody (New-PodeOARequestBody -Schema 'UserIdBody') #> @@ -769,10 +764,7 @@ function Set-PodeOARequest { $RequestBody, [switch] - $PassThru, - - [string[]] - $DefinitionTag + $PassThru ) if ($null -eq $Route) { @@ -782,27 +774,23 @@ function Set-PodeOARequest { foreach ($r in @($Route)) { - $oaDefinitionTag = Test-PodeRouteOADefinitionTag -Route $r -DefinitionTag $DefinitionTag - - foreach ($tag in $oaDefinitionTag) { - if (($null -ne $Parameters) -and ($Parameters.Length -gt 0)) { - $r.OpenApi.Parameters[$tag] = @($Parameters) - } + if (($null -ne $Parameters) -and ($Parameters.Length -gt 0)) { + $r.OpenApi.Parameters = @($Parameters) + } - if ($null -ne $RequestBody) { - # Only 'POST', 'PUT', 'PATCH' can have a request body - if (('POST', 'PUT', 'PATCH') -inotcontains $r.Method ) { - # {0} operations cannot have a Request Body. - throw ($PodeLocale.getRequestBodyNotAllowedExceptionMessage -f $r.Method) - } - $r.OpenApi.RequestBody = $RequestBody + if ($null -ne $RequestBody) { + # Only 'POST', 'PUT', 'PATCH' can have a request body + if (('POST', 'PUT', 'PATCH') -inotcontains $r.Method ) { + # {0} operations cannot have a Request Body. + throw ($PodeLocale.getRequestBodyNotAllowedExceptionMessage -f $r.Method) } - + $r.OpenApi.RequestBody = $RequestBody } - if ($PassThru) { - return $Route - } + } + + if ($PassThru) { + return $Route } } @@ -996,6 +984,7 @@ message: any validation issue $UserInfo = Test-PodeOAJsonSchemaCompliance -Json $UserInfo -SchemaReference 'UserIdSchema'} #> + function Test-PodeOAJsonSchemaCompliance { param ( [Parameter(Mandatory = $true)] @@ -1582,8 +1571,19 @@ function Set-PodeOARouteInfo { throw ($PodeLocale.routeParameterCannotBeNullExceptionMessage) } + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag + foreach ($r in @($Route)) { - $oaDefinitionTag = Test-PodeRouteOADefinitionTag -Route $r -DefinitionTag $DefinitionTag + if ((Compare-Object -ReferenceObject $r.OpenApi.DefinitionTag -DifferenceObject $DefinitionTag).Count -ne 0) { + if ($r.OpenApi.IsDefTagConfigured ) { + # Definition Tag for a Route cannot be changed. + throw ($PodeLocale.definitionTagChangeNotAllowedExceptionMessage) + } + else { + $r.OpenApi.DefinitionTag = $DefinitionTag + $r.OpenApi.IsDefTagConfigured = $true + } + } if ($Summary) { $r.OpenApi.Summary = $Summary @@ -1596,7 +1596,7 @@ function Set-PodeOARouteInfo { # OperationID:$OperationId has to be unique and cannot be applied to an array throw ($PodeLocale.operationIdMustBeUniqueForArrayExceptionMessage -f $OperationId) } - foreach ($tag in $oaDefinitionTag) { + foreach ($tag in $DefinitionTag) { if ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $OperationId) { # OperationID:$OperationId has to be unique throw ($PodeLocale.operationIdMustBeUniqueExceptionMessage -f $OperationId) @@ -1617,15 +1617,6 @@ 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) { @@ -2621,10 +2612,10 @@ function Add-PodeOACallBack { throw ($PodeLocale.routeParameterCannotBeNullExceptionMessage) } - foreach ($r in @($Route)) { - $oaDefinitionTag = Test-PodeRouteOADefinitionTag -Route $r -DefinitionTag $DefinitionTag + $DefinitionTag = Test-PodeOADefinitionTag -Tag $DefinitionTag - foreach ($tag in $oaDefinitionTag) { + foreach ($r in @($Route)) { + foreach ($tag in $DefinitionTag) { if ($Reference) { Test-PodeOAComponentInternal -Field callbacks -DefinitionTag $tag -Name $Reference -PostValidation if (!$Name) { @@ -3552,7 +3543,7 @@ function Test-PodeOADefinitionTag { if ($Tag -and $Tag.Count -gt 0) { foreach ($t in $Tag) { - if (! ($PodeContext.Server.OpenApi.Definitions.Keys -icontains $t)) { + if (! ($PodeContext.Server.OpenApi.Definitions.Keys -ccontains $t)) { # DefinitionTag does not exist. throw ($PodeLocale.definitionTagNotDefinedExceptionMessage -f $t) } diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 004625c06..ffed8e91f 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -430,13 +430,11 @@ function Add-PodeRoute { Arguments = $ArgumentList Method = $_method Path = $Path - IsAsync = $false - AsyncRouteId = "__$($_method)$($Path)_$($_endpoint.Name)_".Replace('/', '_') OpenApi = @{ Path = $OpenApiPath Responses = $DefaultResponse - Parameters = @{} - RequestBody = @{} + Parameters = $null + RequestBody = $null CallBacks = @{} Authentication = @() Servers = @() @@ -1697,34 +1695,14 @@ function Remove-PodeRoute { # select the candidate route for deletion $route = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object { - $_.Endpoint.Name -ieq $EndpointName + $_.Endpoint.Name -ine $EndpointName }) - foreach ($r in $route) { - # remove the runspace - if ($r.IsAsync) { - $asyncRouteId = $r.AsyncRouteId - if ( $asyncRouteId -and $PodeContext.RunspacePools.ContainsKey($asyncRouteId)) { - 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) - } - if ( $PodeContext.AsyncRoutes.Items.ContainsKey($asyncRouteId)) { - $PodeContext.Threads.AsyncRoutes -= $PodeContext.AsyncRoutes.Items[$asyncRouteId].MaxRunspaces - $v = '' - $null = $PodeContext.AsyncRoutes.Items.TryRemove( $asyncRouteId, [ref]$v) - } - } - - # remove the operationId from the openapi operationId list - if ($r.OpenAPI) { - foreach ( $tag in $r.OpenAPI.DefinitionTag) { - if ($tag -and ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $route.OpenAPI.OperationId)) { - $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId | Where-Object { $_ -ne $route.OpenAPI.OperationId } - } + # remove the operationId from the openapi operationId list + if ($route.OpenAPI) { + foreach ( $tag in $route.OpenAPI.DefinitionTag) { + if ($tag -and ($PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId -ccontains $route.OpenAPI.OperationId)) { + $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId = $PodeContext.Server.OpenAPI.Definitions[$tag].hiddenComponents.operationId | Where-Object { $_ -ne $route.OpenAPI.OperationId } } } } diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index e4c19149e..dde5ff1bb 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -805,9 +805,6 @@ Show the object content .PARAMETER ShowType Show the Object Type -.PARAMETER Label -Add a label to the object - .EXAMPLE 'Some output' | Write-PodeHost -ForegroundColor Cyan #> @@ -832,11 +829,7 @@ function Write-PodeHost { [Parameter( Mandatory = $false, ParameterSetName = 'object')] [switch] - $ShowType, - - [Parameter( Mandatory = $false, ParameterSetName = 'object')] - [string] - $Label + $ShowType ) if ($PodeContext.Server.Quiet) { @@ -855,9 +848,6 @@ function Write-PodeHost { if ($ShowType) { $Object = "`tTypeName: $type`n$Object" } - if ($Label){ - $Object = "`tName: $Label$Object" - } } } diff --git a/tests/integration/AsyncRoute.Tests.ps1 b/tests/integration/AsyncRoute.Tests.ps1 deleted file mode 100644 index 7f36a1b09..000000000 --- a/tests/integration/AsyncRoute.Tests.ps1 +++ /dev/null @@ -1,315 +0,0 @@ -[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 deleted file mode 100644 index 1ecc343cf..000000000 --- a/tests/unit/AsyncRoute.Tests.ps1 +++ /dev/null @@ -1,898 +0,0 @@ -[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() - Results = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - HouseKeeping = @{ - TimerInterval = 30 - RetentionMinutes = 10 - } - } - } - - # Example route object to test with - $route = @{ - AsyncRouteId = 'testRoute' - IsAsync = $true - } - $PodeContext.AsyncRoutes.Items[$route.AsyncRouteId] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - $PodeContext.AsyncRoutes.Items[$route.AsyncRouteId].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 }, - @{ AsyncRouteId = 'route2' ; IsAsync = $true } - ) - - $PodeContext.AsyncRoutes.Items['route1'] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - $PodeContext.AsyncRoutes.Items['route1'].Permission = @{} - $PodeContext.AsyncRoutes.Items['route2'] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - $PodeContext.AsyncRoutes.Items['route2'].Permission = @{} - $routes | Set-PodeAsyncRoutePermission -Type 'Read' -Users $users - - $PodeContext.AsyncRoutes.Items['route1'].Permission.Read.Users | Should -Be $users - $PodeContext.AsyncRoutes.Items['route2'].Permission.Read.Users | Should -Be $users - } - - It 'should initialize the Permission object if not already present' { - $PodeContext.AsyncRoutes.Items['testRoute'] = @{Permission = @{Read = @{Users = @('user3') } } } - - Set-PodeAsyncRoutePermission -Route $route -Type 'Read' -Users $users - - $PodeContext.AsyncRoutes.Items['testRoute'].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() - Results = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - HouseKeeping = @{ - TimerInterval = 30 - RetentionMinutes = 10 - } - } - } - - # Example route object to test with - $route = @{ - AsyncRouteId = 'testRoute' - IsAsync = $true - } - $PodeContext.AsyncRoutes.Items['testRoute'] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - - # Initialize permissions for testing remove functionality - $PodeContext.AsyncRoutes.Items['testRoute'] = @{ - 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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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 = $PodeContext.AsyncRoutes.Items['testRoute'].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() - Results = [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.Results[$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.Results[$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.Results[$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() - Results = [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.Results[$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.Results[$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.Results[$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.Results[$operationId1] - } - $operationId3 { - $r | Should -Be $PodeContext.AsyncRoutes.Results[$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 Pode Context - $PodeContext = @{ - AsyncRoutes = @{ - Items = @{ - 'ExamplePool' = @{ - Sse = $null - } - } - Results = @{ - '12345' = @{ - Runspace = [pscustomobject]@{ Handler = [pscustomobject]@{ IsCompleted = $false } } - State = 'Completed' - Result = 'Success' - } - } - } - } - # Mock data setup - $PodeContext = @{ - AsyncRoutes = @{ - Enabled = $true - Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - Results = [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.Results[$operationId1] = $asyncOperationDetails - - $PodeContext.AsyncRoutes.Items['ExamplePool'] = @{ - Sse = $null - } - - } - - It 'Should throw an exception if the route is not marked as async' { - { - $route = @{ - Path = '/not-async' - AsyncRouteId = 'not-async' - IsAsync = $true - } | 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 } - - $result = Add-PodeAsyncRouteSse -Route $route -PassThru - - $result | Should -BeOfType 'hashtable' - $result.Path | Should -Be '/events' - $PodeContext.AsyncRoutes.Items['ExamplePool'].Sse.Name | Should -Be '/events_events' - } - - It 'Should handle multiple routes piped in' { - $routes = @( - @{ Path = '/events1'; AsyncRouteId = 'ExamplePool'; IsAsync = $true }, - @{ Path = '/events2'; AsyncRouteId = 'ExamplePool'; IsAsync = $true } - ) - - $result = $routes | Add-PodeAsyncRouteSse -PassThru - - $result | Should -HaveCount 2 - $PodeContext.AsyncRoutes.Items['ExamplePool'].Sse.Name | Should -Be '/events2_events' - } - - It 'Should return the modified route object when PassThru is specified' { - $route = @{ Path = '/events'; AsyncRouteId = 'ExamplePool'; IsAsync = $true } - - $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() - Results = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - HouseKeeping = @{ - TimerInterval = 30 - RetentionMinutes = 10 - } - } - } - - - } - - - It 'Should correctly mark a route as async and set runspaces' { - $route = @{ Path = '/async'; AsyncRouteId = 'AsyncPool'; 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 - - $PodeContext.AsyncRoutes.Items['AsyncPool'].MinRunspaces | Should -Be 2 - $PodeContext.AsyncRoutes.Items['AsyncPool'].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'; AsyncRouteId = 'AsyncPool'; 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'; AsyncRouteId = 'AsyncPool'; IsAsync = $false; Logic = {} } - - $idGenScript = { return 'CustomId' } - Set-PodeAsyncRoute -Route $route -IdGenerator $idGenScript - - $route.AsyncRouteTaskIdGenerator.Invoke() | Should -Be 'CustomId' - } - - It 'Should use default IdGenerator if none is provided' { - $route = @{ Path = '/async'; AsyncRouteId = 'AsyncPool'; IsAsync = $false; Logic = {} } - - Set-PodeAsyncRoute -Route $route - - $id = $route.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'; AsyncRouteId = 'AsyncPool'; IsAsync = $false; Logic = {} } - - Set-PodeAsyncRoute -Route $route -Timeout 600 - - $PodeContext.AsyncRoutes.Items['AsyncPool'].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 - Items = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() - Results = [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.Results[$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.Results[$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.Results[$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/OpenApi.Tests.ps1 b/tests/unit/OpenApi.Tests.ps1 index 03931b881..aab9ca389 100644 --- a/tests/unit/OpenApi.Tests.ps1 +++ b/tests/unit/OpenApi.Tests.ps1 @@ -3112,9 +3112,7 @@ Describe 'OpenApi' { It 'Sets Parameters on the route if provided' { $route = @{ Method = 'GET' - OpenApi = @{ - Parameters=@{} - } + OpenApi = @{} } $parameters = @( @{ Name = 'param1'; In = 'query' } @@ -3122,7 +3120,7 @@ Describe 'OpenApi' { Set-PodeOARequest -Route $route -Parameters $parameters - $route.OpenApi.Parameters['Default'] | Should -BeExactly $parameters + $route.OpenApi.Parameters | Should -BeExactly $parameters } It 'Sets RequestBody on the route if method is POST' { diff --git a/tests/unit/Routes.Tests.ps1 b/tests/unit/Routes.Tests.ps1 index 24cc94a44..dc5aff5ca 100644 --- a/tests/unit/Routes.Tests.ps1 +++ b/tests/unit/Routes.Tests.ps1 @@ -109,170 +109,52 @@ 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 = @{ - hiddenComponents = @{ - operationId = @() - } - } - } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{}; 'Endpoints' = @{}; 'EndpointsMap' = @{} + 'OpenAPI' = @{ + SelectedDefinitionTag = 'default' + Definitions = @{ + default = Get-PodeOABaseObject } } - 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 | Should -Not -Be $null - $routes.ContainsKey('/users') | Should -Be $true - $routes['/users'].Length | Should -Be 1 - } - - It 'Removes a route and cleans up OpenAPI operationId' { - Add-PodeRoute -PassThru -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } | Set-PodeOARouteInfo -Summary 'Test user' -OperationId 'getUsers' - - $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 | Should -Not -Be $null - $routes.ContainsKey('/users') | Should -Be $false - $PodeContext.Server.OpenAPI.Definitions.default.hiddenComponents.operationId | Should -Not -Contain 'getUsers' - } - - It 'Adds two routes and removes on route and cleans up OpenAPI operationId' { - Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user - - Add-PodeRoute -PassThru -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } | Set-PodeOARouteInfo -Summary 'Test user' -OperationId 'getUsers' - Add-PodeRoute -PassThru -Method Get -Path '/users' -EndpointName user -ScriptBlock { Write-Host 'hello' } | Set-PodeOARouteInfo -Summary 'Test user2' -OperationId 'getUsers2' - - $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' -EndpointName 'user' - - $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 - $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.AsyncRouteId - $PodeContext.RunspacePools[$asyncRouteId].Pool = New-Object PSObject -Property @{ - IsDisposed = $true # to avoid to call BeginClose($null,$null) - } - Remove-PodeRoute -Method Get -Path '/async' - - $PodeContext.RunspacePools.ContainsKey($asyncRouteId) | Should -Be $false - $PodeContext.AsyncRoutes.Items.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.AsyncRouteId].Pool = New-Object PSObject -Property @{ - IsDisposed = $true # to avoid to call BeginClose($null,$null) - } - $PodeContext.Threads.AsyncRoutes | Should -Be ($maxRunspaces + $maxRunspaces) - $PodeContext.RunspacePools.ContainsKey($route2.asyncRouteId) | Should -Be $true - $PodeContext.AsyncRoutes.Items.ContainsKey($route2.asyncRouteId) | Should -Be $true - - $PodeContext.RunspacePools.ContainsKey($route1.asyncRouteId) | Should -Be $true - $PodeContext.AsyncRoutes.Items.ContainsKey($route1.asyncRouteId) | Should -Be $true - - #remove $route1 - Remove-PodeRoute -Method Get -Path '/asyncusers' - - $PodeContext.RunspacePools.ContainsKey($route2.asyncRouteId) | Should -Be $true - $PodeContext.AsyncRoutes.Items.ContainsKey($route2.asyncRouteId) | Should -Be $true - - $PodeContext.RunspacePools.ContainsKey($route1.asyncRouteId) | Should -Be $false - $PodeContext.AsyncRoutes.Items.ContainsKey($route1.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 } @@ -409,8 +291,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*' } @@ -425,7 +307,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*' } From 3cda16ca5f88b0104dd728f53006d4e313fdc50c Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 5 Nov 2024 18:12:50 -0800 Subject: [PATCH 3/3] Update ci-pwsh7_2.yml --- .github/workflows/ci-pwsh7_2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-pwsh7_2.yml b/.github/workflows/ci-pwsh7_2.yml index 932bd785c..468043c36 100644 --- a/.github/workflows/ci-pwsh7_2.yml +++ b/.github/workflows/ci-pwsh7_2.yml @@ -25,7 +25,7 @@ on: env: INVOKE_BUILD_VERSION: '5.11.1' - POWERSHELL_VERSION: '7.2.19' + POWERSHELL_VERSION: '7.2.24' jobs: build: