Skip to content

Commit e9b851c

Browse files
feat(api): add cascade delete support for parent geography resources
1 parent 18f1488 commit e9b851c

6 files changed

Lines changed: 95 additions & 13 deletions

File tree

data/countries.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,15 +271,26 @@ def can_delete_country(country_code: str) -> tuple[bool, str]:
271271

272272
def delete_country(code: str) -> bool:
273273
"""
274-
Delete a country by its code.
275-
Cascading: Deletes all states and cities in this country first.
274+
Delete a country by its code when no dependent states exist.
276275
"""
277-
if not can_delete_country(code)[0]:
278-
raise ValueError(can_delete_country(code)[1])
276+
can_delete, reason = can_delete_country(code)
277+
if not can_delete:
278+
raise ValueError(reason)
279279

280+
result = dbc.delete(COUNTRIES_COLLECT, {COUNTRY_CODE: code})
281+
if result > 0:
282+
country_by_code_cache.invalidate(code.upper())
283+
return True
284+
return False
285+
286+
287+
def delete_country_cascade(code: str) -> bool:
288+
"""
289+
Delete a country and any dependent states/cities.
290+
"""
280291
states.delete_states_by_country(code)
281292

282-
result = dbc.delete(COUNTRIES_COLLECT, {COUNTRY_CODE: code})
293+
result = dbc.delete(COUNTRIES_COLLECT, {COUNTRY_CODE: code.upper()})
283294
if result > 0:
284295
country_by_code_cache.invalidate(code.upper())
285296
return True

data/states.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,16 +220,26 @@ def can_delete_state(state_code: str) -> tuple[bool, str]:
220220

221221
def delete_state(code: str) -> bool:
222222
"""
223-
Delete a state by its code.
224-
Cascading: Deletes all cities in this state first.
223+
Delete a state by its code when no dependent cities exist.
225224
"""
226225
can_delete, reason = can_delete_state(code)
227226
if not can_delete:
228227
raise ValueError(reason)
229228

229+
result = dbc.delete(STATES_COLLECT, {STATE_CODE: code})
230+
if result > 0:
231+
state_by_code_cache.invalidate(code.upper())
232+
return True
233+
return False
234+
235+
236+
def delete_state_cascade(code: str) -> bool:
237+
"""
238+
Delete a state and any dependent cities.
239+
"""
230240
cities.delete_cities_by_state(code)
231241

232-
result = dbc.delete(STATES_COLLECT, {STATE_CODE: code})
242+
result = dbc.delete(STATES_COLLECT, {STATE_CODE: code.upper()})
233243
if result > 0:
234244
state_by_code_cache.invalidate(code.upper())
235245
return True

server/countries_endpoints.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,11 +426,22 @@ def put(self, country_code):
426426
)
427427
def delete(self, country_code):
428428
"""
429-
Delete a country
430-
Removes the country from the database if no dependent states exist.
429+
Delete a country.
430+
By default this fails if dependent states exist.
431+
Pass ?cascade=true to remove dependent states and cities first.
431432
"""
433+
cascade = request.args.get("cascade", "false").lower() in {
434+
"1",
435+
"true",
436+
"yes",
437+
"on",
438+
}
439+
432440
try:
433-
success = countries_data.delete_country(country_code.upper())
441+
if cascade:
442+
success = countries_data.delete_country_cascade(country_code.upper())
443+
else:
444+
success = countries_data.delete_country(country_code.upper())
434445
except ValueError as e:
435446
countries_ns.abort(HTTPStatus.CONFLICT, str(e))
436447
except Exception as e:

server/states_endpoints.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,22 @@ def put(self, state_code: str):
326326
)
327327
def delete(self, state_code: str):
328328
"""
329-
Delete a state by its code. Fails with 409 if dependent cities exist.
329+
Delete a state by its code.
330+
By default this fails with 409 if dependent cities exist.
331+
Pass ?cascade=true to remove dependent cities first.
330332
"""
333+
cascade = request.args.get("cascade", "false").lower() in {
334+
"1",
335+
"true",
336+
"yes",
337+
"on",
338+
}
339+
331340
try:
332-
success = states_data.delete_state(state_code.upper())
341+
if cascade:
342+
success = states_data.delete_state_cascade(state_code.upper())
343+
else:
344+
success = states_data.delete_state(state_code.upper())
333345
except ValueError as e:
334346
states_ns.abort(HTTPStatus.CONFLICT, str(e))
335347
except Exception as e:

server/tests/test_countries_endpoints.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,26 @@ def test_delete_country_with_dependent_states(self, client):
221221
data = json.loads(response.data)
222222
assert '5 state' in data['message'].lower()
223223

224+
def test_delete_country_with_cascade_success(self, client):
225+
"""DELETE /countries/{code}?cascade=true uses cascading delete."""
226+
with patch('data.countries.delete_country_cascade') as mock_delete:
227+
mock_delete.return_value = True
228+
229+
response = client.delete('/countries/US?cascade=true')
230+
231+
assert response.status_code == HTTPStatus.NO_CONTENT
232+
mock_delete.assert_called_once_with('US')
233+
234+
def test_delete_country_with_cascade_not_found(self, client):
235+
"""DELETE /countries/{code}?cascade=true returns 404 when missing."""
236+
with patch('data.countries.delete_country_cascade') as mock_delete:
237+
mock_delete.return_value = False
238+
239+
response = client.delete('/countries/XX?cascade=true')
240+
241+
assert response.status_code == HTTPStatus.NOT_FOUND
242+
mock_delete.assert_called_once_with('XX')
243+
224244
def test_get_countries_by_continent_success(self, client):
225245
"""Test successful retrieval of countries by continent."""
226246
with patch('data.countries.get_countries_by_continent') as mock_get:

server/tests/test_states_endpoints.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,24 @@ def test_delete_state_with_dependent_cities(self, client):
200200
data = resp.get_json()
201201
assert 'depend' in data['message'].lower()
202202

203+
def test_delete_state_with_cascade_success(self, client):
204+
with patch('data.states.delete_state_cascade') as mock_delete:
205+
mock_delete.return_value = True
206+
207+
resp = client.delete('/states/NY?cascade=true')
208+
209+
assert resp.status_code == HTTPStatus.NO_CONTENT
210+
mock_delete.assert_called_once_with('NY')
211+
212+
def test_delete_state_with_cascade_not_found(self, client):
213+
with patch('data.states.delete_state_cascade') as mock_delete:
214+
mock_delete.return_value = False
215+
216+
resp = client.delete('/states/ZZ?cascade=true')
217+
218+
assert resp.status_code == HTTPStatus.NOT_FOUND
219+
mock_delete.assert_called_once_with('ZZ')
220+
203221
def test_get_state_by_name_success(self, client):
204222
"""Test successful retrieval of state by name."""
205223
with patch('data.states.get_state_by_name') as mock_get:

0 commit comments

Comments
 (0)