AppView in a box as a Vite plugin thing hatk.dev
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

format codebase and fix all lint warnings

Remove unused functions (flattenRow, resolveBlobOverrides), fix
empty destructuring pattern, exclude minified admin-auth.js from
lint, exclude docs/superpowers from formatter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+1211 -493
+2 -1
.oxfmtrc.json
··· 4 4 "singleQuote": true, 5 5 "trailingComma": "all", 6 6 "printWidth": 120, 7 - "tabWidth": 2 7 + "tabWidth": 2, 8 + "ignorePatterns": ["docs/superpowers"] 8 9 }
+1 -1
.oxlintrc.json
··· 13 13 "env": { 14 14 "builtin": true 15 15 }, 16 - "ignorePatterns": ["node_modules", "dist", "data", "*.min.js"] 16 + "ignorePatterns": ["node_modules", "dist", "data", "*.min.js", "packages/hatk/public/admin-auth.js"] 17 17 }
+55 -57
docs/site/astro.config.mjs
··· 1 1 // @ts-check 2 - import { defineConfig } from 'astro/config'; 3 - import starlight from '@astrojs/starlight'; 2 + import { defineConfig } from 'astro/config' 3 + import starlight from '@astrojs/starlight' 4 4 5 5 export default defineConfig({ 6 - integrations: [ 7 - starlight({ 8 - title: 'Hatk', 9 - social: [ 10 - { icon: 'github', label: 'GitHub', href: 'https://github.com/bigmoves/atconf-workshop' }, 11 - ], 12 - sidebar: [ 13 - { 14 - label: 'Getting Started', 15 - items: [ 16 - { label: 'Quickstart', slug: 'getting-started/quickstart' }, 17 - { label: 'Project Structure', slug: 'getting-started/project-structure' }, 18 - { label: 'Configuration', slug: 'getting-started/configuration' }, 19 - ], 20 - }, 21 - { 22 - label: 'Guides', 23 - items: [ 24 - { label: 'Frontend (SvelteKit)', slug: 'guides/frontend' }, 25 - { label: 'API Client', slug: 'guides/api-client' }, 26 - { label: 'OAuth', slug: 'guides/oauth' }, 27 - { label: 'Feeds', slug: 'guides/feeds' }, 28 - { label: 'XRPC Handlers', slug: 'guides/xrpc-handlers' }, 29 - { label: 'Labels', slug: 'guides/labels' }, 30 - { label: 'Seeds', slug: 'guides/seeds' }, 31 - { label: 'OpenGraph Images', slug: 'guides/opengraph' }, 32 - { label: 'Hooks', slug: 'guides/hooks' }, 33 - ], 34 - }, 35 - { 36 - label: 'CLI Reference', 37 - items: [ 38 - { label: 'Overview', slug: 'cli' }, 39 - { label: 'Scaffolding', slug: 'cli/scaffold' }, 40 - { label: 'Development', slug: 'cli/development' }, 41 - { label: 'Testing', slug: 'cli/testing' }, 42 - { label: 'Build & Deploy', slug: 'cli/build' }, 43 - ], 44 - }, 45 - { 46 - label: 'API Reference', 47 - items: [ 48 - { label: 'Overview', slug: 'api' }, 49 - { label: 'Records', slug: 'api/records' }, 50 - { label: 'Feeds', slug: 'api/feeds' }, 51 - { label: 'Search', slug: 'api/search' }, 52 - { label: 'Blobs', slug: 'api/blobs' }, 53 - { label: 'Preferences', slug: 'api/preferences' }, 54 - { label: 'Labels', slug: 'api/labels' }, 55 - ], 56 - }, 57 - ], 58 - }), 59 - ], 60 - }); 6 + integrations: [ 7 + starlight({ 8 + title: 'Hatk', 9 + social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/bigmoves/atconf-workshop' }], 10 + sidebar: [ 11 + { 12 + label: 'Getting Started', 13 + items: [ 14 + { label: 'Quickstart', slug: 'getting-started/quickstart' }, 15 + { label: 'Project Structure', slug: 'getting-started/project-structure' }, 16 + { label: 'Configuration', slug: 'getting-started/configuration' }, 17 + ], 18 + }, 19 + { 20 + label: 'Guides', 21 + items: [ 22 + { label: 'Frontend (SvelteKit)', slug: 'guides/frontend' }, 23 + { label: 'API Client', slug: 'guides/api-client' }, 24 + { label: 'OAuth', slug: 'guides/oauth' }, 25 + { label: 'Feeds', slug: 'guides/feeds' }, 26 + { label: 'XRPC Handlers', slug: 'guides/xrpc-handlers' }, 27 + { label: 'Labels', slug: 'guides/labels' }, 28 + { label: 'Seeds', slug: 'guides/seeds' }, 29 + { label: 'OpenGraph Images', slug: 'guides/opengraph' }, 30 + { label: 'Hooks', slug: 'guides/hooks' }, 31 + ], 32 + }, 33 + { 34 + label: 'CLI Reference', 35 + items: [ 36 + { label: 'Overview', slug: 'cli' }, 37 + { label: 'Scaffolding', slug: 'cli/scaffold' }, 38 + { label: 'Development', slug: 'cli/development' }, 39 + { label: 'Testing', slug: 'cli/testing' }, 40 + { label: 'Build & Deploy', slug: 'cli/build' }, 41 + ], 42 + }, 43 + { 44 + label: 'API Reference', 45 + items: [ 46 + { label: 'Overview', slug: 'api' }, 47 + { label: 'Records', slug: 'api/records' }, 48 + { label: 'Feeds', slug: 'api/feeds' }, 49 + { label: 'Search', slug: 'api/search' }, 50 + { label: 'Blobs', slug: 'api/blobs' }, 51 + { label: 'Preferences', slug: 'api/preferences' }, 52 + { label: 'Labels', slug: 'api/labels' }, 53 + ], 54 + }, 55 + ], 56 + }), 57 + ], 58 + })
+1 -1
docs/site/package.json
··· 12 12 "astro": "^5.6.1", 13 13 "sharp": "^0.34.2" 14 14 } 15 - } 15 + }
+5 -5
docs/site/src/content.config.ts
··· 1 - import { defineCollection } from 'astro:content'; 2 - import { docsLoader } from '@astrojs/starlight/loaders'; 3 - import { docsSchema } from '@astrojs/starlight/schema'; 1 + import { defineCollection } from 'astro:content' 2 + import { docsLoader } from '@astrojs/starlight/loaders' 3 + import { docsSchema } from '@astrojs/starlight/schema' 4 4 5 5 export const collections = { 6 - docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 7 - }; 6 + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 7 + }
+5 -5
docs/site/src/content/docs/api/feeds.mdx
··· 12 12 13 13 ### Parameters 14 14 15 - | Name | Type | Required | Default | Description | 16 - |------|------|----------|---------|-------------| 17 - | `feed` | string | Yes | — | Feed name | 18 - | `limit` | integer | No | `30` | Results per page (1–100) | 19 - | `cursor` | string | No | — | Pagination cursor | 15 + | Name | Type | Required | Default | Description | 16 + | -------- | ------- | -------- | ------- | ------------------------ | 17 + | `feed` | string | Yes | — | Feed name | 18 + | `limit` | integer | No | `30` | Results per page (1–100) | 19 + | `cursor` | string | No | — | Pagination cursor | 20 20 21 21 ### Example 22 22
+15 -15
docs/site/src/content/docs/api/index.mdx
··· 45 45 46 46 ## Built-in endpoints 47 47 48 - | Endpoint | Type | Auth | Description | 49 - |----------|------|------|-------------| 50 - | [`getRecord`](/api/records/) | Query | No | Fetch a single record by AT URI | 51 - | [`getRecords`](/api/records/) | Query | No | List records with filters | 52 - | [`createRecord`](/api/records/) | Procedure | Yes | Create a record via user's PDS | 53 - | [`putRecord`](/api/records/) | Procedure | Yes | Create or update a record | 54 - | [`deleteRecord`](/api/records/) | Procedure | Yes | Delete a record | 55 - | [`getFeed`](/api/feeds/) | Query | No | Retrieve a named feed | 56 - | [`describeFeeds`](/api/feeds/) | Query | No | List available feeds | 57 - | [`searchRecords`](/api/search/) | Query | No | Full-text search | 58 - | [`uploadBlob`](/api/blobs/) | Procedure | Yes | Upload a binary blob | 59 - | [`getPreferences`](/api/preferences/) | Query | Yes | Get user preferences | 60 - | [`putPreference`](/api/preferences/) | Procedure | Yes | Set a preference | 61 - | [`describeLabels`](/api/labels/) | Query | No | List label definitions | 62 - | `describeCollections` | Query | No | List indexed collections | 48 + | Endpoint | Type | Auth | Description | 49 + | ------------------------------------- | --------- | ---- | ------------------------------- | 50 + | [`getRecord`](/api/records/) | Query | No | Fetch a single record by AT URI | 51 + | [`getRecords`](/api/records/) | Query | No | List records with filters | 52 + | [`createRecord`](/api/records/) | Procedure | Yes | Create a record via user's PDS | 53 + | [`putRecord`](/api/records/) | Procedure | Yes | Create or update a record | 54 + | [`deleteRecord`](/api/records/) | Procedure | Yes | Delete a record | 55 + | [`getFeed`](/api/feeds/) | Query | No | Retrieve a named feed | 56 + | [`describeFeeds`](/api/feeds/) | Query | No | List available feeds | 57 + | [`searchRecords`](/api/search/) | Query | No | Full-text search | 58 + | [`uploadBlob`](/api/blobs/) | Procedure | Yes | Upload a binary blob | 59 + | [`getPreferences`](/api/preferences/) | Query | Yes | Get user preferences | 60 + | [`putPreference`](/api/preferences/) | Procedure | Yes | Set a preference | 61 + | [`describeLabels`](/api/labels/) | Query | No | List label definitions | 62 + | `describeCollections` | Query | No | List indexed collections |
+4 -4
docs/site/src/content/docs/api/preferences.mdx
··· 43 43 44 44 ### Input 45 45 46 - | Name | Type | Required | Description | 47 - |------|------|----------|-------------| 48 - | `key` | string | Yes | Preference key | 49 - | `value` | any | Yes | Preference value | 46 + | Name | Type | Required | Description | 47 + | ------- | ------ | -------- | ---------------- | 48 + | `key` | string | Yes | Preference key | 49 + | `value` | any | Yes | Preference value | 50 50 51 51 ### Example 52 52
+25 -25
docs/site/src/content/docs/api/records.mdx
··· 12 12 13 13 ### Parameters 14 14 15 - | Name | Type | Required | Description | 16 - |------|------|----------|-------------| 17 - | `uri` | string (AT URI) | Yes | The AT URI of the record | 15 + | Name | Type | Required | Description | 16 + | ----- | --------------- | -------- | ------------------------ | 17 + | `uri` | string (AT URI) | Yes | The AT URI of the record | 18 18 19 19 ### Example 20 20 ··· 47 47 48 48 ### Parameters 49 49 50 - | Name | Type | Required | Default | Description | 51 - |------|------|----------|---------|-------------| 52 - | `collection` | string | Yes | — | Collection NSID | 53 - | `limit` | integer | No | `20` | Results per page (1–100) | 54 - | `cursor` | string | No | — | Pagination cursor | 55 - | `sort` | string | No | — | Sort field | 56 - | `order` | string | No | — | Sort order | 50 + | Name | Type | Required | Default | Description | 51 + | ------------ | ------- | -------- | ------- | ------------------------ | 52 + | `collection` | string | Yes | — | Collection NSID | 53 + | `limit` | integer | No | `20` | Results per page (1–100) | 54 + | `cursor` | string | No | — | Pagination cursor | 55 + | `sort` | string | No | — | Sort field | 56 + | `order` | string | No | — | Sort order | 57 57 58 58 Additional filter parameters are accepted based on the collection's schema — any field defined in the record lexicon can be used as a query parameter. 59 59 ··· 90 90 91 91 ### Input 92 92 93 - | Name | Type | Required | Description | 94 - |------|------|----------|-------------| 95 - | `collection` | string | Yes | Collection NSID | 96 - | `repo` | string (DID) | Yes | The user's DID | 97 - | `record` | object | Yes | The record data | 93 + | Name | Type | Required | Description | 94 + | ------------ | ------------ | -------- | --------------- | 95 + | `collection` | string | Yes | Collection NSID | 96 + | `repo` | string (DID) | Yes | The user's DID | 97 + | `record` | object | Yes | The record data | 98 98 99 99 ### Example 100 100 ··· 133 133 134 134 ### Input 135 135 136 - | Name | Type | Required | Description | 137 - |------|------|----------|-------------| 138 - | `collection` | string | Yes | Collection NSID | 139 - | `rkey` | string | Yes | Record key | 140 - | `record` | object | Yes | The record data | 141 - | `repo` | string (DID) | No | The user's DID (inferred from auth if omitted) | 136 + | Name | Type | Required | Description | 137 + | ------------ | ------------ | -------- | ---------------------------------------------- | 138 + | `collection` | string | Yes | Collection NSID | 139 + | `rkey` | string | Yes | Record key | 140 + | `record` | object | Yes | The record data | 141 + | `repo` | string (DID) | No | The user's DID (inferred from auth if omitted) | 142 142 143 143 ### Example 144 144 ··· 177 177 178 178 ### Input 179 179 180 - | Name | Type | Required | Description | 181 - |------|------|----------|-------------| 182 - | `collection` | string | Yes | Collection NSID | 183 - | `rkey` | string | Yes | Record key | 180 + | Name | Type | Required | Description | 181 + | ------------ | ------ | -------- | --------------- | 182 + | `collection` | string | Yes | Collection NSID | 183 + | `rkey` | string | Yes | Record key | 184 184 185 185 ### Example 186 186
+7 -7
docs/site/src/content/docs/api/search.mdx
··· 12 12 13 13 ### Parameters 14 14 15 - | Name | Type | Required | Default | Description | 16 - |------|------|----------|---------|-------------| 17 - | `collection` | string | Yes | — | Collection NSID to search | 18 - | `q` | string | Yes | — | Search query | 19 - | `limit` | integer | No | `20` | Results per page (1–100) | 20 - | `cursor` | string | No | — | Pagination cursor | 21 - | `fuzzy` | boolean | No | `true` | Enable fuzzy matching | 15 + | Name | Type | Required | Default | Description | 16 + | ------------ | ------- | -------- | ------- | ------------------------- | 17 + | `collection` | string | Yes | — | Collection NSID to search | 18 + | `q` | string | Yes | — | Search query | 19 + | `limit` | integer | No | `20` | Results per page (1–100) | 20 + | `cursor` | string | No | — | Pagination cursor | 21 + | `fuzzy` | boolean | No | `true` | Enable fuzzy matching | 22 22 23 23 ### Example 24 24
+2
docs/site/src/content/docs/cli/build.mdx
··· 18 18 To run in production: 19 19 20 20 1. Build the frontend: 21 + 21 22 ```bash 22 23 hatk build 23 24 ``` 24 25 25 26 2. Start the server: 27 + 26 28 ```bash 27 29 hatk start 28 30 ```
+1
docs/site/src/content/docs/cli/development.mdx
··· 12 12 ``` 13 13 14 14 This runs three steps in sequence: 15 + 15 16 1. **Starts the local PDS** via Docker Compose (if `docker-compose.yml` exists) 16 17 2. **Seeds test data** by running `seeds/seed.ts` 17 18 3. **Starts the Hatk server** with file watching for hot reload
+29 -29
docs/site/src/content/docs/cli/index.mdx
··· 7 7 8 8 ## Getting Started 9 9 10 - | Command | Description | 11 - |---------|-------------| 10 + | Command | Description | 11 + | ----------------- | ------------------------- | 12 12 | `hatk new <name>` | Create a new hatk project | 13 13 14 14 ## Generators 15 15 16 - | Command | Description | 17 - |---------|-------------| 18 - | `hatk generate record <nsid>` | Generate a record lexicon | 19 - | `hatk generate query <nsid>` | Generate a query lexicon | 20 - | `hatk generate procedure <nsid>` | Generate a procedure lexicon | 21 - | `hatk generate feed <name>` | Generate a feed generator | 22 - | `hatk generate xrpc <nsid>` | Generate an XRPC handler | 23 - | `hatk generate label <name>` | Generate a label definition | 24 - | `hatk generate og <name>` | Generate an OpenGraph route | 25 - | `hatk generate job <name>` | Generate a periodic job | 26 - | `hatk generate types` | Regenerate TypeScript from lexicons | 27 - | `hatk destroy <type> <name>` | Remove a generated file | 28 - | `hatk resolve <nsid>` | Fetch a lexicon from the network | 16 + | Command | Description | 17 + | -------------------------------- | ----------------------------------- | 18 + | `hatk generate record <nsid>` | Generate a record lexicon | 19 + | `hatk generate query <nsid>` | Generate a query lexicon | 20 + | `hatk generate procedure <nsid>` | Generate a procedure lexicon | 21 + | `hatk generate feed <name>` | Generate a feed generator | 22 + | `hatk generate xrpc <nsid>` | Generate an XRPC handler | 23 + | `hatk generate label <name>` | Generate a label definition | 24 + | `hatk generate og <name>` | Generate an OpenGraph route | 25 + | `hatk generate job <name>` | Generate a periodic job | 26 + | `hatk generate types` | Regenerate TypeScript from lexicons | 27 + | `hatk destroy <type> <name>` | Remove a generated file | 28 + | `hatk resolve <nsid>` | Fetch a lexicon from the network | 29 29 30 30 ## Development 31 31 32 - | Command | Description | 33 - |---------|-------------| 34 - | `hatk dev` | Start PDS, seed data, and run server with watch | 35 - | `hatk start` | Start the server (production mode) | 36 - | `hatk seed` | Run seed data against local PDS | 37 - | `hatk reset` | Wipe database and PDS | 38 - | `hatk schema` | Print DuckDB schema from lexicons | 32 + | Command | Description | 33 + | ------------- | ----------------------------------------------- | 34 + | `hatk dev` | Start PDS, seed data, and run server with watch | 35 + | `hatk start` | Start the server (production mode) | 36 + | `hatk seed` | Run seed data against local PDS | 37 + | `hatk reset` | Wipe database and PDS | 38 + | `hatk schema` | Print DuckDB schema from lexicons | 39 39 40 40 ## Code Quality 41 41 42 - | Command | Description | 43 - |---------|-------------| 44 - | `hatk test` | Run all tests | 45 - | `hatk check` | Type-check, lint, and format check | 46 - | `hatk format` | Auto-format code | 42 + | Command | Description | 43 + | ------------- | ---------------------------------- | 44 + | `hatk test` | Run all tests | 45 + | `hatk check` | Type-check, lint, and format check | 46 + | `hatk format` | Auto-format code | 47 47 48 48 ## Build 49 49 50 - | Command | Description | 51 - |---------|-------------| 50 + | Command | Description | 51 + | ------------ | --------------------------------- | 52 52 | `hatk build` | Build the frontend for production |
+3 -3
docs/site/src/content/docs/cli/scaffold.mdx
··· 11 11 hatk new <name> [--svelte] 12 12 ``` 13 13 14 - | Option | Description | 15 - |--------|-------------| 16 - | `<name>` | Project directory name (required) | 14 + | Option | Description | 15 + | ---------- | --------------------------------------------------------- | 16 + | `<name>` | Project directory name (required) | 17 17 | `--svelte` | Include a Svelte frontend with `src/routes` and `src/lib` | 18 18 19 19 The command creates the project directory with `config.yaml`, `lexicons/`, `feeds/`, `xrpc/`, `labels/`, `jobs/`, `og/`, `seeds/`, `public/`, `test/`, and the core framework lexicons under `lexicons/dev/hatk/`.
+27 -31
docs/site/src/content/docs/cli/testing.mdx
··· 14 14 hatk test --browser # Playwright browser tests 15 15 ``` 16 16 17 - | Flag | Description | 18 - |------|-------------| 19 - | `--unit` | Run unit tests in `test/feeds/` and `test/xrpc/` | 20 - | `--integration` | Run integration tests in `test/integration/` | 21 - | `--browser` | Run Playwright browser tests in `test/browser/` | 17 + | Flag | Description | 18 + | --------------- | ------------------------------------------------ | 19 + | `--unit` | Run unit tests in `test/feeds/` and `test/xrpc/` | 20 + | `--integration` | Run integration tests in `test/integration/` | 21 + | `--browser` | Run Playwright browser tests in `test/browser/` | 22 22 23 23 Without flags, all test types are run. 24 24 ··· 42 42 43 43 ### Test context API 44 44 45 - | Method | Description | 46 - |--------|-------------| 47 - | `ctx.loadFixtures(dir?)` | Load YAML fixture files from `test/fixtures/` (or a custom path) | 48 - | `ctx.loadFeed(name)` | Load a feed by name. Returns `{ generate(feedContext) }` | 49 - | `ctx.loadXrpc(name)` | Load an XRPC handler by name. Returns `{ handler(ctx) }` | 50 - | `ctx.feedContext(opts?)` | Create a feed context with `limit`, `cursor`, `viewer`, and `params` | 51 - | `ctx.db.query(sql, params?)` | Run a SQL query against the in-memory database | 52 - | `ctx.db.run(sql, ...params)` | Execute a SQL statement | 53 - | `ctx.close()` | Shut down the database | 45 + | Method | Description | 46 + | ---------------------------- | -------------------------------------------------------------------- | 47 + | `ctx.loadFixtures(dir?)` | Load YAML fixture files from `test/fixtures/` (or a custom path) | 48 + | `ctx.loadFeed(name)` | Load a feed by name. Returns `{ generate(feedContext) }` | 49 + | `ctx.loadXrpc(name)` | Load an XRPC handler by name. Returns `{ handler(ctx) }` | 50 + | `ctx.feedContext(opts?)` | Create a feed context with `limit`, `cursor`, `viewer`, and `params` | 51 + | `ctx.db.query(sql, params?)` | Run a SQL query against the in-memory database | 52 + | `ctx.db.run(sql, ...params)` | Execute a SQL statement | 53 + | `ctx.close()` | Shut down the database | 54 54 55 55 ### Testing a feed 56 56 ··· 66 66 const page1 = await feed.generate(ctx.feedContext({ limit: 3 })) 67 67 expect(page1.cursor).toBeDefined() 68 68 69 - const page2 = await feed.generate( 70 - ctx.feedContext({ limit: 3, cursor: page1.cursor }), 71 - ) 69 + const page2 = await feed.generate(ctx.feedContext({ limit: 3, cursor: page1.cursor })) 72 70 expect(page2.items).toHaveLength(3) 73 71 }) 74 72 ``` ··· 152 150 153 151 Use `$now` in fixture values to generate timestamps relative to the current time: 154 152 155 - | Expression | Result | 156 - |------------|--------| 157 - | `$now` | Current time | 158 - | `$now(-5m)` | 5 minutes ago | 159 - | `$now(-2h)` | 2 hours ago | 160 - | `$now(-1d)` | 1 day ago | 153 + | Expression | Result | 154 + | ----------- | ------------------- | 155 + | `$now` | Current time | 156 + | `$now(-5m)` | 5 minutes ago | 157 + | `$now(-2h)` | 2 hours ago | 158 + | `$now(-1d)` | 1 day ago | 161 159 | `$now(30s)` | 30 seconds from now | 162 160 163 161 This keeps fixtures time-relative so tests for "recent" feeds and time-based sorting always work. ··· 180 178 afterAll(async () => server?.close()) 181 179 182 180 test('GET /xrpc/dev.hatk.getFeed returns items', async () => { 183 - const res = await server.fetch( 184 - '/xrpc/dev.hatk.getFeed?feed=recent&limit=5', 185 - ) 181 + const res = await server.fetch('/xrpc/dev.hatk.getFeed?feed=recent&limit=5') 186 182 const data = await res.json() 187 183 expect(data.items.length).toBeGreaterThan(0) 188 184 }) ··· 192 188 193 189 The test server extends the test context with: 194 190 195 - | Method | Description | 196 - |--------|-------------| 197 - | `server.url` | The base URL (e.g., `http://127.0.0.1:54321`) | 198 - | `server.fetch(path, init?)` | Fetch a path on the test server | 191 + | Method | Description | 192 + | ---------------------------------- | ------------------------------------------------------------ | 193 + | `server.url` | The base URL (e.g., `http://127.0.0.1:54321`) | 194 + | `server.fetch(path, init?)` | Fetch a path on the test server | 199 195 | `server.fetchAs(did, path, init?)` | Fetch as an authenticated user (sets `x-test-viewer` header) | 200 - | `server.seed(opts?)` | Get seed helpers for creating records against a real PDS | 196 + | `server.seed(opts?)` | Get seed helpers for creating records against a real PDS |
+14 -14
docs/site/src/content/docs/getting-started/configuration.mdx
··· 89 89 90 90 Controls how the server backfills historical data from the network. 91 91 92 - | Option | Default | Env | Description | 93 - |--------|---------|-----|-------------| 94 - | `parallelism` | `5` | `BACKFILL_PARALLELISM` | Concurrent repo fetches | 95 - | `fetchTimeout` | `300` | `BACKFILL_FETCH_TIMEOUT` | Timeout per repo (seconds) | 96 - | `maxRetries` | `5` | `BACKFILL_MAX_RETRIES` | Max retry attempts for failed repos | 97 - | `fullNetwork` | `false` | `BACKFILL_FULL_NETWORK` | Backfill the entire network | 98 - | `repos` | — | `BACKFILL_REPOS` | Pin specific DIDs to backfill (comma-separated) | 99 - | `signalCollections` | — | — | Collections that trigger backfill (defaults to top-level `collections`) | 92 + | Option | Default | Env | Description | 93 + | ------------------- | ------- | ------------------------ | ----------------------------------------------------------------------- | 94 + | `parallelism` | `5` | `BACKFILL_PARALLELISM` | Concurrent repo fetches | 95 + | `fetchTimeout` | `300` | `BACKFILL_FETCH_TIMEOUT` | Timeout per repo (seconds) | 96 + | `maxRetries` | `5` | `BACKFILL_MAX_RETRIES` | Max retry attempts for failed repos | 97 + | `fullNetwork` | `false` | `BACKFILL_FULL_NETWORK` | Backfill the entire network | 98 + | `repos` | — | `BACKFILL_REPOS` | Pin specific DIDs to backfill (comma-separated) | 99 + | `signalCollections` | — | — | Collections that trigger backfill (defaults to top-level `collections`) | 100 100 101 101 ## Full-text search 102 102 ··· 125 125 126 126 Array of allowed OAuth clients. Each client needs: 127 127 128 - | Field | Description | 129 - |-------|-------------| 130 - | `client_id` | Client identifier URL | 131 - | `client_name` | Human-readable name | 132 - | `redirect_uris` | Allowed redirect URIs | 133 - | `scope` | Optional scope override | 128 + | Field | Description | 129 + | --------------- | ----------------------- | 130 + | `client_id` | Client identifier URL | 131 + | `client_name` | Human-readable name | 132 + | `redirect_uris` | Allowed redirect URIs | 133 + | `scope` | Optional scope override |
+6 -6
docs/site/src/content/docs/getting-started/project-structure.mdx
··· 194 194 195 195 Test files organized by type: 196 196 197 - | Directory | Purpose | 198 - |-----------|---------| 199 - | `test/feeds/` | Feed generator unit tests | 200 - | `test/xrpc/` | XRPC handler tests | 197 + | Directory | Purpose | 198 + | ------------------- | ---------------------------- | 199 + | `test/feeds/` | Feed generator unit tests | 200 + | `test/xrpc/` | XRPC handler tests | 201 201 | `test/integration/` | End-to-end integration tests | 202 - | `test/browser/` | Playwright browser tests | 203 - | `test/fixtures/` | Shared test data and helpers | 202 + | `test/browser/` | Playwright browser tests | 203 + | `test/fixtures/` | Shared test data and helpers | 204 204 205 205 Run all tests with `hatk test`. See [Testing](/cli/testing/) for details. 206 206
+1
docs/site/src/content/docs/getting-started/quickstart.mdx
··· 51 51 ``` 52 52 53 53 This starts: 54 + 54 55 1. A local PDS via Docker 55 56 2. Runs your seed data 56 57 3. Starts the Hatk server with file watching
+4 -11
docs/site/src/content/docs/guides/api-client.mdx
··· 13 13 import type { XrpcSchema } from '$hatk' 14 14 import { getAuthFetch } from './auth' 15 15 16 - export const api = createClient<XrpcSchema>( 17 - typeof window !== 'undefined' ? window.location.origin : '', 18 - { 19 - fetch: (url, opts) => getAuthFetch()(url as string, opts), 20 - }, 21 - ) 16 + export const api = createClient<XrpcSchema>(typeof window !== 'undefined' ? window.location.origin : '', { 17 + fetch: (url, opts) => getAuthFetch()(url as string, opts), 18 + }) 22 19 ``` 23 20 24 21 The `XrpcSchema` type is generated from your lexicons and provides full type safety for endpoint names, parameters, inputs, and outputs. ··· 57 54 Upload binary data (e.g., images): 58 55 59 56 ```typescript 60 - const result = await api.upload( 61 - 'dev.hatk.uploadBlob', 62 - file, 63 - 'image/jpeg', 64 - ) 57 + const result = await api.upload('dev.hatk.uploadBlob', file, 'image/jpeg') 65 58 // result: { blob: { ref: { $link: '...' }, ... } } 66 59 ``` 67 60
+30 -30
docs/site/src/content/docs/guides/feeds.mdx
··· 13 13 14 14 ## `defineFeed` options 15 15 16 - | Field | Required | Description | 17 - |-------|----------|-------------| 18 - | `collection` | Yes (unless `hydrate` provided) | The collection this feed queries | 19 - | `label` | Yes | Human-readable name shown in `describeFeeds` | 20 - | `view` | No | View definition to use for auto-hydration | 21 - | `generate` | Yes | Function that returns record URIs | 22 - | `hydrate` | No | Function that enriches resolved records | 16 + | Field | Required | Description | 17 + | ------------ | ------------------------------- | -------------------------------------------- | 18 + | `collection` | Yes (unless `hydrate` provided) | The collection this feed queries | 19 + | `label` | Yes | Human-readable name shown in `describeFeeds` | 20 + | `view` | No | View definition to use for auto-hydration | 21 + | `generate` | Yes | Function that returns record URIs | 22 + | `hydrate` | No | Function that enriches resolved records | 23 23 24 24 --- 25 25 ··· 29 29 30 30 ### Context 31 31 32 - | Field | Type | Description | 33 - |-------|------|-------------| 34 - | `db.query` | function | Run SQL queries against DuckDB | 35 - | `params` | `Record<string, string>` | Query string parameters from the request | 36 - | `limit` | number | Requested page size | 37 - | `cursor` | string \| undefined | Pagination cursor from the client | 38 - | `viewer` | `{ did: string }` \| null | The authenticated user, or null | 39 - | `ok` | function | Wraps your return value with type checking | 40 - | `packCursor` | function | Encode a `(primary, cid)` pair into an opaque cursor string | 41 - | `unpackCursor` | function | Decode a cursor back into `{ primary, cid }` or null | 42 - | `isTakendown` | function | Check if a DID has been taken down | 43 - | `filterTakendownDids` | function | Filter a list of DIDs, returning those that are taken down | 44 - | `paginate` | function | Run a paginated query — handles cursor, ORDER BY, LIMIT automatically | 32 + | Field | Type | Description | 33 + | --------------------- | ------------------------- | --------------------------------------------------------------------- | 34 + | `db.query` | function | Run SQL queries against DuckDB | 35 + | `params` | `Record<string, string>` | Query string parameters from the request | 36 + | `limit` | number | Requested page size | 37 + | `cursor` | string \| undefined | Pagination cursor from the client | 38 + | `viewer` | `{ did: string }` \| null | The authenticated user, or null | 39 + | `ok` | function | Wraps your return value with type checking | 40 + | `packCursor` | function | Encode a `(primary, cid)` pair into an opaque cursor string | 41 + | `unpackCursor` | function | Decode a cursor back into `{ primary, cid }` or null | 42 + | `isTakendown` | function | Check if a DID has been taken down | 43 + | `filterTakendownDids` | function | Filter a list of DIDs, returning those that are taken down | 44 + | `paginate` | function | Run a paginated query — handles cursor, ORDER BY, LIMIT automatically | 45 45 46 46 ### Cursor pagination 47 47 ··· 94 94 95 95 ### Context 96 96 97 - | Field | Type | Description | 98 - |-------|------|-------------| 99 - | `items` | `Row[]` | The resolved records (each has `uri`, `did`, `handle`, `value`) | 100 - | `viewer` | `{ did: string }` \| null | The authenticated user, or null | 101 - | `db.query` | function | Run SQL queries against DuckDB | 102 - | `getRecords` | function | Fetch records by URI from another collection | 103 - | `lookup` | function | Look up records by a field value (e.g. profiles by DID) | 104 - | `count` | function | Count records by field value | 105 - | `labels` | function | Query labels for a list of URIs | 106 - | `blobUrl` | function | Resolve a blob reference to a CDN URL | 97 + | Field | Type | Description | 98 + | ------------ | ------------------------- | --------------------------------------------------------------- | 99 + | `items` | `Row[]` | The resolved records (each has `uri`, `did`, `handle`, `value`) | 100 + | `viewer` | `{ did: string }` \| null | The authenticated user, or null | 101 + | `db.query` | function | Run SQL queries against DuckDB | 102 + | `getRecords` | function | Fetch records by URI from another collection | 103 + | `lookup` | function | Look up records by a field value (e.g. profiles by DID) | 104 + | `count` | function | Count records by field value | 105 + | `labels` | function | Query labels for a list of URIs | 106 + | `blobUrl` | function | Resolve a blob reference to a CDN URL | 107 107 108 108 ### Example 109 109
+3 -3
docs/site/src/content/docs/guides/hooks.mdx
··· 25 25 26 26 ### Context 27 27 28 - | Field | Type | Description | 29 - |-------|------|-------------| 30 - | `did` | string | The DID of the user who just logged in | 28 + | Field | Type | Description | 29 + | ------------ | -------- | -------------------------------------------------------- | 30 + | `did` | string | The DID of the user who just logged in | 31 31 | `ensureRepo` | function | Marks the user's repo as pending and triggers a backfill | 32 32 33 33 ### How `ensureRepo` works
+26 -32
docs/site/src/content/docs/guides/labels.mdx
··· 20 20 ```typescript 21 21 import type { LabelRuleContext } from 'hatk/labels' 22 22 23 - const EXPLICIT_PATTERNS = [ 24 - /\(explicit\)/i, 25 - /\[explicit\]/i, 26 - /\bexplicit version\b/i, 27 - ] 23 + const EXPLICIT_PATTERNS = [/\(explicit\)/i, /\[explicit\]/i, /\bexplicit version\b/i] 28 24 29 25 export default { 30 26 definition: { ··· 69 65 70 66 ## `LabelDefinition` 71 67 72 - | Field | Type | Description | 73 - |-------|------|-------------| 74 - | `identifier` | string | Unique label ID | 75 - | `severity` | `'alert'` \| `'inform'` \| `'none'` | How urgently to surface the label | 76 - | `blurs` | `'media'` \| `'content'` \| `'none'` | What to blur when label is applied | 77 - | `defaultSetting` | `'warn'` \| `'hide'` \| `'ignore'` | Default user-facing behavior | 78 - | `locales` | array | Localized name and description | 68 + | Field | Type | Description | 69 + | ---------------- | ------------------------------------ | ---------------------------------- | 70 + | `identifier` | string | Unique label ID | 71 + | `severity` | `'alert'` \| `'inform'` \| `'none'` | How urgently to surface the label | 72 + | `blurs` | `'media'` \| `'content'` \| `'none'` | What to blur when label is applied | 73 + | `defaultSetting` | `'warn'` \| `'hide'` \| `'ignore'` | Default user-facing behavior | 74 + | `locales` | array | Localized name and description | 79 75 80 76 ## Evaluation context 81 77 82 78 The `evaluate` function receives a `LabelRuleContext` with: 83 79 84 - | Field | Type | Description | 85 - |-------|------|-------------| 86 - | `db.query` | function | Run SQL queries against DuckDB | 87 - | `db.run` | function | Execute SQL statements | 88 - | `record.uri` | string | AT URI of the record being evaluated | 89 - | `record.cid` | string | CID of the record | 90 - | `record.did` | string | DID of the record author | 91 - | `record.collection` | string | Collection NSID | 92 - | `record.value` | object | The record's fields | 80 + | Field | Type | Description | 81 + | ------------------- | -------- | ------------------------------------ | 82 + | `db.query` | function | Run SQL queries against DuckDB | 83 + | `db.run` | function | Execute SQL statements | 84 + | `record.uri` | string | AT URI of the record being evaluated | 85 + | `record.cid` | string | CID of the record | 86 + | `record.did` | string | DID of the record author | 87 + | `record.collection` | string | Collection NSID | 88 + | `record.value` | object | The record's fields | 93 89 94 90 Label rules run automatically when records are indexed. Return an array of label identifier strings to apply, or an empty array to skip. 95 91 ··· 119 115 const last = rows[rows.length - 1] 120 116 return ctx.ok({ 121 117 uris: rows.map((r) => r.uri), 122 - cursor: hasMore && last 123 - ? ctx.packCursor(last.indexed_at, last.cid) 124 - : undefined, 118 + cursor: hasMore && last ? ctx.packCursor(last.indexed_at, last.cid) : undefined, 125 119 }) 126 120 }, 127 121 ··· 139 133 140 134 The `ctx.labels()` method returns a `Map<string, Label[]>` where each label has: 141 135 142 - | Field | Type | Description | 143 - |-------|------|-------------| 144 - | `src` | string | DID of the label creator | 145 - | `uri` | string | AT URI of the labeled resource | 146 - | `val` | string | Label identifier (e.g. `"explicit"`) | 147 - | `neg` | boolean | If true, this negates a previous label | 148 - | `cts` | string | Timestamp when the label was created | 149 - | `exp` | string \| null | Expiration timestamp | 136 + | Field | Type | Description | 137 + | ----- | -------------- | -------------------------------------- | 138 + | `src` | string | DID of the label creator | 139 + | `uri` | string | AT URI of the labeled resource | 140 + | `val` | string | Label identifier (e.g. `"explicit"`) | 141 + | `neg` | boolean | If true, this negates a previous label | 142 + | `cts` | string | Timestamp when the label was created | 143 + | `exp` | string \| null | Expiration timestamp | 150 144 151 145 Only active labels are returned — expired labels and labels that have been negated are automatically filtered out. 152 146
+13 -13
docs/site/src/content/docs/guides/oauth.mdx
··· 30 30 31 31 ### Constructor options 32 32 33 - | Option | Default | Description | 34 - |--------|---------|-------------| 35 - | `server` | — | Hatk server URL | 36 - | `clientId` | `window.location.origin` | OAuth client ID (must match client metadata endpoint) | 37 - | `redirectUri` | current page URL | Where to redirect after authorization | 38 - | `scope` | `'atproto'` | OAuth scopes to request | 33 + | Option | Default | Description | 34 + | ------------- | ------------------------ | ----------------------------------------------------- | 35 + | `server` | — | Hatk server URL | 36 + | `clientId` | `window.location.origin` | OAuth client ID (must match client metadata endpoint) | 37 + | `redirectUri` | current page URL | Where to redirect after authorization | 38 + | `scope` | `'atproto'` | OAuth scopes to request | 39 39 40 40 ### Scopes 41 41 ··· 145 145 146 146 The OAuth flow uses these endpoints on your hatk server: 147 147 148 - | Endpoint | Purpose | 149 - |----------|---------| 150 - | `POST /oauth/par` | Pushed Authorization Request — initiates the flow | 151 - | `GET /oauth/authorize` | Redirects to the user's PDS | 152 - | `POST /oauth/token` | Exchanges authorization code for tokens | 153 - | `GET /oauth/jwks` | Public keys for token verification | 154 - | `GET /oauth/client-metadata.json` | Client metadata discovery | 148 + | Endpoint | Purpose | 149 + | --------------------------------- | ------------------------------------------------- | 150 + | `POST /oauth/par` | Pushed Authorization Request — initiates the flow | 151 + | `GET /oauth/authorize` | Redirects to the user's PDS | 152 + | `POST /oauth/token` | Exchanges authorization code for tokens | 153 + | `GET /oauth/jwks` | Public keys for token verification | 154 + | `GET /oauth/client-metadata.json` | Client metadata discovery | 155 155 156 156 All token requests include DPoP proofs (ECDSA P-256 key pairs stored in IndexedDB), which bind access tokens to the specific browser that requested them. 157 157
+27 -25
docs/site/src/content/docs/guides/opengraph.mdx
··· 61 61 This means your page routes and OG routes stay in sync automatically. If you have `og/artist.ts` with `path: '/og/artist/:name'`, then any visitor to `/artist/radiohead` gets meta tags injected: 62 62 63 63 ```html 64 - <meta property="og:image" content="https://yourapp.com/og/artist/radiohead"> 65 - <meta property="og:image:width" content="1200"> 66 - <meta property="og:image:height" content="630"> 67 - <meta name="twitter:card" content="summary_large_image"> 64 + <meta property="og:image" content="https://yourapp.com/og/artist/radiohead" /> 65 + <meta property="og:image:width" content="1200" /> 66 + <meta property="og:image:height" content="630" /> 67 + <meta name="twitter:card" content="summary_large_image" /> 68 68 ``` 69 69 70 70 ## The generate function ··· 73 73 74 74 The `generate` function receives an `OpengraphContext` with: 75 75 76 - | Field | Type | Description | 77 - |-------|------|-------------| 78 - | `db.query` | function | Run SQL queries against DuckDB | 79 - | `params` | object | URL path parameters (e.g. `{ artist: 'Radiohead' }`) | 76 + | Field | Type | Description | 77 + | ------------ | -------- | ------------------------------------------------------- | 78 + | `db.query` | function | Run SQL queries against DuckDB | 79 + | `params` | object | URL path parameters (e.g. `{ artist: 'Radiohead' }`) | 80 80 | `fetchImage` | function | Fetch a remote image and return it as a base64 data URL | 81 - | `lookup` | function | Look up records by field value | 82 - | `count` | function | Count records by field value | 83 - | `labels` | function | Query labels for record URIs | 84 - | `blobUrl` | function | Resolve a blob reference to a CDN URL | 81 + | `lookup` | function | Look up records by field value | 82 + | `count` | function | Count records by field value | 83 + | `labels` | function | Query labels for record URIs | 84 + | `blobUrl` | function | Resolve a blob reference to a CDN URL | 85 85 86 86 ### Return value 87 87 88 88 Return an `OpengraphResult`: 89 89 90 - | Field | Required | Description | 91 - |-------|----------|-------------| 92 - | `element` | Yes | A satori virtual DOM tree (see below) | 93 - | `options` | No | Override `width` (default 1200), `height` (default 630), or provide custom `fonts` | 94 - | `meta` | No | `title` and `description` for the injected meta tags | 90 + | Field | Required | Description | 91 + | --------- | -------- | ---------------------------------------------------------------------------------- | 92 + | `element` | Yes | A satori virtual DOM tree (see below) | 93 + | `options` | No | Override `width` (default 1200), `height` (default 630), or provide custom `fonts` | 94 + | `meta` | No | `title` and `description` for the injected meta tags | 95 95 96 96 ### Virtual DOM 97 97 ··· 164 164 children: [ 165 165 // Artist image 166 166 ...(artUrl 167 - ? [{ 168 - type: 'img', 169 - props: { 170 - src: artUrl, 171 - width: 300, 172 - height: 300, 173 - style: { borderRadius: '20px', objectFit: 'cover' }, 167 + ? [ 168 + { 169 + type: 'img', 170 + props: { 171 + src: artUrl, 172 + width: 300, 173 + height: 300, 174 + style: { borderRadius: '20px', objectFit: 'cover' }, 175 + }, 174 176 }, 175 - }] 177 + ] 176 178 : []), 177 179 // Text content 178 180 {
+5 -5
docs/site/src/content/docs/guides/seeds.mdx
··· 52 52 53 53 ## `seed()` helpers 54 54 55 - | Function | Description | 56 - |----------|-------------| 57 - | `createAccount(handle)` | Create a test account on the local PDS. Returns `{ did, handle }` | 58 - | `createRecord(account, collection, record, opts?)` | Create a record. Pass `{ rkey }` in opts for a specific record key | 59 - | `uploadBlob(account, filePath)` | Upload a file as a blob. Returns a blob reference for use in records | 55 + | Function | Description | 56 + | -------------------------------------------------- | -------------------------------------------------------------------- | 57 + | `createAccount(handle)` | Create a test account on the local PDS. Returns `{ did, handle }` | 58 + | `createRecord(account, collection, record, opts?)` | Create a record. Pass `{ rkey }` in opts for a specific record key | 59 + | `uploadBlob(account, filePath)` | Upload a file as a blob. Returns a blob reference for use in records | 60 60 61 61 ## Tips 62 62
+21 -21
docs/site/src/content/docs/guides/xrpc-handlers.mdx
··· 79 79 80 80 Both `defineQuery` and `defineProcedure` receive the same context: 81 81 82 - | Field | Type | Description | 83 - |-------|------|-------------| 84 - | `db.query` | function | Run SQL queries against DuckDB | 85 - | `db.run` | function | Execute SQL statements | 86 - | `params` | object | Typed parameters from the lexicon schema | 87 - | `input` | object | Request body (procedures only), typed from the lexicon's input schema | 88 - | `limit` | number | Requested page size | 89 - | `cursor` | string \| undefined | Pagination cursor | 90 - | `viewer` | `{ did: string }` \| null | The authenticated user, or null | 91 - | `ok` | function | Wraps your return value with type checking | 92 - | `packCursor` | function | Encode a `(primary, cid)` pair into a cursor string | 93 - | `unpackCursor` | function | Decode a cursor back into `{ primary, cid }` | 94 - | `search` | function | Full-text search a collection | 95 - | `resolve` | function | Resolve AT URIs into full records | 96 - | `lookup` | function | Look up records by a field value | 97 - | `count` | function | Count records by field value | 98 - | `exists` | function | Check if a record exists matching field filters | 99 - | `labels` | function | Query labels for a list of URIs | 100 - | `blobUrl` | function | Resolve a blob reference to a CDN URL | 101 - | `isTakendown` | function | Check if a DID has been taken down | 102 - | `filterTakendownDids` | function | Filter a list of DIDs, returning those taken down | 82 + | Field | Type | Description | 83 + | --------------------- | ------------------------- | --------------------------------------------------------------------- | 84 + | `db.query` | function | Run SQL queries against DuckDB | 85 + | `db.run` | function | Execute SQL statements | 86 + | `params` | object | Typed parameters from the lexicon schema | 87 + | `input` | object | Request body (procedures only), typed from the lexicon's input schema | 88 + | `limit` | number | Requested page size | 89 + | `cursor` | string \| undefined | Pagination cursor | 90 + | `viewer` | `{ did: string }` \| null | The authenticated user, or null | 91 + | `ok` | function | Wraps your return value with type checking | 92 + | `packCursor` | function | Encode a `(primary, cid)` pair into a cursor string | 93 + | `unpackCursor` | function | Decode a cursor back into `{ primary, cid }` | 94 + | `search` | function | Full-text search a collection | 95 + | `resolve` | function | Resolve AT URIs into full records | 96 + | `lookup` | function | Look up records by a field value | 97 + | `count` | function | Count records by field value | 98 + | `exists` | function | Check if a record exists matching field filters | 99 + | `labels` | function | Query labels for a list of URIs | 100 + | `blobUrl` | function | Resolve a blob reference to a CDN URL | 101 + | `isTakendown` | function | Check if a DID has been taken down | 102 + | `filterTakendownDids` | function | Filter a list of DIDs, returning those taken down | 103 103 104 104 ## Errors 105 105
+1 -1
packages/hatk/package.json
··· 22 22 "build": "vite build" 23 23 }, 24 24 "dependencies": { 25 - "@hatk/oauth-client": "*", 26 25 "@bigmoves/lexicon": "^0.2.1", 27 26 "@duckdb/node-api": "^1.4.4-r.1", 27 + "@hatk/oauth-client": "*", 28 28 "@resvg/resvg-js": "^2.6.2", 29 29 "satori": "^0.19.2", 30 30 "vitest": "^4",
+59 -19
packages/hatk/src/cli.ts
··· 14 14 try { 15 15 const res = await fetch('http://localhost:2583/xrpc/_health') 16 16 if (res.ok) return 17 - } catch { } 17 + } catch {} 18 18 // Start it 19 19 console.log('[dev] starting PDS...') 20 20 execSync('docker compose up -d', { stdio: 'inherit', cwd: process.cwd() }) ··· 22 22 for (let i = 0; i < 30; i++) { 23 23 try { 24 24 const res = await fetch('http://localhost:2583/xrpc/_health') 25 - if (res.ok) { console.log('[dev] PDS ready'); return } 26 - } catch { } 25 + if (res.ok) { 26 + console.log('[dev] PDS ready') 27 + return 28 + } 29 + } catch {} 27 30 await new Promise((r) => setTimeout(r, 1000)) 28 31 } 29 32 console.error('[dev] PDS failed to start') ··· 349 352 350 353 const withSvelte = args.includes('--svelte') 351 354 mkdirSync(dir) 352 - const subs = ['lexicons', 'feeds', 'xrpc', 'og', 'labels', 'jobs', 'seeds', 'setup', 'public', 'test', 'test/feeds', 'test/xrpc', 'test/integration', 'test/browser', 'test/fixtures'] 355 + const subs = [ 356 + 'lexicons', 357 + 'feeds', 358 + 'xrpc', 359 + 'og', 360 + 'labels', 361 + 'jobs', 362 + 'seeds', 363 + 'setup', 364 + 'public', 365 + 'test', 366 + 'test/feeds', 367 + 'test/xrpc', 368 + 'test/integration', 369 + 'test/browser', 370 + 'test/fixtures', 371 + ] 353 372 if (withSvelte) subs.push('src', 'src/routes', 'src/lib') 354 373 for (const sub of subs) { 355 374 mkdirSync(join(dir, sub)) ··· 1331 1350 for (const { nsid, defType } of entries) { 1332 1351 if (!defType) continue 1333 1352 // createRecord/deleteRecord/putRecord get typed overrides after RecordRegistry 1334 - if (nsid === 'dev.hatk.createRecord' || nsid === 'dev.hatk.deleteRecord' || nsid === 'dev.hatk.putRecord') continue 1353 + if (nsid === 'dev.hatk.createRecord' || nsid === 'dev.hatk.deleteRecord' || nsid === 'dev.hatk.putRecord') 1354 + continue 1335 1355 const varName = varNames.get(nsid)! 1336 1356 const typeName = capitalize(varName) 1337 1357 const wrapper = wrapperMap[defType] ··· 1443 1463 // Pattern 3: cross-namespace view — has explicit ref to a record-type lexicon 1444 1464 if (!found) { 1445 1465 const recordRef = Object.values(def.properties).find( 1446 - (p: any) => p.type === 'ref' && !p.ref.startsWith('#') && lexicons.get(p.ref)?.defs?.main?.type === 'record', 1466 + (p: any) => 1467 + p.type === 'ref' && !p.ref.startsWith('#') && lexicons.get(p.ref)?.defs?.main?.type === 'record', 1447 1468 ) as any 1448 1469 if (recordRef) { 1449 1470 viewEntries.push({ fullNsid, typeName: name, collection: recordRef.ref }) ··· 1554 1575 } else { 1555 1576 const name = args[2] 1556 1577 if (!type || !name || !templates[type]) { 1557 - console.error(`Usage: hatk generate <${[...Object.keys(templates), ...Object.keys(lexiconTemplates)].join('|')}|types> <name>`) 1578 + console.error( 1579 + `Usage: hatk generate <${[...Object.keys(templates), ...Object.keys(lexiconTemplates)].join('|')}|types> <name>`, 1580 + ) 1558 1581 process.exit(1) 1559 1582 } 1560 1583 ··· 1706 1729 console.log('[check] tsc (server)...') 1707 1730 try { 1708 1731 execSync('npx tsc --noEmit -p tsconfig.server.json', { stdio: 'inherit', cwd: process.cwd() }) 1709 - } catch { failed = true } 1732 + } catch { 1733 + failed = true 1734 + } 1710 1735 } 1711 1736 1712 1737 // Svelte type checking (if SvelteKit project) 1713 1738 if (existsSync(resolve('svelte.config.js')) && existsSync(resolve('src/app.html'))) { 1714 1739 console.log('[check] svelte-check...') 1715 1740 try { 1716 - execSync('npx svelte-kit sync && npx svelte-check --tsconfig ./tsconfig.json', { stdio: 'inherit', cwd: process.cwd() }) 1717 - } catch { failed = true } 1741 + execSync('npx svelte-kit sync && npx svelte-check --tsconfig ./tsconfig.json', { 1742 + stdio: 'inherit', 1743 + cwd: process.cwd(), 1744 + }) 1745 + } catch { 1746 + failed = true 1747 + } 1718 1748 } 1719 1749 1720 1750 // Lint 1721 1751 console.log('[check] oxlint...') 1722 1752 try { 1723 1753 execSync('npx oxlint .', { stdio: 'inherit', cwd: process.cwd() }) 1724 - } catch { failed = true } 1754 + } catch { 1755 + failed = true 1756 + } 1725 1757 1726 1758 if (failed) process.exit(1) 1727 1759 } else if (command === 'test') { 1728 1760 const knownFlags = new Set(['--unit', '--integration', '--browser', '--verbose']) 1729 1761 const parsedFlags = args.slice(1).filter((a) => knownFlags.has(a)) 1730 - const extraArgs = args.slice(1).filter((a) => !knownFlags.has(a)).join(' ') 1762 + const extraArgs = args 1763 + .slice(1) 1764 + .filter((a) => !knownFlags.has(a)) 1765 + .join(' ') 1731 1766 const flag = parsedFlags.find((f) => f !== '--verbose') || null 1732 1767 const verbose = parsedFlags.includes('--verbose') 1733 1768 if (!verbose && !process.env.DEBUG) process.env.DEBUG = '0' ··· 1771 1806 1772 1807 if (runBrowser) { 1773 1808 const browserDir = resolve(process.cwd(), 'test/browser') 1774 - const hasBrowserTests = existsSync(browserDir) && readdirSync(browserDir).some((f) => f.endsWith('.test.ts') || f.endsWith('.spec.ts')) 1809 + const hasBrowserTests = 1810 + existsSync(browserDir) && readdirSync(browserDir).some((f) => f.endsWith('.test.ts') || f.endsWith('.spec.ts')) 1775 1811 if (hasBrowserTests) { 1776 1812 console.log('[test] running browser tests...') 1777 1813 try { ··· 1828 1864 const instance = await DuckDBInstance.create(config.database) 1829 1865 const con = await instance.connect() 1830 1866 1831 - const tables = (await (await con.runAndReadAll( 1832 - `SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name`, 1833 - )).getRowObjects()) as { table_name: string }[] 1867 + const tables = (await ( 1868 + await con.runAndReadAll( 1869 + `SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name`, 1870 + ) 1871 + ).getRowObjects()) as { table_name: string }[] 1834 1872 1835 1873 for (const { table_name } of tables) { 1836 1874 console.log(`"${table_name}"`) 1837 - const cols = (await (await con.runAndReadAll( 1838 - `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = '${table_name}' ORDER BY ordinal_position`, 1839 - )).getRowObjects()) as { column_name: string; data_type: string; is_nullable: string }[] 1875 + const cols = (await ( 1876 + await con.runAndReadAll( 1877 + `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = '${table_name}' ORDER BY ordinal_position`, 1878 + ) 1879 + ).getRowObjects()) as { column_name: string; data_type: string; is_nullable: string }[] 1840 1880 1841 1881 for (const col of cols) { 1842 1882 const nullable = col.is_nullable === 'YES' ? '' : ' NOT NULL'
+27 -24
packages/hatk/src/db.ts
··· 11 11 const schemas = new Map<string, TableSchema>() 12 12 13 13 export function closeDatabase(): void { 14 - try { readCon?.closeSync() } catch {} 15 - try { con?.closeSync() } catch {} 16 - try { instance?.closeSync() } catch {} 14 + try { 15 + readCon?.closeSync() 16 + } catch {} 17 + try { 18 + con?.closeSync() 19 + } catch {} 20 + try { 21 + instance?.closeSync() 22 + } catch {} 17 23 } 18 24 19 25 let writeQueue = Promise.resolve() ··· 413 419 const unionValue = record[union.fieldName] 414 420 if (!unionValue || !unionValue.$type) continue 415 421 416 - const branch = union.branches.find(b => b.type === unionValue.$type) 422 + const branch = union.branches.find((b) => b.type === unionValue.$type) 417 423 if (!branch) continue 418 424 419 425 // Delete existing branch rows (handles INSERT OR REPLACE) ··· 704 710 705 711 // Delete existing child rows for these URIs, then merge staging 706 712 const uriPlaceholders = recs.map((_, i) => `$${i + 1}`).join(',') 707 - const delStmt = await con.prepare( 708 - `DELETE FROM ${child.tableName} WHERE parent_uri IN (${uriPlaceholders})`, 713 + const delStmt = await con.prepare(`DELETE FROM ${child.tableName} WHERE parent_uri IN (${uriPlaceholders})`) 714 + bindParams( 715 + delStmt, 716 + recs.map((r) => r.uri), 709 717 ) 710 - bindParams(delStmt, recs.map((r) => r.uri)) 711 718 await delStmt.run() 712 719 713 720 const childSelectCols = childAllCols.map((name) => { ··· 799 806 800 807 // Delete existing branch rows for these URIs, then merge staging 801 808 const uriPlaceholders = recs.map((_, i) => `$${i + 1}`).join(',') 802 - const delStmt = await con.prepare( 803 - `DELETE FROM ${branch.tableName} WHERE parent_uri IN (${uriPlaceholders})`, 809 + const delStmt = await con.prepare(`DELETE FROM ${branch.tableName} WHERE parent_uri IN (${uriPlaceholders})`) 810 + bindParams( 811 + delStmt, 812 + recs.map((r) => r.uri), 804 813 ) 805 - bindParams(delStmt, recs.map((r) => r.uri)) 806 814 await delStmt.run() 807 815 808 816 const branchSelectCols = branchAllCols.map((name) => { ··· 892 900 childData.set(child.fieldName, childRows) 893 901 } 894 902 for (const row of rows) { 895 - (row as any).__childData = childData 903 + ;(row as any).__childData = childData 896 904 } 897 905 } 898 906 ··· 909 917 unionData.set(union.fieldName, branchData) 910 918 } 911 919 for (const row of rows) { 912 - (row as any).__unionData = unionData 920 + ;(row as any).__unionData = unionData 913 921 } 914 922 } 915 923 ··· 983 991 984 992 // Attach child data to rows for reshapeRow 985 993 for (const row of rows) { 986 - (row as any).__childData = childData 994 + ;(row as any).__childData = childData 987 995 if (unionData.size > 0) (row as any).__unionData = unionData 988 996 } 989 997 ··· 1293 1301 childData.set(child.fieldName, childRows) 1294 1302 } 1295 1303 for (const row of rows) { 1296 - (row as any).__childData = childData 1304 + ;(row as any).__childData = childData 1297 1305 } 1298 1306 } 1299 1307 ··· 1342 1350 return v 1343 1351 } 1344 1352 1345 - export async function getChildRows( 1346 - childTableName: string, 1347 - parentUris: string[], 1348 - ): Promise<Map<string, any[]>> { 1353 + export async function getChildRows(childTableName: string, parentUris: string[]): Promise<Map<string, any[]>> { 1349 1354 if (parentUris.length === 0) return new Map() 1350 1355 const placeholders = parentUris.map((_, i) => `$${i + 1}`).join(',') 1351 - const rows = await all( 1352 - `SELECT * FROM ${childTableName} WHERE parent_uri IN (${placeholders})`, 1353 - ...parentUris, 1354 - ) 1356 + const rows = await all(`SELECT * FROM ${childTableName} WHERE parent_uri IN (${placeholders})`, ...parentUris) 1355 1357 const result = new Map<string, any[]>() 1356 1358 for (const row of rows) { 1357 1359 const key = row.parent_uri as string ··· 1549 1551 } 1550 1552 1551 1553 export async function backfillChildTables(): Promise<void> { 1552 - for (const [collection, schema] of schemas) { 1554 + for (const [, schema] of schemas) { 1553 1555 for (const child of schema.children) { 1554 1556 // Check if child table needs backfill (significantly fewer rows than parent) 1555 1557 const mainCount = (await all(`SELECT COUNT(*)::INTEGER as n FROM ${schema.tableName}`))[0]?.n || 0 1556 1558 if (mainCount === 0) continue 1557 - const childCount = (await all(`SELECT COUNT(DISTINCT parent_uri)::INTEGER as n FROM ${child.tableName}`))[0]?.n || 0 1559 + const childCount = 1560 + (await all(`SELECT COUNT(DISTINCT parent_uri)::INTEGER as n FROM ${child.tableName}`))[0]?.n || 0 1558 1561 if (childCount >= mainCount * 0.9) continue 1559 1562 1560 1563 console.log(`[db] Backfilling ${child.tableName} from ${schema.tableName}...`)
+17 -4
packages/hatk/src/feeds.ts
··· 53 53 54 54 // --- Typed feed helper --- 55 55 56 - type FeedGenerate = (ctx: FeedContext & { ok: (value: FeedResult) => Checked<FeedResult> }) => Promise<Checked<FeedResult>> 56 + type FeedGenerate = ( 57 + ctx: FeedContext & { ok: (value: FeedResult) => Checked<FeedResult> }, 58 + ) => Promise<Checked<FeedResult>> 57 59 58 60 type FeedOpts = 59 - | { collection: string; view?: string; label: string; generate: FeedGenerate; hydrate?: (ctx: HydrateContext<any>) => Promise<unknown[]> } 60 - | { collection?: never; view?: never; label: string; generate: FeedGenerate; hydrate: (ctx: HydrateContext<any>) => Promise<unknown[]> } 61 + | { 62 + collection: string 63 + view?: string 64 + label: string 65 + generate: FeedGenerate 66 + hydrate?: (ctx: HydrateContext<any>) => Promise<unknown[]> 67 + } 68 + | { 69 + collection?: never 70 + view?: never 71 + label: string 72 + generate: FeedGenerate 73 + hydrate: (ctx: HydrateContext<any>) => Promise<unknown[]> 74 + } 61 75 62 76 export function createPaginate(deps: { 63 77 db: { query: (sql: string, params?: any[]) => Promise<any[]> } ··· 211 225 212 226 return { uris: result.uris, cursor: result.cursor } 213 227 } 214 - 215 228 216 229 export function listFeeds(): { name: string; label: string }[] { 217 230 return Array.from(feeds.values()).map((f) => ({ name: f.name, label: f.label }))
+571 -1
packages/hatk/src/fts.ts
··· 188 188 */ 189 189 // DuckDB's built-in English stop words (571 words) — must match stopwords='english' in create_fts_index 190 190 const ENGLISH_STOP_WORDS = new Set([ 191 - "a","a's","able","about","above","according","accordingly","across","actually","after","afterwards","again","against","ain't","all","allow","allows","almost","alone","along","already","also","although","always","am","among","amongst","an","and","another","any","anybody","anyhow","anyone","anything","anyway","anyways","anywhere","apart","appear","appreciate","appropriate","are","aren't","around","as","aside","ask","asking","associated","at","available","away","awfully","b","be","became","because","become","becomes","becoming","been","before","beforehand","behind","being","believe","below","beside","besides","best","better","between","beyond","both","brief","but","by","c","c'mon","c's","came","can","can't","cannot","cant","cause","causes","certain","certainly","changes","clearly","co","com","come","comes","concerning","consequently","consider","considering","contain","containing","contains","corresponding","could","couldn't","course","currently","d","definitely","described","despite","did","didn't","different","do","does","doesn't","doing","don't","done","down","downwards","during","e","each","edu","eg","eight","either","else","elsewhere","enough","entirely","especially","et","etc","even","ever","every","everybody","everyone","everything","everywhere","ex","exactly","example","except","f","far","few","fifth","first","five","followed","following","follows","for","former","formerly","forth","four","from","further","furthermore","g","get","gets","getting","given","gives","go","goes","going","gone","got","gotten","greetings","h","had","hadn't","happens","hardly","has","hasn't","have","haven't","having","he","he's","hello","help","hence","her","here","here's","hereafter","hereby","herein","hereupon","hers","herself","hi","him","himself","his","hither","hopefully","how","howbeit","however","i","i'd","i'll","i'm","i've","ie","if","ignored","immediate","in","inasmuch","inc","indeed","indicate","indicated","indicates","inner","insofar","instead","into","inward","is","isn't","it","it'd","it'll","it's","its","itself","j","just","k","keep","keeps","kept","know","known","knows","l","last","lately","later","latter","latterly","least","less","lest","let","let's","like","liked","likely","little","look","looking","looks","ltd","m","mainly","many","may","maybe","me","mean","meanwhile","merely","might","more","moreover","most","mostly","much","must","my","myself","n","name","namely","nd","near","nearly","necessary","need","needs","neither","never","nevertheless","new","next","nine","no","nobody","non","none","noone","nor","normally","not","nothing","novel","now","nowhere","o","obviously","of","off","often","oh","ok","okay","old","on","once","one","ones","only","onto","or","other","others","otherwise","ought","our","ours","ourselves","out","outside","over","overall","own","p","particular","particularly","per","perhaps","placed","please","plus","possible","presumably","probably","provides","q","que","quite","qv","r","rather","rd","re","really","reasonably","regarding","regardless","regards","relatively","respectively","right","s","said","same","saw","say","saying","says","second","secondly","see","seeing","seem","seemed","seeming","seems","seen","self","selves","sensible","sent","serious","seriously","seven","several","shall","she","should","shouldn't","since","six","so","some","somebody","somehow","someone","something","sometime","sometimes","somewhat","somewhere","soon","sorry","specified","specify","specifying","still","sub","such","sup","sure","t","t's","take","taken","tell","tends","th","than","thank","thanks","thanx","that","that's","thats","the","their","theirs","them","themselves","then","thence","there","there's","thereafter","thereby","therefore","therein","theres","thereupon","these","they","they'd","they'll","they're","they've","think","third","this","thorough","thoroughly","those","though","three","through","throughout","thru","thus","to","together","too","took","toward","towards","tried","tries","truly","try","trying","twice","two","u","un","under","unfortunately","unless","unlikely","until","unto","up","upon","us","use","used","useful","uses","using","usually","uucp","v","value","various","very","via","viz","vs","w","want","wants","was","wasn't","way","we","we'd","we'll","we're","we've","welcome","well","went","were","weren't","what","what's","whatever","when","whence","whenever","where","where's","whereafter","whereas","whereby","wherein","whereupon","wherever","whether","which","while","whither","who","who's","whoever","whole","whom","whose","why","will","willing","wish","with","within","without","won't","wonder","would","would","wouldn't","x","y","yes","yet","you","you'd","you'll","you're","you've","your","yours","yourself","yourselves","z","zero", 191 + 'a', 192 + "a's", 193 + 'able', 194 + 'about', 195 + 'above', 196 + 'according', 197 + 'accordingly', 198 + 'across', 199 + 'actually', 200 + 'after', 201 + 'afterwards', 202 + 'again', 203 + 'against', 204 + "ain't", 205 + 'all', 206 + 'allow', 207 + 'allows', 208 + 'almost', 209 + 'alone', 210 + 'along', 211 + 'already', 212 + 'also', 213 + 'although', 214 + 'always', 215 + 'am', 216 + 'among', 217 + 'amongst', 218 + 'an', 219 + 'and', 220 + 'another', 221 + 'any', 222 + 'anybody', 223 + 'anyhow', 224 + 'anyone', 225 + 'anything', 226 + 'anyway', 227 + 'anyways', 228 + 'anywhere', 229 + 'apart', 230 + 'appear', 231 + 'appreciate', 232 + 'appropriate', 233 + 'are', 234 + "aren't", 235 + 'around', 236 + 'as', 237 + 'aside', 238 + 'ask', 239 + 'asking', 240 + 'associated', 241 + 'at', 242 + 'available', 243 + 'away', 244 + 'awfully', 245 + 'b', 246 + 'be', 247 + 'became', 248 + 'because', 249 + 'become', 250 + 'becomes', 251 + 'becoming', 252 + 'been', 253 + 'before', 254 + 'beforehand', 255 + 'behind', 256 + 'being', 257 + 'believe', 258 + 'below', 259 + 'beside', 260 + 'besides', 261 + 'best', 262 + 'better', 263 + 'between', 264 + 'beyond', 265 + 'both', 266 + 'brief', 267 + 'but', 268 + 'by', 269 + 'c', 270 + "c'mon", 271 + "c's", 272 + 'came', 273 + 'can', 274 + "can't", 275 + 'cannot', 276 + 'cant', 277 + 'cause', 278 + 'causes', 279 + 'certain', 280 + 'certainly', 281 + 'changes', 282 + 'clearly', 283 + 'co', 284 + 'com', 285 + 'come', 286 + 'comes', 287 + 'concerning', 288 + 'consequently', 289 + 'consider', 290 + 'considering', 291 + 'contain', 292 + 'containing', 293 + 'contains', 294 + 'corresponding', 295 + 'could', 296 + "couldn't", 297 + 'course', 298 + 'currently', 299 + 'd', 300 + 'definitely', 301 + 'described', 302 + 'despite', 303 + 'did', 304 + "didn't", 305 + 'different', 306 + 'do', 307 + 'does', 308 + "doesn't", 309 + 'doing', 310 + "don't", 311 + 'done', 312 + 'down', 313 + 'downwards', 314 + 'during', 315 + 'e', 316 + 'each', 317 + 'edu', 318 + 'eg', 319 + 'eight', 320 + 'either', 321 + 'else', 322 + 'elsewhere', 323 + 'enough', 324 + 'entirely', 325 + 'especially', 326 + 'et', 327 + 'etc', 328 + 'even', 329 + 'ever', 330 + 'every', 331 + 'everybody', 332 + 'everyone', 333 + 'everything', 334 + 'everywhere', 335 + 'ex', 336 + 'exactly', 337 + 'example', 338 + 'except', 339 + 'f', 340 + 'far', 341 + 'few', 342 + 'fifth', 343 + 'first', 344 + 'five', 345 + 'followed', 346 + 'following', 347 + 'follows', 348 + 'for', 349 + 'former', 350 + 'formerly', 351 + 'forth', 352 + 'four', 353 + 'from', 354 + 'further', 355 + 'furthermore', 356 + 'g', 357 + 'get', 358 + 'gets', 359 + 'getting', 360 + 'given', 361 + 'gives', 362 + 'go', 363 + 'goes', 364 + 'going', 365 + 'gone', 366 + 'got', 367 + 'gotten', 368 + 'greetings', 369 + 'h', 370 + 'had', 371 + "hadn't", 372 + 'happens', 373 + 'hardly', 374 + 'has', 375 + "hasn't", 376 + 'have', 377 + "haven't", 378 + 'having', 379 + 'he', 380 + "he's", 381 + 'hello', 382 + 'help', 383 + 'hence', 384 + 'her', 385 + 'here', 386 + "here's", 387 + 'hereafter', 388 + 'hereby', 389 + 'herein', 390 + 'hereupon', 391 + 'hers', 392 + 'herself', 393 + 'hi', 394 + 'him', 395 + 'himself', 396 + 'his', 397 + 'hither', 398 + 'hopefully', 399 + 'how', 400 + 'howbeit', 401 + 'however', 402 + 'i', 403 + "i'd", 404 + "i'll", 405 + "i'm", 406 + "i've", 407 + 'ie', 408 + 'if', 409 + 'ignored', 410 + 'immediate', 411 + 'in', 412 + 'inasmuch', 413 + 'inc', 414 + 'indeed', 415 + 'indicate', 416 + 'indicated', 417 + 'indicates', 418 + 'inner', 419 + 'insofar', 420 + 'instead', 421 + 'into', 422 + 'inward', 423 + 'is', 424 + "isn't", 425 + 'it', 426 + "it'd", 427 + "it'll", 428 + "it's", 429 + 'its', 430 + 'itself', 431 + 'j', 432 + 'just', 433 + 'k', 434 + 'keep', 435 + 'keeps', 436 + 'kept', 437 + 'know', 438 + 'known', 439 + 'knows', 440 + 'l', 441 + 'last', 442 + 'lately', 443 + 'later', 444 + 'latter', 445 + 'latterly', 446 + 'least', 447 + 'less', 448 + 'lest', 449 + 'let', 450 + "let's", 451 + 'like', 452 + 'liked', 453 + 'likely', 454 + 'little', 455 + 'look', 456 + 'looking', 457 + 'looks', 458 + 'ltd', 459 + 'm', 460 + 'mainly', 461 + 'many', 462 + 'may', 463 + 'maybe', 464 + 'me', 465 + 'mean', 466 + 'meanwhile', 467 + 'merely', 468 + 'might', 469 + 'more', 470 + 'moreover', 471 + 'most', 472 + 'mostly', 473 + 'much', 474 + 'must', 475 + 'my', 476 + 'myself', 477 + 'n', 478 + 'name', 479 + 'namely', 480 + 'nd', 481 + 'near', 482 + 'nearly', 483 + 'necessary', 484 + 'need', 485 + 'needs', 486 + 'neither', 487 + 'never', 488 + 'nevertheless', 489 + 'new', 490 + 'next', 491 + 'nine', 492 + 'no', 493 + 'nobody', 494 + 'non', 495 + 'none', 496 + 'noone', 497 + 'nor', 498 + 'normally', 499 + 'not', 500 + 'nothing', 501 + 'novel', 502 + 'now', 503 + 'nowhere', 504 + 'o', 505 + 'obviously', 506 + 'of', 507 + 'off', 508 + 'often', 509 + 'oh', 510 + 'ok', 511 + 'okay', 512 + 'old', 513 + 'on', 514 + 'once', 515 + 'one', 516 + 'ones', 517 + 'only', 518 + 'onto', 519 + 'or', 520 + 'other', 521 + 'others', 522 + 'otherwise', 523 + 'ought', 524 + 'our', 525 + 'ours', 526 + 'ourselves', 527 + 'out', 528 + 'outside', 529 + 'over', 530 + 'overall', 531 + 'own', 532 + 'p', 533 + 'particular', 534 + 'particularly', 535 + 'per', 536 + 'perhaps', 537 + 'placed', 538 + 'please', 539 + 'plus', 540 + 'possible', 541 + 'presumably', 542 + 'probably', 543 + 'provides', 544 + 'q', 545 + 'que', 546 + 'quite', 547 + 'qv', 548 + 'r', 549 + 'rather', 550 + 'rd', 551 + 're', 552 + 'really', 553 + 'reasonably', 554 + 'regarding', 555 + 'regardless', 556 + 'regards', 557 + 'relatively', 558 + 'respectively', 559 + 'right', 560 + 's', 561 + 'said', 562 + 'same', 563 + 'saw', 564 + 'say', 565 + 'saying', 566 + 'says', 567 + 'second', 568 + 'secondly', 569 + 'see', 570 + 'seeing', 571 + 'seem', 572 + 'seemed', 573 + 'seeming', 574 + 'seems', 575 + 'seen', 576 + 'self', 577 + 'selves', 578 + 'sensible', 579 + 'sent', 580 + 'serious', 581 + 'seriously', 582 + 'seven', 583 + 'several', 584 + 'shall', 585 + 'she', 586 + 'should', 587 + "shouldn't", 588 + 'since', 589 + 'six', 590 + 'so', 591 + 'some', 592 + 'somebody', 593 + 'somehow', 594 + 'someone', 595 + 'something', 596 + 'sometime', 597 + 'sometimes', 598 + 'somewhat', 599 + 'somewhere', 600 + 'soon', 601 + 'sorry', 602 + 'specified', 603 + 'specify', 604 + 'specifying', 605 + 'still', 606 + 'sub', 607 + 'such', 608 + 'sup', 609 + 'sure', 610 + 't', 611 + "t's", 612 + 'take', 613 + 'taken', 614 + 'tell', 615 + 'tends', 616 + 'th', 617 + 'than', 618 + 'thank', 619 + 'thanks', 620 + 'thanx', 621 + 'that', 622 + "that's", 623 + 'thats', 624 + 'the', 625 + 'their', 626 + 'theirs', 627 + 'them', 628 + 'themselves', 629 + 'then', 630 + 'thence', 631 + 'there', 632 + "there's", 633 + 'thereafter', 634 + 'thereby', 635 + 'therefore', 636 + 'therein', 637 + 'theres', 638 + 'thereupon', 639 + 'these', 640 + 'they', 641 + "they'd", 642 + "they'll", 643 + "they're", 644 + "they've", 645 + 'think', 646 + 'third', 647 + 'this', 648 + 'thorough', 649 + 'thoroughly', 650 + 'those', 651 + 'though', 652 + 'three', 653 + 'through', 654 + 'throughout', 655 + 'thru', 656 + 'thus', 657 + 'to', 658 + 'together', 659 + 'too', 660 + 'took', 661 + 'toward', 662 + 'towards', 663 + 'tried', 664 + 'tries', 665 + 'truly', 666 + 'try', 667 + 'trying', 668 + 'twice', 669 + 'two', 670 + 'u', 671 + 'un', 672 + 'under', 673 + 'unfortunately', 674 + 'unless', 675 + 'unlikely', 676 + 'until', 677 + 'unto', 678 + 'up', 679 + 'upon', 680 + 'us', 681 + 'use', 682 + 'used', 683 + 'useful', 684 + 'uses', 685 + 'using', 686 + 'usually', 687 + 'uucp', 688 + 'v', 689 + 'value', 690 + 'various', 691 + 'very', 692 + 'via', 693 + 'viz', 694 + 'vs', 695 + 'w', 696 + 'want', 697 + 'wants', 698 + 'was', 699 + "wasn't", 700 + 'way', 701 + 'we', 702 + "we'd", 703 + "we'll", 704 + "we're", 705 + "we've", 706 + 'welcome', 707 + 'well', 708 + 'went', 709 + 'were', 710 + "weren't", 711 + 'what', 712 + "what's", 713 + 'whatever', 714 + 'when', 715 + 'whence', 716 + 'whenever', 717 + 'where', 718 + "where's", 719 + 'whereafter', 720 + 'whereas', 721 + 'whereby', 722 + 'wherein', 723 + 'whereupon', 724 + 'wherever', 725 + 'whether', 726 + 'which', 727 + 'while', 728 + 'whither', 729 + 'who', 730 + "who's", 731 + 'whoever', 732 + 'whole', 733 + 'whom', 734 + 'whose', 735 + 'why', 736 + 'will', 737 + 'willing', 738 + 'wish', 739 + 'with', 740 + 'within', 741 + 'without', 742 + "won't", 743 + 'wonder', 744 + 'would', 745 + 'would', 746 + "wouldn't", 747 + 'x', 748 + 'y', 749 + 'yes', 750 + 'yet', 751 + 'you', 752 + "you'd", 753 + "you'll", 754 + "you're", 755 + "you've", 756 + 'your', 757 + 'yours', 758 + 'yourself', 759 + 'yourselves', 760 + 'z', 761 + 'zero', 192 762 ]) 193 763 194 764 /**
+6 -4
packages/hatk/src/hydrate.ts
··· 62 62 } 63 63 64 64 // Return in original URI order, reshaped 65 - return uris.map((uri) => { 66 - const row = primaryRecords.get(uri) 67 - return reshapeRow(row, row?.__childData, row?.__unionData) 68 - }).filter((r): r is Row<unknown> => r != null) 65 + return uris 66 + .map((uri) => { 67 + const row = primaryRecords.get(uri) 68 + return reshapeRow(row, row?.__childData, row?.__unionData) 69 + }) 70 + .filter((r): r is Row<unknown> => r != null) 69 71 } 70 72 71 73 // --- Context Builder ---
+113 -9
packages/hatk/src/lexicon-resolve.ts
··· 40 40 const data = await response.json() 41 41 if (!data.Answer) return [] 42 42 43 - return data.Answer 44 - .filter((record: any) => record.type === 16) 45 - .map((record: any) => (record.data?.replace(/^"|"$/g, '') ?? '')) 43 + return data.Answer.filter((record: any) => record.type === 16).map( 44 + (record: any) => record.data?.replace(/^"|"$/g, '') ?? '', 45 + ) 46 46 } catch { 47 47 return [] 48 48 } ··· 52 52 53 53 function extractPdsEndpoint(didDoc: any): string | null { 54 54 if (!didDoc?.service || !Array.isArray(didDoc.service)) return null 55 - const pdsService = didDoc.service.find( 56 - (s: any) => s.id === '#atproto_pds' || s.type === 'AtprotoPersonalDataServer', 57 - ) 55 + const pdsService = didDoc.service.find((s: any) => s.id === '#atproto_pds' || s.type === 'AtprotoPersonalDataServer') 58 56 return pdsService?.serviceEndpoint ?? null 59 57 } 60 58 ··· 85 83 // --- Built-in core schemas (not published via DNS) --- 86 84 87 85 const coreSchemas: Record<string, Lexicon> = { 88 - 'com.atproto.repo.strongRef': {"lexicon":1,"id":"com.atproto.repo.strongRef","description":"A URI with a content-hash fingerprint.","defs":{"main":{"type":"object","required":["uri","cid"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"}}}}}, 89 - 'com.atproto.label.defs': {"lexicon":1,"id":"com.atproto.label.defs","defs":{"label":{"type":"object","description":"Metadata tag on an atproto resource (eg, repo or record).","required":["src","uri","val","cts"],"properties":{"ver":{"type":"integer"},"src":{"type":"string","format":"did"},"uri":{"type":"string","format":"uri"},"cid":{"type":"string","format":"cid"},"val":{"type":"string","maxLength":128},"neg":{"type":"boolean"},"cts":{"type":"string","format":"datetime"},"exp":{"type":"string","format":"datetime"},"sig":{"type":"bytes"}}},"selfLabels":{"type":"object","description":"Metadata tags on an atproto record, published by the author within the record.","required":["values"],"properties":{"values":{"type":"array","items":{"type":"ref","ref":"#selfLabel"},"maxLength":10}}},"selfLabel":{"type":"object","required":["val"],"properties":{"val":{"type":"string","maxLength":128}}},"labelValueDefinition":{"type":"object","description":"Declares a label value and its expected interpretations and behaviors.","required":["identifier","severity","blurs","locales"],"properties":{"identifier":{"type":"string","maxLength":100},"severity":{"type":"string","knownValues":["inform","alert","none"]},"blurs":{"type":"string","knownValues":["content","media","none"]},"defaultSetting":{"type":"string","knownValues":["ignore","warn","hide"]},"adultOnly":{"type":"boolean"},"locales":{"type":"array","items":{"type":"ref","ref":"#labelValueDefinitionStrings"}}}},"labelValueDefinitionStrings":{"type":"object","required":["lang","name","description"],"properties":{"lang":{"type":"string","format":"language"},"name":{"type":"string","maxLength":640},"description":{"type":"string","maxLength":100000}}},"labelValue":{"type":"string","knownValues":["!hide","!no-promote","!warn","!no-unauthenticated","dmca-violation","doxxing","porn","sexual","nudity","nsfl","gore"]}}}, 90 - 'com.atproto.moderation.defs': {"lexicon":1,"id":"com.atproto.moderation.defs","defs":{"reasonType":{"type":"string","knownValues":["com.atproto.moderation.defs#reasonSpam","com.atproto.moderation.defs#reasonViolation","com.atproto.moderation.defs#reasonMisleading","com.atproto.moderation.defs#reasonSexual","com.atproto.moderation.defs#reasonRude","com.atproto.moderation.defs#reasonOther","com.atproto.moderation.defs#reasonAppeal"]},"reasonSpam":{"type":"token","description":"Spam: frequent unwanted promotion, replies, mentions."},"reasonViolation":{"type":"token","description":"Direct violation of server rules, laws, terms of service."},"reasonMisleading":{"type":"token","description":"Misleading identity, affiliation, or content."},"reasonSexual":{"type":"token","description":"Unwanted or mislabeled sexual content."},"reasonRude":{"type":"token","description":"Rude, harassing, explicit, or otherwise unwelcoming behavior."},"reasonOther":{"type":"token","description":"Reports not falling under another report category."},"reasonAppeal":{"type":"token","description":"Appeal a previously taken moderation action."},"subjectType":{"type":"string","description":"Tag describing a type of subject that might be reported.","knownValues":["account","record","chat"]}}}, 86 + 'com.atproto.repo.strongRef': { 87 + lexicon: 1, 88 + id: 'com.atproto.repo.strongRef', 89 + description: 'A URI with a content-hash fingerprint.', 90 + defs: { 91 + main: { 92 + type: 'object', 93 + required: ['uri', 'cid'], 94 + properties: { uri: { type: 'string', format: 'at-uri' }, cid: { type: 'string', format: 'cid' } }, 95 + }, 96 + }, 97 + }, 98 + 'com.atproto.label.defs': { 99 + lexicon: 1, 100 + id: 'com.atproto.label.defs', 101 + defs: { 102 + label: { 103 + type: 'object', 104 + description: 'Metadata tag on an atproto resource (eg, repo or record).', 105 + required: ['src', 'uri', 'val', 'cts'], 106 + properties: { 107 + ver: { type: 'integer' }, 108 + src: { type: 'string', format: 'did' }, 109 + uri: { type: 'string', format: 'uri' }, 110 + cid: { type: 'string', format: 'cid' }, 111 + val: { type: 'string', maxLength: 128 }, 112 + neg: { type: 'boolean' }, 113 + cts: { type: 'string', format: 'datetime' }, 114 + exp: { type: 'string', format: 'datetime' }, 115 + sig: { type: 'bytes' }, 116 + }, 117 + }, 118 + selfLabels: { 119 + type: 'object', 120 + description: 'Metadata tags on an atproto record, published by the author within the record.', 121 + required: ['values'], 122 + properties: { values: { type: 'array', items: { type: 'ref', ref: '#selfLabel' }, maxLength: 10 } }, 123 + }, 124 + selfLabel: { type: 'object', required: ['val'], properties: { val: { type: 'string', maxLength: 128 } } }, 125 + labelValueDefinition: { 126 + type: 'object', 127 + description: 'Declares a label value and its expected interpretations and behaviors.', 128 + required: ['identifier', 'severity', 'blurs', 'locales'], 129 + properties: { 130 + identifier: { type: 'string', maxLength: 100 }, 131 + severity: { type: 'string', knownValues: ['inform', 'alert', 'none'] }, 132 + blurs: { type: 'string', knownValues: ['content', 'media', 'none'] }, 133 + defaultSetting: { type: 'string', knownValues: ['ignore', 'warn', 'hide'] }, 134 + adultOnly: { type: 'boolean' }, 135 + locales: { type: 'array', items: { type: 'ref', ref: '#labelValueDefinitionStrings' } }, 136 + }, 137 + }, 138 + labelValueDefinitionStrings: { 139 + type: 'object', 140 + required: ['lang', 'name', 'description'], 141 + properties: { 142 + lang: { type: 'string', format: 'language' }, 143 + name: { type: 'string', maxLength: 640 }, 144 + description: { type: 'string', maxLength: 100000 }, 145 + }, 146 + }, 147 + labelValue: { 148 + type: 'string', 149 + knownValues: [ 150 + '!hide', 151 + '!no-promote', 152 + '!warn', 153 + '!no-unauthenticated', 154 + 'dmca-violation', 155 + 'doxxing', 156 + 'porn', 157 + 'sexual', 158 + 'nudity', 159 + 'nsfl', 160 + 'gore', 161 + ], 162 + }, 163 + }, 164 + }, 165 + 'com.atproto.moderation.defs': { 166 + lexicon: 1, 167 + id: 'com.atproto.moderation.defs', 168 + defs: { 169 + reasonType: { 170 + type: 'string', 171 + knownValues: [ 172 + 'com.atproto.moderation.defs#reasonSpam', 173 + 'com.atproto.moderation.defs#reasonViolation', 174 + 'com.atproto.moderation.defs#reasonMisleading', 175 + 'com.atproto.moderation.defs#reasonSexual', 176 + 'com.atproto.moderation.defs#reasonRude', 177 + 'com.atproto.moderation.defs#reasonOther', 178 + 'com.atproto.moderation.defs#reasonAppeal', 179 + ], 180 + }, 181 + reasonSpam: { type: 'token', description: 'Spam: frequent unwanted promotion, replies, mentions.' }, 182 + reasonViolation: { type: 'token', description: 'Direct violation of server rules, laws, terms of service.' }, 183 + reasonMisleading: { type: 'token', description: 'Misleading identity, affiliation, or content.' }, 184 + reasonSexual: { type: 'token', description: 'Unwanted or mislabeled sexual content.' }, 185 + reasonRude: { type: 'token', description: 'Rude, harassing, explicit, or otherwise unwelcoming behavior.' }, 186 + reasonOther: { type: 'token', description: 'Reports not falling under another report category.' }, 187 + reasonAppeal: { type: 'token', description: 'Appeal a previously taken moderation action.' }, 188 + subjectType: { 189 + type: 'string', 190 + description: 'Tag describing a type of subject that might be reported.', 191 + knownValues: ['account', 'record', 'chat'], 192 + }, 193 + }, 194 + }, 91 195 } 92 196 93 197 // --- Resolver ---
+6 -7
packages/hatk/src/oauth/discovery.ts
··· 41 41 return res.json() 42 42 } 43 43 44 - export async function discoverAuthServer(did: string, plcUrl: string): Promise<{ 44 + export async function discoverAuthServer( 45 + did: string, 46 + plcUrl: string, 47 + ): Promise<{ 45 48 pdsEndpoint: string 46 49 authServerEndpoint: string 47 50 authServerMetadata: AuthServerMetadata ··· 59 62 } 60 63 61 64 export async function resolveHandle(handle: string, relayUrl?: string): Promise<string> { 62 - const baseUrl = relayUrl?.includes('localhost:2583') 63 - ? 'http://localhost:2583' 64 - : 'https://bsky.social' 65 - const res = await fetch( 66 - `${baseUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`, 67 - ) 65 + const baseUrl = relayUrl?.includes('localhost:2583') ? 'http://localhost:2583' : 'https://bsky.social' 66 + const res = await fetch(`${baseUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`) 68 67 if (!res.ok) throw new Error(`resolveHandle failed: ${res.status}`) 69 68 const data = await res.json() 70 69 return data.did
+1 -1
packages/hatk/src/oauth/server.ts
··· 38 38 const SERVER_KEY_KID = 'appview-oauth-key' 39 39 40 40 async function resolveHandleForDid(did: string): Promise<string | undefined> { 41 - const rows = await querySQL('SELECT handle FROM _repos WHERE did = $1', [did]) as { handle: string }[] 41 + const rows = (await querySQL('SELECT handle FROM _repos WHERE did = $1', [did])) as { handle: string }[] 42 42 return rows[0]?.handle || undefined 43 43 } 44 44
+17 -32
packages/hatk/src/schema.ts
··· 10 10 } 11 11 12 12 export interface UnionBranchSchema { 13 - type: string // full $type string (e.g., 'app.bsky.embed.images') 14 - branchName: string // short name for table suffix (e.g., 'images') 15 - tableName: string // quoted table name 16 - columns: ColumnDef[] // branch properties as columns 17 - isArray: boolean // true if the branch wraps an array of objects 18 - arrayField?: string // if isArray, the property name containing the array 19 - wrapperField?: string // if set, data is nested under this key (e.g., 'external' for embed.external) 13 + type: string // full $type string (e.g., 'app.bsky.embed.images') 14 + branchName: string // short name for table suffix (e.g., 'images') 15 + tableName: string // quoted table name 16 + columns: ColumnDef[] // branch properties as columns 17 + isArray: boolean // true if the branch wraps an array of objects 18 + arrayField?: string // if isArray, the property name containing the array 19 + wrapperField?: string // if set, data is nested under this key (e.g., 'external' for embed.external) 20 20 } 21 21 22 22 export interface UnionFieldSchema { 23 - fieldName: string // original camelCase field name (e.g., 'embed') 23 + fieldName: string // original camelCase field name (e.g., 'embed') 24 24 branches: UnionBranchSchema[] 25 25 } 26 26 ··· 34 34 } 35 35 36 36 export interface ChildTableSchema { 37 - parentCollection: string // parent NSID 38 - fieldName: string // original camelCase field name (e.g., "artists") 39 - tableName: string // quoted "{collection}__{fieldName}" 40 - columns: ColumnDef[] // columns from the item object properties 37 + parentCollection: string // parent NSID 38 + fieldName: string // original camelCase field name (e.g., "artists") 39 + tableName: string // quoted "{collection}__{fieldName}" 40 + columns: ColumnDef[] // columns from the item object properties 41 41 } 42 42 43 43 // Convert camelCase to snake_case ··· 135 135 return [...storedLexicons.values()] 136 136 } 137 137 138 - function resolveArrayItemProperties( 139 - items: any, 140 - defs: Record<string, any>, 141 - ): Record<string, any> | null { 138 + function resolveArrayItemProperties(items: any, defs: Record<string, any>): Record<string, any> | null { 142 139 if (!items) return null 143 140 144 141 // Inline object with properties ··· 159 156 } 160 157 161 158 /** Resolve a ref string to its definition object */ 162 - function resolveRefDef( 163 - ref: string, 164 - defs: Record<string, any>, 165 - lexicons?: Map<string, any>, 166 - ): any | null { 159 + function resolveRefDef(ref: string, defs: Record<string, any>, lexicons?: Map<string, any>): any | null { 167 160 if (ref.startsWith('#')) { 168 161 return defs?.[ref.slice(1)] || null 169 162 } ··· 222 215 if ((onlyProp as any).type === 'array' && (onlyProp as any).items) { 223 216 // Single array property (like embed.images wrapping images[]) 224 217 const items = (onlyProp as any).items 225 - const itemDef = items.type === 'ref' && items.ref 226 - ? resolveRefDef(items.ref, branchDefs, lexicons) 227 - : items 218 + const itemDef = items.type === 'ref' && items.ref ? resolveRefDef(items.ref, branchDefs, lexicons) : items 228 219 if (itemDef?.type === 'object' && itemDef.properties) { 229 220 isArray = true 230 221 arrayField = onlyField ··· 399 390 // Child table DDL 400 391 const childDDL: string[] = [] 401 392 for (const child of schema.children) { 402 - const childLines: string[] = [ 403 - ' parent_uri TEXT NOT NULL', 404 - ' parent_did TEXT NOT NULL', 405 - ] 393 + const childLines: string[] = [' parent_uri TEXT NOT NULL', ' parent_did TEXT NOT NULL'] 406 394 for (const col of child.columns) { 407 395 const nullable = col.notNull ? ' NOT NULL' : '' 408 396 childLines.push(` ${col.name} ${col.duckdbType}${nullable}`) ··· 422 410 // Union branch table DDL 423 411 for (const union of schema.unions) { 424 412 for (const branch of union.branches) { 425 - const branchLines: string[] = [ 426 - ' parent_uri TEXT NOT NULL', 427 - ' parent_did TEXT NOT NULL', 428 - ] 413 + const branchLines: string[] = [' parent_uri TEXT NOT NULL', ' parent_did TEXT NOT NULL'] 429 414 for (const col of branch.columns) { 430 415 const nullable = col.notNull ? ' NOT NULL' : '' 431 416 branchLines.push(` ${col.name} ${col.duckdbType}${nullable}`)
+3 -1
packages/hatk/src/seed.ts
··· 49 49 ): Promise<{ uri: string; cid: string }> { 50 50 const error = validateRecord(lexiconArray, collection, record) 51 51 if (error) { 52 - throw new Error(`[seed] validation error in ${collection}: ${error.path ? error.path + ': ' : ''}${error.message}`) 52 + throw new Error( 53 + `[seed] validation error in ${collection}: ${error.path ? error.path + ': ' : ''}${error.message}`, 54 + ) 53 55 } 54 56 55 57 const body: Record<string, unknown> = {
+4 -4
packages/hatk/src/server.ts
··· 462 462 const rec = await getRecordByUri(q) 463 463 if (rec) { 464 464 const labelsMap = await queryLabelsForUris([rec.uri]) 465 - jsonResponse(res, { records: [{ ...reshapeRow(rec, rec?.__childData), labels: labelsMap.get(rec.uri) || [] }] }) 465 + jsonResponse(res, { 466 + records: [{ ...reshapeRow(rec, rec?.__childData), labels: labelsMap.get(rec.uri) || [] }], 467 + }) 466 468 } else { 467 469 jsonResponse(res, { records: [] }) 468 470 } ··· 1030 1032 1031 1033 function jsonResponse(res: any, data: any): void { 1032 1034 res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }) 1033 - res.end( 1034 - JSON.stringify(data, (_, v) => normalizeValue(v)), 1035 - ) 1035 + res.end(JSON.stringify(data, (_, v) => normalizeValue(v))) 1036 1036 } 1037 1037 1038 1038 function jsonError(res: any, status: number, message: string): void {
+9 -6
packages/hatk/src/test-browser.ts
··· 19 19 */ 20 20 export const test = base.extend<{}, WorkerFixtures>({ 21 21 // eslint-disable-next-line no-empty-pattern -- Playwright fixture API requires the deps arg 22 - server: [async ({}, use) => { 23 - const server = await startTestServer() 24 - await server.loadFixtures() 25 - await use(server) 26 - await server.close() 27 - }, { scope: 'worker' }], 22 + server: [ 23 + async (_deps, use) => { 24 + const server = await startTestServer() 25 + await server.loadFixtures() 26 + await use(server) 27 + await server.close() 28 + }, 29 + { scope: 'worker' }, 30 + ], 28 31 }) 29 32 30 33 export { expect }
+45 -13
packages/hatk/src/test.ts
··· 3 3 import { readdirSync, readFileSync } from 'node:fs' 4 4 import YAML from 'yaml' 5 5 import { loadConfig, type HatkConfig } from './config.ts' 6 - import { loadLexicons, storeLexicons, discoverCollections, generateTableSchema, generateCreateTableSQL } from './schema.ts' 6 + import { 7 + loadLexicons, 8 + storeLexicons, 9 + discoverCollections, 10 + generateTableSchema, 11 + generateCreateTableSQL, 12 + } from './schema.ts' 7 13 import { initDatabase, querySQL, runSQL, insertRecord, closeDatabase } from './db.ts' 8 14 import { initFeeds, executeFeed, listFeeds, createPaginate } from './feeds.ts' 9 15 import { initXrpc, executeXrpc, listXrpc, configureRelay } from './xrpc.ts' ··· 24 30 loadFixtures: (dir?: string) => Promise<void> 25 31 loadFeed: (name: string) => { generate: (ctx: FeedContext) => Promise<any> } 26 32 loadXrpc: (name: string) => { handler: (ctx: any) => Promise<any> } 27 - feedContext: (opts?: { limit?: number; cursor?: string; viewer?: { did: string } | null; params?: Record<string, string> }) => FeedContext 33 + feedContext: (opts?: { 34 + limit?: number 35 + cursor?: string 36 + viewer?: { did: string } | null 37 + params?: Record<string, string> 38 + }) => FeedContext 28 39 close: () => Promise<void> 29 40 /** @internal */ _config: HatkConfig 30 41 /** @internal */ _collections: string[] ··· 93 104 94 105 // Discover views + hooks 95 106 discoverViews() 96 - try { await loadOnLoginHook(resolve(configDir, 'hooks')) } catch {} 107 + try { 108 + await loadOnLoginHook(resolve(configDir, 'hooks')) 109 + } catch {} 97 110 98 111 // Skip setup hooks in test context — they're for server boot-time 99 112 // initialization (e.g. importing large datasets) and not appropriate for tests ··· 126 139 const row = interpolateHelpers(rec) 127 140 await runSQL( 128 141 `INSERT OR IGNORE INTO _repos (did, status, handle, backfilled_at) VALUES ($1, $2, $3, $4)`, 129 - row.did, row.status || 'active', row.handle || row.did.split(':').pop() + '.test', new Date().toISOString(), 142 + row.did, 143 + row.status || 'active', 144 + row.handle || row.did.split(':').pop() + '.test', 145 + new Date().toISOString(), 130 146 ) 131 147 } 132 148 } ··· 152 168 const row = interpolateHelpers(rec) 153 169 const vals = keys.map((k) => row[k]) 154 170 const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ') 155 - await runSQL(`INSERT INTO "${tableName}" (${keys.map((k) => `"${k}"`).join(', ')}) VALUES (${placeholders})`, ...vals) 171 + await runSQL( 172 + `INSERT INTO "${tableName}" (${keys.map((k) => `"${k}"`).join(', ')}) VALUES (${placeholders})`, 173 + ...vals, 174 + ) 156 175 } 157 176 continue 158 177 } ··· 171 190 seenDids.add(did) 172 191 await runSQL( 173 192 `INSERT OR IGNORE INTO _repos (did, status, handle, backfilled_at) VALUES ($1, $2, $3, $4)`, 174 - did, 'active', did.split(':').pop() + '.test', new Date().toISOString(), 193 + did, 194 + 'active', 195 + did.split(':').pop() + '.test', 196 + new Date().toISOString(), 175 197 ) 176 198 } 177 199 await insertRecord(tableName, uri, cid, did, fields) ··· 180 202 }, 181 203 loadFeed: (name) => { 182 204 const feedList = listFeeds() 183 - if (!feedList.find((f) => f.name === name)) throw new Error(`Feed "${name}" not found. Available: ${feedList.map((f) => f.name).join(', ')}`) 205 + if (!feedList.find((f) => f.name === name)) 206 + throw new Error(`Feed "${name}" not found. Available: ${feedList.map((f) => f.name).join(', ')}`) 184 207 return { 185 208 generate: (ctx: FeedContext) => executeFeed(name, ctx.params || {}, ctx.cursor, ctx.limit, ctx.viewer), 186 209 } 187 210 }, 188 211 loadXrpc: (name) => { 189 212 const xrpcList = listXrpc() 190 - if (!xrpcList.includes(name)) throw new Error(`XRPC handler "${name}" not found. Available: ${xrpcList.join(', ')}`) 213 + if (!xrpcList.includes(name)) 214 + throw new Error(`XRPC handler "${name}" not found. Available: ${xrpcList.join(', ')}`) 191 215 return { 192 216 handler: (ctx: any) => { 193 217 const params = { ...ctx.params } ··· 263 287 const did = req.headers['x-test-viewer'] 264 288 return typeof did === 'string' ? { did } : null 265 289 } 266 - const httpServer = startServer(0, ctx._collections, ctx._config.publicDir, ctx._config.oauth, ctx._config.admins, resolveViewer) 290 + const httpServer = startServer( 291 + 0, 292 + ctx._collections, 293 + ctx._config.publicDir, 294 + ctx._config.oauth, 295 + ctx._config.admins, 296 + resolveViewer, 297 + ) 267 298 await new Promise<void>((resolve) => httpServer.on('listening', resolve)) 268 299 const port = (httpServer.address() as any).port 269 300 const url = `http://127.0.0.1:${port}` ··· 273 304 url, 274 305 port, 275 306 fetch: (path, init) => fetch(`${url}${path}`, init), 276 - fetchAs: (did, path, init) => fetch(`${url}${path}`, { 277 - ...init, 278 - headers: { ...init?.headers, 'x-test-viewer': did }, 279 - }), 307 + fetchAs: (did, path, init) => 308 + fetch(`${url}${path}`, { 309 + ...init, 310 + headers: { ...init?.headers, 'x-test-viewer': did }, 311 + }), 280 312 seed: (seedOpts) => createSeedHelpers(seedOpts), 281 313 waitForRecord: async (uri, timeoutMs = 10_000) => { 282 314 const start = Date.now()
-23
packages/hatk/src/views.ts
··· 5 5 6 6 import { log } from './logger.ts' 7 7 import { getAllLexicons, getLexicon } from './schema.ts' 8 - import { blobUrl } from './xrpc.ts' 9 - import type { Row, FlatRow } from './lex-types.ts' 10 8 11 9 // --- Types --- 12 10 ··· 238 236 return blobs 239 237 } 240 238 241 - 242 - /** Flatten a Row<T> into a view object: { uri, did, handle, ...value, ...overrides } */ 243 - function flattenRow<T>(row: Row<T>, overrides?: Record<string, unknown>): FlatRow<T> { 244 - if (!row) return null as any 245 - return { 246 - uri: row.uri, 247 - did: row.did, 248 - handle: row.handle, 249 - ...(row.value as any), 250 - ...overrides, 251 - } as FlatRow<T> 252 - } 253 - 254 - /** Resolve blob fields on a record to CDN URLs. */ 255 - function resolveBlobOverrides(item: Row<unknown>, blobFields: Map<string, string>): Record<string, unknown> { 256 - const overrides: Record<string, unknown> = {} 257 - for (const [fieldName, preset] of blobFields) { 258 - overrides[fieldName] = blobUrl(item.did, (item.value as any)?.[fieldName], preset as any) 259 - } 260 - return overrides 261 - }