11"""Flask JSON API for the notes app, backed directly by D1."""
22
3+ from asyncio import get_event_loop
4+
35from flask import Flask , g , jsonify , request
46from werkzeug .exceptions import HTTPException
57
6- from d1 import D1 , D1Error
7-
88app = Flask (__name__ )
99# Preserve our own key ordering in JSON responses rather than sorting them.
1010app .json .sort_keys = False
1414VALID_FILTERS = ("all" , "active" , "done" )
1515
1616
17- def get_db () -> D1 :
17+ class D1Error (RuntimeError ):
18+ """A D1 query failed. Wraps the underlying error message."""
19+
20+
21+ def get_db ():
1822 """Return the D1 wrapper for the current request."""
1923 if "db" not in g :
2024 # Cache on g for the lifetime of the request
21- g .db = D1 ( request .environ ["workers.env" ].DB )
25+ g .db = request .environ ["workers.env" ].DB
2226 return g .db
2327
2428
29+ def run_db (task ):
30+ try :
31+ return get_event_loop ().run_until_complete (task )
32+ except Exception as exc :
33+ # D1 surfaces SQL errors as JS exceptions; re-raise as a Python error so
34+ # we can register a custom error handler.
35+ raise D1Error (f"{ exc } " ) from exc
36+
37+
2538# --------------------------------------------------------------------------
2639# Serialization / validation
2740# --------------------------------------------------------------------------
@@ -117,13 +130,15 @@ def list_notes():
117130
118131 sql = sql .format (where = where )
119132
120- rows = get_db ().query (sql , params )
121- return jsonify ({"notes" : [serialize (r ) for r in rows ]})
133+ rows = run_db ( get_db ().prepare (sql ). bind ( * params ). all ())[ "results" ]
134+ return jsonify ({"notes" : [serialize (dict ( r ) ) for r in rows ]})
122135
123136
124137@app .get ("/api/notes/<int:note_id>" )
125138def get_note (note_id : int ):
126- row = get_db ().first ("SELECT * FROM notes WHERE id = ?" , (note_id ,))
139+ row = run_db (
140+ get_db ().prepare ("SELECT * FROM notes WHERE id = ?" ).bind (note_id ).first ()
141+ )
127142 if row is None :
128143 return jsonify ({"error" : "Note not found." }), 404
129144 return jsonify (serialize (row ))
@@ -138,9 +153,13 @@ def create_note():
138153 body = _clean_body (data .get ("body" , "" ))
139154
140155 # RETURNING to insert and read the stored row in a single round trip.
141- row = get_db ().first (
142- "INSERT INTO notes (title, body) VALUES (?, ?) RETURNING *" ,
143- (title , body ),
156+ row = run_db (
157+ get_db ()
158+ .prepare (
159+ "INSERT INTO notes (title, body) VALUES (?, ?) RETURNING *" ,
160+ )
161+ .bind (title , body )
162+ .first ()
144163 )
145164 return jsonify (serialize (row )), 201
146165
@@ -169,9 +188,11 @@ def update_note(note_id: int):
169188 # The f-string interpolates the updates fragments, every one of which is a
170189 # hardcoded literal from the branches above so this expands to one of seven
171190 # fixed statements. Values are not interpolated.
172- row = get_db ().first (
173- f"UPDATE notes SET { ', ' .join (updates )} WHERE id = ? RETURNING *" ,
174- params ,
191+ row = run_db (
192+ get_db ()
193+ .prepare (f"UPDATE notes SET { ', ' .join (updates )} WHERE id = ? RETURNING *" )
194+ .bind (* params )
195+ .first ()
175196 )
176197 if row is None :
177198 return jsonify ({"error" : "Note not found." }), 404
@@ -180,7 +201,9 @@ def update_note(note_id: int):
180201
181202@app .delete ("/api/notes/<int:note_id>" )
182203def delete_note (note_id : int ):
183- meta = get_db ().run ("DELETE FROM notes WHERE id = ?" , (note_id ,))
204+ meta = run_db (
205+ get_db ().prepare ("DELETE FROM notes WHERE id = ?" ).bind (note_id ).run ()
206+ )["meta" ]
184207 if not meta .get ("changes" ):
185208 return jsonify ({"error" : "Note not found." }), 404
186209 return "" , 204
@@ -189,14 +212,14 @@ def delete_note(note_id: int):
189212@app .delete ("/api/notes/delete_completed" )
190213def delete_completed ():
191214 """Bulk-delete every completed note."""
192- meta = get_db ().run ("DELETE FROM notes WHERE done = 1" )
215+ meta = run_db ( get_db ().prepare ("DELETE FROM notes WHERE done = 1" ). run ())[ "meta" ]
193216 return jsonify ({"deleted" : meta .get ("changes" , 0 )})
194217
195218
196219@app .get ("/api/health" )
197220def health ():
198221 """Confirms the Worker is up and the D1 binding actually responds."""
199- get_db ().first ("SELECT 1 AS ok" )
222+ run_db ( get_db ().prepare ("SELECT 1 AS ok" ). first () )
200223 return jsonify ({"status" : "ok" })
201224
202225
0 commit comments