Skip to main content

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: the claims table: 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: the feedback table keyed by turn_id, with rating, 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:

MethodMust
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 as null, never undefined.
  • 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 through parseOverridden); recent items filtered by rating and capability, newest first, capped by clampFeedbackLimit. Convert timestamps with toIso.

4. Select it​

Add the store to DatabaseProvider and both factories:

api/src/claims/repository.ts
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 null handling for optional feedback fields.

Then run the API integration test against it if you can, and update Data stores.