Add a data store
The claims and feedback stores are ports with two adapters each, D1 and Neon. A third store is two adapter files, a migration folder and two lines of selection logic. Nothing outside api/src/claims/ and api/src/feedback/ changes.
1. Schema
Create db/migrations/<store>/ with the same numbered migrations as the others, written for the new dialect:
0001_init.sql: theclaimstable:id(primary key),line_of_business,scenario,status,title,data(the whole document),created_at,updated_at, with indexes on the three filter columns.0002_feedback.sql: thefeedbacktable keyed byturn_id, withrating,capability,claim_id,prompt_fingerprint,channel,surface,data, and timestamps.
Store data in the database's native JSON type if it has one, so filters such as currency and the showcase list can be pushed into the query.
2. The claims adapter
api/src/claims/<store>.ts, implementing ClaimRepository:
| Method | Must |
|---|---|
list(filter) | Filter by line, data.currency, scenario and status; search id, title and claimant name case-insensitively; order by id; page with clampLimit(filter.limit) and offset; return the total of all matches. |
get(id) | Return Claim.parse(document) or null. Always parse: that is where defaults are applied. |
upsert(claims) | Insert or replace by id, keeping the denormalised columns in step with the document. Batch where the store allows. |
scenarios() | Counts grouped by line of business and scenario, as numbers. |
randomId(filter) | A random id within the filter, optionally one whose metadata.demo.showcases contains filter.capability. |
Use the existing adapters as the template: they build the where clause once and share it between list, count and random.
3. The feedback adapter
api/src/feedback/<store>.ts, implementing FeedbackRepository:
record(entry): upsert by turn id; optional fields stored asnull, neverundefined.remove(turnId): delete; deleting a vote that does not exist is not an error.report(filter): totals; tallies by capability; tallies by capability and prompt fingerprint (with the overridden ids, parsed throughparseOverridden); recent items filtered by rating and capability, newest first, capped byclampFeedbackLimit. Convert timestamps withtoIso.
4. Select it
Add the store to DatabaseProvider and both factories:
case 'mystore':
if (!env.MYSTORE_URL) throw new Error('DATABASE_PROVIDER is "mystore" but MYSTORE_URL is not set.');
return makeMyStoreRepository(env.MYSTORE_URL);
Do the same in makeFeedbackRepository, add the variables to Env, and document them.
5. The CLI
api/scripts/lib/stores.ts is the CLI's view of a store. Add a target if the generator and copy-claims should reach it. If the Worker adapter can run in Node, reuse it as the Neon target does.
6. Test it
Follow the existing tests in api/src/claims/claims.test.ts and api/src/feedback/feedback.test.ts:
- If the store can run in-process (as SQLite does for D1), test against the real thing with the real migrations.
- If it cannot, mock the driver and assert on the SQL text and the parameter order, which is where adapters go wrong.
- Cover filters, paging totals, upsert-replaces, the showcase filter, and
nullhandling for optional feedback fields.
Then run the API integration test against it if you can, and update Data stores.