Skip to content

Add Django-inspired ORM for Cloudflare D1 and update handlers#30

Open
Copilot wants to merge 2 commits intomainfrom
copilot/develop-secure-orm
Open

Add Django-inspired ORM for Cloudflare D1 and update handlers#30
Copilot wants to merge 2 commits intomainfrom
copilot/develop-secure-orm

Conversation

Copy link
Contributor

Copilot AI commented Feb 24, 2026

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.py

  • QuerySet — immutable, chainable query builder; every method returns a new instance
  • Model — base class; subclasses declare only table_name
  • Security: user-supplied values are always bound parameters (?); field names embedded in SQL are validated against a safe-character allowlist ([a-zA-Z0-9_]) — unsafe names raise ValueError before SQL is built
# Before – repeated boilerplate
result = await db.prepare('SELECT * FROM domains WHERE id = ?').bind(domain_id).first()
domain = result.to_py() if hasattr(result, 'to_py') else dict(result)

# After – ORM handles conversion and parameterisation
domain = await Domain.objects(db).get(id=domain_id)

# Chainable queries
rows = await Bug.objects(db).filter(status='open', domain=5).order_by('-created').paginate(2, 20).all()
total = await User.objects(db).filter(is_active=1).count()
await User.objects(db).filter(id=user_id).update(is_active=True)

New: src/models.py

Thin model definitions (Domain, Bug, User, Tag, DomainTag, BugScreenshot, BugTag, UserFollow, UserBugUpvote, UserBugSave, UserBugFlag) — each declares only table_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 COUNT
  • auth.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 SQL

New: tests/test_orm.py

63 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.

Co-authored-by: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com>
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 24, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

Copilot AI changed the title [WIP] Develop safe and secure ORM based on Django Add Django-inspired ORM for Cloudflare D1 and update handlers Feb 24, 2026
@DonnieBLT DonnieBLT marked this pull request as ready for review February 24, 2026 06:58
Copilot AI review requested due to automatic review settings February 24, 2026 06:58
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py with QuerySet (chainable query builder) and Model (base class) providing Django-like API (filter, exclude, order_by, paginate, etc.)
  • Added src/models.py defining 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.

Comment on lines +93 to +95
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()
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +182 to +196
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
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

# ---------------------------------------------------------------------------
# Allowed characters for field / table identifiers embedded in SQL.
# Only lowercase letters, digits and underscore are permitted.
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# Only lowercase letters, digits and underscore are permitted.
# Only letters (uppercase and lowercase), digits and underscore are permitted.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants