Skip to content

Dev - #38

Merged
andesvel merged 22 commits into
mainfrom
dev
Sep 23, 2026
Merged

Dev#38
andesvel merged 22 commits into
mainfrom
dev

Conversation

@andesvel

Copy link
Copy Markdown
Owner

This pull request adds a comprehensive repository test suite for the data-access layer in app/repository/, introduces new repository functions for click tracking, and documents the Nuto project with a detailed technical overview. The main themes are improved test coverage, new repository features, and enhanced documentation.

Repository test coverage:

  • Added full integration tests for the urls, clicks, users, and url-cache repositories, using real D1 and KV bindings via miniflare. These tests cover all CRUD operations, privacy guarantees, and edge cases for each repository module. [1] [2] [3] [4]

Repository features:

  • Implemented new repository functions insertClick and updateLastClicked in app/repository/clicks.ts to support privacy-safe click logging and last-click timestamp updates.

Documentation:

  • Added AGENTS.md, a technical project overview that explains the architecture, tech stack, backend conventions, security/privacy rules, and known issues for Nuto. This serves as onboarding and reference documentation for contributors and AI assistants.

Consolidates the redirect-loop detection logic that api.links.tsx and
redirect.tsx each implement privately (extractSlugOnSameHostFromLongUrl /
extractSlugOnSameHost and createsCycleAtPersist / createsCycle).

The walk algorithm is extracted verbatim into createsCycle(); the DB
fetch is injected as a lookup callback so each route keeps its own
error-handling semantics. extractSlugOnSameHost() is exported for
upcoming unit tests.
…tems

Document Nuto's architecture (React Router v7 loaders/actions as the
backend, D1 + KV, Clerk), the security invariants verified against the
code (no IP storage in clicks, svix-verified webhooks, parameterized
queries), and known pending items that require discussion before any
change (unsalted SHA-256 link passwords, dead dashboard action,
user.updated binding undefined).
Drop the private extractSlugOnSameHostFromLongUrl and
createsCycleAtPersist copies in favor of utils/cycle-detection.
The DB fetch becomes a getLongUrlBySlug helper passed as the lookup
callback; it keeps propagating errors to handleCreate/handleUpdate's
existing catch blocks, preserving the endpoints' observable behavior.
Drop the private extractSlugOnSameHost and createsCycle copies in
favor of utils/cycle-detection. getLongUrlBySlug stays local as the
lookup callback and keeps swallowing DB errors (returning null), so
the redirect proceeds as before on lookup failures.

The 'context as unknown as AppLoadContext' cast in the action's call
site is no longer needed: the callback captures context directly.
app/repository/urls.ts: urls-table queries (existence check, redirect
lookups, ownership checks, dashboard listing, insert/update/delete)
moved verbatim from api.links.tsx, redirect.tsx and dashboard.tsx.

app/repository/clicks.ts: click logging statements from redirect.tsx.

app/repository/users.ts: users-table statements from webhooks.clerk.tsx.
updateUser keeps accepting subscriptionPlan: undefined — the existing
bind(undefined) behavior is preserved and remains a pending item.

app/repository/url-cache.ts: URL_STORE read/write/delete with the same
JSON entry shape and raw-string fallback used by redirect.tsx.

No route uses these yet; wiring comes in the following commits.
handleGetLink, handleCreate, handleUpdate and handleDelete now
orchestrate app/repository/urls.ts and url-cache.ts instead of calling
DB.prepare / URL_STORE directly. shortCodeTaken keeps its DB-first,
KV-second ordering, and the create path still writes the same KV entry
(with storedHash) and expiration options. Queries and observable
behavior are unchanged.
The loader and action now orchestrate app/repository/urls.ts,
clicks.ts and url-cache.ts instead of calling DB.prepare / URL_STORE
directly. KV hits resolve through getRedirectEntry with the same
JSON-entry parsing and raw-string fallback; the DB fallback keeps the
expiry check, cleanup delete and 30-day KV repopulation with
storedHash. Cycle-check lookups keep swallowing errors via .catch so
redirects proceed as before on lookup failures.
user.created / user.updated / user.deleted now call
app/repository/users.ts. The event payload is still verified with
svix before any DB write. user.updated keeps binding undefined for
subscription_plan (the known runtime issue stays a pending item);
queries and observable behavior are unchanged.
The loader's links query moves to listActiveLinksForUser; the (dead,
pending-removal) action's insert moves to insertLinkWithCreatedAt.
Both keep their exact SQL and behavior — including the action storing
the raw password, which stays a pending item. Wiring dashboard keeps
every loader/action compliant with the repository-layer constraint.
Add app/repository/ to the structure tree and mention the cycle
detection module under utils/ now that both exist.
Cover extractSlugOnSameHost (same-host extraction, port stripping,
scheme prepending, case-insensitive hostname comparison, null/invalid
inputs) and createsCycle (self, two-link and longer cycles, unknown
slugs, off-host exits, maxDepth cap, lookup interaction).

The host-header port cases act as the regression guard for
host:port parsing: any mutation that breaks calling split (instead
of indexing it) or changes the port-stripping line fails these
tests before reaching production.
Integration tests against real D1 (miniflare via cloudflare:test):
insert/lookup round-trips, password and expiry fields, ownership
checks, click-counted details, dashboard listing (active-only,
per-user), rename, field update and delete.
insertClick persists only the privacy-safe columns (url_id,
clicked_at, country, user_agent) — asserted by checking the exact
column set of the stored row, guarding the no-IP invariant.
updateLastClicked stamps urls.last_clicked.
insertUser / updateUser / deleteUserById against real D1. updateUser
is tested on the valid-plan path only; the undefined subscription_plan
bind is the known pending runtime issue and is not codified here.
KV round-trip with storedHash, cache miss, and the two legacy
fallbacks (raw non-JSON value, JSON without longUrl) that the
redirect loader relies on, plus delete.
Three findings from the refactor, all awaiting discussion before any
change: the update path writes KV entries without storedHash which
lets the loader's password wall be skipped on KV hits; the unused
GET /api/links loader exposes the stored SHA-256 hash; and the Clerk
webhook logs user emails (PII) with observability enabled.
Reorganize into the standard context format: project overview, tech
stack, structure, key commands, git workflow, environment setup,
backend conventions, security notes, known issues, important files,
build/deploy, and notes for AI assistants.

Phase-plan conditionals are gone: the repository-layer rule is now
unconditional (the layer landed on this stack). Known issues keep
the security findings that still await a decision, without line
numbers so they survive future edits. Git workflow rules from this
session are documented as standing rules.
Add CLAUDE.md: project structure, verified constraints, and pending items
…tion

Consolidate cycle detection into a shared utils module
Extract repository layer for D1 and KV data access
Add cycle-detection and repository tests; rename CLAUDE.md to AGENTS.md
@andesvel
andesvel merged commit c947f20 into main Sep 23, 2026
4 checks passed
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.

1 participant