diff --git a/src/writer/app_runner.py b/src/writer/app_runner.py index 5286b12c4..5531a59ce 100644 --- a/src/writer/app_runner.py +++ b/src/writer/app_runner.py @@ -313,10 +313,11 @@ def _handle_message(self, session_id: str, request: AppProcessServerRequest) -> ) if self.mode == "edit" and type == "hashRequest": + hash_request_payload = HashRequestPayload.model_validate(request.payload) return AppProcessServerResponse( status="ok", status_message=None, - payload=self._handle_hash_request(request.payload) + payload=self._handle_hash_request(hash_request_payload) ) if self.mode == "edit" and type == "componentUpdate": diff --git a/src/writer/blocks/httprequest.py b/src/writer/blocks/httprequest.py index aedb86de7..de2642236 100644 --- a/src/writer/blocks/httprequest.py +++ b/src/writer/blocks/httprequest.py @@ -1,4 +1,5 @@ import re + import requests from writer.abstract import register_abstract_template diff --git a/src/writer/blocks/runworkflow.py b/src/writer/blocks/runworkflow.py index 94645a326..00e1189ec 100644 --- a/src/writer/blocks/runworkflow.py +++ b/src/writer/blocks/runworkflow.py @@ -23,7 +23,7 @@ def register(cls, type: str): "name": "Payload", "desc": "The value specified will be available using the template syntax i.e. @{payload}", "default": "{}", - "type": "Object", + "type": "Text", "control": "Textarea" }, }, @@ -45,7 +45,7 @@ def register(cls, type: str): def run(self): try: workflow_key = self._get_field("workflowKey") - payload = self._get_field("payload", as_json=True) + payload = self._get_field("payload") expanded_execution_environment = self.execution_environment | { "payload": payload } return_value = self.runner.run_workflow_by_key(workflow_key, expanded_execution_environment) self.result = return_value diff --git a/src/writer/crypto.py b/src/writer/crypto.py index 9333a264e..7cf1e834a 100644 --- a/src/writer/crypto.py +++ b/src/writer/crypto.py @@ -18,6 +18,6 @@ def get_hash(message: str): def verify_hash_in_request(message: str, request: Request): auth_header = request.headers.get("Authorization") if not auth_header: - raise HTTPException(status_code=401, detail=f"Unauthorized. Token not specified.") + raise HTTPException(status_code=401, detail="Unauthorized. Token not specified.") if auth_header != f"Bearer {get_hash(message)}": - raise HTTPException(status_code=403, detail=f"Forbidden. Incorrect token.") \ No newline at end of file + raise HTTPException(status_code=403, detail="Forbidden. Incorrect token.") \ No newline at end of file diff --git a/src/writer/serve.py b/src/writer/serve.py index e412ee408..2e42ed4ab 100644 --- a/src/writer/serve.py +++ b/src/writer/serve.py @@ -5,7 +5,6 @@ import json import logging import mimetypes -import redis import os import os.path import pathlib @@ -18,6 +17,7 @@ from typing import Any, Callable, Dict, List, Literal, Optional, Set, Tuple, Union, cast from urllib.parse import urlsplit +import redis import uvicorn from fastapi import FastAPI, HTTPException, Request, Response from fastapi.responses import FileResponse, JSONResponse @@ -99,7 +99,10 @@ def generate_job_id(self): return str(job_id) def set(self, job_id: str, value: Any): - ttl = int(os.getenv("WRITER_REDIS_TTL")) if os.getenv("WRITER_REDIS_TTL") else RedisJobVault.DEFAULT_TTL + ttl = RedisJobVault.DEFAULT_TTL + env_ttl = os.getenv("WRITER_REDIS_TTL") + if env_ttl is not None: + ttl = int(env_ttl) json_str = json.dumps(value) self.redis_client.set(f"job:{job_id}", json_str, ex=ttl) @@ -327,12 +330,13 @@ def update_job(job_id: str, job_info: dict): def job_done_callback(task: asyncio.Task, job_id: str): try: - apsr: Optional[AppProcessServerResponse] = None - apsr = task.result() - if apsr.status != "ok": + apsr: Optional[AppProcessServerResponse] = task.result() + if apsr is None or apsr.status != "ok": update_job(job_id, {"status": "error"}) return - result = apsr.payload.result.get("result") + result = None + if apsr.payload and apsr.payload.result: + result = apsr.payload.result.get("result") update_job(job_id, { "status": "complete", "result": serialize_result(result) @@ -347,10 +351,12 @@ def job_done_callback(task: asyncio.Task, job_id: str): proposedSessionId=None )) + if not app_response or not app_response.payload: + raise HTTPException(status_code=500, detail="Cannot initialize session.") session_id = app_response.payload.sessionId is_session_ok = await app_runner.check_session(session_id) if not is_session_ok: - return + raise HTTPException(status_code=500, detail="Cannot initialize session.") loop = asyncio.get_running_loop() task = loop.create_task(app_runner.handle_event( @@ -548,16 +554,13 @@ async def _handle_hash_request(websocket: WebSocket, session_id: str, req_messag trackingId=req_message.trackingId, payload=None ) - res_payload: str = None apsr: Optional[AppProcessServerResponse] = None apsr = await app_runner.handle_hash_request(session_id, HashRequestPayload( message=req_message.payload.get("message", "") )) if apsr is not None and apsr.payload is not None: - res_payload = typing.cast( + response.payload = typing.cast( HashRequestResponsePayload, apsr.payload).model_dump() - if res_payload is not None: - response.payload = res_payload await websocket.send_json(response.model_dump()) async def _stream_outgoing_announcements(websocket: WebSocket): diff --git a/src/writer/ss_types.py b/src/writer/ss_types.py index f453e386c..efa801cf6 100644 --- a/src/writer/ss_types.py +++ b/src/writer/ss_types.py @@ -26,7 +26,7 @@ def read(self) -> Any: ServeMode = Literal["run", "edit"] MessageType = Literal["sessionInit", "componentUpdate", "event", "codeUpdate", "codeSave", "checkSession", - "keepAlive", "stateEnquiry", "setUserinfo", "stateContent"] + "keepAlive", "stateEnquiry", "setUserinfo", "stateContent", "hashRequest"] class AbstractTemplate(BaseModel): diff --git a/tests/backend/test_serve.py b/tests/backend/test_serve.py index a39af0688..0a9fb4375 100644 --- a/tests/backend/test_serve.py +++ b/tests/backend/test_serve.py @@ -4,10 +4,10 @@ import fastapi import fastapi.testclient import pytest -from writer import crypto import writer.abstract import writer.serve from fastapi import FastAPI +from writer import crypto from tests.backend import test_app_dir, test_multiapp_dir @@ -246,7 +246,7 @@ def test_create_workflow_job_api_incorrect_token(self, monkeypatch): workflow_key = "workflow2" with fastapi.testclient.TestClient(asgi_app) as client: - create_job_token = crypto.get_hash(f"not_the_right_message") + create_job_token = crypto.get_hash("not_the_right_message") res = client.post(f"/api/job/workflow/{workflow_key}", json={ "proposedSessionId": None }, headers={ @@ -262,7 +262,7 @@ def test_create_workflow_job_api_incorrect_token_for_get(self, monkeypatch): workflow_key = "workflow2" with fastapi.testclient.TestClient(asgi_app) as client: - create_job_token = crypto.get_hash(f"not_the_right_message") + create_job_token = crypto.get_hash("not_the_right_message") res = client.post(f"/api/job/workflow/{workflow_key}", json={ "proposedSessionId": None }, headers={ diff --git a/tests/backend/testapp/main.py b/tests/backend/testapp/main.py index 20932e03a..6e08476ba 100644 --- a/tests/backend/testapp/main.py +++ b/tests/backend/testapp/main.py @@ -10,7 +10,6 @@ import writer.core from writer import WriterUIManager - writer.Config.feature_flags.append("workflows") writer.Config.feature_flags.append("flag_one") writer.Config.feature_flags.append("flag_two") diff --git a/tests/e2e/presets/workflows/.wf/components-workflows_workflow-0-auxjfi7lssb268ly.jsonl b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-0-auxjfi7lssb268ly.jsonl index 4216ffb42..e1d0ebeda 100644 --- a/tests/e2e/presets/workflows/.wf/components-workflows_workflow-0-auxjfi7lssb268ly.jsonl +++ b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-0-auxjfi7lssb268ly.jsonl @@ -1,4 +1,4 @@ {"id": "auxjfi7lssb268ly", "type": "workflows_workflow", "content": {"key": "handle_object"}, "handlers": {}, "isCodeManaged": false, "parentId": "workflows_root", "position": 0} {"id": "8y56lmia3wu99jhl", "type": "workflows_parsejson", "content": {"plainText": "{\"color\": \"@{payload}\", \"object\": \"@{context.item.object}\"}"}, "handlers": {}, "isCodeManaged": false, "outs": [{"toNodeId": "xy6vdzh2pm55alc0", "outId": "success"}], "parentId": "auxjfi7lssb268ly", "position": 0, "x": 150, "y": 319} {"id": "xy6vdzh2pm55alc0", "type": "workflows_setstate", "content": {"alias": "Save the JSON", "element": "json_e2e", "value": "@{result}"}, "handlers": {}, "isCodeManaged": false, "outs": [{"toNodeId": "mve8ssvtk0pvw5yf", "outId": "success"}], "parentId": "auxjfi7lssb268ly", "position": 1, "x": 537, "y": 321} -{"id": "mve8ssvtk0pvw5yf", "type": "workflows_returnvalue", "content": {"alias": "", "value": "@{json_e2e}"}, "handlers": {}, "isCodeManaged": false, "parentId": "auxjfi7lssb268ly", "position": 2, "x": 876, "y": 317} +{"id": "mve8ssvtk0pvw5yf", "type": "workflows_returnvalue", "content": {"alias": "", "value": "@{json_e2e}"}, "handlers": {}, "isCodeManaged": false, "parentId": "auxjfi7lssb268ly", "position": 2, "x": 885, "y": 331} diff --git a/tests/e2e/presets/workflows/.wf/components-workflows_workflow-1-n20uom1t17z7c1h8.jsonl b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-1-n20uom1t17z7c1h8.jsonl new file mode 100644 index 000000000..d6dc3cbaf --- /dev/null +++ b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-1-n20uom1t17z7c1h8.jsonl @@ -0,0 +1,2 @@ +{"id": "n20uom1t17z7c1h8", "type": "workflows_workflow", "content": {"key": "repeat_payload"}, "handlers": {}, "isCodeManaged": false, "parentId": "workflows_root", "position": 1} +{"id": "5rwx9ukywrkz2f8t", "type": "workflows_returnvalue", "content": {"alias": "Repeat payload", "value": "@{payload}"}, "handlers": {}, "isCodeManaged": false, "parentId": "n20uom1t17z7c1h8", "position": 0, "x": 371, "y": 270} diff --git a/tests/e2e/presets/workflows/.wf/components-workflows_workflow-2-bjhk2qqylt0ijn50.jsonl b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-2-bjhk2qqylt0ijn50.jsonl new file mode 100644 index 000000000..bbcb3c43c --- /dev/null +++ b/tests/e2e/presets/workflows/.wf/components-workflows_workflow-2-bjhk2qqylt0ijn50.jsonl @@ -0,0 +1,3 @@ +{"id": "bjhk2qqylt0ijn50", "type": "workflows_workflow", "content": {}, "handlers": {}, "isCodeManaged": false, "parentId": "workflows_root", "position": 2} +{"id": "60xs6i5w0ckdiymh", "type": "workflows_runworkflow", "content": {"payload": "blue", "workflowKey": "repeat_payload"}, "handlers": {}, "isCodeManaged": false, "outs": [{"toNodeId": "htzhnqlqe1u02l3o", "outId": "success"}], "parentId": "bjhk2qqylt0ijn50", "position": 0, "x": 244, "y": 282} +{"id": "htzhnqlqe1u02l3o", "type": "workflows_returnvalue", "content": {"value": "@{result}"}, "handlers": {}, "isCodeManaged": false, "parentId": "bjhk2qqylt0ijn50", "position": 1, "x": 622, "y": 287} diff --git a/tests/e2e/presets/workflows/.wf/metadata.json b/tests/e2e/presets/workflows/.wf/metadata.json index da9c77c7c..8792752a3 100644 --- a/tests/e2e/presets/workflows/.wf/metadata.json +++ b/tests/e2e/presets/workflows/.wf/metadata.json @@ -1,3 +1,3 @@ { - "writer_version": "0.8.0rc3" + "writer_version": "0.8.3rc1" } \ No newline at end of file diff --git a/tests/e2e/tests/workflows.spec.ts b/tests/e2e/tests/workflows.spec.ts index e579b960c..8854efd19 100644 --- a/tests/e2e/tests/workflows.spec.ts +++ b/tests/e2e/tests/workflows.spec.ts @@ -22,7 +22,7 @@ test.describe("Workflows", () => { { object: "cup", color: "pink" }, ]; - inputData.forEach(({ object, color }) => { + for (const { object, color } of inputData) { test(`Test context and payload in Workflows for ${object} ${color}`, async ({ page, }) => { @@ -41,16 +41,14 @@ test.describe("Workflows", () => { `.BuilderModal [data-automation-key="return-value"]`, ); const expectedTexts = ["color", color, "object", object]; - expectedTexts.forEach( - async (text) => await expect(resultsLocator).toContainText(text), - ); - expectedTexts.forEach( - async (text) => await expect(returnValueLocator).toContainText(text), - ); + for (const text of expectedTexts) { + await expect(resultsLocator).toContainText(text); + await expect(returnValueLocator).toContainText(text); + } }); - }); + } - test("Create workflow and run workflow handle_object from it", async ({ + test("Create workflow and run workflow repeat_payload from it", async ({ page, }) => { await page.locator(`[data-automation-action="set-mode-workflows"]`).click(); @@ -75,16 +73,9 @@ test.describe("Workflows", () => { const returnValueBlock = page.locator(`.WorkflowsNode.wf-type-workflows_returnvalue`); await runWorkflowBlock.click(); - await page.locator(`.BuilderFieldsText[data-automation-key="workflowKey"] input`).fill("handle_object"); - const executionEnv = { - "payload": "blue", - "context": { - "item": { - "object": "bottle" - } - } - }; - await page.locator(`.BuilderFieldsObject[data-automation-key="executionEnv"] textarea`).fill(JSON.stringify(executionEnv)); + await page.locator(`.BuilderFieldsText[data-automation-key="workflowKey"] input`).fill("repeat_payload"); + const payload = "blue"; + await page.locator(`.BuilderFieldsText[data-automation-key="payload"] textarea`).fill(payload); await page.locator(`[data-automation-action="collapse-settings"]`).click(); await runWorkflowBlock.locator(".ball.success").dragTo(returnValueBlock); @@ -96,16 +87,13 @@ test.describe("Workflows", () => { await page.locator(`[data-automation-action="toggle-panel"][data-automation-key="log"]`).click(); const rowsLocator = page.locator(".BuilderPanelSwitcher div.row"); - await expect(rowsLocator).toHaveCount(5); + await expect(rowsLocator).toHaveCount(3); const rowLocator = rowsLocator.filter({ hasText: "Return value" }).first();; await rowLocator.getByRole("button", { name: "Details" }).click(); await expect(page.locator(".BuilderModal")).toBeVisible(); const returnValueLocator = page.locator( `.BuilderModal [data-automation-key="return-value"]`, ); - const expectedTexts = ["color", "blue", "object", "bottle"]; - expectedTexts.forEach( - async (text) => await expect(returnValueLocator).toContainText(text), - ); + await expect(returnValueLocator).toContainText("blue"); }); }); \ No newline at end of file