From 848ac563c4d6efd17c5ed2dbd667df322776da82 Mon Sep 17 00:00:00 2001 From: Peter Krenesky Date: Mon, 7 Aug 2023 17:40:55 -0700 Subject: [PATCH 1/5] Hooks for interacting with REST based API --- frontend/utils/hooks/useCreateAPI.js | 31 +++++++++++ frontend/utils/hooks/useCreateUpdateAPI.js | 26 +++++++++ frontend/utils/hooks/useDeleteAPI.js | 24 +++++++++ frontend/utils/hooks/useDetailAPI.js | 33 ++++++++++++ frontend/utils/hooks/useObjectEditorView.js | 60 +++++++++++++++++++++ frontend/utils/hooks/usePaginatedAPI.js | 43 +++++++++++++++ frontend/utils/hooks/useUpdateAPI.js | 31 +++++++++++ 7 files changed, 248 insertions(+) create mode 100644 frontend/utils/hooks/useCreateAPI.js create mode 100644 frontend/utils/hooks/useCreateUpdateAPI.js create mode 100644 frontend/utils/hooks/useDeleteAPI.js create mode 100644 frontend/utils/hooks/useDetailAPI.js create mode 100644 frontend/utils/hooks/useObjectEditorView.js create mode 100644 frontend/utils/hooks/usePaginatedAPI.js create mode 100644 frontend/utils/hooks/useUpdateAPI.js diff --git a/frontend/utils/hooks/useCreateAPI.js b/frontend/utils/hooks/useCreateAPI.js new file mode 100644 index 00000000..47e2f005 --- /dev/null +++ b/frontend/utils/hooks/useCreateAPI.js @@ -0,0 +1,31 @@ +import { useCallback, useState } from "react"; +import axios from "axios"; + +const useCreateAPI = (url) => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const create = useCallback( + async (data) => { + setIsLoading(true); + try { + const response = await axios.post(url, data); + setIsLoading(false); + return response.data; + } catch (err) { + setIsLoading(false); + setError(err); + throw err; + } + }, + [url] + ); + + return { + create, + isLoading, + error, + }; +}; + +export default useCreateAPI; diff --git a/frontend/utils/hooks/useCreateUpdateAPI.js b/frontend/utils/hooks/useCreateUpdateAPI.js new file mode 100644 index 00000000..ac423829 --- /dev/null +++ b/frontend/utils/hooks/useCreateUpdateAPI.js @@ -0,0 +1,26 @@ +import useCreateAPI from "utils/hooks/useCreateAPI"; +import useUpdateAPI from "utils/hooks/useUpdateAPI"; + +/** + * Hook that encapsulates both create and update APIs into a single save function. + * @param createURL + * @param updateURL + * @returns {{isLoading: boolean, save: ((function(*): Promise)|*)}} + */ +export const useCreateUpdateAPI = (createURL, updateURL) => { + const { create, isLoading: isCreateLoading } = useCreateAPI(createURL); + const { update, isLoading: isUpdateLoading } = useUpdateAPI(updateURL); + + const save = async (data) => { + if (data.id) { + return await update(data); + } else { + return await create(data); + } + }; + + return { + save, + isLoading: isCreateLoading || isUpdateLoading, + }; +}; diff --git a/frontend/utils/hooks/useDeleteAPI.js b/frontend/utils/hooks/useDeleteAPI.js new file mode 100644 index 00000000..6b691b57 --- /dev/null +++ b/frontend/utils/hooks/useDeleteAPI.js @@ -0,0 +1,24 @@ +import { useCallback, useState } from "react"; +import axios from "axios"; + +export const useDeleteAPI = (endpoint) => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const deleteData = useCallback(async () => { + setIsLoading(true); + try { + await axios.delete(endpoint); + } catch (err) { + setError(err); + } finally { + setIsLoading(false); + } + }, [endpoint]); + + return { + call: deleteData, + isLoading, + error, + }; +}; diff --git a/frontend/utils/hooks/useDetailAPI.js b/frontend/utils/hooks/useDetailAPI.js new file mode 100644 index 00000000..fd367bff --- /dev/null +++ b/frontend/utils/hooks/useDetailAPI.js @@ -0,0 +1,33 @@ +import { useState, useEffect, useCallback } from "react"; +import axios from "axios"; + +export const useDetailAPI = (endpoint, { load = true } = {}) => { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const loadData = useCallback(async () => { + setIsLoading(true); + try { + const response = await axios.get(endpoint); + setData(response.data); + } catch (err) { + setError(err); + } finally { + setIsLoading(false); + } + }, [endpoint]); + + useEffect(() => { + if (load) { + loadData(); + } + }, [endpoint, load]); + + return { + data, + load: loadData, + isLoading, + error, + }; +}; diff --git a/frontend/utils/hooks/useObjectEditorView.js b/frontend/utils/hooks/useObjectEditorView.js new file mode 100644 index 00000000..1f128623 --- /dev/null +++ b/frontend/utils/hooks/useObjectEditorView.js @@ -0,0 +1,60 @@ +import { useEffect, useState } from "react"; +import { v4 as uuid4 } from "uuid"; + +/** + * Hook for handling the state of an object editor view. + * state for handling whether how to load the data (new vs existing) + * and when to reset the editor when opened. The cached state does not + * reset when the url changes as protection against reloading when + * creating new chains. This state tracks when to reset the cache. + * + * @param id - object id, or undefined/null if new + * @param load - function to load the object when needed + */ +export const useObjectEditorView = (id, load) => { + const [idRef, setIdRef] = useState(null); + const [isNew, setIsNew] = useState(null); + const [wasCreated, setWasCreated] = useState(null); + useEffect(() => { + const firstRender = isNew === null; + if (firstRender) { + // first render caches whether this started as a new chain + setIsNew(id === undefined); + } else { + // switch from existing to new + if (id === undefined && !isNew) { + setIsNew(true); + setWasCreated(false); + } + // a new chain was created + if (id !== undefined && isNew) { + setWasCreated(true); + } + // switch from created to new + if (id === undefined && wasCreated) { + setIsNew(true); + setWasCreated(false); + setIdRef(uuid4()); + } + } + }, [id]); + + useEffect(() => { + // load chain if id is provided on view load + // otherwise state will be handled internally by the editor + if (isNew === false) { + load(); + setIdRef(id); + } else { + // create a uuid here to force a new editor. This helps detect + // creating a new object after a new object was just created. + setIdRef(uuid4()); + } + }, [isNew]); + + return { + isNew, + idRef, + wasCreated, + }; +}; diff --git a/frontend/utils/hooks/usePaginatedAPI.js b/frontend/utils/hooks/usePaginatedAPI.js new file mode 100644 index 00000000..42f72dd0 --- /dev/null +++ b/frontend/utils/hooks/usePaginatedAPI.js @@ -0,0 +1,43 @@ +import { useCallback, useEffect, useState } from "react"; +import axios from "axios"; + +export function usePaginatedAPI( + endpoint, + { offset = 0, limit = 10, load = true, loadDependencies = [] } = {} +) { + const [page, setPage] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const _load = useCallback( + async ({ search } = {}) => { + setIsLoading(true); + const params = { limit, offset }; + if (search) { + params.search = search; + } + try { + const response = await axios.get(endpoint, { + params, + }); + setPage(response.data); + } catch (error) { + console.error("Failed to fetch data:", error); + } finally { + setIsLoading(false); + } + }, + [endpoint] + ); + + useEffect(() => { + if (load) { + _load(); + } + }, [_load, ...loadDependencies]); + + return { + page, + isLoading, + load: _load, + }; +} diff --git a/frontend/utils/hooks/useUpdateAPI.js b/frontend/utils/hooks/useUpdateAPI.js new file mode 100644 index 00000000..3ef0d662 --- /dev/null +++ b/frontend/utils/hooks/useUpdateAPI.js @@ -0,0 +1,31 @@ +import { useCallback, useState } from "react"; +import axios from "axios"; + +const useUpdateAPI = (url, { onSuccess, onError } = {}) => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const update = useCallback( + async (data) => { + setIsLoading(true); + try { + const response = await axios.put(url, data); + setIsLoading(false); + return response.data; + } catch (err) { + setIsLoading(false); + setError(err); + throw err; + } + }, + [url] + ); + + return { + update, + isLoading, + error, + }; +}; + +export default useUpdateAPI; From 2fbeccbee1d5e11637a5279fdc837e02a9a5cce1 Mon Sep 17 00:00:00 2001 From: Peter Krenesky Date: Mon, 7 Aug 2023 17:44:18 -0700 Subject: [PATCH 2/5] get_agents endpoint now has search and pagination --- ix/api/agents/endpoints.py | 15 ++++++++++----- ix/api/agents/types.py | 8 ++++++++ ix/api/tests/test_agents.py | 18 ++++++++++-------- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/ix/api/agents/endpoints.py b/ix/api/agents/endpoints.py index c3a37bfa..c838a36b 100644 --- a/ix/api/agents/endpoints.py +++ b/ix/api/agents/endpoints.py @@ -1,11 +1,12 @@ +from asgiref.sync import sync_to_async from django.db.models import Q from fastapi import HTTPException, APIRouter -from typing import List, Optional +from typing import Optional from pydantic import BaseModel from uuid import UUID from ix.agents.models import Agent from ix.api.chains.endpoints import DeletedItem -from ix.api.agents.types import Agent as AgentPydantic +from ix.api.agents.types import Agent as AgentPydantic, AgentPage __all__ = ["router", "AgentCreateUpdate"] @@ -38,14 +39,18 @@ async def get_agent(agent_id: str): return AgentPydantic.from_orm(agent) -@router.get("/agents/", response_model=List[AgentPydantic], tags=["Agents"]) -async def get_agents(search: Optional[str] = None): +@router.get("/agents/", response_model=AgentPage, tags=["Agents"]) +async def get_agents(search: Optional[str] = None, limit: int = 10, offset: int = 0): query = ( Agent.objects.filter(Q(name__icontains=search) | Q(alias__icontains=search)) if search else Agent.objects.all() ) - return [AgentPydantic.from_orm(agent) async for agent in query] + + # punting on async implementation of pagination until later + return await sync_to_async(AgentPage.paginate)( + output_model=AgentPydantic, queryset=query, limit=limit, offset=offset + ) @router.put("/agents/{agent_id}", response_model=AgentPydantic, tags=["Agents"]) diff --git a/ix/api/agents/types.py b/ix/api/agents/types.py index 331659b5..d7862b48 100644 --- a/ix/api/agents/types.py +++ b/ix/api/agents/types.py @@ -1,10 +1,13 @@ from datetime import datetime +from typing import List from uuid import UUID from pydantic import BaseModel, Field import logging +from ix.utils.graphene.pagination import QueryPage + logger = logging.getLogger(__name__) @@ -20,3 +23,8 @@ class Agent(BaseModel): class Config: orm_mode = True + + +class AgentPage(QueryPage[Agent]): + # override objects, FastAPI isn't detecting QueryPage type + objects: List[Agent] diff --git a/ix/api/tests/test_agents.py b/ix/api/tests/test_agents.py index c98a0c6f..fcaba922 100644 --- a/ix/api/tests/test_agents.py +++ b/ix/api/tests/test_agents.py @@ -16,11 +16,12 @@ async def test_get_agents(self, anode_types): response = await ac.get("/agents/") assert response.status_code == 200, response.content - result = response.json() + page = response.json() # Check that we got a list of agents - assert len(result) >= 2 - agent_ids = [agent["id"] for agent in result] + objects = page["objects"] + assert len(objects) >= 2 + agent_ids = [agent["id"] for agent in objects] assert str(agent_1.id) in agent_ids assert str(agent_2.id) in agent_ids @@ -31,12 +32,13 @@ async def test_search_agents(self, achat): response = await ac.get(f"/agents/?search={search_term}") assert response.status_code == 200, response.content - result = response.json() - assert len(result) > 0 + page = response.json() + objects = page["objects"] + assert len(objects) > 0 assert ( - search_term in result[0]["name"] - or search_term in result[0]["purpose"] - or search_term in result[0]["alias"] + search_term in objects[0]["name"] + or search_term in objects[0]["purpose"] + or search_term in objects[0]["alias"] ) async def test_get_agent_detail(self, anode_types): From d79f3cce70e19bd8f8567bcf5f270e24d44aca22 Mon Sep 17 00:00:00 2001 From: Peter Krenesky Date: Mon, 7 Aug 2023 17:46:19 -0700 Subject: [PATCH 3/5] get_chains endpoint now supports search and pagination --- ix/api/chains/endpoints.py | 20 +++++++++++++++----- ix/api/chains/types.py | 7 +++++++ ix/api/tests/test_chains.py | 5 +++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/ix/api/chains/endpoints.py b/ix/api/chains/endpoints.py index f0d934b3..0ab30996 100644 --- a/ix/api/chains/endpoints.py +++ b/ix/api/chains/endpoints.py @@ -1,13 +1,14 @@ from typing import Optional, List, Literal from uuid import UUID +from asgiref.sync import sync_to_async from django.db.models import Q from fastapi import APIRouter, HTTPException from pydantic import BaseModel from ix.chains.models import Chain, ChainNode, NodeType, ChainEdge -from ix.api.chains.types import Chain as ChainPydantic +from ix.api.chains.types import Chain as ChainPydantic, ChainQueryPage from ix.api.chains.types import NodeType as NodeTypePydantic from ix.api.chains.types import Node as NodePydantic from ix.api.chains.types import Edge as EdgePydantic @@ -20,10 +21,19 @@ class DeletedItem(BaseModel): id: UUID -@router.get("/chains/", response_model=List[ChainPydantic], tags=["Chains"]) -async def get_chains(): - chains = Chain.objects.all() - return [ChainPydantic.from_orm(chain) async for chain in chains] +@router.get("/chains/", response_model=ChainQueryPage, tags=["Chains"]) +async def get_chains(search: Optional[str] = None, limit: int = 10, offset: int = 0): + query = ( + Chain.objects.filter(Q(name__icontains=search)) + if search + else Chain.objects.all() + ) + query = query.order_by("-created_at") + + # punting on async implementation of pagination until later + return await sync_to_async(ChainQueryPage.paginate)( + output_model=ChainPydantic, queryset=query, limit=limit, offset=offset + ) @router.post("/chains/", response_model=ChainPydantic, tags=["Chains"]) diff --git a/ix/api/chains/types.py b/ix/api/chains/types.py index c066f1f7..1c98f5a6 100644 --- a/ix/api/chains/types.py +++ b/ix/api/chains/types.py @@ -4,6 +4,8 @@ from uuid import UUID, uuid4 from pydantic import BaseModel, Field, root_validator +from ix.utils.graphene.pagination import QueryPage + class InputType(str, Enum): SLIDER = "slider" @@ -37,6 +39,11 @@ class Config: orm_mode = True +class ChainQueryPage(QueryPage[Chain]): + # override objects, FastAPI isn't detecting QueryPage type + objects: List[Chain] + + class Position(BaseModel): x: float y: float diff --git a/ix/api/tests/test_chains.py b/ix/api/tests/test_chains.py index e9ac58aa..8ce0b1b8 100644 --- a/ix/api/tests/test_chains.py +++ b/ix/api/tests/test_chains.py @@ -180,8 +180,9 @@ async def test_get_chains(self, anode_types): result = response.json() # Check that we got the correct chains back - assert len(result) == 2 - chain_ids = {chain["id"] for chain in result} + objects = result["objects"] + assert len(objects) == 2 + chain_ids = {chain["id"] for chain in objects} assert str(chain1.id) in chain_ids assert str(chain2.id) in chain_ids From 20c5c16b537d2604f9c5876b16d7441fdbc345a7 Mon Sep 17 00:00:00 2001 From: Peter Krenesky Date: Mon, 7 Aug 2023 17:51:20 -0700 Subject: [PATCH 4/5] update /chain/ to /chains/ for consistency with other endpoints --- ix/api/chains/endpoints.py | 18 +++++++++--------- ix/api/tests/test_chains.py | 26 +++++++++++++------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/ix/api/chains/endpoints.py b/ix/api/chains/endpoints.py index 0ab30996..8b68d5ca 100644 --- a/ix/api/chains/endpoints.py +++ b/ix/api/chains/endpoints.py @@ -154,7 +154,7 @@ class UpdateRoot(BaseModel): @router.post( - "/chain/{chain_id}/set_root/", response_model=UpdatedRoot, tags=["Chain Editor"] + "/chains/{chain_id}/set_root/", response_model=UpdatedRoot, tags=["Chain Editor"] ) async def set_chain_root(chain_id: UUID, update_root: UpdateRoot): # update old roots: @@ -186,7 +186,7 @@ class AddNode(BaseModel): position: Optional[Position] -@router.post("/chain/nodes", response_model=NodePydantic, tags=["Chain Editor"]) +@router.post("/chains/nodes", response_model=NodePydantic, tags=["Chain Editor"]) async def add_chain_node(node: AddNode): if not node.chain_id: chain = Chain(name="Unnamed", description="") @@ -207,7 +207,7 @@ class UpdateNode(BaseModel): @router.put( - "/chain/nodes/{node_id}", response_model=NodePydantic, tags=["Chain Editor"] + "/chains/nodes/{node_id}", response_model=NodePydantic, tags=["Chain Editor"] ) async def update_chain_node(node_id: UUID, data: UpdateNode): try: @@ -222,7 +222,7 @@ async def update_chain_node(node_id: UUID, data: UpdateNode): @router.post( - "/chain/nodes/{node_id}/position", + "/chains/nodes/{node_id}/position", response_model=NodePydantic, tags=["Chain Editor"], ) @@ -234,7 +234,7 @@ async def update_chain_node_position(node_id: UUID, position: Position): @router.delete( - "/chain/nodes/{node_id}", response_model=DeletedItem, tags=["Chain Editor"] + "/chains/nodes/{node_id}", response_model=DeletedItem, tags=["Chain Editor"] ) async def delete_chain_node(node_id: UUID): node = await ChainNode.objects.aget(id=node_id) @@ -245,7 +245,7 @@ async def delete_chain_node(node_id: UUID): return DeletedItem(id=node_id) -@router.post("/chain/edges", response_model=EdgePydantic, tags=["Chain Editor"]) +@router.post("/chains/edges", response_model=EdgePydantic, tags=["Chain Editor"]) async def add_chain_edge(edge: EdgePydantic): new_edge = ChainEdge(**edge.dict()) await new_edge.asave() @@ -261,7 +261,7 @@ class UpdateEdge(BaseModel): @router.put( - "/chain/edges/{edge_id}", response_model=EdgePydantic, tags=["Chain Editor"] + "/chains/edges/{edge_id}", response_model=EdgePydantic, tags=["Chain Editor"] ) async def update_chain_edge(edge_id: UUID, edge: UpdateEdge): try: @@ -276,7 +276,7 @@ async def update_chain_edge(edge_id: UUID, edge: UpdateEdge): @router.delete( - "/chain/edges/{edge_id}", response_model=DeletedItem, tags=["Chain Editor"] + "/chains/edges/{edge_id}", response_model=DeletedItem, tags=["Chain Editor"] ) async def delete_chain_edge(edge_id: UUID): edge = await ChainEdge.objects.aget(id=edge_id) @@ -291,7 +291,7 @@ class GraphModel(BaseModel): edges: List[EdgePydantic] -@router.get("/chain/{chain_id}/graph", response_model=GraphModel, tags=["Chains"]) +@router.get("/chains/{chain_id}/graph", response_model=GraphModel, tags=["Chains"]) async def get_chain_graph(chain_id: UUID): """Return chain and all it's nodes and edges.""" chain = await Chain.objects.aget(id=chain_id) diff --git a/ix/api/tests/test_chains.py b/ix/api/tests/test_chains.py index 8ce0b1b8..b78dc086 100644 --- a/ix/api/tests/test_chains.py +++ b/ix/api/tests/test_chains.py @@ -307,7 +307,7 @@ async def test_set_chain_root_no_root_exists(self, anode_types): data = {"node_id": str(node.id)} async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.post(f"/chain/{chain.id}/set_root/", json=data) + response = await ac.post(f"/chains/{chain.id}/set_root/", json=data) assert response.status_code == 200 result = response.json() @@ -323,7 +323,7 @@ async def test_set_chain_root_exists(self, anode_types): data = {"node_id": str(new_root.id), "chain_id": str(chain.id)} async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.post(f"/chain/{chain.id}/set_root/", json=data) + response = await ac.post(f"/chains/{chain.id}/set_root/", json=data) assert response.status_code == 200 result = response.json() @@ -340,7 +340,7 @@ async def test_remove_chain_root(self, anode_types): data = {"node_id": None, "chain_id": str(chain.id)} async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.post(f"/chain/{chain.id}/set_root/", json=data) + response = await ac.post(f"/chains/{chain.id}/set_root/", json=data) assert response.status_code == 200, response.content result = response.json() @@ -367,7 +367,7 @@ async def test_add_first_node(self, anode_types): # Execute the API request async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.post("/chain/nodes", json=data) + response = await ac.post("/chains/nodes", json=data) # Assert the result assert response.status_code == 200, response.json() @@ -400,7 +400,7 @@ async def test_update_node(self, anode_types): } async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.put(f"/chain/nodes/{node.id}", json=data) + response = await ac.put(f"/chains/nodes/{node.id}", json=data) # Assert the result assert response.status_code == 200, response.json() @@ -422,7 +422,7 @@ async def test_update_non_existent_chain_node(self): async with AsyncClient(app=app, base_url="http://test") as ac: response = await ac.put( - f"/chain/nodes/{non_existent_node_id}", json=update_data + f"/chains/nodes/{non_existent_node_id}", json=update_data ) assert response.status_code == 404, response.content @@ -437,7 +437,7 @@ async def test_delete_node(self, anode_types): # Execute the API request async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.delete(f"/chain/nodes/{node.id}") + response = await ac.delete(f"/chains/nodes/{node.id}") # Assert the result assert response.status_code == 200 @@ -466,7 +466,7 @@ async def test_update_position(self, anode_types): # Execute the API request async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.post(f"/chain/nodes/{node.id}/position", json=data) + response = await ac.post(f"/chains/nodes/{node.id}/position", json=data) # Assert the result assert response.status_code == 200, response.json() @@ -499,7 +499,7 @@ async def test_add_chain_edge(self, anode_types): } async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.post("/chain/edges", json=data) + response = await ac.post("/chains/edges", json=data) # Assert the result assert response.status_code == 200, response.json() @@ -528,7 +528,7 @@ async def test_update_chain_edge(self, anode_types): } async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.put(f"/chain/edges/{edge.id}", json=data) + response = await ac.put(f"/chains/edges/{edge.id}", json=data) # Assert the result assert response.status_code == 200, response.json() @@ -542,7 +542,7 @@ async def test_delete_chain_edge(self, anode_types): edge = await afake_chain_edge() async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.delete(f"/chain/edges/{edge.id}") + response = await ac.delete(f"/chains/edges/{edge.id}") # Assert the result assert response.status_code == 200 @@ -568,7 +568,7 @@ async def test_update_non_existent_chain_edge(self, anode_types): } async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.put(f"/chain/edges/{non_existent_edge_id}", json=data) + response = await ac.put(f"/chains/edges/{non_existent_edge_id}", json=data) assert response.status_code == 404, response.content @@ -582,7 +582,7 @@ async def test_add_chain_edge(self, anode_types): await afake_chain_edge(source=node1, target=node2) async with AsyncClient(app=app, base_url="http://test") as ac: - response = await ac.get(f"/chain/{chain.id}/graph") + response = await ac.get(f"/chains/{chain.id}/graph") assert response.status_code == 200, response.content data = response.json() From 7c6738044d286ac9ab893bfb597794118a85fd53 Mon Sep 17 00:00:00 2001 From: Peter Krenesky Date: Mon, 7 Aug 2023 17:51:49 -0700 Subject: [PATCH 5/5] ChainGraph now includes node types --- ix/api/chains/endpoints.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ix/api/chains/endpoints.py b/ix/api/chains/endpoints.py index 8b68d5ca..a216edef 100644 --- a/ix/api/chains/endpoints.py +++ b/ix/api/chains/endpoints.py @@ -289,6 +289,7 @@ class GraphModel(BaseModel): chain: ChainPydantic nodes: List[NodePydantic] edges: List[EdgePydantic] + types: List[NodeTypePydantic] @router.get("/chains/{chain_id}/graph", response_model=GraphModel, tags=["Chains"]) @@ -306,8 +307,12 @@ async def get_chain_graph(chain_id: UUID): async for edge in edge_queryset: edges.append(EdgePydantic.from_orm(edge)) + types_in_chain = NodeType.objects.filter(chainnode__chain_id=chain_id) + types = [NodeTypePydantic.from_orm(node_type) async for node_type in types_in_chain] + return GraphModel( chain=ChainPydantic.from_orm(chain), nodes=nodes, edges=edges, + types=types, )