Add Django-inspired ORM for Cloudflare D1 and update handlers#30
Add Django-inspired ORM for Cloudflare D1 and update handlers#30
Conversation
Co-authored-by: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
blt-api | 81a4a86 | Commit Preview URL Branch Preview URL |
Feb 24 2026, 06:51 AM |
There was a problem hiding this comment.
Pull request overview
This PR introduces a Django-inspired ORM layer for Cloudflare D1 (SQLite) to replace scattered raw SQL queries with a centralized, type-safe query abstraction. The ORM provides parameterized query building with comprehensive SQL injection protection through identifier validation and bound parameters.
Changes:
- Added
src/libs/orm.pywithQuerySet(chainable query builder) andModel(base class) providing Django-like API (filter, exclude, order_by, paginate, etc.) - Added
src/models.pydefining 12 model classes mapping to D1 tables (Domain, Bug, User, Tag, and junction tables) - Updated handlers (auth.py, users.py, domains.py, bugs.py) to use ORM for simple queries while keeping raw SQL for complex JOINs
- Added comprehensive test suite (
tests/test_orm.py) with 63 tests covering SQL generation, security, and async operations
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/libs/orm.py |
Core ORM implementation with QuerySet builder, Model base class, identifier validation, and result conversion |
src/models.py |
Thin model definitions for all database tables (domains, bugs, users, tags, junction tables) |
tests/test_orm.py |
Comprehensive test suite covering query building, SQL injection prevention, and async executors |
src/handlers/users.py |
Migrated to ORM for list/get/profile/bugs/domains; retained raw SQL for follower/following JOINs |
src/handlers/domains.py |
Migrated to ORM for list/detail endpoints; retained raw SQL for tags JOIN query |
src/handlers/bugs.py |
Uses ORM for count queries; retained raw SQL for multi-table JOIN display query |
src/handlers/auth.py |
Migrated signup (User.create), signin (filter), and email verification (update) to ORM |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| existing_user = await User.objects(db).filter(username=body["username"]).first() | ||
| if not existing_user: | ||
| existing_user = await User.objects(db).filter(email=body["email"]).first() |
There was a problem hiding this comment.
The username/email existence check now uses two sequential queries instead of a single query with OR. The old code used SELECT id FROM users WHERE username = ? OR email = ? but the new code performs two separate queries. This could impact performance during signup, though the impact is minimal since signups are typically infrequent. Consider adding OR support to the ORM in the future to optimize this pattern.
| def limit(self, n: int) -> "QuerySet": | ||
| """Limit the number of rows returned.""" | ||
| if not isinstance(n, int) or n < 0: | ||
| raise ValueError("limit() requires a non-negative integer.") | ||
| qs = self._clone() | ||
| qs._limit_val = n | ||
| return qs | ||
|
|
||
| def offset(self, n: int) -> "QuerySet": | ||
| """Skip the first *n* rows.""" | ||
| if not isinstance(n, int) or n < 0: | ||
| raise ValueError("offset() requires a non-negative integer.") | ||
| qs = self._clone() | ||
| qs._offset_val = n | ||
| return qs |
There was a problem hiding this comment.
The limit() and offset() methods validate that values are non-negative integers, but there are no tests in test_orm.py to verify that ValueError is raised for invalid inputs (e.g., negative numbers or non-integers). Consider adding test cases like test_limit_negative_raises() and test_offset_non_integer_raises() to ensure proper error handling.
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Allowed characters for field / table identifiers embedded in SQL. | ||
| # Only lowercase letters, digits and underscore are permitted. |
There was a problem hiding this comment.
The comment states "Only lowercase letters, digits and underscore are permitted" but the actual frozenset on line 37 includes both lowercase and uppercase letters (a-z and A-Z). Update the comment to accurately reflect that uppercase letters are also allowed.
| # Only lowercase letters, digits and underscore are permitted. | |
| # Only letters (uppercase and lowercase), digits and underscore are permitted. |
Raw SQL was scattered across handlers with repeated D1 result-conversion boilerplate and no centralized query abstraction. This adds a lightweight Django-style ORM layer that centralises parameterisation and identifier validation.
New:
src/libs/orm.pyQuerySet— immutable, chainable query builder; every method returns a new instanceModel— base class; subclasses declare onlytable_name?); field names embedded in SQL are validated against a safe-character allowlist ([a-zA-Z0-9_]) — unsafe names raiseValueErrorbefore SQL is builtNew:
src/models.pyThin model definitions (
Domain,Bug,User,Tag,DomainTag,BugScreenshot,BugTag,UserFollow,UserBugUpvote,UserBugSave,UserBugFlag) — each declares onlytable_name.Updated handlers
domains.py— list and detail endpoints use ORM; tags endpoint keeps raw parameterized SQL (JOIN)users.py— list, get, profile, bugs, domains use ORM; follower/following list queries retain raw SQL for the JOIN but use ORM for COUNTauth.py— signup (User.create()), signin (.filter(username=...)), email-verify (.update(is_active=True))bugs.py— count uses ORM; the multi-JOIN display query keeps raw parameterized SQLNew:
tests/test_orm.py63 unit tests covering all lookup operators, SQL-injection prevention (malicious field names rejected; malicious values confirmed to stay in bind params, never in SQL text), async executors, and
Model.create/update_by_id.💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.