Skip to content

Commit

Permalink
feat: New paths for API (#76)
Browse files Browse the repository at this point in the history
* Adds GET paths for finding entry parents and children
* Adds POST path for updating entry children
* Adds POST paths for creation of new root node
* Adds DELETE path for deleting a node
* Some helper functions have also been created

Fixes:  #38
Fixes: #75
  • Loading branch information
aadarsh-ram committed Sep 13, 2022
1 parent d71da9c commit 2901cf7
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 13 deletions.
49 changes: 46 additions & 3 deletions backend/editor/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@

# DB helper imports
from .entries import initialize_db, shutdown_db
from .entries import get_all_nodes, get_nodes, get_children, get_parents
from .entries import update_nodes
from .entries import get_all_nodes, get_nodes, get_children, get_parents, get_label
from .entries import update_nodes, update_node_children
from .entries import create_node, add_node_to_end, add_node_to_beginning, delete_node
#------------------------------------------------------------------------#

app = FastAPI(title="Open Food Facts Taxonomy Editor API")
Expand Down Expand Up @@ -188,6 +189,25 @@ async def findFooter(response: Response):

# Post methods

@app.post("/nodes")
async def createNode(request: Request):
"""
Creating a new node in a taxonomy
"""
incomingData = await request.json()
id = incomingData["id"]
main_language = incomingData["main_language"]
if (id == None):
raise HTTPException(status_code=400, detail="Invalid id")
if (main_language == None):
raise HTTPException(status_code=400, detail="Invalid main language code")

create_node(get_label(id), id, main_language)
if (get_label(id) == "ENTRY"):
add_node_to_end(get_label(id), id)
else:
add_node_to_beginning(get_label(id), id)

@app.post("/entry/{entry}")
async def editEntry(request: Request, entry: str):
"""
Expand All @@ -200,6 +220,18 @@ async def editEntry(request: Request, entry: str):
updatedEntry = list(result)
return updatedEntry

@app.post("/entry/{entry}/children")
async def editEntryChildren(request: Request, entry: str):
"""
Editing an entry's children in a taxonomy.
New children can be added, old children can be removed.
URL will be of format '/entry/<id>/children'
"""
incomingData = await request.json()
result = update_node_children(entry, incomingData)
updatedChildren = list(result)
return updatedChildren

@app.post("/synonym/{synonym}")
async def editSynonyms(request: Request, synonym: str):
"""
Expand Down Expand Up @@ -242,4 +274,15 @@ async def editFooter(incomingData: Footer):
convertedData = incomingData.dict()
result = update_nodes("TEXT", "__footer__", convertedData)
updatedFooter = list(result)
return updatedFooter
return updatedFooter

# Delete methods

@app.delete("/nodes")
async def deleteNode(request: Request):
"""
Deleting given node from a taxonomy
"""
incomingData = await request.json()
id = incomingData["id"]
delete_node(get_label(id), id)
160 changes: 150 additions & 10 deletions backend/editor/entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,104 @@ def shutdown_db():
session.close()
driver.close()

def get_label(id):
"""
Helper function for getting the label for a given id
"""
if (id.startswith('stopword')): return 'STOPWORDS'
elif (id.startswith('synonym')): return 'SYNONYMS'
elif (id.startswith('__header__') or id.startswith('__footer__')): return 'TEXT'
else: return 'ENTRY'

def create_node(label, entry, main_language_code):
"""
Helper function used for creating a node with given id and label
"""
query = [f"""CREATE (n:{label})\n"""]
params = {"id": entry}

# Build all basic keys of a node
if (label == "ENTRY"):
canonical_tag = entry.split(":", 1)[1]
query.append(f""" SET n.main_language = $main_language_code """) # Required for only an entry
params["main_language_code"] = main_language_code
else:
canonical_tag = ""

query.append(f""" SET n.id = $id """)
query.append(f""" SET n.tags_{main_language_code} = [$canonical_tag] """)
query.append(f""" SET n.preceding_lines = [] """)

params["canonical_tag"] = canonical_tag
result = session.run(" ".join(query), params)
return result

def add_node_to_end(label, entry):
"""
Helper function which adds an existing node to end of taxonomy
"""
# Delete relationship between current last node and __footer__
query = f"""
MATCH (last_node)-[r:is_before]->(footer:TEXT) WHERE footer.id = "__footer__" DELETE r
RETURN last_node
"""
result = session.run(query)
end_node = result.data()[0]['last_node']
end_node_label = get_label(end_node['id']) # Get current last node ID

# Rebuild relationships by inserting incoming node at the end
query = []
query = f"""
MATCH (new_node:{label}) WHERE new_node.id = $id
MATCH (last_node:{end_node_label}) WHERE last_node.id = $endnodeid
MATCH (footer:TEXT) WHERE footer.id = "__footer__"
CREATE (last_node)-[:is_before]->(new_node)
CREATE (new_node)-[:is_before]->(footer)
"""
result = session.run(query, {"id": entry, "endnodeid": end_node['id']})

def add_node_to_beginning(label, entry):
"""
Helper function which adds an existing node to beginning of taxonomy
"""
# Delete relationship between current first node and __header__
query = f"""
MATCH (header:TEXT)-[r:is_before]->(first_node) WHERE header.id = "__header__" DELETE r
RETURN first_node
"""
result = session.run(query)
start_node = result.data()[0]['first_node']
start_node_label = get_label(start_node['id']) # Get current first node ID

# Rebuild relationships by inserting incoming node at the beginning
query= f"""
MATCH (new_node:{label}) WHERE new_node.id = $id
MATCH (first_node:{start_node_label}) WHERE first_node.id = $startnodeid
MATCH (header:TEXT) WHERE header.id = "__header__"
CREATE (new_node)-[:is_before]->(first_node)
CREATE (header)-[:is_before]->(new_node)
"""
result = session.run(query, {"id": entry, "startnodeid": start_node['id']})

def delete_node(label, entry):
"""
Helper function used for deleting a node with given id and label
"""
# Finding node to be deleted using node ID
query = f"""
// Find node to be deleted using node ID
MATCH (deleted_node:{label})-[:is_before]->(next_node) WHERE deleted_node.id = $id
MATCH (previous_node)-[:is_before]->(deleted_node)
// Remove node
DETACH DELETE (deleted_node)
// Rebuild relationships after deletion
CREATE (previous_node)-[:is_before]->(next_node)
"""
result = session.run(query, {"id": entry})
return result

def get_all_nodes(label):
"""
Helper function used for getting all nodes with/without given label
Expand Down Expand Up @@ -48,8 +146,8 @@ def get_parents(entry):
Helper function used for getting node parents with given id
"""
query = f"""
MATCH (a:ENTRY)-[r:is_child_of]->(b) WHERE a.id = $id
RETURN b.id
MATCH (child_node:ENTRY)-[r:is_child_of]->(parent) WHERE child_node.id = $id
RETURN parent.id
"""
result = session.run(query, {"id": entry})
return result
Expand All @@ -59,25 +157,25 @@ def get_children(entry):
Helper function used for getting node children with given id
"""
query = f"""
MATCH (b)-[r:is_child_of]->(a:ENTRY) WHERE a.id = $id
RETURN b.id
MATCH (child)-[r:is_child_of]->(parent_node:ENTRY) WHERE parent_node.id = $id
RETURN child.id
"""
result = session.run(query, {"id": entry})
return result

def update_nodes(label, entry, incomingData):
def update_nodes(label, entry, new_node_keys):
"""
Helper function used for updation of node with given id and label
"""
# Sanity check keys
for key in incomingData.keys():
for key in new_node_keys.keys():
if not re.match(r"^\w+$", key) or key == "id":
raise ValueError("Invalid key: %s", key)

# Get current node information and deleted keys
curr_node = get_nodes(label, entry).data()[0]['n']
curr_node_keys = list(curr_node.keys())
deleted_keys = (set(curr_node_keys) ^ set(incomingData))
deleted_keys = (set(curr_node_keys) ^ set(new_node_keys))

# Check for keys having null/empty values
for key in curr_node_keys:
Expand All @@ -94,11 +192,53 @@ def update_nodes(label, entry, incomingData):
query.append(f"""\nREMOVE n.{key}\n""")

# Update keys
for key in incomingData.keys():
for key in new_node_keys.keys():
query.append(f"""\nSET n.{key} = ${key}\n""")

query.append(f"""RETURN n""")

params = dict(incomingData, id=entry)
params = dict(new_node_keys, id=entry)
result = session.run(" ".join(query), params)
return result
return result

def update_node_children(entry, new_children_ids):
"""
Helper function used for updation of node children with given id
"""
# Parse node ids from Neo4j Record object
current_children = [record["child.id"] for record in list(get_children(entry))]
deleted_children = set(current_children) - set(new_children_ids)
added_children = set(new_children_ids) - set(current_children)

# Delete relationships
for child in deleted_children:
query = f"""
MATCH (deleted_child:ENTRY)-[rel:is_child_of]->(parent:ENTRY)
WHERE parent.id = $id AND deleted_child.id = $child
DELETE rel
"""
session.run(query, {"id": entry, "child": child})

# Create non-existing nodes
query = """MATCH (child:ENTRY) WHERE child.id in $ids RETURN child.id"""
existing_ids = [record['child.id'] for record in session.run(query, ids=list(added_children))]
to_create = added_children - set(existing_ids)

for child in to_create:
main_language_code = child.split(":", 1)[0]
create_node("ENTRY", child, main_language_code)

# TODO: We would prefer to add the node just after its parent entry
add_node_to_end("ENTRY", child)

# Stores result of last query executed
result = []
for child in added_children:
# Create new relationships if it doesn't exist
query = f"""
MATCH (parent:ENTRY), (new_child:ENTRY) WHERE parent.id = $id AND new_child.id = $child
MERGE (new_child)-[r:is_child_of]->(parent)
"""
result = session.run(query, {"id": entry, "child": child})

return result

0 comments on commit 2901cf7

Please sign in to comment.