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

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 - }