From f9a8ac360d65fbd1b971123df7f0bc83d421b179 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:47:02 +0100 Subject: [PATCH] add lexicons --- .gitignore | 4 +- AGENTS.md | 61 ++++++++ README.md | 57 ++++++- .../app/blento/card/getRecord.json | 66 ++++++++ .../app/blento/card/getRecords.json | 97 ++++++++++++ .../app/blento/card/getStats.json | 35 +++++ .../app/blento/card/getUsers.json | 61 ++++++++ .../lexicon/calendar/event/getRecord.json | 73 +++++++++ .../lexicon/calendar/event/getRecords.json | 120 +++++++++++++++ .../lexicon/calendar/event/getStats.json | 35 +++++ .../lexicon/calendar/event/getUsers.json | 61 ++++++++ .../lexicon/calendar/rsvp/getRecord.json | 67 +++++++++ .../lexicon/calendar/rsvp/getRecords.json | 106 +++++++++++++ .../lexicon/calendar/rsvp/getStats.json | 35 +++++ .../lexicon/calendar/rsvp/getUsers.json | 61 ++++++++ .../contrail/admin/getCursor.json | 27 ++++ .../contrail/admin/getOverview.json | 51 +++++++ lexicons-generated/contrail/admin/sync.json | 47 ++++++ lexicons-pulled/README.md | 5 + .../community/lexicon/calendar/event.json | 142 ++++++++++++++++++ .../community/lexicon/calendar/rsvp.json | 43 ++++++ .../community/lexicon/location/address.json | 40 +++++ .../community/lexicon/location/fsq.json | 28 ++++ .../community/lexicon/location/geo.json | 27 ++++ .../community/lexicon/location/hthree.json | 22 +++ src/core/types.ts | 3 +- wrangler.jsonc | 39 ++--- 27 files changed, 1388 insertions(+), 25 deletions(-) create mode 100644 AGENTS.md create mode 100644 lexicons-generated/app/blento/card/getRecord.json create mode 100644 lexicons-generated/app/blento/card/getRecords.json create mode 100644 lexicons-generated/app/blento/card/getStats.json create mode 100644 lexicons-generated/app/blento/card/getUsers.json create mode 100644 lexicons-generated/community/lexicon/calendar/event/getRecord.json create mode 100644 lexicons-generated/community/lexicon/calendar/event/getRecords.json create mode 100644 lexicons-generated/community/lexicon/calendar/event/getStats.json create mode 100644 lexicons-generated/community/lexicon/calendar/event/getUsers.json create mode 100644 lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json create mode 100644 lexicons-generated/community/lexicon/calendar/rsvp/getRecords.json create mode 100644 lexicons-generated/community/lexicon/calendar/rsvp/getStats.json create mode 100644 lexicons-generated/community/lexicon/calendar/rsvp/getUsers.json create mode 100644 lexicons-generated/contrail/admin/getCursor.json create mode 100644 lexicons-generated/contrail/admin/getOverview.json create mode 100644 lexicons-generated/contrail/admin/sync.json create mode 100644 lexicons-pulled/README.md create mode 100644 lexicons-pulled/community/lexicon/calendar/event.json create mode 100644 lexicons-pulled/community/lexicon/calendar/rsvp.json create mode 100644 lexicons-pulled/community/lexicon/location/address.json create mode 100644 lexicons-pulled/community/lexicon/location/fsq.json create mode 100644 lexicons-pulled/community/lexicon/location/geo.json create mode 100644 lexicons-pulled/community/lexicon/location/hthree.json diff --git a/.gitignore b/.gitignore index 6180cc9..0d2b6de 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,7 @@ node_modules .wrangler dist -# Generated +# Generated (internal only) lex.config.js -lexicons-pulled/ -lexicons-generated/ src/lexicon-types/ src/core/queryable.generated.ts \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c85958e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,61 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What is Contrail? + +Contrail is an AT Protocol (Bluesky) data indexer that runs on Cloudflare Workers + D1. You define collections in `src/config.ts`, and it automatically handles Jetstream ingestion, PDS backfill, user discovery via relays, and serves typed XRPC query endpoints. + +## Commands + +```bash +pnpm install # Install dependencies +pnpm dev # Start wrangler dev server (port 8787) +pnpm dev:auto # Start dev with auto-ingestion every 60s +pnpm generate # Regenerate lexicons + types from config +pnpm generate:pull # Pull lexicons from network, detect fields, generate types +pnpm typecheck # Run tsc --noEmit +pnpm ingest # Manually trigger ingestion cycle (requires dev server running) +pnpm sync # Discover users + backfill records from PDS (requires dev server) +pnpm deploy # Deploy to Cloudflare Workers +``` + +## Architecture + +### Entry points + +- **`src/adapters/worker.ts`** — Cloudflare Worker entry point. Handles HTTP fetch (Hono app) and scheduled cron (ingestion cycle). +- **`src/adapters/sqlite.ts`** — Local SQLite adapter wrapping better-sqlite3 to match the D1 `Database` interface. Excluded from tsconfig for Worker builds. + +### Core modules (`src/core/`) + +- **`types.ts`** — Central types (`Database`, `ContrailConfig`, `RecordRow`, `IngestEvent`) and config resolution logic. The `Database`/`Statement` interface abstracts over D1 and SQLite. +- **`jetstream.ts`** — Connects to Bluesky Jetstream WebSocket endpoints, collects events, runs full ingest cycles (load cursor → ingest → apply → save cursor → refresh identities). +- **`backfill.ts`** — Two functions: `backfillUser` (pages through a user's PDS via `com.atproto.repo.listRecords`) and `discoverDIDs` (discovers users via `com.atproto.sync.listReposByCollection` from relay endpoints). +- **`identity.ts`** — DID/handle resolution and caching in the `identities` table. +- **`client.ts`** — Creates authenticated AT Protocol XRPC clients for a given DID. +- **`queryable.generated.ts`** — Auto-generated file mapping collections to their queryable fields. **Do not edit manually** — run `pnpm generate`. + +### Database (`src/core/db/`) + +- **`schema.ts`** — Schema initialization: base tables (records, counts, backfills, discovery, cursor, identities) + dynamic indexes derived from queryable fields and relations. +- **`records.ts`** — Core DB operations: cursor management, applying ingest events (upsert/delete + relation count updates), querying records with filters/pagination. + +### Router (`src/core/router/`) + +- **`index.ts`** — Hono app factory with CORS, health checks, admin routes, and per-collection routes. +- **`collection.ts`** — Registers dynamic XRPC endpoints per collection: `getRecords`, `getRecord`, `getUsers`, `getStats`, plus custom queries. +- **`admin.ts`** — Admin endpoints (`sync`, `getCursor`, `getOverview`) protected by `ADMIN_SECRET`. +- **`hydrate.ts`** — Hydration: embedding related records into responses. +- **`profiles.ts`** — Profile resolution: attaching handle + profile data to responses. + +### Code generation (`scripts/generate-lexicons.ts`) + +This script reads `src/config.ts`, auto-detects queryable fields from lexicon JSON files, generates XRPC lexicon definitions under `lexicons-generated/`, updates `lex.config.js`, and writes `src/core/queryable.generated.ts`. The `lex-cli generate` step then produces TypeScript types under `src/lexicon-types/`. + +### Key patterns + +- **Config-driven**: `src/config.ts` is the single source of truth. Adding a collection there drives schema creation, endpoint registration, Jetstream subscriptions, and code generation. +- **Database abstraction**: `Database`/`Statement` interfaces in `types.ts` let the same core logic run against both Cloudflare D1 and local SQLite. +- **Dependent collections**: Collections with `discover: false` (e.g., profiles) only index records for DIDs already known from discoverable collections. +- **Relations**: Materialized counts in the `counts` table, updated on every ingest event. `groupBy` splits counts by a field value. diff --git a/README.md b/README.md index 7348d76..c21b659 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,8 @@ pnpm sync # discover users + backfill records from PDS ### Production ```bash -wrangler d1 create contrail +npx wrangler d1 create contrail # Add database_id to wrangler.toml -wrangler secret put ADMIN_SECRET pnpm deploy # to sync in production, run it locally but set your d1 to remote, then run pnpm sync @@ -138,3 +137,57 @@ All endpoints at `/xrpc/{nsid}.{method}`: # RSVPs for a specific event with profiles /xrpc/community.lexicon.calendar.rsvp.getRecords?subjectUri=at://did:plc:.../community.lexicon.calendar.event/...&profiles=true ``` + +## Typesafe Client Usage + +You can get fully typed XRPC queries for any Contrail instance using [`@atcute/lex-cli`](https://github.com/mary-ext/atcute). The lexicon files are committed to the repo, so you can pull them directly via the git source. + +### Setup + +```bash +npm install @atcute/client @atcute/lexicons @atcute/lex-cli +``` + +Create a `lex.config.js` pointing at the Contrail instance's repo: + +```js +import { defineLexiconConfig } from "@atcute/lex-cli"; + +export default defineLexiconConfig({ + outdir: "src/lexicon-types/", + imports: ["@atcute/atproto"], + pull: { + outdir: "lexicons/", + sources: [ + { + type: "git", + remote: "https://github.com/USER/REPO.git", // the Contrail instance repo + pattern: ["lexicons-generated/**/*.json", "lexicons-pulled/**/*.json"], + }, + ], + }, +}); +``` + +Then pull and generate: + +```bash +npx lex-cli pull && npx lex-cli generate +``` + +### Usage + +Import the generated types (side-effect import registers them with `@atcute/client`), then query with full type safety: + +```ts +import "./lexicon-types/index.js"; // registers ambient types +import { XRPC } from "@atcute/client"; + +const rpc = new XRPC({ handler: /* your handler */ }); + +const { data } = await rpc.get("community.lexicon.calendar.event.getRecords", { + params: { status: "going", limit: 10 }, // typed params +}); + +data.records // typed as Record[] +``` diff --git a/lexicons-generated/app/blento/card/getRecord.json b/lexicons-generated/app/blento/card/getRecord.json new file mode 100644 index 0000000..215b8fa --- /dev/null +++ b/lexicons-generated/app/blento/card/getRecord.json @@ -0,0 +1,66 @@ +{ + "lexicon": 1, + "id": "app.blento.card.getRecord", + "defs": { + "main": { + "type": "query", + "description": "Get a single app.blento.card record by AT URI", + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "AT URI of the record" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "#record" + } + } + }, + "record": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "unknown" + }, + "time_us": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/app/blento/card/getRecords.json b/lexicons-generated/app/blento/card/getRecords.json new file mode 100644 index 0000000..6f78440 --- /dev/null +++ b/lexicons-generated/app/blento/card/getRecords.json @@ -0,0 +1,97 @@ +{ + "lexicon": 1, + "id": "app.blento.card.getRecords", + "defs": { + "main": { + "type": "query", + "description": "Query app.blento.card records with filters", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "did": { + "type": "string", + "format": "did" + }, + "include": { + "type": "string", + "description": "Comma-separated include names" + }, + "min": { + "type": "string", + "description": "Min filter as field:value (range fields or count types). Repeatable." + }, + "max": { + "type": "string", + "description": "Max filter as field:value (range fields). Repeatable." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "type": "array", + "items": { + "type": "ref", + "ref": "#record" + } + }, + "cursor": { + "type": "string" + } + } + } + } + }, + "record": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "unknown" + }, + "time_us": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/app/blento/card/getStats.json b/lexicons-generated/app/blento/card/getStats.json new file mode 100644 index 0000000..7202b82 --- /dev/null +++ b/lexicons-generated/app/blento/card/getStats.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "app.blento.card.getStats", + "defs": { + "main": { + "type": "query", + "description": "Get stats for app.blento.card", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "collection", + "unique_users", + "total_records" + ], + "properties": { + "collection": { + "type": "string" + }, + "unique_users": { + "type": "integer" + }, + "total_records": { + "type": "integer" + }, + "last_record_time_us": { + "type": "integer" + } + } + } + } + } + } +} diff --git a/lexicons-generated/app/blento/card/getUsers.json b/lexicons-generated/app/blento/card/getUsers.json new file mode 100644 index 0000000..d3a1185 --- /dev/null +++ b/lexicons-generated/app/blento/card/getUsers.json @@ -0,0 +1,61 @@ +{ + "lexicon": 1, + "id": "app.blento.card.getUsers", + "defs": { + "main": { + "type": "query", + "description": "List users who have app.blento.card records", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "users" + ], + "properties": { + "users": { + "type": "array", + "items": { + "type": "ref", + "ref": "#userRecord" + } + }, + "cursor": { + "type": "string" + } + } + } + } + }, + "userRecord": { + "type": "object", + "required": [ + "did", + "record_count" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "record_count": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/community/lexicon/calendar/event/getRecord.json b/lexicons-generated/community/lexicon/calendar/event/getRecord.json new file mode 100644 index 0000000..154051c --- /dev/null +++ b/lexicons-generated/community/lexicon/calendar/event/getRecord.json @@ -0,0 +1,73 @@ +{ + "lexicon": 1, + "id": "community.lexicon.calendar.event.getRecord", + "defs": { + "main": { + "type": "query", + "description": "Get a single community.lexicon.calendar.event record by AT URI", + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "AT URI of the record" + }, + "rsvpsPreview": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "description": "Number of rsvps previews per group" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "#record" + } + } + }, + "record": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "community.lexicon.calendar.event#main" + }, + "time_us": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/community/lexicon/calendar/event/getRecords.json b/lexicons-generated/community/lexicon/calendar/event/getRecords.json new file mode 100644 index 0000000..13a341d --- /dev/null +++ b/lexicons-generated/community/lexicon/calendar/event/getRecords.json @@ -0,0 +1,120 @@ +{ + "lexicon": 1, + "id": "community.lexicon.calendar.event.getRecords", + "defs": { + "main": { + "type": "query", + "description": "Query community.lexicon.calendar.event records with filters", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "did": { + "type": "string", + "format": "did" + }, + "include": { + "type": "string", + "description": "Comma-separated include names" + }, + "mode": { + "type": "string", + "description": "Filter by mode" + }, + "name": { + "type": "string", + "description": "Filter by name" + }, + "status": { + "type": "string", + "description": "Filter by status" + }, + "description": { + "type": "string", + "description": "Filter by description" + }, + "min": { + "type": "string", + "description": "Min filter as field:value (range fields or count types). Repeatable." + }, + "max": { + "type": "string", + "description": "Max filter as field:value (range fields). Repeatable." + }, + "rsvpsPreview": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "description": "Number of rsvps previews per record" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "type": "array", + "items": { + "type": "ref", + "ref": "#record" + } + }, + "cursor": { + "type": "string" + } + } + } + } + }, + "record": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "community.lexicon.calendar.event#main" + }, + "time_us": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/community/lexicon/calendar/event/getStats.json b/lexicons-generated/community/lexicon/calendar/event/getStats.json new file mode 100644 index 0000000..6768d79 --- /dev/null +++ b/lexicons-generated/community/lexicon/calendar/event/getStats.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "community.lexicon.calendar.event.getStats", + "defs": { + "main": { + "type": "query", + "description": "Get stats for community.lexicon.calendar.event", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "collection", + "unique_users", + "total_records" + ], + "properties": { + "collection": { + "type": "string" + }, + "unique_users": { + "type": "integer" + }, + "total_records": { + "type": "integer" + }, + "last_record_time_us": { + "type": "integer" + } + } + } + } + } + } +} diff --git a/lexicons-generated/community/lexicon/calendar/event/getUsers.json b/lexicons-generated/community/lexicon/calendar/event/getUsers.json new file mode 100644 index 0000000..224b5c9 --- /dev/null +++ b/lexicons-generated/community/lexicon/calendar/event/getUsers.json @@ -0,0 +1,61 @@ +{ + "lexicon": 1, + "id": "community.lexicon.calendar.event.getUsers", + "defs": { + "main": { + "type": "query", + "description": "List users who have community.lexicon.calendar.event records", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "users" + ], + "properties": { + "users": { + "type": "array", + "items": { + "type": "ref", + "ref": "#userRecord" + } + }, + "cursor": { + "type": "string" + } + } + } + } + }, + "userRecord": { + "type": "object", + "required": [ + "did", + "record_count" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "record_count": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json b/lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json new file mode 100644 index 0000000..0839a38 --- /dev/null +++ b/lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json @@ -0,0 +1,67 @@ +{ + "lexicon": 1, + "id": "community.lexicon.calendar.rsvp.getRecord", + "defs": { + "main": { + "type": "query", + "description": "Get a single community.lexicon.calendar.rsvp record by AT URI", + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "AT URI of the record" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "#record" + } + } + }, + "record": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "community.lexicon.calendar.rsvp#main" + }, + "time_us": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/community/lexicon/calendar/rsvp/getRecords.json b/lexicons-generated/community/lexicon/calendar/rsvp/getRecords.json new file mode 100644 index 0000000..499053d --- /dev/null +++ b/lexicons-generated/community/lexicon/calendar/rsvp/getRecords.json @@ -0,0 +1,106 @@ +{ + "lexicon": 1, + "id": "community.lexicon.calendar.rsvp.getRecords", + "defs": { + "main": { + "type": "query", + "description": "Query community.lexicon.calendar.rsvp records with filters", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "did": { + "type": "string", + "format": "did" + }, + "include": { + "type": "string", + "description": "Comma-separated include names" + }, + "status": { + "type": "string", + "description": "Filter by status" + }, + "subjectUri": { + "type": "string", + "description": "Filter by subject.uri" + }, + "min": { + "type": "string", + "description": "Min filter as field:value (range fields or count types). Repeatable." + }, + "max": { + "type": "string", + "description": "Max filter as field:value (range fields). Repeatable." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "type": "array", + "items": { + "type": "ref", + "ref": "#record" + } + }, + "cursor": { + "type": "string" + } + } + } + } + }, + "record": { + "type": "object", + "required": [ + "uri", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "record": { + "type": "ref", + "ref": "community.lexicon.calendar.rsvp#main" + }, + "time_us": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/community/lexicon/calendar/rsvp/getStats.json b/lexicons-generated/community/lexicon/calendar/rsvp/getStats.json new file mode 100644 index 0000000..4308aea --- /dev/null +++ b/lexicons-generated/community/lexicon/calendar/rsvp/getStats.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "community.lexicon.calendar.rsvp.getStats", + "defs": { + "main": { + "type": "query", + "description": "Get stats for community.lexicon.calendar.rsvp", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "collection", + "unique_users", + "total_records" + ], + "properties": { + "collection": { + "type": "string" + }, + "unique_users": { + "type": "integer" + }, + "total_records": { + "type": "integer" + }, + "last_record_time_us": { + "type": "integer" + } + } + } + } + } + } +} diff --git a/lexicons-generated/community/lexicon/calendar/rsvp/getUsers.json b/lexicons-generated/community/lexicon/calendar/rsvp/getUsers.json new file mode 100644 index 0000000..dd12160 --- /dev/null +++ b/lexicons-generated/community/lexicon/calendar/rsvp/getUsers.json @@ -0,0 +1,61 @@ +{ + "lexicon": 1, + "id": "community.lexicon.calendar.rsvp.getUsers", + "defs": { + "main": { + "type": "query", + "description": "List users who have community.lexicon.calendar.rsvp records", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "users" + ], + "properties": { + "users": { + "type": "array", + "items": { + "type": "ref", + "ref": "#userRecord" + } + }, + "cursor": { + "type": "string" + } + } + } + } + }, + "userRecord": { + "type": "object", + "required": [ + "did", + "record_count" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "record_count": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/contrail/admin/getCursor.json b/lexicons-generated/contrail/admin/getCursor.json new file mode 100644 index 0000000..2366d95 --- /dev/null +++ b/lexicons-generated/contrail/admin/getCursor.json @@ -0,0 +1,27 @@ +{ + "lexicon": 1, + "id": "contrail.admin.getCursor", + "defs": { + "main": { + "type": "query", + "description": "Get the current cursor position", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "time_us": { + "type": "integer" + }, + "date": { + "type": "string" + }, + "seconds_ago": { + "type": "integer" + } + } + } + } + } + } +} diff --git a/lexicons-generated/contrail/admin/getOverview.json b/lexicons-generated/contrail/admin/getOverview.json new file mode 100644 index 0000000..e48317c --- /dev/null +++ b/lexicons-generated/contrail/admin/getOverview.json @@ -0,0 +1,51 @@ +{ + "lexicon": 1, + "id": "contrail.admin.getOverview", + "defs": { + "main": { + "type": "query", + "description": "Get an overview of all indexed collections", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "total_records", + "collections" + ], + "properties": { + "total_records": { + "type": "integer" + }, + "collections": { + "type": "array", + "items": { + "type": "ref", + "ref": "#collectionStats" + } + } + } + } + } + }, + "collectionStats": { + "type": "object", + "required": [ + "collection", + "records", + "unique_users" + ], + "properties": { + "collection": { + "type": "string" + }, + "records": { + "type": "integer" + }, + "unique_users": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/contrail/admin/sync.json b/lexicons-generated/contrail/admin/sync.json new file mode 100644 index 0000000..e8b0155 --- /dev/null +++ b/lexicons-generated/contrail/admin/sync.json @@ -0,0 +1,47 @@ +{ + "lexicon": 1, + "id": "contrail.admin.sync", + "defs": { + "main": { + "type": "query", + "description": "Discover users from relays and backfill their records from PDS", + "parameters": { + "type": "params", + "properties": { + "concurrency": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 10 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "discovered", + "backfilled", + "remaining", + "done" + ], + "properties": { + "discovered": { + "type": "integer" + }, + "backfilled": { + "type": "integer" + }, + "remaining": { + "type": "integer" + }, + "done": { + "type": "boolean" + } + } + } + } + } + } +} diff --git a/lexicons-pulled/README.md b/lexicons-pulled/README.md new file mode 100644 index 0000000..bdc8bab --- /dev/null +++ b/lexicons-pulled/README.md @@ -0,0 +1,5 @@ +# lexicon sources + +this directory contains lexicon documents pulled from the following sources: + +- atproto (nsids: app.blento.card, community.lexicon.calendar.event, community.lexicon.calendar.rsvp, community.lexicon.location.address, community.lexicon.location.fsq, community.lexicon.location.geo, community.lexicon.location.hthree) diff --git a/lexicons-pulled/community/lexicon/calendar/event.json b/lexicons-pulled/community/lexicon/calendar/event.json new file mode 100644 index 0000000..96e54cb --- /dev/null +++ b/lexicons-pulled/community/lexicon/calendar/event.json @@ -0,0 +1,142 @@ +{ + "id": "community.lexicon.calendar.event", + "defs": { + "uri": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string", + "description": "The display name of the URI." + } + }, + "description": "A URI associated with the event." + }, + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": ["createdAt", "name"], + "properties": { + "mode": { + "ref": "community.lexicon.calendar.event#mode", + "type": "ref", + "description": "The attendance mode of the event." + }, + "name": { + "type": "string", + "description": "The name of the event." + }, + "uris": { + "type": "array", + "items": { + "ref": "community.lexicon.calendar.event#uri", + "type": "ref" + }, + "description": "URIs associated with the event." + }, + "endsAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when the event ends." + }, + "status": { + "ref": "community.lexicon.calendar.event#status", + "type": "ref", + "description": "The status of the event." + }, + "startsAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when the event starts." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when the event was created." + }, + "locations": { + "type": "array", + "items": { + "refs": [ + "community.lexicon.calendar.event#uri", + "community.lexicon.location.address", + "community.lexicon.location.fsq", + "community.lexicon.location.geo", + "community.lexicon.location.hthree" + ], + "type": "union" + }, + "description": "The locations where the event takes place." + }, + "description": { + "type": "string", + "description": "The description of the event." + } + } + }, + "description": "A calendar event." + }, + "mode": { + "type": "string", + "default": "community.lexicon.calendar.event#inperson", + "description": "The mode of the event.", + "knownValues": [ + "community.lexicon.calendar.event#hybrid", + "community.lexicon.calendar.event#inperson", + "community.lexicon.calendar.event#virtual" + ] + }, + "hybrid": { + "type": "token", + "description": "A hybrid event that takes place both online and offline." + }, + "status": { + "type": "string", + "default": "community.lexicon.calendar.event#scheduled", + "description": "The status of the event.", + "knownValues": [ + "community.lexicon.calendar.event#cancelled", + "community.lexicon.calendar.event#planned", + "community.lexicon.calendar.event#postponed", + "community.lexicon.calendar.event#rescheduled", + "community.lexicon.calendar.event#scheduled" + ] + }, + "planned": { + "type": "token", + "description": "The event has been created, but not finalized." + }, + "virtual": { + "type": "token", + "description": "A virtual event that takes place online." + }, + "inperson": { + "type": "token", + "description": "An in-person event that takes place offline." + }, + "cancelled": { + "type": "token", + "description": "The event has been cancelled." + }, + "postponed": { + "type": "token", + "description": "The event has been postponed and a new start date has not been set." + }, + "scheduled": { + "type": "token", + "description": "The event has been created and scheduled." + }, + "rescheduled": { + "type": "token", + "description": "The event has been rescheduled." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons-pulled/community/lexicon/calendar/rsvp.json b/lexicons-pulled/community/lexicon/calendar/rsvp.json new file mode 100644 index 0000000..ac61fd3 --- /dev/null +++ b/lexicons-pulled/community/lexicon/calendar/rsvp.json @@ -0,0 +1,43 @@ +{ + "id": "community.lexicon.calendar.rsvp", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": ["subject", "status"], + "properties": { + "status": { + "type": "string", + "default": "community.lexicon.calendar.rsvp#going", + "knownValues": [ + "community.lexicon.calendar.rsvp#interested", + "community.lexicon.calendar.rsvp#going", + "community.lexicon.calendar.rsvp#notgoing" + ] + }, + "subject": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + }, + "description": "An RSVP for an event." + }, + "going": { + "type": "token", + "description": "Going to the event" + }, + "notgoing": { + "type": "token", + "description": "Not going to the event" + }, + "interested": { + "type": "token", + "description": "Interested in the event" + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons-pulled/community/lexicon/location/address.json b/lexicons-pulled/community/lexicon/location/address.json new file mode 100644 index 0000000..acfa0fc --- /dev/null +++ b/lexicons-pulled/community/lexicon/location/address.json @@ -0,0 +1,40 @@ +{ + "id": "community.lexicon.location.address", + "defs": { + "main": { + "type": "object", + "required": ["country"], + "properties": { + "name": { + "type": "string", + "description": "The name of the location." + }, + "region": { + "type": "string", + "description": "The administrative region of the country. For example, a state in the USA." + }, + "street": { + "type": "string", + "description": "The street address." + }, + "country": { + "type": "string", + "maxLength": 10, + "minLength": 2, + "description": "The ISO 3166 country code. Preferably the 2-letter code." + }, + "locality": { + "type": "string", + "description": "The locality of the region. For example, a city in the USA." + }, + "postalCode": { + "type": "string", + "description": "The postal code of the location." + } + }, + "description": "A physical location in the form of a street address." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons-pulled/community/lexicon/location/fsq.json b/lexicons-pulled/community/lexicon/location/fsq.json new file mode 100644 index 0000000..91faa7a --- /dev/null +++ b/lexicons-pulled/community/lexicon/location/fsq.json @@ -0,0 +1,28 @@ +{ + "id": "community.lexicon.location.fsq", + "defs": { + "main": { + "type": "object", + "required": ["fsq_place_id"], + "properties": { + "name": { + "type": "string", + "description": "The name of the location." + }, + "latitude": { + "type": "string" + }, + "longitude": { + "type": "string" + }, + "fsq_place_id": { + "type": "string", + "description": "The unique identifier of a Foursquare POI." + } + }, + "description": "A physical location contained in the Foursquare Open Source Places dataset." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons-pulled/community/lexicon/location/geo.json b/lexicons-pulled/community/lexicon/location/geo.json new file mode 100644 index 0000000..8676479 --- /dev/null +++ b/lexicons-pulled/community/lexicon/location/geo.json @@ -0,0 +1,27 @@ +{ + "id": "community.lexicon.location.geo", + "defs": { + "main": { + "type": "object", + "required": ["latitude", "longitude"], + "properties": { + "name": { + "type": "string", + "description": "The name of the location." + }, + "altitude": { + "type": "string" + }, + "latitude": { + "type": "string" + }, + "longitude": { + "type": "string" + } + }, + "description": "A physical location in the form of a WGS84 coordinate." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons-pulled/community/lexicon/location/hthree.json b/lexicons-pulled/community/lexicon/location/hthree.json new file mode 100644 index 0000000..fa6cb99 --- /dev/null +++ b/lexicons-pulled/community/lexicon/location/hthree.json @@ -0,0 +1,22 @@ +{ + "id": "community.lexicon.location.hthree", + "defs": { + "main": { + "type": "object", + "required": ["value"], + "properties": { + "name": { + "type": "string", + "description": "The name of the location." + }, + "value": { + "type": "string", + "description": "The h3 encoded location." + } + }, + "description": "A physical location in the form of a H3 encoded location." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/src/core/types.ts b/src/core/types.ts index bf9bcbe..c35d124 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -47,8 +47,7 @@ export const DEFAULT_JETSTREAMS = [ ]; export const DEFAULT_RELAYS = [ - "https://relay1.us-east.bsky.network", - "https://relay2.us-east.bsky.network", + "https://relay1.us-east.bsky.network" ]; export interface ContrailConfig { diff --git a/wrangler.jsonc b/wrangler.jsonc index 6b6dace..38a6ad1 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,19 +1,22 @@ { - "$schema": "node_modules/wrangler/config-schema.json", - "name": "contrail", - "main": "src/adapters/worker.ts", - "compatibility_date": "2025-12-25", - "observability": { - "enabled": true - }, - "d1_databases": [ - { - "binding": "DB", - "database_name": "contrail", - "database_id": "FILL_IN_DATABASE_ID" - } - ], - "triggers": { - "crons": ["*/1 * * * *"] - } -} + "$schema": "node_modules/wrangler/config-schema.json", + "name": "contrail", + "main": "src/adapters/worker.ts", + "compatibility_date": "2025-12-25", + "observability": { + "enabled": true + }, + "d1_databases": [ + { + "binding": "DB", + "database_name": "contrail", + "database_id": "8e4b8951-bdc1-4906-a68a-9987d89898c4", + "remote": true + } + ], + "triggers": { + "crons": [ + "*/1 * * * *" + ] + } +} \ No newline at end of file -- 2.51.2