Skip to content

Commit cc7df50

Browse files
refactor: add types to find_one_entry routes + start refactor PropertyTable (#481)
* feat: add EntryNode type for find_one_entry route * fix: update frontend to handle new API * fix: lint backend * fix: respond to comment
1 parent 6003639 commit cc7df50

File tree

12 files changed

+188
-89
lines changed

12 files changed

+188
-89
lines changed

backend/editor/api.py

+4-8
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@
3232
from . import graph_db
3333

3434
# Controller imports
35-
from .controllers import project_controller, search_controller
35+
from .controllers import node_controller, project_controller, search_controller
3636
from .entries import TaxonomyGraph
3737

3838
# Custom exceptions
3939
from .exceptions import GithubBranchExistsError, GithubUploadError
4040

4141
# Data model imports
42-
from .models.node_models import EntryNodeCreate, ErrorNode, Footer, Header, NodeType
42+
from .models.node_models import EntryNode, EntryNodeCreate, ErrorNode, Footer, Header, NodeType
4343
from .models.project_models import Project, ProjectEdit, ProjectStatus
4444
from .models.search_models import EntryNodeSearchResult
4545
from .scheduler import scheduler_lifespan
@@ -199,16 +199,12 @@ async def find_all_root_nodes(response: Response, branch: str, taxonomy_name: st
199199

200200

201201
@app.get("/{taxonomy_name}/{branch}/entry/{entry}")
202-
async def find_one_entry(response: Response, branch: str, taxonomy_name: str, entry: str):
202+
async def find_one_entry(branch: str, taxonomy_name: str, entry: str) -> EntryNode:
203203
"""
204204
Get entry corresponding to id within taxonomy
205205
"""
206206
taxonomy = TaxonomyGraph(branch, taxonomy_name)
207-
one_entry = await taxonomy.get_nodes("ENTRY", entry)
208-
209-
check_single(one_entry)
210-
211-
return one_entry[0]["n"]
207+
return await node_controller.get_entry_node(taxonomy.project_name, entry)
212208

213209

214210
@app.get("/{taxonomy_name}/{branch}/entry/{entry}/parents")

backend/editor/controllers/node_controller.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from openfoodfacts_taxonomy_parser import utils as parser_utils
22

33
from ..graph_db import get_current_transaction
4-
from ..models.node_models import EntryNodeCreate, ErrorNode
4+
from ..models.node_models import EntryNode, EntryNodeCreate, ErrorNode
5+
from .utils.result_utils import get_unique_record
56

67

78
async def delete_project_nodes(project_id: str):
@@ -45,6 +46,22 @@ async def create_entry_node(
4546
return (await result.data())[0]["n.id"]
4647

4748

49+
async def get_entry_node(project_id: str, node_id: str) -> EntryNode:
50+
query = (
51+
f"""
52+
MATCH (n:{project_id}:ENTRY
53+
"""
54+
+ """{id: $node_id})
55+
RETURN n
56+
"""
57+
)
58+
params = {"node_id": node_id}
59+
result = await get_current_transaction().run(query, params)
60+
61+
entry_record = await get_unique_record(result, node_id)
62+
return EntryNode(**entry_record["n"])
63+
64+
4865
async def get_error_node(project_id: str) -> ErrorNode | None:
4966
query = """
5067
MATCH (n:ERRORS {id: $project_id})

backend/editor/controllers/project_controller.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
from fastapi import HTTPException
2-
31
from ..graph_db import get_current_transaction
42
from ..models.project_models import Project, ProjectCreate, ProjectEdit, ProjectStatus
53
from .node_controller import delete_project_nodes
4+
from .utils.result_utils import get_unique_record
65

76

87
async def get_project(project_id: str) -> Project:
@@ -15,10 +14,8 @@ async def get_project(project_id: str) -> Project:
1514
"""
1615
params = {"project_id": project_id}
1716
result = await get_current_transaction().run(query, params)
18-
project = await result.single()
19-
if project is None:
20-
raise HTTPException(status_code=404, detail="Project not found")
21-
return Project(**project["p"])
17+
project_record = await get_unique_record(result, project_id)
18+
return Project(**project_record["p"])
2219

2320

2421
async def get_projects_by_status(status: ProjectStatus) -> list[Project]:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from fastapi import HTTPException
2+
from neo4j import AsyncResult, Record
3+
4+
5+
async def get_unique_record(result: AsyncResult, record_id: str | None = None) -> Record:
6+
"""
7+
Gets the unique record from a Cypher query result
8+
9+
Raises:
10+
404 HTTPException: If no record is found
11+
500 HTTPException: If multiple records are found
12+
"""
13+
record = await result.fetch(1)
14+
if record is None:
15+
exception_message = f"Record {record_id} not found" if record_id else "Record not found"
16+
raise HTTPException(status_code=404, detail=exception_message)
17+
18+
remaining_record = await result.peek()
19+
if remaining_record is not None:
20+
exception_message = (
21+
f"Multiple records with id {record_id} found" if record_id else "Multiple records found"
22+
)
23+
raise HTTPException(
24+
status_code=500,
25+
detail=exception_message,
26+
)
27+
28+
return record[0]

backend/editor/models/node_models.py

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class EntryNode(BaseModel):
3535
properties: dict[str, str]
3636
comments: dict[str, list[str]]
3737
is_external: bool = False
38+
original_taxonomy: str | None = None
3839

3940
@model_validator(mode="before")
4041
@classmethod

backend/openapi/openapi.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,11 @@
268268
"responses": {
269269
"200": {
270270
"description": "Successful Response",
271-
"content": { "application/json": { "schema": {} } }
271+
"content": {
272+
"application/json": {
273+
"schema": { "$ref": "#/components/schemas/EntryNode" }
274+
}
275+
}
272276
},
273277
"422": {
274278
"description": "Validation Error",
@@ -1217,6 +1221,10 @@
12171221
"type": "boolean",
12181222
"title": "Isexternal",
12191223
"default": false
1224+
},
1225+
"originalTaxonomy": {
1226+
"anyOf": [{ "type": "string" }, { "type": "null" }],
1227+
"title": "Originaltaxonomy"
12201228
}
12211229
},
12221230
"type": "object",
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
1+
/**
2+
* @deprecated Migrate to @/client/models/EntryNode when possible
3+
*/
4+
export type DestructuredEntryNode = {
5+
id: string;
6+
precedingLines: Array<string>;
7+
srcPosition: number;
8+
mainLanguage: string;
9+
isExternal: boolean;
10+
originalTaxonomy: string | null;
11+
// TODO: Use updated types from the API
12+
[key: string]: any;
13+
// tags: Record<string, Array<string>>;
14+
// properties: Record<string, string>;
15+
// comments: Record<string, Array<string>>;
16+
};
17+
118
export type ParentsAPIResponse = string[];

taxonomy-editor-frontend/src/client/models/EntryNode.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ export type EntryNode = {
1111
properties: Record<string, string>;
1212
comments: Record<string, Array<string>>;
1313
isExternal: boolean;
14+
originalTaxonomy: string | null;
1415
};

taxonomy-editor-frontend/src/client/services/DefaultService.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/* tslint:disable */
44
/* eslint-disable */
55
import type { Body_upload_taxonomy__taxonomy_name___branch__upload_post } from "../models/Body_upload_taxonomy__taxonomy_name___branch__upload_post";
6+
import type { EntryNode } from "../models/EntryNode";
67
import type { EntryNodeCreate } from "../models/EntryNodeCreate";
78
import type { EntryNodeSearchResult } from "../models/EntryNodeSearchResult";
89
import type { ErrorNode } from "../models/ErrorNode";
@@ -180,14 +181,14 @@ export class DefaultService {
180181
* @param branch
181182
* @param taxonomyName
182183
* @param entry
183-
* @returns any Successful Response
184+
* @returns EntryNode Successful Response
184185
* @throws ApiError
185186
*/
186187
public static findOneEntryTaxonomyNameBranchEntryEntryGet(
187188
branch: string,
188189
taxonomyName: string,
189190
entry: string
190-
): CancelablePromise<any> {
191+
): CancelablePromise<EntryNode> {
191192
return __request(OpenAPI, {
192193
method: "GET",
193194
url: "/{taxonomy_name}/{branch}/entry/{entry}",

taxonomy-editor-frontend/src/pages/project/editentry/AccumulateAllComponents.tsx

+27-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Alert, Box, Snackbar, Typography, Button } from "@mui/material";
22
import SaveIcon from "@mui/icons-material/Save";
33
import CircularProgress from "@mui/material/CircularProgress";
4-
import { useState } from "react";
4+
import { useMemo, useState } from "react";
55
import ListEntryParents from "./ListEntryParents";
66
import ListEntryChildren from "./ListEntryChildren";
77
import { ListTranslations } from "./ListTranslations";
@@ -12,6 +12,7 @@ import { createURL, getNodeType, toSnakeCase } from "@/utils";
1212
import { useNavigate } from "react-router-dom";
1313
import { useQuery } from "@tanstack/react-query";
1414
import { DefaultService } from "@/client";
15+
import { DestructuredEntryNode } from "@/backend-types/types";
1516

1617
interface AccumulateAllComponentsProps {
1718
id: string;
@@ -38,7 +39,7 @@ const AccumulateAllComponents = ({
3839
const isEntry = getNodeType(id) === "entry";
3940

4041
const {
41-
data: node,
42+
data: rawNode,
4243
isPending,
4344
isError,
4445
error,
@@ -58,8 +59,29 @@ const AccumulateAllComponents = ({
5859
);
5960
},
6061
});
61-
const [nodeObject, setNodeObject] = useState(null); // Storing updates to node
62-
const [originalNodeObject, setOriginalNodeObject] = useState(null); // For tracking changes
62+
63+
// Intermediate destructuring step. Migrate to @/client/models/EntryNode when possible
64+
const node: DestructuredEntryNode | null = useMemo(() => {
65+
if (!rawNode) return null;
66+
const {
67+
tags: _tags,
68+
properties: _properties,
69+
comments: _comments,
70+
...destructuredNode
71+
} = {
72+
...rawNode,
73+
...rawNode.tags,
74+
...rawNode.properties,
75+
...rawNode.comments,
76+
};
77+
return destructuredNode;
78+
}, [rawNode]);
79+
80+
const [nodeObject, setNodeObject] = useState<DestructuredEntryNode | null>(
81+
null
82+
); // Storing updates to node
83+
const [originalNodeObject, setOriginalNodeObject] =
84+
useState<DestructuredEntryNode | null>(null); // For tracking changes
6385
const [updateChildren, setUpdateChildren] = useState(null); // Storing updates of children in node
6486
const [previousUpdateChildren, setPreviousUpdateChildren] = useState(null); // Tracking changes of children
6587
const [open, setOpen] = useState(false); // Used for Dialog component
@@ -118,8 +140,7 @@ const AccumulateAllComponents = ({
118140
// Function handling updation of node
119141
const handleSubmit = () => {
120142
if (!nodeObject) return;
121-
const data = Object.assign({}, nodeObject);
122-
delete data["id"]; // ID not allowed in POST
143+
const { id: _, ...data } = { ...nodeObject }; // ID not allowed in POST
123144

124145
const dataToBeSent = {};
125146
// Remove UUIDs from data

0 commit comments

Comments
 (0)