Repository navigation
feat(api): GET /v1/offers and /v1/offers/{id} with filters, cursor pagination, envelope and KV cache (#19) - #76
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #19.
What changed
offersCratefield module (crates/tokker-api/src/offers.rs), registered in the Worker next toTokkerApi. Cratefield mounts each module at/v1/<name>, andTokkerApiis namedtokker, so/v1/offersneeds its own module. It declares theDbandKeyValueports.GET /v1/offerstakesmodel,provider,creator,provider_type,open_weights,max_input,max_output,currency,stale,sort=blended|input|output(defaultblended, the 3:1 mix),cursorandlimit(1–200, default 50).max_*filters compare the USD columns. A row with no USD price is excluded from them.GET /v1/offers/{provider}/{model}returns one row.jsoncolumn from D1, soprovenance,last_verified_atand the raw native andusdprices are always present."unknown"stays the string"unknown", andserde_json'sfloat_roundtripis enabled so numbers come back unchanged.staleandstale_sinceare stamped at read time by the existingfreshness::staleness(Freshness SLAs and stale flags: computed in the build and returned on every row #18).{as_of, source_commit, license, data: [...], next_cursor}; the item response is{as_of, source_commit, license, data: {...}}.as_ofis the latestlast_verified_atacross offers and plans.source_commitfalls back to"unknown"when the mirror hasn't written it.licensefalls back to the dataset'slicensestring. A test pins that fallback todata/pricing.json.detailand in aparameterextension. An unknown id returns 404 naming the id.crates/tokker-api/src/offers_query.rs) is pure code.{sort, value, id}.ORDER BY col IS NULL, col, id(unpriced rows last, id breaks ties), and the Rust cursor comparison follows the same order. A cursor minted under anothersortis a 400.offers:v1:<generation>:<today>:<canonical query | id>. Only 200 responses are cached, with a 1-hour TTL.pub async fn invalidate_cache(kv, dataset) -> Result<(), KvError>is the hook for the mirror's diff step. It writes the loaded dataset's identity (its source commit) as the generation, so it is idempotent and has no read-modify-write race. It returns an error the loader should treat as a failed load.migrations/0002_dataset_meta.sqladds adataset_meta(key, value)table for the mirror to fill withsource_commitandlicense.docs/plan.md§3 now lists the full parameter set and briefly describes the contract.sea-query(the Db port's parameter type). New dev-dependencies:cratefield-adapter-sqlite = "=0.3.0",tokio,tower,async-trait.How it was verified
cargo fmt --all -- --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace(41 passed),RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace,cargo build -p tokker-worker --target wasm32-unknown-unknown.cargo tree -d --depth 0 | grep cratefieldprints nothing.crates/tokker-api/tests/offers_routes.rs) run the real Harness router withoneshot.migrations/*.sql. It also mimics D1 returning a whole-number REAL as an integer.cache_write_per_mtok: "unknown"round-tripping unchanged, in both the list and the item routewrangler dev --local(wrangler 4.151.1) with local D1 seeded fromdata/pricing.json(480 offers, 103 plans):sort=input, cursor follow-up, item, 404 problem+json, 400 forlimit=500.xai/grok-4.7comes back withcache_write_per_mtok: "unknown"as a string./v1/offers7.8 ms,?sort=input&limit=57.7 ms, the item route 7.2 ms,?limit=200(337 KB) 8.3 ms. The target is 50 ms. Uncached D1 requests took 35–111 ms.Worth a close look
offers.rs607,offers_query.rs656 including its unit tests,tests/offers_routes.rs1,020, migration 19), plus 253 lines in tracked files, 223 of themCargo.lock. Most of it is the route and parser tests the acceptance criteria ask for. The tests were consolidated after review (table-driven 400 cases, shared setup, one parameterised cache test).Cache-Control. cratefield-core 0.8.0 forcesCache-Control: no-storeon every/v1/*response (security_headers_layerinsrc/http.rs), and modules can't opt out. So there is no shared-cache header; caching is server-side KV only. Changing that would be an upstream Cratefield/harness change.stalefilter and keyset paging run in Rust after the SQL fetch, because staleness depends onfetch_recipe.volatility, which has no D1 column. Fine at 480 rows; if the table grows a lot, push the cursor andLIMITinto SQL, which needs avolatilitycolumn.dataset_metaand callsinvalidate_cacheis upstream work (plan §5.2 G1) and doesn't exist yet. Until it does, the envelope serves the fallbacks and cache entries roll over only by TTL or day.offers_query.rs, so the wasm build pulls in no extra crate.migrations/0002_dataset_meta.sqlstill needs a remote D1 migration when an owner deploys.Provenance
🤖 Generated by Colonizer in a microVM
Co-Authored-By: Colonizer Settlers 331648616+colonizer-settlers@users.noreply.github.com