Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New paths for API #76

Merged
merged 6 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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):
alexgarel marked this conversation as resolved.
Show resolved Hide resolved
"""
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