From e8ac2cc1f688d1fee4a3b0c3273a6cb3bfa55af4 Mon Sep 17 00:00:00 2001 From: trezy Date: Thu, 12 Mar 2026 14:50:21 +0000 Subject: [PATCH] deploy: 6506ff283f22f1017167a06bbe4e3963a19c950e --- 404.html | 2 +- ...d74f7.132c49f2.js => 22dd74f7.d275db6e.js} | 2 +- assets/js/2d1b556d.316fda02.js | 1 - assets/js/2d1b556d.cb991d70.js | 1 + assets/js/774696b9.08c2c2c4.js | 1 + assets/js/774696b9.aaf7ca2b.js | 1 - assets/js/920ba7ef.2e58966a.js | 1 - assets/js/920ba7ef.b802a833.js | 1 + assets/js/e7f37855.5017c79c.js | 1 - assets/js/e7f37855.6b9a8f63.js | 1 + ...n.3142db48.js => runtime~main.d4fb769f.js} | 2 +- getting-started/authentication/index.html | 2 +- getting-started/configuration/index.html | 2 +- getting-started/dashboard/index.html | 2 +- getting-started/deployment/docker/index.html | 2 +- getting-started/deployment/other/index.html | 2 +- getting-started/deployment/railway/index.html | 2 +- getting-started/quickstart/index.html | 2 +- guides/api-keys/index.html | 2 +- guides/backfill/index.html | 2 +- guides/event-logs/index.html | 2 +- guides/index-hooks/index.html | 32 +++++++++++++------ guides/lexicons/index.html | 2 +- guides/scripting/index.html | 10 +++--- index.html | 2 +- reference/admin-api/index.html | 2 +- reference/architecture/index.html | 2 +- reference/changelog/index.html | 2 +- reference/glossary/index.html | 2 +- reference/production-deployment/index.html | 2 +- reference/scripts/algolia-sync/index.html | 4 +-- reference/scripts/batch-save/index.html | 2 +- reference/scripts/cascading-delete/index.html | 2 +- .../scripts/complex-mutations/index.html | 2 +- reference/scripts/create-record/index.html | 2 +- reference/scripts/expanded-query/index.html | 2 +- reference/scripts/get-record/index.html | 2 +- reference/scripts/list-or-fetch/index.html | 2 +- reference/scripts/meilisearch-sync/index.html | 4 +-- reference/scripts/paginated-list/index.html | 2 +- reference/scripts/sidecar-records/index.html | 2 +- reference/scripts/update-or-delete/index.html | 2 +- reference/scripts/upsert-record/index.html | 2 +- reference/troubleshooting/index.html | 2 +- reference/xrpc-api/index.html | 2 +- tutorials/statusphere/index.html | 2 +- 46 files changed, 69 insertions(+), 57 deletions(-) rename assets/js/{22dd74f7.132c49f2.js => 22dd74f7.d275db6e.js} (58%) delete mode 100644 assets/js/2d1b556d.316fda02.js create mode 100644 assets/js/2d1b556d.cb991d70.js create mode 100644 assets/js/774696b9.08c2c2c4.js delete mode 100644 assets/js/774696b9.aaf7ca2b.js delete mode 100644 assets/js/920ba7ef.2e58966a.js create mode 100644 assets/js/920ba7ef.b802a833.js delete mode 100644 assets/js/e7f37855.5017c79c.js create mode 100644 assets/js/e7f37855.6b9a8f63.js rename assets/js/{runtime~main.3142db48.js => runtime~main.d4fb769f.js} (67%) diff --git a/404.html b/404.html index 975aa47..a1e3b43 100644 --- a/404.html +++ b/404.html @@ -4,7 +4,7 @@ HappyView - + diff --git a/assets/js/22dd74f7.132c49f2.js b/assets/js/22dd74f7.d275db6e.js similarity index 58% rename from assets/js/22dd74f7.132c49f2.js rename to assets/js/22dd74f7.d275db6e.js index a8548d8..785f883 100644 --- a/assets/js/22dd74f7.132c49f2.js +++ b/assets/js/22dd74f7.d275db6e.js @@ -1 +1 @@ -"use strict";(self.webpackChunkhappyview_docs=self.webpackChunkhappyview_docs||[]).push([[1567],{5226(e){e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"docs":[{"type":"link","href":"/","label":"Introduction","docId":"README","unlisted":false},{"type":"category","label":"Getting Started","items":[{"type":"link","href":"/getting-started/quickstart","label":"Quickstart","docId":"getting-started/quickstart","unlisted":false},{"type":"category","label":"Deployment","items":[{"type":"link","href":"/getting-started/deployment/railway","label":"Railway","docId":"getting-started/deployment/railway","unlisted":false},{"type":"link","href":"/getting-started/deployment/docker","label":"Docker","docId":"getting-started/deployment/docker","unlisted":false},{"type":"link","href":"/getting-started/deployment/other","label":"Other","docId":"getting-started/deployment/other","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"link","href":"/getting-started/authentication","label":"Authentication","docId":"getting-started/authentication","unlisted":false},{"type":"link","href":"/getting-started/configuration","label":"Configuration","docId":"getting-started/configuration","unlisted":false},{"type":"link","href":"/getting-started/dashboard","label":"Dashboard","docId":"getting-started/dashboard","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Tutorials","items":[{"type":"link","href":"/tutorials/statusphere","label":"Statusphere","docId":"tutorials/statusphere","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Guides","items":[{"type":"link","href":"/guides/lexicons","label":"Lexicons","docId":"guides/lexicons","unlisted":false},{"type":"link","href":"/guides/scripting","label":"Lua Scripting","docId":"guides/scripting","unlisted":false},{"type":"link","href":"/guides/index-hooks","label":"Index Hooks","docId":"guides/index-hooks","unlisted":false},{"type":"link","href":"/guides/backfill","label":"Backfill","docId":"guides/backfill","unlisted":false},{"type":"link","href":"/guides/event-logs","label":"Event Logs","docId":"guides/event-logs","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Reference","items":[{"type":"link","href":"/reference/xrpc-api","label":"XRPC API","docId":"reference/xrpc-api","unlisted":false},{"type":"link","href":"/reference/admin-api","label":"Admin API","docId":"reference/admin-api","unlisted":false},{"type":"category","label":"Script Examples","items":[{"type":"link","href":"/reference/scripts/get-record","label":"Get a Record","docId":"reference/scripts/get-record","unlisted":false},{"type":"link","href":"/reference/scripts/create-record","label":"Create Record","docId":"reference/scripts/create-record","unlisted":false},{"type":"link","href":"/reference/scripts/upsert-record","label":"Upsert Record","docId":"reference/scripts/upsert-record","unlisted":false},{"type":"link","href":"/reference/scripts/paginated-list","label":"Paginated List","docId":"reference/scripts/paginated-list","unlisted":false},{"type":"link","href":"/reference/scripts/list-or-fetch","label":"List or Fetch","docId":"reference/scripts/list-or-fetch","unlisted":false},{"type":"link","href":"/reference/scripts/expanded-query","label":"Expanded Query","docId":"reference/scripts/expanded-query","unlisted":false},{"type":"link","href":"/reference/scripts/update-or-delete","label":"Update or Delete","docId":"reference/scripts/update-or-delete","unlisted":false},{"type":"link","href":"/reference/scripts/batch-save","label":"Batch Save","docId":"reference/scripts/batch-save","unlisted":false},{"type":"link","href":"/reference/scripts/sidecar-records","label":"Sidecar Records","docId":"reference/scripts/sidecar-records","unlisted":false},{"type":"link","href":"/reference/scripts/cascading-delete","label":"Cascading Delete","docId":"reference/scripts/cascading-delete","unlisted":false},{"type":"link","href":"/reference/scripts/complex-mutations","label":"Complex Mutations","docId":"reference/scripts/complex-mutations","unlisted":false},{"type":"link","href":"/reference/scripts/algolia-sync","label":"Algolia Sync","docId":"reference/scripts/algolia-sync","unlisted":false},{"type":"link","href":"/reference/scripts/meilisearch-sync","label":"Meilisearch Sync","docId":"reference/scripts/meilisearch-sync","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"link","href":"/reference/glossary","label":"Glossary","docId":"reference/glossary","unlisted":false},{"type":"link","href":"/reference/architecture","label":"Architecture","docId":"reference/architecture","unlisted":false},{"type":"link","href":"/reference/troubleshooting","label":"Troubleshooting","docId":"reference/troubleshooting","unlisted":false},{"type":"link","href":"/reference/production-deployment","label":"Production","docId":"reference/production-deployment","unlisted":false}],"collapsed":true,"collapsible":true}]},"docs":{"getting-started/authentication":{"id":"getting-started/authentication","title":"Authentication","description":"HappyView uses AT Protocol OAuth for authentication, handled by an external AIP instance. HappyView does not store credentials or issue tokens: all OAuth is delegated to AIP.","sidebar":"docs"},"getting-started/configuration":{"id":"getting-started/configuration","title":"Configuration","description":"HappyView is configured via environment variables. A .env file in the project root is loaded automatically on startup. See Deployment for local setup or Production Deployment for production setup.","sidebar":"docs"},"getting-started/dashboard":{"id":"getting-started/dashboard","title":"Dashboard","description":"HappyView ships with a web dashboard that provides a visual interface for everything the admin API offers: managing lexicons, viewing indexed records, and monitoring backfill jobs. It runs as a separate Next.js application alongside the Rust backend.","sidebar":"docs"},"getting-started/deployment/docker":{"id":"getting-started/deployment/docker","title":"Local Development with Docker","description":"This guide runs the full HappyView stack locally using Docker Compose: Postgres, Tap, HappyView, and the web dashboard.","sidebar":"docs"},"getting-started/deployment/other":{"id":"getting-started/deployment/other","title":"Local Development from Source","description":"This guide runs HappyView directly with cargo run, with you managing Postgres, AIP, and Tap separately. If you\'d rather use Docker Compose to run everything together, see Local Development with Docker.","sidebar":"docs"},"getting-started/deployment/railway":{"id":"getting-started/deployment/railway","title":"Deploy on Railway","description":"The fastest way to get HappyView running is with Railway. This template deploys HappyView, AIP (OAuth provider), Tap (real-time data and backfill), and Postgres with a single click:","sidebar":"docs"},"getting-started/quickstart":{"id":"getting-started/quickstart","title":"Quickstart","description":"This page walks you through the fastest path to a working HappyView instance. By the end, you\'ll have an AppView that indexes records from the AT Protocol network and serves XRPC endpoints.","sidebar":"docs"},"guides/api-keys":{"id":"guides/api-keys","title":"API Keys","description":"API keys let you authenticate with the admin API without going through the OAuth flow. They\'re useful for CI/CD pipelines, scripts, and any automation that needs to manage your HappyView instance programmatically."},"guides/backfill":{"id":"guides/backfill","title":"Backfill","description":"When you add a new record-type lexicon, HappyView starts indexing new records from that moment via Tap. But what about records that already exist on the network? That\'s what backfill does: HappyView discovers repos via the relay and delegates the actual record fetching to Tap.","sidebar":"docs"},"guides/event-logs":{"id":"guides/event-logs","title":"Event Logs","description":"HappyView maintains an internal event log that records system activity \u2014 lexicon changes, record operations, Lua script executions and errors, admin actions, backfill jobs, and Tap connectivity. Events are stored in a Postgres table and queryable via the admin API.","sidebar":"docs"},"guides/index-hooks":{"id":"guides/index-hooks","title":"Index Hooks","description":"Index hooks are Lua scripts that run automatically whenever a record in a collection is created, updated, or deleted on the network. They let you react to record changes in real time \u2014 push data to search engines, sync with external APIs, send notifications, or build materialized views.","sidebar":"docs"},"guides/lexicons":{"id":"guides/lexicons","title":"Lexicons","description":"Lexicons are the core building block of HappyView. They\'re AT Protocol schema definitions that describe your data model, and HappyView uses them to decide which records to index from the network and what XRPC endpoints to serve.","sidebar":"docs"},"guides/scripting":{"id":"guides/scripting","title":"Lua Scripting","description":"Without Lua scripts, HappyView\'s query endpoints return raw records and procedure endpoints proxy simple creates and updates. Lua scripts let you go much further:","sidebar":"docs"},"README":{"id":"README","title":"HappyView","description":"HappyView is the best way to build an AppView for the AT Protocol. Upload your lexicon schemas and get a fully functional AppView, complete with XRPC endpoints, OAuth, real-time network sync, and historical backfill, without writing a single line of server code.","sidebar":"docs"},"reference/admin-api":{"id":"reference/admin-api","title":"Admin API","description":"The admin API lets you manage lexicons, monitor records, run backfill jobs, and control admin access. All endpoints live under /admin and require an AIP-issued Bearer token from a DID that exists in the admins table. You can also manage all of this through the web dashboard.","sidebar":"docs"},"reference/architecture":{"id":"reference/architecture","title":"Architecture","description":"Guide for contributors working on HappyView itself. For a user-facing overview, see the Introduction.","sidebar":"docs"},"reference/changelog":{"id":"reference/changelog","title":"Changelog","description":"v1.9.0 \u2014 Event Logs"},"reference/glossary":{"id":"reference/glossary","title":"Glossary","description":"Key terms used throughout the HappyView documentation. For a broader introduction to the AT Protocol, see the official ATProto glossary.","sidebar":"docs"},"reference/production-deployment":{"id":"reference/production-deployment","title":"Deployment","description":"HappyView requires a Postgres database and an AIP instance for OAuth. The Quickstart covers the fastest path with Railway. This page covers other deployment options.","sidebar":"docs"},"reference/scripts/algolia-sync":{"id":"reference/scripts/algolia-sync","title":"Index Hook: Algolia Sync","description":"Push records to an Algolia search index whenever they are created, updated, or deleted on the network.","sidebar":"docs"},"reference/scripts/batch-save":{"id":"reference/scripts/batch-save","title":"Procedure: Batch Save","description":"Use Record.save_all() to create multiple records in parallel.","sidebar":"docs"},"reference/scripts/cascading-delete":{"id":"reference/scripts/cascading-delete","title":"Procedure: Cascading Delete","description":"Delete a record and all related records across collections.","sidebar":"docs"},"reference/scripts/complex-mutations":{"id":"reference/scripts/complex-mutations","title":"Procedure: Complex Mutations","description":"Load an existing record, apply multiple transformations, and save it back.","sidebar":"docs"},"reference/scripts/create-record":{"id":"reference/scripts/create-record","title":"Procedure: Create a Record","description":"The simplest write: take the request body, save it as a record, and return the URI.","sidebar":"docs"},"reference/scripts/expanded-query":{"id":"reference/scripts/expanded-query","title":"Query: Expanded Query with Profiles","description":"List statuses and include the profile of each user who created one.","sidebar":"docs"},"reference/scripts/get-record":{"id":"reference/scripts/get-record","title":"Query: Get a Single Record","description":"Fetch a single record by its AT URI.","sidebar":"docs"},"reference/scripts/list-or-fetch":{"id":"reference/scripts/list-or-fetch","title":"Query: List or Fetch Records","description":"This query handles both single-record lookups (when a uri param is provided) and paginated listing.","sidebar":"docs"},"reference/scripts/meilisearch-sync":{"id":"reference/scripts/meilisearch-sync","title":"Index Hook: Meilisearch Sync","description":"Push records to a Meilisearch search index whenever they are created, updated, or deleted on the network.","sidebar":"docs"},"reference/scripts/paginated-list":{"id":"reference/scripts/paginated-list","title":"Query: Paginated List","description":"List records from a collection with cursor-based pagination and an optional DID filter.","sidebar":"docs"},"reference/scripts/sidecar-records":{"id":"reference/scripts/sidecar-records","title":"Procedure: Create Sidecar Records","description":"Create two records with different collection NSIDs but the same rkey, linking them together by key.","sidebar":"docs"},"reference/scripts/update-or-delete":{"id":"reference/scripts/update-or-delete","title":"Procedure: Update or Delete","description":"A single endpoint that handles create, update, and delete based on the input fields.","sidebar":"docs"},"reference/scripts/upsert-record":{"id":"reference/scripts/upsert-record","title":"Procedure: Upsert a Record","description":"Create a new record, or update an existing one if the client provides its rkey.","sidebar":"docs"},"reference/troubleshooting":{"id":"reference/troubleshooting","title":"Troubleshooting","description":"Common issues and how to resolve them.","sidebar":"docs"},"reference/xrpc-api":{"id":"reference/xrpc-api","title":"XRPC API","description":"XRPC is the HTTP-based RPC protocol used by the AT Protocol. HappyView dynamically registers XRPC endpoints based on your uploaded lexicons: query lexicons become GET /xrpc/ routes, procedure lexicons become POST /xrpc/{nsid} routes.","sidebar":"docs"},"tutorials/statusphere":{"id":"tutorials/statusphere","title":"Tutorial: Statusphere with HappyView","description":"Statusphere is an example AT Protocol application where users set their current status as a single emoji. It\'s a great way to learn how HappyView works because the data model is simple but the queries are interesting.","sidebar":"docs"}}}}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkhappyview_docs=self.webpackChunkhappyview_docs||[]).push([[1567],{5226(e){e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"docs":[{"type":"link","href":"/","label":"Introduction","docId":"README","unlisted":false},{"type":"category","label":"Getting Started","items":[{"type":"link","href":"/getting-started/quickstart","label":"Quickstart","docId":"getting-started/quickstart","unlisted":false},{"type":"category","label":"Deployment","items":[{"type":"link","href":"/getting-started/deployment/railway","label":"Railway","docId":"getting-started/deployment/railway","unlisted":false},{"type":"link","href":"/getting-started/deployment/docker","label":"Docker","docId":"getting-started/deployment/docker","unlisted":false},{"type":"link","href":"/getting-started/deployment/other","label":"Other","docId":"getting-started/deployment/other","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"link","href":"/getting-started/authentication","label":"Authentication","docId":"getting-started/authentication","unlisted":false},{"type":"link","href":"/getting-started/configuration","label":"Configuration","docId":"getting-started/configuration","unlisted":false},{"type":"link","href":"/getting-started/dashboard","label":"Dashboard","docId":"getting-started/dashboard","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Tutorials","items":[{"type":"link","href":"/tutorials/statusphere","label":"Statusphere","docId":"tutorials/statusphere","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Guides","items":[{"type":"link","href":"/guides/lexicons","label":"Lexicons","docId":"guides/lexicons","unlisted":false},{"type":"link","href":"/guides/scripting","label":"Lua Scripting","docId":"guides/scripting","unlisted":false},{"type":"link","href":"/guides/index-hooks","label":"Index Hooks","docId":"guides/index-hooks","unlisted":false},{"type":"link","href":"/guides/backfill","label":"Backfill","docId":"guides/backfill","unlisted":false},{"type":"link","href":"/guides/event-logs","label":"Event Logs","docId":"guides/event-logs","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Reference","items":[{"type":"link","href":"/reference/xrpc-api","label":"XRPC API","docId":"reference/xrpc-api","unlisted":false},{"type":"link","href":"/reference/admin-api","label":"Admin API","docId":"reference/admin-api","unlisted":false},{"type":"category","label":"Script Examples","items":[{"type":"link","href":"/reference/scripts/get-record","label":"Get a Record","docId":"reference/scripts/get-record","unlisted":false},{"type":"link","href":"/reference/scripts/create-record","label":"Create Record","docId":"reference/scripts/create-record","unlisted":false},{"type":"link","href":"/reference/scripts/upsert-record","label":"Upsert Record","docId":"reference/scripts/upsert-record","unlisted":false},{"type":"link","href":"/reference/scripts/paginated-list","label":"Paginated List","docId":"reference/scripts/paginated-list","unlisted":false},{"type":"link","href":"/reference/scripts/list-or-fetch","label":"List or Fetch","docId":"reference/scripts/list-or-fetch","unlisted":false},{"type":"link","href":"/reference/scripts/expanded-query","label":"Expanded Query","docId":"reference/scripts/expanded-query","unlisted":false},{"type":"link","href":"/reference/scripts/update-or-delete","label":"Update or Delete","docId":"reference/scripts/update-or-delete","unlisted":false},{"type":"link","href":"/reference/scripts/batch-save","label":"Batch Save","docId":"reference/scripts/batch-save","unlisted":false},{"type":"link","href":"/reference/scripts/sidecar-records","label":"Sidecar Records","docId":"reference/scripts/sidecar-records","unlisted":false},{"type":"link","href":"/reference/scripts/cascading-delete","label":"Cascading Delete","docId":"reference/scripts/cascading-delete","unlisted":false},{"type":"link","href":"/reference/scripts/complex-mutations","label":"Complex Mutations","docId":"reference/scripts/complex-mutations","unlisted":false},{"type":"link","href":"/reference/scripts/algolia-sync","label":"Algolia Sync","docId":"reference/scripts/algolia-sync","unlisted":false},{"type":"link","href":"/reference/scripts/meilisearch-sync","label":"Meilisearch Sync","docId":"reference/scripts/meilisearch-sync","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"link","href":"/reference/glossary","label":"Glossary","docId":"reference/glossary","unlisted":false},{"type":"link","href":"/reference/architecture","label":"Architecture","docId":"reference/architecture","unlisted":false},{"type":"link","href":"/reference/troubleshooting","label":"Troubleshooting","docId":"reference/troubleshooting","unlisted":false},{"type":"link","href":"/reference/production-deployment","label":"Production","docId":"reference/production-deployment","unlisted":false}],"collapsed":true,"collapsible":true}]},"docs":{"getting-started/authentication":{"id":"getting-started/authentication","title":"Authentication","description":"HappyView uses AT Protocol OAuth for authentication, handled by an external AIP instance. HappyView does not store credentials or issue tokens: all OAuth is delegated to AIP.","sidebar":"docs"},"getting-started/configuration":{"id":"getting-started/configuration","title":"Configuration","description":"HappyView is configured via environment variables. A .env file in the project root is loaded automatically on startup. See Deployment for local setup or Production Deployment for production setup.","sidebar":"docs"},"getting-started/dashboard":{"id":"getting-started/dashboard","title":"Dashboard","description":"HappyView ships with a web dashboard that provides a visual interface for everything the admin API offers: managing lexicons, viewing indexed records, and monitoring backfill jobs. It runs as a separate Next.js application alongside the Rust backend.","sidebar":"docs"},"getting-started/deployment/docker":{"id":"getting-started/deployment/docker","title":"Local Development with Docker","description":"This guide runs the full HappyView stack locally using Docker Compose: Postgres, Tap, HappyView, and the web dashboard.","sidebar":"docs"},"getting-started/deployment/other":{"id":"getting-started/deployment/other","title":"Local Development from Source","description":"This guide runs HappyView directly with cargo run, with you managing Postgres, AIP, and Tap separately. If you\'d rather use Docker Compose to run everything together, see Local Development with Docker.","sidebar":"docs"},"getting-started/deployment/railway":{"id":"getting-started/deployment/railway","title":"Deploy on Railway","description":"The fastest way to get HappyView running is with Railway. This template deploys HappyView, AIP (OAuth provider), Tap (real-time data and backfill), and Postgres with a single click:","sidebar":"docs"},"getting-started/quickstart":{"id":"getting-started/quickstart","title":"Quickstart","description":"This page walks you through the fastest path to a working HappyView instance. By the end, you\'ll have an AppView that indexes records from the AT Protocol network and serves XRPC endpoints.","sidebar":"docs"},"guides/api-keys":{"id":"guides/api-keys","title":"API Keys","description":"API keys let you authenticate with the admin API without going through the OAuth flow. They\'re useful for CI/CD pipelines, scripts, and any automation that needs to manage your HappyView instance programmatically."},"guides/backfill":{"id":"guides/backfill","title":"Backfill","description":"When you add a new record-type lexicon, HappyView starts indexing new records from that moment via Tap. But what about records that already exist on the network? That\'s what backfill does: HappyView discovers repos via the relay and delegates the actual record fetching to Tap.","sidebar":"docs"},"guides/event-logs":{"id":"guides/event-logs","title":"Event Logs","description":"HappyView maintains an internal event log that records system activity \u2014 lexicon changes, record operations, Lua script executions and errors, admin actions, backfill jobs, and Tap connectivity. Events are stored in a Postgres table and queryable via the admin API.","sidebar":"docs"},"guides/index-hooks":{"id":"guides/index-hooks","title":"Index Hooks","description":"Index hooks are Lua scripts that run automatically whenever a record in a collection is created, updated, or deleted on the network. They run before the record is indexed, giving you the ability to filter out unwanted records, transform record data before storage, or trigger side effects like syncing with external services.","sidebar":"docs"},"guides/lexicons":{"id":"guides/lexicons","title":"Lexicons","description":"Lexicons are the core building block of HappyView. They\'re AT Protocol schema definitions that describe your data model, and HappyView uses them to decide which records to index from the network and what XRPC endpoints to serve.","sidebar":"docs"},"guides/scripting":{"id":"guides/scripting","title":"Lua Scripting","description":"Without Lua scripts, HappyView\'s query endpoints return raw records and procedure endpoints proxy simple creates and updates. Lua scripts let you go much further:","sidebar":"docs"},"README":{"id":"README","title":"HappyView","description":"HappyView is the best way to build an AppView for the AT Protocol. Upload your lexicon schemas and get a fully functional AppView, complete with XRPC endpoints, OAuth, real-time network sync, and historical backfill, without writing a single line of server code.","sidebar":"docs"},"reference/admin-api":{"id":"reference/admin-api","title":"Admin API","description":"The admin API lets you manage lexicons, monitor records, run backfill jobs, and control admin access. All endpoints live under /admin and require an AIP-issued Bearer token from a DID that exists in the admins table. You can also manage all of this through the web dashboard.","sidebar":"docs"},"reference/architecture":{"id":"reference/architecture","title":"Architecture","description":"Guide for contributors working on HappyView itself. For a user-facing overview, see the Introduction.","sidebar":"docs"},"reference/changelog":{"id":"reference/changelog","title":"Changelog","description":"v1.9.0 \u2014 Event Logs"},"reference/glossary":{"id":"reference/glossary","title":"Glossary","description":"Key terms used throughout the HappyView documentation. For a broader introduction to the AT Protocol, see the official ATProto glossary.","sidebar":"docs"},"reference/production-deployment":{"id":"reference/production-deployment","title":"Deployment","description":"HappyView requires a Postgres database and an AIP instance for OAuth. The Quickstart covers the fastest path with Railway. This page covers other deployment options.","sidebar":"docs"},"reference/scripts/algolia-sync":{"id":"reference/scripts/algolia-sync","title":"Index Hook: Algolia Sync","description":"Push records to an Algolia search index whenever they are created, updated, or deleted on the network.","sidebar":"docs"},"reference/scripts/batch-save":{"id":"reference/scripts/batch-save","title":"Procedure: Batch Save","description":"Use Record.save_all() to create multiple records in parallel.","sidebar":"docs"},"reference/scripts/cascading-delete":{"id":"reference/scripts/cascading-delete","title":"Procedure: Cascading Delete","description":"Delete a record and all related records across collections.","sidebar":"docs"},"reference/scripts/complex-mutations":{"id":"reference/scripts/complex-mutations","title":"Procedure: Complex Mutations","description":"Load an existing record, apply multiple transformations, and save it back.","sidebar":"docs"},"reference/scripts/create-record":{"id":"reference/scripts/create-record","title":"Procedure: Create a Record","description":"The simplest write: take the request body, save it as a record, and return the URI.","sidebar":"docs"},"reference/scripts/expanded-query":{"id":"reference/scripts/expanded-query","title":"Query: Expanded Query with Profiles","description":"List statuses and include the profile of each user who created one.","sidebar":"docs"},"reference/scripts/get-record":{"id":"reference/scripts/get-record","title":"Query: Get a Single Record","description":"Fetch a single record by its AT URI.","sidebar":"docs"},"reference/scripts/list-or-fetch":{"id":"reference/scripts/list-or-fetch","title":"Query: List or Fetch Records","description":"This query handles both single-record lookups (when a uri param is provided) and paginated listing.","sidebar":"docs"},"reference/scripts/meilisearch-sync":{"id":"reference/scripts/meilisearch-sync","title":"Index Hook: Meilisearch Sync","description":"Push records to a Meilisearch search index whenever they are created, updated, or deleted on the network.","sidebar":"docs"},"reference/scripts/paginated-list":{"id":"reference/scripts/paginated-list","title":"Query: Paginated List","description":"List records from a collection with cursor-based pagination and an optional DID filter.","sidebar":"docs"},"reference/scripts/sidecar-records":{"id":"reference/scripts/sidecar-records","title":"Procedure: Create Sidecar Records","description":"Create two records with different collection NSIDs but the same rkey, linking them together by key.","sidebar":"docs"},"reference/scripts/update-or-delete":{"id":"reference/scripts/update-or-delete","title":"Procedure: Update or Delete","description":"A single endpoint that handles create, update, and delete based on the input fields.","sidebar":"docs"},"reference/scripts/upsert-record":{"id":"reference/scripts/upsert-record","title":"Procedure: Upsert a Record","description":"Create a new record, or update an existing one if the client provides its rkey.","sidebar":"docs"},"reference/troubleshooting":{"id":"reference/troubleshooting","title":"Troubleshooting","description":"Common issues and how to resolve them.","sidebar":"docs"},"reference/xrpc-api":{"id":"reference/xrpc-api","title":"XRPC API","description":"XRPC is the HTTP-based RPC protocol used by the AT Protocol. HappyView dynamically registers XRPC endpoints based on your uploaded lexicons: query lexicons become GET /xrpc/ routes, procedure lexicons become POST /xrpc/{nsid} routes.","sidebar":"docs"},"tutorials/statusphere":{"id":"tutorials/statusphere","title":"Tutorial: Statusphere with HappyView","description":"Statusphere is an example AT Protocol application where users set their current status as a single emoji. It\'s a great way to learn how HappyView works because the data model is simple but the queries are interesting.","sidebar":"docs"}}}}')}}]); \ No newline at end of file diff --git a/assets/js/2d1b556d.316fda02.js b/assets/js/2d1b556d.316fda02.js deleted file mode 100644 index 152b8d9..0000000 --- a/assets/js/2d1b556d.316fda02.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkhappyview_docs=self.webpackChunkhappyview_docs||[]).push([[1952],{8385(e,n,t){t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>h,frontMatter:()=>o,metadata:()=>r,toc:()=>l});const r=JSON.parse('{"id":"reference/scripts/algolia-sync","title":"Index Hook: Algolia Sync","description":"Push records to an Algolia search index whenever they are created, updated, or deleted on the network.","source":"@site/docs/reference/scripts/algolia-sync.md","sourceDirName":"reference/scripts","slug":"/reference/scripts/algolia-sync","permalink":"/reference/scripts/algolia-sync","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{},"sidebar":"docs","previous":{"title":"Complex Mutations","permalink":"/reference/scripts/complex-mutations"},"next":{"title":"Meilisearch Sync","permalink":"/reference/scripts/meilisearch-sync"}}');var s=t(4848),i=t(8453);const o={},c="Index Hook: Algolia Sync",d={},l=[{value:"How it works",id:"how-it-works",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Use case",id:"use-case",level:2}];function a(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.header,{children:(0,s.jsx)(n.h1,{id:"index-hook-algolia-sync",children:"Index Hook: Algolia Sync"})}),"\n",(0,s.jsx)(n.p,{children:"Push records to an Algolia search index whenever they are created, updated, or deleted on the network."}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Lexicon type:"})," record (index hook)"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n local headers = {\n ["X-Algolia-API-Key"] = "your-api-key",\n ["X-Algolia-Application-Id"] = "your-app-id",\n ["Content-Type"] = "application/json"\n }\n\n if action == "delete" then\n http.delete("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {\n headers = headers\n })\n else\n http.put("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {\n headers = headers,\n body = json.encode({\n objectID = uri,\n collection = collection,\n did = did,\n record = record\n })\n })\n end\nend\n'})}),"\n",(0,s.jsx)(n.h2,{id:"how-it-works",children:"How it works"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["On ",(0,s.jsx)(n.strong,{children:"create"})," or ",(0,s.jsx)(n.strong,{children:"update"}),": sends a ",(0,s.jsx)(n.code,{children:"PUT"})," request to Algolia's index API with the record data, using the AT URI as the ",(0,s.jsx)(n.code,{children:"objectID"}),". Algolia upserts the object \u2014 if it already exists, it's replaced."]}),"\n",(0,s.jsxs)(n.li,{children:["On ",(0,s.jsx)(n.strong,{children:"delete"}),": sends a ",(0,s.jsx)(n.code,{children:"DELETE"})," request to remove the object from the index by its AT URI."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"json.encode()"})," function converts the Lua table into a JSON string for the request body. See ",(0,s.jsx)(n.a,{href:"/guides/index-hooks#json-api",children:"JSON API"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsx)(n.p,{children:"Replace the placeholder values:"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Placeholder"}),(0,s.jsx)(n.th,{children:"Value"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"your-api-key"})}),(0,s.jsx)(n.td,{children:"Your Algolia Admin API key (with write permissions)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"your-app-id"})}),(0,s.jsx)(n.td,{children:"Your Algolia Application ID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"YOUR-APP"})}),(0,s.jsx)(n.td,{children:"Your Algolia application subdomain (same as the Application ID)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"records"})}),(0,s.jsx)(n.td,{children:"The Algolia index name (choose any name you like)"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"use-case",children:"Use case"}),"\n",(0,s.jsx)(n.p,{children:"This hook keeps an external search index in sync with your indexed records in real time. Users searching through Algolia get results that reflect the latest state of the network without polling or scheduled jobs."}),"\n",(0,s.jsxs)(n.p,{children:["Combine this with a ",(0,s.jsx)(n.a,{href:"/guides/scripting",children:"query script"})," that searches Algolia instead of the local database for a full-text search experience that goes beyond what ",(0,s.jsx)(n.code,{children:"db.search"})," offers."]})]})}function h(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(a,{...e})}):a(e)}},8453(e,n,t){t.d(n,{R:()=>o,x:()=>c});var r=t(6540);const s={},i=r.createContext(s);function o(e){const n=r.useContext(i);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2d1b556d.cb991d70.js b/assets/js/2d1b556d.cb991d70.js new file mode 100644 index 0000000..906bd92 --- /dev/null +++ b/assets/js/2d1b556d.cb991d70.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkhappyview_docs=self.webpackChunkhappyview_docs||[]).push([[1952],{8385(e,n,r){r.r(n),r.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>h,frontMatter:()=>o,metadata:()=>t,toc:()=>l});const t=JSON.parse('{"id":"reference/scripts/algolia-sync","title":"Index Hook: Algolia Sync","description":"Push records to an Algolia search index whenever they are created, updated, or deleted on the network.","source":"@site/docs/reference/scripts/algolia-sync.md","sourceDirName":"reference/scripts","slug":"/reference/scripts/algolia-sync","permalink":"/reference/scripts/algolia-sync","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{},"sidebar":"docs","previous":{"title":"Complex Mutations","permalink":"/reference/scripts/complex-mutations"},"next":{"title":"Meilisearch Sync","permalink":"/reference/scripts/meilisearch-sync"}}');var s=r(4848),i=r(8453);const o={},c="Index Hook: Algolia Sync",d={},l=[{value:"How it works",id:"how-it-works",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Use case",id:"use-case",level:2}];function a(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.header,{children:(0,s.jsx)(n.h1,{id:"index-hook-algolia-sync",children:"Index Hook: Algolia Sync"})}),"\n",(0,s.jsx)(n.p,{children:"Push records to an Algolia search index whenever they are created, updated, or deleted on the network."}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Lexicon type:"})," record (index hook)"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n local headers = {\n ["X-Algolia-API-Key"] = "your-api-key",\n ["X-Algolia-Application-Id"] = "your-app-id",\n ["Content-Type"] = "application/json"\n }\n\n if action == "delete" then\n http.delete("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {\n headers = headers\n })\n else\n http.put("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {\n headers = headers,\n body = json.encode({\n objectID = uri,\n collection = collection,\n did = did,\n record = record\n })\n })\n end\n\n return record\nend\n'})}),"\n",(0,s.jsx)(n.h2,{id:"how-it-works",children:"How it works"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["On ",(0,s.jsx)(n.strong,{children:"create"})," or ",(0,s.jsx)(n.strong,{children:"update"}),": sends a ",(0,s.jsx)(n.code,{children:"PUT"})," request to Algolia's index API with the record data, using the AT URI as the ",(0,s.jsx)(n.code,{children:"objectID"}),". Algolia upserts the object \u2014 if it already exists, it's replaced."]}),"\n",(0,s.jsxs)(n.li,{children:["On ",(0,s.jsx)(n.strong,{children:"delete"}),": sends a ",(0,s.jsx)(n.code,{children:"DELETE"})," request to remove the object from the index by its AT URI."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"json.encode()"})," function converts the Lua table into a JSON string for the request body. See ",(0,s.jsx)(n.a,{href:"/guides/index-hooks#json-api",children:"JSON API"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsx)(n.p,{children:"Replace the placeholder values:"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Placeholder"}),(0,s.jsx)(n.th,{children:"Value"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"your-api-key"})}),(0,s.jsx)(n.td,{children:"Your Algolia Admin API key (with write permissions)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"your-app-id"})}),(0,s.jsx)(n.td,{children:"Your Algolia Application ID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"YOUR-APP"})}),(0,s.jsx)(n.td,{children:"Your Algolia application subdomain (same as the Application ID)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"records"})}),(0,s.jsx)(n.td,{children:"The Algolia index name (choose any name you like)"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"use-case",children:"Use case"}),"\n",(0,s.jsx)(n.p,{children:"This hook keeps an external search index in sync with your indexed records in real time. Users searching through Algolia get results that reflect the latest state of the network without polling or scheduled jobs."}),"\n",(0,s.jsxs)(n.p,{children:["Combine this with a ",(0,s.jsx)(n.a,{href:"/guides/scripting",children:"query script"})," that searches Algolia instead of the local database for a full-text search experience that goes beyond what ",(0,s.jsx)(n.code,{children:"db.search"})," offers."]})]})}function h(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(a,{...e})}):a(e)}},8453(e,n,r){r.d(n,{R:()=>o,x:()=>c});var t=r(6540);const s={},i=t.createContext(s);function o(e){const n=t.useContext(i);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),t.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/774696b9.08c2c2c4.js b/assets/js/774696b9.08c2c2c4.js new file mode 100644 index 0000000..cf57daf --- /dev/null +++ b/assets/js/774696b9.08c2c2c4.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkhappyview_docs=self.webpackChunkhappyview_docs||[]).push([[8865],{8158(e,n,r){r.r(n),r.d(n,{assets:()=>c,contentTitle:()=>l,default:()=>a,frontMatter:()=>t,metadata:()=>d,toc:()=>o});const d=JSON.parse('{"id":"guides/index-hooks","title":"Index Hooks","description":"Index hooks are Lua scripts that run automatically whenever a record in a collection is created, updated, or deleted on the network. They run before the record is indexed, giving you the ability to filter out unwanted records, transform record data before storage, or trigger side effects like syncing with external services.","source":"@site/docs/guides/index-hooks.md","sourceDirName":"guides","slug":"/guides/index-hooks","permalink":"/guides/index-hooks","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{},"sidebar":"docs","previous":{"title":"Lua Scripting","permalink":"/guides/scripting"},"next":{"title":"Backfill","permalink":"/guides/backfill"}}');var s=r(4848),i=r(8453);const t={},l="Index Hooks",c={},o=[{value:"Attaching a hook",id:"attaching-a-hook",level:2},{value:"Script structure",id:"script-structure",level:2},{value:"Context globals",id:"context-globals",level:2},{value:"Available APIs",id:"available-apis",level:2},{value:"Error handling and retries",id:"error-handling-and-retries",level:2},{value:"Performance considerations",id:"performance-considerations",level:3},{value:"Dead letter table",id:"dead-letter-table",level:3},{value:"Examples",id:"examples",level:2},{value:"Filter out records missing a required field",id:"filter-out-records-missing-a-required-field",level:3},{value:"Transform a record before storage",id:"transform-a-record-before-storage",level:3},{value:"Post to a webhook",id:"post-to-a-webhook",level:3},{value:"Sync to Algolia",id:"sync-to-algolia",level:3},{value:"Sync to Meilisearch",id:"sync-to-meilisearch",level:3},{value:"Next steps",id:"next-steps",level:2}];function h(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.header,{children:(0,s.jsx)(n.h1,{id:"index-hooks",children:"Index Hooks"})}),"\n",(0,s.jsxs)(n.p,{children:["Index hooks are Lua scripts that run automatically whenever a record in a collection is created, updated, or deleted on the network. They run ",(0,s.jsx)(n.strong,{children:"before"})," the record is indexed, giving you the ability to filter out unwanted records, transform record data before storage, or trigger side effects like syncing with external services."]}),"\n",(0,s.jsxs)(n.p,{children:["Unlike ",(0,s.jsx)(n.a,{href:"/guides/scripting",children:"query and procedure scripts"})," that run in response to XRPC requests, index hooks are triggered by the firehose."]}),"\n",(0,s.jsx)(n.h2,{id:"attaching-a-hook",children:"Attaching a hook"}),"\n",(0,s.jsxs)(n.p,{children:["Each record-type lexicon can have one index hook. You can add it through the ",(0,s.jsx)(n.a,{href:"/getting-started/dashboard",children:"dashboard"}),' (click "Add Index Hook" on any record lexicon\'s detail page) or via the ',(0,s.jsx)(n.a,{href:"/reference/admin-api#upload--upsert-a-lexicon",children:"admin API"})," by including the ",(0,s.jsx)(n.code,{children:"index_hook"})," field when uploading a lexicon."]}),"\n",(0,s.jsx)(n.h2,{id:"script-structure",children:"Script structure"}),"\n",(0,s.jsxs)(n.p,{children:["Like query and procedure scripts, index hooks must define a ",(0,s.jsx)(n.code,{children:"handle()"})," function:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n if action == "delete" then\n log("deleted " .. uri)\n else\n log(action .. " " .. uri)\n end\nend\n'})}),"\n",(0,s.jsx)(n.p,{children:"The function is called once per record event. The return value controls what happens next:"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Return value"}),(0,s.jsx)(n.th,{children:"Effect"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"nil"})}),(0,s.jsxs)(n.td,{children:["The record is ",(0,s.jsx)(n.strong,{children:"not"})," indexed (skipped entirely)"]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"A table"}),(0,s.jsx)(n.td,{children:"That table is stored as the record instead"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"true"})}),(0,s.jsx)(n.td,{children:"The original record is stored as-is"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.em,{children:"(no hook)"})}),(0,s.jsx)(n.td,{children:"The original record is stored as-is"})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["On ",(0,s.jsx)(n.strong,{children:"delete"})," events, returning ",(0,s.jsx)(n.code,{children:"nil"})," skips the delete (the record stays in the database)."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Important:"})," If your hook has side effects (e.g. syncing to a search index) but you want normal indexing to proceed, return ",(0,s.jsx)(n.code,{children:"record"})," or ",(0,s.jsx)(n.code,{children:"true"})," \u2014 not nothing. A missing return statement returns ",(0,s.jsx)(n.code,{children:"nil"}),", which ",(0,s.jsx)(n.strong,{children:"skips indexing"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["If the hook errors after all retries, the system ",(0,s.jsx)(n.strong,{children:"fails open"})," \u2014 the original record is stored and the failed event is dead-lettered for later inspection."]}),"\n",(0,s.jsx)(n.h2,{id:"context-globals",children:"Context globals"}),"\n",(0,s.jsxs)(n.p,{children:["These globals are set before ",(0,s.jsx)(n.code,{children:"handle()"})," is called:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Global"}),(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"action"})}),(0,s.jsx)(n.td,{children:"string"}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:'"create"'}),", ",(0,s.jsx)(n.code,{children:'"update"'}),", or ",(0,s.jsx)(n.code,{children:'"delete"'})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"uri"})}),(0,s.jsx)(n.td,{children:"string"}),(0,s.jsxs)(n.td,{children:["The full AT URI (e.g. ",(0,s.jsx)(n.code,{children:"at://did:plc:abc/col/rkey"}),")"]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"did"})}),(0,s.jsx)(n.td,{children:"string"}),(0,s.jsx)(n.td,{children:"The repo DID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"collection"})}),(0,s.jsx)(n.td,{children:"string"}),(0,s.jsx)(n.td,{children:"The collection NSID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"rkey"})}),(0,s.jsx)(n.td,{children:"string"}),(0,s.jsx)(n.td,{children:"The record key"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"record"})}),(0,s.jsx)(n.td,{children:"table?"}),(0,s.jsx)(n.td,{children:"The full record as a Lua table (nil on delete)"})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["Index hooks do ",(0,s.jsx)(n.strong,{children:"not"})," have access to ",(0,s.jsx)(n.code,{children:"caller_did"}),", ",(0,s.jsx)(n.code,{children:"input"}),", ",(0,s.jsx)(n.code,{children:"params"}),", ",(0,s.jsx)(n.code,{children:"method"}),", or the ",(0,s.jsx)(n.code,{children:"Record"})," API. They run from the firehose, not from a user request."]}),"\n",(0,s.jsx)(n.h2,{id:"available-apis",children:"Available APIs"}),"\n",(0,s.jsx)(n.p,{children:"Index hooks have access to:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/guides/scripting#database-api",children:"Database API"})})," \u2014 ",(0,s.jsx)(n.code,{children:"db.query"}),", ",(0,s.jsx)(n.code,{children:"db.get"}),", ",(0,s.jsx)(n.code,{children:"db.search"}),", ",(0,s.jsx)(n.code,{children:"db.backlinks"}),", ",(0,s.jsx)(n.code,{children:"db.count"}),", ",(0,s.jsx)(n.code,{children:"db.raw"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/guides/scripting#http-api",children:"HTTP API"})})," \u2014 ",(0,s.jsx)(n.code,{children:"http.get"}),", ",(0,s.jsx)(n.code,{children:"http.post"}),", ",(0,s.jsx)(n.code,{children:"http.put"}),", ",(0,s.jsx)(n.code,{children:"http.patch"}),", ",(0,s.jsx)(n.code,{children:"http.delete"}),", ",(0,s.jsx)(n.code,{children:"http.head"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/guides/scripting#json-api",children:"JSON API"})})," \u2014 ",(0,s.jsx)(n.code,{children:"json.encode"}),", ",(0,s.jsx)(n.code,{children:"json.decode"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/guides/scripting#utility-globals",children:"Utility globals"})})," \u2014 ",(0,s.jsx)(n.code,{children:"log()"}),", ",(0,s.jsx)(n.code,{children:"now()"}),", ",(0,s.jsx)(n.code,{children:"TID()"}),", ",(0,s.jsx)(n.code,{children:"toarray()"})]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"error-handling-and-retries",children:"Error handling and retries"}),"\n",(0,s.jsx)(n.p,{children:"Index hooks are designed to be resilient:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["If a hook fails, it retries up to ",(0,s.jsx)(n.strong,{children:"3 times"})," with exponential backoff (1s, 2s, 4s delays)."]}),"\n",(0,s.jsxs)(n.li,{children:["If all retries are exhausted, the failed event is inserted into the ",(0,s.jsx)(n.code,{children:"dead_letter_hooks"})," table for later inspection."]}),"\n",(0,s.jsxs)(n.li,{children:["On failure the system ",(0,s.jsx)(n.strong,{children:"fails open"})," \u2014 the original record is stored as-is so indexing is not permanently blocked."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Failed hooks are logged as errors. Check the ",(0,s.jsx)(n.a,{href:"/guides/event-logs",children:"event logs"})," or query the ",(0,s.jsx)(n.code,{children:"dead_letter_hooks"})," table directly to find and replay failures."]}),"\n",(0,s.jsx)(n.h3,{id:"performance-considerations",children:"Performance considerations"}),"\n",(0,s.jsx)(n.p,{children:"Because hooks run synchronously before indexing, they block the firehose consumer while executing. With retry logic (1s + 2s + 4s backoff), a persistently failing hook could block for ~7 seconds per record. Keep hook scripts fast and ensure external services they depend on are reliable."}),"\n",(0,s.jsx)(n.h3,{id:"dead-letter-table",children:"Dead letter table"}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"dead_letter_hooks"})," table stores events that failed all retry attempts:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Column"}),(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"id"})}),(0,s.jsx)(n.td,{children:"UUID"}),(0,s.jsx)(n.td,{children:"Primary key"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"lexicon_id"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The lexicon NSID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"uri"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The AT URI of the record"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"did"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The repo DID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"collection"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The collection NSID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"rkey"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The record key"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"action"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"create"}),", ",(0,s.jsx)(n.code,{children:"update"}),", or ",(0,s.jsx)(n.code,{children:"delete"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"record"})}),(0,s.jsx)(n.td,{children:"jsonb"}),(0,s.jsx)(n.td,{children:"The record data (null on delete)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"error"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The error message from the last attempt"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"attempts"})}),(0,s.jsx)(n.td,{children:"int"}),(0,s.jsx)(n.td,{children:"Total number of attempts made"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"created_at"})}),(0,s.jsx)(n.td,{children:"timestamptz"}),(0,s.jsx)(n.td,{children:"When the failure was recorded"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,s.jsx)(n.h3,{id:"filter-out-records-missing-a-required-field",children:"Filter out records missing a required field"}),"\n",(0,s.jsxs)(n.p,{children:["Skip indexing any record that doesn't have a ",(0,s.jsx)(n.code,{children:"title"})," field:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n if action == "delete" then\n return record -- allow deletes to proceed\n end\n\n if record.title == nil or record.title == "" then\n return nil -- skip: no title\n end\n\n return record\nend\n'})}),"\n",(0,s.jsx)(n.h3,{id:"transform-a-record-before-storage",children:"Transform a record before storage"}),"\n",(0,s.jsx)(n.p,{children:"Enrich a record with a computed field before it is stored:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n if action == "delete" then\n return record\n end\n\n record.slug = string.lower(string.gsub(record.title or "", "%s+", "-"))\n return record\nend\n'})}),"\n",(0,s.jsx)(n.h3,{id:"post-to-a-webhook",children:"Post to a webhook"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n http.post("https://hooks.example.com/records", {\n headers = { ["Content-Type"] = "application/json" },\n body = json.encode({\n action = action,\n uri = uri,\n did = did,\n record = record\n })\n })\n return record\nend\n'})}),"\n",(0,s.jsx)(n.h3,{id:"sync-to-algolia",children:"Sync to Algolia"}),"\n",(0,s.jsx)(n.p,{children:"Push records to an Algolia search index on create/update, and remove them on delete:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n local headers = {\n ["X-Algolia-API-Key"] = "your-api-key",\n ["X-Algolia-Application-Id"] = "your-app-id",\n ["Content-Type"] = "application/json"\n }\n\n if action == "delete" then\n http.delete("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {\n headers = headers\n })\n else\n http.put("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {\n headers = headers,\n body = json.encode({\n objectID = uri,\n collection = collection,\n did = did,\n record = record\n })\n })\n end\n\n return record\nend\n'})}),"\n",(0,s.jsxs)(n.p,{children:["See the full ",(0,s.jsx)(n.a,{href:"/reference/scripts/algolia-sync",children:"Algolia sync reference"})," for more detail."]}),"\n",(0,s.jsx)(n.h3,{id:"sync-to-meilisearch",children:"Sync to Meilisearch"}),"\n",(0,s.jsx)(n.p,{children:"Push records to a self-hosted Meilisearch index on create/update, and remove them on delete:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n local headers = {\n ["Authorization"] = "Bearer " .. env.MEILISEARCH_API_KEY,\n ["Content-Type"] = "application/json"\n }\n\n if action == "delete" then\n http.delete(env.MEILISEARCH_URL .. "/indexes/records/documents/" .. uri, {\n headers = headers\n })\n else\n http.post(env.MEILISEARCH_URL .. "/indexes/records/documents", {\n headers = headers,\n body = json.encode(toarray({\n {\n id = uri,\n collection = collection,\n did = did,\n record = record\n }\n }))\n })\n end\n\n return record\nend\n'})}),"\n",(0,s.jsxs)(n.p,{children:["See the full ",(0,s.jsx)(n.a,{href:"/reference/scripts/meilisearch-sync",children:"Meilisearch sync reference"})," for more detail."]}),"\n",(0,s.jsx)(n.h2,{id:"next-steps",children:"Next steps"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"/guides/scripting",children:"Lua Scripting"}),": Full reference for the sandbox, APIs, and debugging"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"/guides/lexicons",children:"Lexicons"}),": Understand how record, query, and procedure lexicons work together"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"/reference/admin-api#upload--upsert-a-lexicon",children:"Admin API"}),": Upload lexicons with index hooks via the API"]}),"\n"]})]})}function a(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},8453(e,n,r){r.d(n,{R:()=>t,x:()=>l});var d=r(6540);const s={},i=d.createContext(s);function t(e){const n=d.useContext(i);return d.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:t(e.components),d.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/774696b9.aaf7ca2b.js b/assets/js/774696b9.aaf7ca2b.js deleted file mode 100644 index c62f715..0000000 --- a/assets/js/774696b9.aaf7ca2b.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkhappyview_docs=self.webpackChunkhappyview_docs||[]).push([[8865],{8158(e,n,d){d.r(n),d.d(n,{assets:()=>c,contentTitle:()=>l,default:()=>a,frontMatter:()=>t,metadata:()=>r,toc:()=>o});const r=JSON.parse('{"id":"guides/index-hooks","title":"Index Hooks","description":"Index hooks are Lua scripts that run automatically whenever a record in a collection is created, updated, or deleted on the network. They let you react to record changes in real time \u2014 push data to search engines, sync with external APIs, send notifications, or build materialized views.","source":"@site/docs/guides/index-hooks.md","sourceDirName":"guides","slug":"/guides/index-hooks","permalink":"/guides/index-hooks","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{},"sidebar":"docs","previous":{"title":"Lua Scripting","permalink":"/guides/scripting"},"next":{"title":"Backfill","permalink":"/guides/backfill"}}');var s=d(4848),i=d(8453);const t={},l="Index Hooks",c={},o=[{value:"Attaching a hook",id:"attaching-a-hook",level:2},{value:"Script structure",id:"script-structure",level:2},{value:"Context globals",id:"context-globals",level:2},{value:"Available APIs",id:"available-apis",level:2},{value:"Error handling and retries",id:"error-handling-and-retries",level:2},{value:"Dead letter table",id:"dead-letter-table",level:3},{value:"Examples",id:"examples",level:2},{value:"Post to a webhook",id:"post-to-a-webhook",level:3},{value:"Sync to Algolia",id:"sync-to-algolia",level:3},{value:"Sync to Meilisearch",id:"sync-to-meilisearch",level:3},{value:"Next steps",id:"next-steps",level:2}];function h(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.header,{children:(0,s.jsx)(n.h1,{id:"index-hooks",children:"Index Hooks"})}),"\n",(0,s.jsx)(n.p,{children:"Index hooks are Lua scripts that run automatically whenever a record in a collection is created, updated, or deleted on the network. They let you react to record changes in real time \u2014 push data to search engines, sync with external APIs, send notifications, or build materialized views."}),"\n",(0,s.jsxs)(n.p,{children:["Unlike ",(0,s.jsx)(n.a,{href:"/guides/scripting",children:"query and procedure scripts"})," that run in response to XRPC requests, index hooks are triggered by the firehose. They run asynchronously and never block record indexing."]}),"\n",(0,s.jsx)(n.h2,{id:"attaching-a-hook",children:"Attaching a hook"}),"\n",(0,s.jsxs)(n.p,{children:["Each record-type lexicon can have one index hook. You can add it through the ",(0,s.jsx)(n.a,{href:"/getting-started/dashboard",children:"dashboard"}),' (click "Add Index Hook" on any record lexicon\'s detail page) or via the ',(0,s.jsx)(n.a,{href:"/reference/admin-api#upload--upsert-a-lexicon",children:"admin API"})," by including the ",(0,s.jsx)(n.code,{children:"index_hook"})," field when uploading a lexicon."]}),"\n",(0,s.jsx)(n.h2,{id:"script-structure",children:"Script structure"}),"\n",(0,s.jsxs)(n.p,{children:["Like query and procedure scripts, index hooks must define a ",(0,s.jsx)(n.code,{children:"handle()"})," function:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n if action == "delete" then\n log("deleted " .. uri)\n else\n log(action .. " " .. uri)\n end\nend\n'})}),"\n",(0,s.jsx)(n.p,{children:"The function is called once per record event. There is no return value \u2014 index hooks are fire-and-forget from the caller's perspective."}),"\n",(0,s.jsx)(n.h2,{id:"context-globals",children:"Context globals"}),"\n",(0,s.jsxs)(n.p,{children:["These globals are set before ",(0,s.jsx)(n.code,{children:"handle()"})," is called:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Global"}),(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"action"})}),(0,s.jsx)(n.td,{children:"string"}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:'"create"'}),", ",(0,s.jsx)(n.code,{children:'"update"'}),", or ",(0,s.jsx)(n.code,{children:'"delete"'})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"uri"})}),(0,s.jsx)(n.td,{children:"string"}),(0,s.jsxs)(n.td,{children:["The full AT URI (e.g. ",(0,s.jsx)(n.code,{children:"at://did:plc:abc/col/rkey"}),")"]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"did"})}),(0,s.jsx)(n.td,{children:"string"}),(0,s.jsx)(n.td,{children:"The repo DID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"collection"})}),(0,s.jsx)(n.td,{children:"string"}),(0,s.jsx)(n.td,{children:"The collection NSID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"rkey"})}),(0,s.jsx)(n.td,{children:"string"}),(0,s.jsx)(n.td,{children:"The record key"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"record"})}),(0,s.jsx)(n.td,{children:"table?"}),(0,s.jsx)(n.td,{children:"The full record as a Lua table (nil on delete)"})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["Index hooks do ",(0,s.jsx)(n.strong,{children:"not"})," have access to ",(0,s.jsx)(n.code,{children:"caller_did"}),", ",(0,s.jsx)(n.code,{children:"input"}),", ",(0,s.jsx)(n.code,{children:"params"}),", ",(0,s.jsx)(n.code,{children:"method"}),", or the ",(0,s.jsx)(n.code,{children:"Record"})," API. They run from the firehose, not from a user request."]}),"\n",(0,s.jsx)(n.h2,{id:"available-apis",children:"Available APIs"}),"\n",(0,s.jsx)(n.p,{children:"Index hooks have access to:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/guides/scripting#database-api",children:"Database API"})})," \u2014 ",(0,s.jsx)(n.code,{children:"db.query"}),", ",(0,s.jsx)(n.code,{children:"db.get"}),", ",(0,s.jsx)(n.code,{children:"db.search"}),", ",(0,s.jsx)(n.code,{children:"db.backlinks"}),", ",(0,s.jsx)(n.code,{children:"db.count"}),", ",(0,s.jsx)(n.code,{children:"db.raw"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/guides/scripting#http-api",children:"HTTP API"})})," \u2014 ",(0,s.jsx)(n.code,{children:"http.get"}),", ",(0,s.jsx)(n.code,{children:"http.post"}),", ",(0,s.jsx)(n.code,{children:"http.put"}),", ",(0,s.jsx)(n.code,{children:"http.patch"}),", ",(0,s.jsx)(n.code,{children:"http.delete"}),", ",(0,s.jsx)(n.code,{children:"http.head"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/guides/scripting#json-api",children:"JSON API"})})," \u2014 ",(0,s.jsx)(n.code,{children:"json.encode"}),", ",(0,s.jsx)(n.code,{children:"json.decode"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:(0,s.jsx)(n.a,{href:"/guides/scripting#utility-globals",children:"Utility globals"})})," \u2014 ",(0,s.jsx)(n.code,{children:"log()"}),", ",(0,s.jsx)(n.code,{children:"now()"}),", ",(0,s.jsx)(n.code,{children:"TID()"}),", ",(0,s.jsx)(n.code,{children:"toarray()"})]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"error-handling-and-retries",children:"Error handling and retries"}),"\n",(0,s.jsx)(n.p,{children:"Index hooks are designed to be resilient:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["If a hook fails, it retries up to ",(0,s.jsx)(n.strong,{children:"3 times"})," with exponential backoff (1s, 2s, 4s delays)."]}),"\n",(0,s.jsxs)(n.li,{children:["If all retries are exhausted, the failed event is inserted into the ",(0,s.jsx)(n.code,{children:"dead_letter_hooks"})," table for later inspection."]}),"\n",(0,s.jsx)(n.li,{children:"Hook failures never block record indexing \u2014 the record is always indexed regardless of whether the hook succeeds."}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Failed hooks are logged as errors. Check the ",(0,s.jsx)(n.a,{href:"/guides/event-logs",children:"event logs"})," or query the ",(0,s.jsx)(n.code,{children:"dead_letter_hooks"})," table directly to find and replay failures."]}),"\n",(0,s.jsx)(n.h3,{id:"dead-letter-table",children:"Dead letter table"}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"dead_letter_hooks"})," table stores events that failed all retry attempts:"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Column"}),(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"id"})}),(0,s.jsx)(n.td,{children:"UUID"}),(0,s.jsx)(n.td,{children:"Primary key"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"lexicon_id"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The lexicon NSID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"uri"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The AT URI of the record"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"did"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The repo DID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"collection"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The collection NSID"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"rkey"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The record key"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"action"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"create"}),", ",(0,s.jsx)(n.code,{children:"update"}),", or ",(0,s.jsx)(n.code,{children:"delete"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"record"})}),(0,s.jsx)(n.td,{children:"jsonb"}),(0,s.jsx)(n.td,{children:"The record data (null on delete)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"error"})}),(0,s.jsx)(n.td,{children:"text"}),(0,s.jsx)(n.td,{children:"The error message from the last attempt"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"attempts"})}),(0,s.jsx)(n.td,{children:"int"}),(0,s.jsx)(n.td,{children:"Total number of attempts made"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"created_at"})}),(0,s.jsx)(n.td,{children:"timestamptz"}),(0,s.jsx)(n.td,{children:"When the failure was recorded"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,s.jsx)(n.h3,{id:"post-to-a-webhook",children:"Post to a webhook"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n http.post("https://hooks.example.com/records", {\n headers = { ["Content-Type"] = "application/json" },\n body = json.encode({\n action = action,\n uri = uri,\n did = did,\n record = record\n })\n })\nend\n'})}),"\n",(0,s.jsx)(n.h3,{id:"sync-to-algolia",children:"Sync to Algolia"}),"\n",(0,s.jsx)(n.p,{children:"Push records to an Algolia search index on create/update, and remove them on delete:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n local headers = {\n ["X-Algolia-API-Key"] = "your-api-key",\n ["X-Algolia-Application-Id"] = "your-app-id",\n ["Content-Type"] = "application/json"\n }\n\n if action == "delete" then\n http.delete("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {\n headers = headers\n })\n else\n http.put("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {\n headers = headers,\n body = json.encode({\n objectID = uri,\n collection = collection,\n did = did,\n record = record\n })\n })\n end\nend\n'})}),"\n",(0,s.jsxs)(n.p,{children:["See the full ",(0,s.jsx)(n.a,{href:"/reference/scripts/algolia-sync",children:"Algolia sync reference"})," for more detail."]}),"\n",(0,s.jsx)(n.h3,{id:"sync-to-meilisearch",children:"Sync to Meilisearch"}),"\n",(0,s.jsx)(n.p,{children:"Push records to a self-hosted Meilisearch index on create/update, and remove them on delete:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-lua",children:'function handle()\n local headers = {\n ["Authorization"] = "Bearer " .. env.MEILISEARCH_API_KEY,\n ["Content-Type"] = "application/json"\n }\n\n if action == "delete" then\n http.delete(env.MEILISEARCH_URL .. "/indexes/records/documents/" .. uri, {\n headers = headers\n })\n else\n http.post(env.MEILISEARCH_URL .. "/indexes/records/documents", {\n headers = headers,\n body = json.encode(toarray({\n {\n id = uri,\n collection = collection,\n did = did,\n record = record\n }\n }))\n })\n end\nend\n'})}),"\n",(0,s.jsxs)(n.p,{children:["See the full ",(0,s.jsx)(n.a,{href:"/reference/scripts/meilisearch-sync",children:"Meilisearch sync reference"})," for more detail."]}),"\n",(0,s.jsx)(n.h2,{id:"next-steps",children:"Next steps"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"/guides/scripting",children:"Lua Scripting"}),": Full reference for the sandbox, APIs, and debugging"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"/guides/lexicons",children:"Lexicons"}),": Understand how record, query, and procedure lexicons work together"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"/reference/admin-api#upload--upsert-a-lexicon",children:"Admin API"}),": Upload lexicons with index hooks via the API"]}),"\n"]})]})}function a(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},8453(e,n,d){d.d(n,{R:()=>t,x:()=>l});var r=d(6540);const s={},i=r.createContext(s);function t(e){const n=r.useContext(i);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:t(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/920ba7ef.2e58966a.js b/assets/js/920ba7ef.2e58966a.js deleted file mode 100644 index 19d4bd2..0000000 --- a/assets/js/920ba7ef.2e58966a.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkhappyview_docs=self.webpackChunkhappyview_docs||[]).push([[5953],{1189(e,r,n){n.r(r),n.d(r,{assets:()=>a,contentTitle:()=>t,default:()=>o,frontMatter:()=>i,metadata:()=>s,toc:()=>c});const s=JSON.parse('{"id":"guides/scripting","title":"Lua Scripting","description":"Without Lua scripts, HappyView\'s query endpoints return raw records and procedure endpoints proxy simple creates and updates. Lua scripts let you go much further:","source":"@site/docs/guides/scripting.md","sourceDirName":"guides","slug":"/guides/scripting","permalink":"/guides/scripting","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{},"sidebar":"docs","previous":{"title":"Lexicons","permalink":"/guides/lexicons"},"next":{"title":"Index Hooks","permalink":"/guides/index-hooks"}}');var l=n(4848),d=n(8453);const i={},t="Lua Scripting",a={},c=[{value:"Script structure",id:"script-structure",level:2},{value:"Sandbox",id:"sandbox",level:2},{value:"Context globals",id:"context-globals",level:2},{value:"Procedure globals",id:"procedure-globals",level:3},{value:"Query globals",id:"query-globals",level:3},{value:"Utility globals",id:"utility-globals",level:2},{value:"toarray",id:"toarray",level:3},{value:"Record API",id:"record-api",level:2},{value:"Constructor",id:"constructor",level:3},{value:"Static methods",id:"static-methods",level:3},{value:"Instance methods",id:"instance-methods",level:3},{value:"Instance fields",id:"instance-fields",level:3},{value:"Schema validation",id:"schema-validation",level:3},{value:"Save behavior",id:"save-behavior",level:3},{value:"Database API",id:"database-api",level:2},{value:"db.query",id:"dbquery",level:3},{value:"db.get",id:"dbget",level:3},{value:"db.search",id:"dbsearch",level:3},{value:"db.backlinks",id:"dbbacklinks",level:3},{value:"db.count",id:"dbcount",level:3},{value:"db.raw",id:"dbraw",level:3},{value:"HTTP API",id:"http-api",level:2},{value:"Methods",id:"methods",level:3},{value:"Options",id:"options",level:3},{value:"Response",id:"response",level:3},{value:"Examples",id:"examples",level:3},{value:"JSON API",id:"json-api",level:2},{value:"json.encode",id:"jsonencode",level:3},{value:"json.decode",id:"jsondecode",level:3},{value:"Standard libraries",id:"standard-libraries",level:2},{value:"Debugging",id:"debugging",level:2},{value:"Logging",id:"logging",level:3},{value:"Error messages",id:"error-messages",level:3},{value:"Common mistakes",id:"common-mistakes",level:3},{value:"Example scripts",id:"example-scripts",level:2},{value:"Next steps",id:"next-steps",level:2}];function h(e){const r={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components},{Details:n}=r;return n||function(e,r){throw new Error("Expected "+(r?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(r.header,{children:(0,l.jsx)(r.h1,{id:"lua-scripting",children:"Lua Scripting"})}),"\n",(0,l.jsx)(r.p,{children:"Without Lua scripts, HappyView's query endpoints return raw records and procedure endpoints proxy simple creates and updates. Lua scripts let you go much further:"}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsx)(r.li,{children:"Add filtering logic"}),"\n",(0,l.jsx)(r.li,{children:"Transform responses"}),"\n",(0,l.jsx)(r.li,{children:"Validate input"}),"\n",(0,l.jsx)(r.li,{children:"Compose multi-record operations"}),"\n",(0,l.jsx)(r.li,{children:"Build entirely custom behavior"}),"\n"]}),"\n",(0,l.jsxs)(r.p,{children:["Scripts are attached to query and procedure lexicons and run in a sandboxed Lua VM with access to the ",(0,l.jsx)(r.a,{href:"#record-api",children:"Record API"}),", a ",(0,l.jsx)(r.a,{href:"#database-api",children:"read-only database API"}),", an ",(0,l.jsx)(r.a,{href:"#http-api",children:"HTTP client API"}),", a ",(0,l.jsx)(r.a,{href:"#json-api",children:"JSON API"}),", and a set of ",(0,l.jsx)(r.a,{href:"#context-globals",children:"context globals"}),"."]}),"\n",(0,l.jsxs)(r.p,{children:["For scripts that react to record changes from the network (rather than XRPC requests), see ",(0,l.jsx)(r.a,{href:"/guides/index-hooks",children:"Index Hooks"}),"."]}),"\n",(0,l.jsx)(r.h2,{id:"script-structure",children:"Script structure"}),"\n",(0,l.jsxs)(r.p,{children:["Every script must define a ",(0,l.jsx)(r.code,{children:"handle()"})," function. HappyView calls it when the XRPC endpoint is hit and returns its result as JSON to the client."]}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'function handle()\n -- your logic here\n return { key = "value" }\nend\n'})}),"\n",(0,l.jsxs)(r.p,{children:["You can define helper functions and variables outside ",(0,l.jsx)(r.code,{children:"handle()"}),". They're evaluated once when the script loads, then ",(0,l.jsx)(r.code,{children:"handle()"})," is called per request."]}),"\n",(0,l.jsx)(r.h2,{id:"sandbox",children:"Sandbox"}),"\n",(0,l.jsxs)(r.p,{children:["Scripts run in a restricted environment. The following standard Lua modules are ",(0,l.jsx)(r.strong,{children:"removed"})," and unavailable:"]}),"\n",(0,l.jsxs)(r.p,{children:[(0,l.jsx)(r.code,{children:"os"}),", ",(0,l.jsx)(r.code,{children:"io"}),", ",(0,l.jsx)(r.code,{children:"debug"}),", ",(0,l.jsx)(r.code,{children:"package"}),", ",(0,l.jsx)(r.code,{children:"require"}),", ",(0,l.jsx)(r.code,{children:"dofile"}),", ",(0,l.jsx)(r.code,{children:"loadfile"}),", ",(0,l.jsx)(r.code,{children:"load"}),", ",(0,l.jsx)(r.code,{children:"collectgarbage"})]}),"\n",(0,l.jsx)(r.p,{children:"An instruction limit of 1,000,000 prevents infinite loops. Exceeding it terminates the script with an error."}),"\n",(0,l.jsx)(r.h2,{id:"context-globals",children:"Context globals"}),"\n",(0,l.jsxs)(r.p,{children:["These globals are set automatically before ",(0,l.jsx)(r.code,{children:"handle()"})," is called."]}),"\n",(0,l.jsx)(r.h3,{id:"procedure-globals",children:"Procedure globals"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Global"}),(0,l.jsx)(r.th,{children:"Type"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"method"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsxs)(r.td,{children:["The XRPC method name (e.g. ",(0,l.jsx)(r.code,{children:"xyz.statusphere.setStatus"}),")"]})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"input"})}),(0,l.jsx)(r.td,{children:"table"}),(0,l.jsx)(r.td,{children:"Parsed JSON request body"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"caller_did"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"DID of the authenticated user"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"collection"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Target collection NSID"})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"query-globals",children:"Query globals"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Global"}),(0,l.jsx)(r.th,{children:"Type"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"method"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"The XRPC method name"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"params"})}),(0,l.jsx)(r.td,{children:"table"}),(0,l.jsx)(r.td,{children:"Query string parameters (all values are strings)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"collection"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Target collection NSID"})]})]})]}),"\n",(0,l.jsxs)(r.p,{children:["Queries are unauthenticated: there is no ",(0,l.jsx)(r.code,{children:"caller_did"})," or ",(0,l.jsx)(r.code,{children:"input"}),"."]}),"\n",(0,l.jsx)(r.h2,{id:"utility-globals",children:"Utility globals"}),"\n",(0,l.jsx)(r.p,{children:"Available in both queries and procedures:"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Function"}),(0,l.jsx)(r.th,{children:"Returns"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"now()"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Current UTC timestamp in ISO 8601 format"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"log(message)"})}),(0,l.jsx)(r.td,{children:"\u2014"}),(0,l.jsx)(r.td,{children:"Log a message (appears in server logs at debug level)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"TID()"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Generate a fresh AT Protocol TID (13-character sortable identifier)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"toarray(table)"})}),(0,l.jsx)(r.td,{children:"table"}),(0,l.jsxs)(r.td,{children:["Mark a table as a JSON array for serialization (see ",(0,l.jsx)(r.a,{href:"#toarray",children:"below"}),")"]})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"toarray",children:"toarray"}),"\n",(0,l.jsxs)(r.p,{children:["Lua tables don't distinguish between arrays and objects. When a table is serialized to JSON, an empty table ",(0,l.jsx)(r.code,{children:"{}"})," becomes a JSON object ",(0,l.jsx)(r.code,{children:"{}"})," instead of an array ",(0,l.jsx)(r.code,{children:"[]"}),". The ",(0,l.jsx)(r.code,{children:"toarray()"})," function marks a table so it always serializes as a JSON array \u2014 even when empty."]}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'return { items = toarray(results) }\n-- With results: [{"name": "a"}, {"name": "b"}]\n-- Without results: {"items": []} (not {"items": {}})\n'})}),"\n",(0,l.jsxs)(r.p,{children:["You don't need ",(0,l.jsx)(r.code,{children:"toarray()"})," on results from ",(0,l.jsx)(r.code,{children:"db.query"}),", ",(0,l.jsx)(r.code,{children:"db.search"}),", ",(0,l.jsx)(r.code,{children:"db.backlinks"}),", or ",(0,l.jsx)(r.code,{children:"db.raw"})," \u2014 those already return properly marked arrays. Use it when you build a table yourself with ",(0,l.jsx)(r.code,{children:"table.insert()"}),"."]}),"\n",(0,l.jsx)(r.h2,{id:"record-api",children:"Record API"}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.code,{children:"Record"})," API is only available in ",(0,l.jsx)(r.strong,{children:"procedure"})," scripts. It handles creating, updating, loading, and deleting AT Protocol records. Writes are proxied to the caller's PDS and indexed locally."]}),"\n",(0,l.jsx)(r.h3,{id:"constructor",children:"Constructor"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local r = Record("xyz.statusphere.status", { status = "\\ud83d\\ude0a", createdAt = now() })\n'})}),"\n",(0,l.jsxs)(r.p,{children:["Creates a new record instance for the given collection. The optional second argument sets initial field values. The record's ",(0,l.jsx)(r.code,{children:"_key_type"})," is automatically set from the lexicon's ",(0,l.jsx)(r.code,{children:"key"})," definition. Default values from the schema are populated for any missing fields."]}),"\n",(0,l.jsx)(r.h3,{id:"static-methods",children:"Static methods"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'-- Save multiple records in parallel\nRecord.save_all({ record1, record2, record3 })\n\n-- Load a record from the local database by AT URI\nlocal r = Record.load("at://did:plc:abc/xyz.statusphere.status/abc123")\n-- Returns nil if not found\n\n-- Load multiple records in parallel\nlocal records = Record.load_all({ uri1, uri2 })\n-- Returns nil entries for URIs not found\n'})}),"\n",(0,l.jsx)(r.h3,{id:"instance-methods",children:"Instance methods"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'-- Save (creates or updates depending on whether _uri is set)\nr:save()\n\n-- Delete from PDS and local database\nr:delete()\n\n-- Set the record key type (tid, any, nsid, or literal:*)\nr:set_key_type("tid")\n\n-- Set a specific record key\nr:set_rkey("my-key")\n\n-- Auto-generate a record key based on _key_type\nlocal key = r:generate_rkey()\n'})}),"\n",(0,l.jsx)(r.p,{children:(0,l.jsxs)(r.strong,{children:["Key type behavior for ",(0,l.jsx)(r.code,{children:"generate_rkey()"}),":"]})}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Key type"}),(0,l.jsx)(r.th,{children:"Generated rkey"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"tid"})}),(0,l.jsx)(r.td,{children:"Sortable timestamp-based ID"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"any"})}),(0,l.jsxs)(r.td,{children:["Same as ",(0,l.jsx)(r.code,{children:"tid"})]})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"literal:value"})}),(0,l.jsx)(r.td,{children:"The literal value after the colon"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"nsid"})}),(0,l.jsxs)(r.td,{children:["Error \u2014 use ",(0,l.jsx)(r.code,{children:"set_rkey()"})," instead"]})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"instance-fields",children:"Instance fields"}),"\n",(0,l.jsx)(r.p,{children:"These fields are set automatically and are read-only (writes raise an error):"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Field"}),(0,l.jsx)(r.th,{children:"Type"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_uri"})}),(0,l.jsx)(r.td,{children:"string?"}),(0,l.jsxs)(r.td,{children:["AT URI \u2014 set after ",(0,l.jsx)(r.code,{children:"save()"}),", cleared after ",(0,l.jsx)(r.code,{children:"delete()"})]})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_cid"})}),(0,l.jsx)(r.td,{children:"string?"}),(0,l.jsxs)(r.td,{children:["Content hash \u2014 set after ",(0,l.jsx)(r.code,{children:"save()"}),", cleared after ",(0,l.jsx)(r.code,{children:"delete()"})]})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_key_type"})}),(0,l.jsx)(r.td,{children:"string?"}),(0,l.jsx)(r.td,{children:"Record key type from the lexicon definition"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_rkey"})}),(0,l.jsx)(r.td,{children:"string?"}),(0,l.jsxs)(r.td,{children:["Record key \u2014 set via ",(0,l.jsx)(r.code,{children:"set_rkey()"})," or ",(0,l.jsx)(r.code,{children:"generate_rkey()"})]})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_collection"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Collection NSID (always set)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_schema"})}),(0,l.jsx)(r.td,{children:"table?"}),(0,l.jsx)(r.td,{children:"Schema definition from the lexicon (used for validation)"})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"schema-validation",children:"Schema validation"}),"\n",(0,l.jsx)(r.p,{children:"When a record has a schema (loaded from the lexicon):"}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.strong,{children:"On save:"})," required fields are checked, and missing required fields raise an error"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.strong,{children:"On construction:"})," default values from schema properties are auto-populated"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.strong,{children:"On save:"})," only fields defined in the schema's ",(0,l.jsx)(r.code,{children:"properties"})," are sent to the PDS"]}),"\n"]}),"\n",(0,l.jsx)(r.h3,{id:"save-behavior",children:"Save behavior"}),"\n",(0,l.jsxs)(r.p,{children:[(0,l.jsx)(r.code,{children:"r:save()"})," auto-detects create vs update:"]}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:["If ",(0,l.jsx)(r.code,{children:"_uri"})," is nil \u2192 calls ",(0,l.jsx)(r.code,{children:"createRecord"})," on the PDS"]}),"\n",(0,l.jsxs)(r.li,{children:["If ",(0,l.jsx)(r.code,{children:"_uri"})," is set \u2192 calls ",(0,l.jsx)(r.code,{children:"putRecord"})," on the PDS"]}),"\n"]}),"\n",(0,l.jsxs)(r.p,{children:["After a successful save, ",(0,l.jsx)(r.code,{children:"_uri"})," and ",(0,l.jsx)(r.code,{children:"_cid"})," are updated on the record instance."]}),"\n",(0,l.jsx)(r.h2,{id:"database-api",children:"Database API"}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.code,{children:"db"})," table provides read-only access to indexed records. Available in both queries and procedures."]}),"\n",(0,l.jsx)(r.h3,{id:"dbquery",children:"db.query"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local result = db.query({\n collection = "xyz.statusphere.status", -- required\n did = "did:plc:abc", -- optional: filter by DID\n limit = 20, -- optional: max 100, default 20\n offset = 0, -- optional: for pagination\n sort = "name", -- optional: field to sort by, default "indexed_at"\n sortDirection = "asc", -- optional: "asc" or "desc", default "desc"\n})\n\n-- result.records \u2014 array of record tables (each includes a "uri" field)\n-- result.cursor \u2014 present when more records exist\n'})}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.code,{children:"sort"})," field can be a top-level column (",(0,l.jsx)(r.code,{children:"indexed_at"}),", ",(0,l.jsx)(r.code,{children:"did"}),", ",(0,l.jsx)(r.code,{children:"uri"}),") or any field inside the record's ",(0,l.jsx)(r.code,{children:"value"})," object (e.g. ",(0,l.jsx)(r.code,{children:"name"}),", ",(0,l.jsx)(r.code,{children:"createdAt"}),"). Field names must contain only alphanumeric characters and underscores."]}),"\n",(0,l.jsx)(r.h3,{id:"dbget",children:"db.get"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local record = db.get("at://did:plc:abc/xyz.statusphere.status/abc123")\n-- Returns the record table or nil\n-- The returned table includes a "uri" field\n'})}),"\n",(0,l.jsx)(r.h3,{id:"dbsearch",children:"db.search"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local result = db.search({\n collection = "xyz.statusphere.status", -- required\n field = "displayName", -- required: record field to search\n query = "alice", -- required: search term\n limit = 10, -- optional: max 100, default 10\n})\n\n-- result.records \u2014 array of matching records, ranked by relevance:\n-- exact match > prefix match > contains match, then alphabetical\n'})}),"\n",(0,l.jsx)(r.h3,{id:"dbbacklinks",children:"db.backlinks"}),"\n",(0,l.jsx)(r.p,{children:"Find records that reference a given AT URI anywhere in their data. Useful for finding likes on a post, replies to a thread, or any record that links to another."}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local result = db.backlinks({\n collection = "xyz.statusphere.status", -- required\n uri = "at://did:plc:abc/xyz.statusphere.status/foo", -- required: the URI to find references to\n did = "did:plc:abc", -- optional: filter by DID\n limit = 20, -- optional: max 100, default 20\n offset = 0, -- optional: for pagination\n})\n\n-- result.records \u2014 array of records whose data contains the given URI\n-- result.cursor \u2014 present when more records exist\n'})}),"\n",(0,l.jsxs)(r.p,{children:["The search checks the full record data, so it works regardless of which field holds the reference (",(0,l.jsx)(r.code,{children:"subject"}),", ",(0,l.jsx)(r.code,{children:"parent"}),", ",(0,l.jsx)(r.code,{children:"reply.root"}),", etc.)."]}),"\n",(0,l.jsx)(r.h3,{id:"dbcount",children:"db.count"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local n = db.count("xyz.statusphere.status")\nlocal n = db.count("xyz.statusphere.status", "did:plc:abc") -- filter by DID\n'})}),"\n",(0,l.jsx)(r.h3,{id:"dbraw",children:"db.raw"}),"\n",(0,l.jsxs)(r.p,{children:["Run a raw SQL query against the database. Only ",(0,l.jsx)(r.code,{children:"SELECT"})," statements are allowed."]}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local rows = db.raw(\n "SELECT uri, did, record FROM records WHERE collection = $1 AND did = $2 LIMIT $3",\n { "xyz.statusphere.status", "did:plc:abc", 10 }\n)\n\nfor _, row in ipairs(rows) do\n -- row.uri, row.did, row.record (JSONB is returned as a Lua table)\nend\n'})}),"\n",(0,l.jsxs)(r.p,{children:["Parameters are passed as an array and bound to ",(0,l.jsx)(r.code,{children:"$1"}),", ",(0,l.jsx)(r.code,{children:"$2"}),", etc. Supported parameter types: strings, integers, numbers, booleans, and nil."]}),"\n",(0,l.jsx)(r.p,{children:"Column types are mapped automatically:"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Postgres type"}),(0,l.jsx)(r.th,{children:"Lua type"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsxs)(r.td,{children:[(0,l.jsx)(r.code,{children:"TEXT"}),", ",(0,l.jsx)(r.code,{children:"VARCHAR"})]}),(0,l.jsx)(r.td,{children:"string"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsxs)(r.td,{children:[(0,l.jsx)(r.code,{children:"INT4"}),", ",(0,l.jsx)(r.code,{children:"INT8"})]}),(0,l.jsx)(r.td,{children:"integer"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsxs)(r.td,{children:[(0,l.jsx)(r.code,{children:"FLOAT4"}),", ",(0,l.jsx)(r.code,{children:"FLOAT8"})]}),(0,l.jsx)(r.td,{children:"number"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"BOOL"})}),(0,l.jsx)(r.td,{children:"boolean"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsxs)(r.td,{children:[(0,l.jsx)(r.code,{children:"JSON"}),", ",(0,l.jsx)(r.code,{children:"JSONB"})]}),(0,l.jsx)(r.td,{children:"table"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"TIMESTAMPTZ"})}),(0,l.jsx)(r.td,{children:"string (ISO 8601)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:"Other"}),(0,l.jsx)(r.td,{children:"string (fallback)"})]})]})]}),"\n",(0,l.jsx)(r.h2,{id:"http-api",children:"HTTP API"}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.code,{children:"http"})," table provides async HTTP client functions. Available in both queries and procedures."]}),"\n",(0,l.jsx)(r.h3,{id:"methods",children:"Methods"}),"\n",(0,l.jsxs)(r.p,{children:["All methods take a URL and an optional options table, and return a ",(0,l.jsx)(r.a,{href:"#response",children:"response table"}),"."]}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:"http.get(url, opts?)\nhttp.post(url, opts?)\nhttp.put(url, opts?)\nhttp.patch(url, opts?)\nhttp.delete(url, opts?)\nhttp.head(url, opts?)\n"})}),"\n",(0,l.jsx)(r.h3,{id:"options",children:"Options"}),"\n",(0,l.jsx)(r.p,{children:"The optional second argument is a table with:"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Field"}),(0,l.jsx)(r.th,{children:"Type"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"headers"})}),(0,l.jsx)(r.td,{children:"table"}),(0,l.jsx)(r.td,{children:"Request headers as key-value string pairs"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"body"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Request body (ignored for GET and HEAD)"})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"response",children:"Response"}),"\n",(0,l.jsx)(r.p,{children:"Every method returns a table with:"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Field"}),(0,l.jsx)(r.th,{children:"Type"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"status"})}),(0,l.jsx)(r.td,{children:"integer"}),(0,l.jsx)(r.td,{children:"HTTP status code"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"body"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Response body text (empty string for HEAD)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"headers"})}),(0,l.jsx)(r.td,{children:"table"}),(0,l.jsx)(r.td,{children:"Response headers as key-value pairs (lowercase keys)"})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"examples",children:"Examples"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'-- Simple GET\nlocal resp = http.get("https://api.example.com/data")\n-- resp.status = 200, resp.body = "...", resp.headers["content-type"] = "application/json"\n\n-- GET with custom headers\nlocal resp = http.get("https://api.example.com/data", {\n headers = { ["authorization"] = "Bearer token123" }\n})\n\n-- POST with JSON body\nlocal resp = http.post("https://api.example.com/hook", {\n body = \'{"key": "value"}\',\n headers = { ["content-type"] = "application/json" }\n})\n\n-- PUT, PATCH, DELETE, HEAD follow the same pattern\nlocal resp = http.put(url, { body = data, headers = { ... } })\nlocal resp = http.patch(url, { body = data, headers = { ... } })\nlocal resp = http.delete(url, { headers = { ... } })\nlocal resp = http.head(url)\n'})}),"\n",(0,l.jsx)(r.h2,{id:"json-api",children:"JSON API"}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.code,{children:"json"})," global provides JSON serialization and deserialization. Available in queries, procedures, and ",(0,l.jsx)(r.a,{href:"/guides/index-hooks",children:"index hooks"}),"."]}),"\n",(0,l.jsx)(r.h3,{id:"jsonencode",children:"json.encode"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local str = json.encode({ key = "value", items = { 1, 2, 3 } })\n-- \'{"key":"value","items":[1,2,3]}\'\n'})}),"\n",(0,l.jsx)(r.p,{children:"Converts a Lua table to a JSON string."}),"\n",(0,l.jsx)(r.h3,{id:"jsondecode",children:"json.decode"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local tbl = json.decode(\'{"key": "value"}\')\n-- tbl.key == "value"\n'})}),"\n",(0,l.jsx)(r.p,{children:"Parses a JSON string into a Lua table. Returns an error if the input is not valid JSON."}),"\n",(0,l.jsx)(r.h2,{id:"standard-libraries",children:"Standard libraries"}),"\n",(0,l.jsx)(r.p,{children:"The following Lua 5.4 standard library modules are available:"}),"\n",(0,l.jsxs)(n,{children:[(0,l.jsx)("summary",{children:(0,l.jsx)(r.p,{children:(0,l.jsx)(r.code,{children:"string"})})}),(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.byte",children:(0,l.jsx)(r.code,{children:"byte"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.char",children:(0,l.jsx)(r.code,{children:"char"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.find",children:(0,l.jsx)(r.code,{children:"find"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.format",children:(0,l.jsx)(r.code,{children:"format"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.gmatch",children:(0,l.jsx)(r.code,{children:"gmatch"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.gsub",children:(0,l.jsx)(r.code,{children:"gsub"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.len",children:(0,l.jsx)(r.code,{children:"len"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.lower",children:(0,l.jsx)(r.code,{children:"lower"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.match",children:(0,l.jsx)(r.code,{children:"match"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.rep",children:(0,l.jsx)(r.code,{children:"rep"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.reverse",children:(0,l.jsx)(r.code,{children:"reverse"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.sub",children:(0,l.jsx)(r.code,{children:"sub"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.upper",children:(0,l.jsx)(r.code,{children:"upper"})})}),"\n"]})]}),"\n",(0,l.jsxs)(n,{children:[(0,l.jsx)("summary",{children:(0,l.jsx)(r.p,{children:(0,l.jsx)(r.code,{children:"table"})})}),(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.concat",children:(0,l.jsx)(r.code,{children:"concat"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.insert",children:(0,l.jsx)(r.code,{children:"insert"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.remove",children:(0,l.jsx)(r.code,{children:"remove"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.sort",children:(0,l.jsx)(r.code,{children:"sort"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.unpack",children:(0,l.jsx)(r.code,{children:"unpack"})})}),"\n"]})]}),"\n",(0,l.jsxs)(n,{children:[(0,l.jsx)("summary",{children:(0,l.jsx)(r.p,{children:(0,l.jsx)(r.code,{children:"math"})})}),(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.abs",children:(0,l.jsx)(r.code,{children:"abs"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.ceil",children:(0,l.jsx)(r.code,{children:"ceil"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.floor",children:(0,l.jsx)(r.code,{children:"floor"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.max",children:(0,l.jsx)(r.code,{children:"max"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.min",children:(0,l.jsx)(r.code,{children:"min"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.random",children:(0,l.jsx)(r.code,{children:"random"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.sqrt",children:(0,l.jsx)(r.code,{children:"sqrt"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.huge",children:(0,l.jsx)(r.code,{children:"huge"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.pi",children:(0,l.jsx)(r.code,{children:"pi"})})}),"\n"]})]}),"\n",(0,l.jsxs)(n,{children:[(0,l.jsx)("summary",{children:(0,l.jsx)(r.p,{children:"Standard builtins"})}),(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-print",children:(0,l.jsx)(r.code,{children:"print"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-tostring",children:(0,l.jsx)(r.code,{children:"tostring"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-tonumber",children:(0,l.jsx)(r.code,{children:"tonumber"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-type",children:(0,l.jsx)(r.code,{children:"type"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-pairs",children:(0,l.jsx)(r.code,{children:"pairs"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-ipairs",children:(0,l.jsx)(r.code,{children:"ipairs"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-next",children:(0,l.jsx)(r.code,{children:"next"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-select",children:(0,l.jsx)(r.code,{children:"select"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.unpack",children:(0,l.jsx)(r.code,{children:"unpack"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-error",children:(0,l.jsx)(r.code,{children:"error"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-pcall",children:(0,l.jsx)(r.code,{children:"pcall"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-xpcall",children:(0,l.jsx)(r.code,{children:"xpcall"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-assert",children:(0,l.jsx)(r.code,{children:"assert"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-setmetatable",children:(0,l.jsx)(r.code,{children:"setmetatable"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-getmetatable",children:(0,l.jsx)(r.code,{children:"getmetatable"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-rawget",children:(0,l.jsx)(r.code,{children:"rawget"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-rawset",children:(0,l.jsx)(r.code,{children:"rawset"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-rawequal",children:(0,l.jsx)(r.code,{children:"rawequal"})})}),"\n"]})]}),"\n",(0,l.jsx)(r.h2,{id:"debugging",children:"Debugging"}),"\n",(0,l.jsx)(r.h3,{id:"logging",children:"Logging"}),"\n",(0,l.jsxs)(r.p,{children:["Use ",(0,l.jsx)(r.code,{children:"log()"})," to trace script execution. Output appears in the server logs at ",(0,l.jsx)(r.strong,{children:"debug"})," level with the field ",(0,l.jsx)(r.code,{children:"lua_log"}),":"]}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'function handle()\n log("handle called with params: " .. tostring(params.limit))\n local result = db.query({ collection = collection, limit = params.limit })\n log("query returned " .. #result.records .. " records")\n return result\nend\n'})}),"\n",(0,l.jsxs)(r.p,{children:["To see log output, make sure your ",(0,l.jsx)(r.code,{children:"RUST_LOG"})," environment variable includes debug level for HappyView (the default ",(0,l.jsx)(r.code,{children:"happyview=debug"})," works). See ",(0,l.jsx)(r.a,{href:"/getting-started/configuration",children:"Configuration"}),"."]}),"\n",(0,l.jsx)(r.h3,{id:"error-messages",children:"Error messages"}),"\n",(0,l.jsxs)(r.p,{children:["When a script fails, the client receives a generic ",(0,l.jsx)(r.code,{children:"500"})," response:"]}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.code,{children:'{"error": "script execution failed"}'}),": covers syntax errors, runtime errors, missing ",(0,l.jsx)(r.code,{children:"handle()"})," function, and errors raised with ",(0,l.jsx)(r.code,{children:"error()"})]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.code,{children:'{"error": "script exceeded execution time limit"}'}),": the script hit the 1,000,000 instruction limit"]}),"\n"]}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.strong,{children:"full error message"})," is logged server-side at error level. Check the server logs to see the actual Lua error, including line numbers and stack traces."]}),"\n",(0,l.jsx)(r.h3,{id:"common-mistakes",children:"Common mistakes"}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsxs)(r.strong,{children:["Missing ",(0,l.jsx)(r.code,{children:"handle()"})," function"]}),": Every script must define a global ",(0,l.jsx)(r.code,{children:"handle()"}),' function. If it\'s missing or misspelled, the script fails silently with "script execution failed".']}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsxs)(r.strong,{children:["Calling ",(0,l.jsx)(r.code,{children:"error()"})," for expected conditions"]}),": Lua's ",(0,l.jsx)(r.code,{children:"error()"}),' triggers a 500 response. For expected conditions like "record not found", return a structured error response instead: ',(0,l.jsx)(r.code,{children:'return { error = "not found" }'}),"."]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.strong,{children:"Infinite loops"}),": The sandbox enforces a 1,000,000 instruction limit. If your script processes large data sets, paginate with ",(0,l.jsx)(r.code,{children:"db.query()"})," limits instead of loading everything at once."]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsxs)(r.strong,{children:["Forgetting ",(0,l.jsx)(r.code,{children:"params"})," values are strings"]}),": All query string parameters arrive as strings. Use ",(0,l.jsx)(r.code,{children:"tonumber(params.limit)"})," if you need a number."]}),"\n"]}),"\n",(0,l.jsx)(r.h2,{id:"example-scripts",children:"Example scripts"}),"\n",(0,l.jsx)(r.p,{children:"See the example script references for complete, ready-to-use scripts:"}),"\n",(0,l.jsx)(r.p,{children:(0,l.jsx)(r.strong,{children:"Queries:"})}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/get-record",children:"Get a record"})," \u2014 fetch a single record by AT URI"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/paginated-list",children:"Paginated list"})," \u2014 list records with cursor-based pagination and DID filtering"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/list-or-fetch",children:"List or fetch"})," \u2014 combined single-record lookup and paginated listing"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/expanded-query",children:"Expanded query"})," \u2014 list statuses with user profiles in a single response"]}),"\n"]}),"\n",(0,l.jsx)(r.p,{children:(0,l.jsx)(r.strong,{children:"Procedures:"})}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/create-record",children:"Create a record"})," \u2014 simple write that saves input as a record"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/upsert-record",children:"Upsert a record"})," \u2014 create or update using a deterministic rkey"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/update-or-delete",children:"Update or delete"})," \u2014 single endpoint handling create, update, and delete"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/batch-save",children:"Batch save"})," \u2014 create multiple records in parallel with ",(0,l.jsx)(r.code,{children:"Record.save_all()"})]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/sidecar-records",children:"Sidecar records"})," \u2014 create linked records across collections with a shared rkey"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/cascading-delete",children:"Cascading delete"})," \u2014 delete a record and all related records"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/complex-mutations",children:"Complex mutations"})," \u2014 load, transform, and save a record with multiple field changes"]}),"\n"]}),"\n",(0,l.jsx)(r.p,{children:(0,l.jsx)(r.strong,{children:"Index Hooks:"})}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/algolia-sync",children:"Algolia sync"})," \u2014 push records to an Algolia search index on create/update/delete"]}),"\n"]}),"\n",(0,l.jsx)(r.h2,{id:"next-steps",children:"Next steps"}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/guides/index-hooks",children:"Index Hooks"}),": React to record changes from the network in real time"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/guides/lexicons",children:"Lexicons"}),": Understand how record, query, and procedure lexicons work together"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/xrpc-api",children:"XRPC API"}),": See how endpoints behave with and without Lua scripts"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/getting-started/dashboard#lua-editor",children:"Dashboard"}),": Use the web editor with context-aware completions"]}),"\n"]})]})}function o(e={}){const{wrapper:r}={...(0,d.R)(),...e.components};return r?(0,l.jsx)(r,{...e,children:(0,l.jsx)(h,{...e})}):h(e)}},8453(e,r,n){n.d(r,{R:()=>i,x:()=>t});var s=n(6540);const l={},d=s.createContext(l);function i(e){const r=s.useContext(d);return s.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function t(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(l):e.components||l:i(e.components),s.createElement(d.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/920ba7ef.b802a833.js b/assets/js/920ba7ef.b802a833.js new file mode 100644 index 0000000..a853e92 --- /dev/null +++ b/assets/js/920ba7ef.b802a833.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkhappyview_docs=self.webpackChunkhappyview_docs||[]).push([[5953],{1189(e,r,n){n.r(r),n.d(r,{assets:()=>a,contentTitle:()=>t,default:()=>o,frontMatter:()=>i,metadata:()=>s,toc:()=>c});const s=JSON.parse('{"id":"guides/scripting","title":"Lua Scripting","description":"Without Lua scripts, HappyView\'s query endpoints return raw records and procedure endpoints proxy simple creates and updates. Lua scripts let you go much further:","source":"@site/docs/guides/scripting.md","sourceDirName":"guides","slug":"/guides/scripting","permalink":"/guides/scripting","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{},"sidebar":"docs","previous":{"title":"Lexicons","permalink":"/guides/lexicons"},"next":{"title":"Index Hooks","permalink":"/guides/index-hooks"}}');var l=n(4848),d=n(8453);const i={},t="Lua Scripting",a={},c=[{value:"Script structure",id:"script-structure",level:2},{value:"Sandbox",id:"sandbox",level:2},{value:"Context globals",id:"context-globals",level:2},{value:"Procedure globals",id:"procedure-globals",level:3},{value:"Query globals",id:"query-globals",level:3},{value:"Utility globals",id:"utility-globals",level:2},{value:"toarray",id:"toarray",level:3},{value:"Record API",id:"record-api",level:2},{value:"Constructor",id:"constructor",level:3},{value:"Static methods",id:"static-methods",level:3},{value:"Instance methods",id:"instance-methods",level:3},{value:"Instance fields",id:"instance-fields",level:3},{value:"Schema validation",id:"schema-validation",level:3},{value:"Save behavior",id:"save-behavior",level:3},{value:"Database API",id:"database-api",level:2},{value:"db.query",id:"dbquery",level:3},{value:"db.get",id:"dbget",level:3},{value:"db.search",id:"dbsearch",level:3},{value:"db.backlinks",id:"dbbacklinks",level:3},{value:"db.count",id:"dbcount",level:3},{value:"db.raw",id:"dbraw",level:3},{value:"HTTP API",id:"http-api",level:2},{value:"Methods",id:"methods",level:3},{value:"Options",id:"options",level:3},{value:"Response",id:"response",level:3},{value:"Examples",id:"examples",level:3},{value:"JSON API",id:"json-api",level:2},{value:"json.encode",id:"jsonencode",level:3},{value:"json.decode",id:"jsondecode",level:3},{value:"Standard libraries",id:"standard-libraries",level:2},{value:"Debugging",id:"debugging",level:2},{value:"Logging",id:"logging",level:3},{value:"Error messages",id:"error-messages",level:3},{value:"Common mistakes",id:"common-mistakes",level:3},{value:"Example scripts",id:"example-scripts",level:2},{value:"Next steps",id:"next-steps",level:2}];function h(e){const r={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components},{Details:n}=r;return n||function(e,r){throw new Error("Expected "+(r?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(r.header,{children:(0,l.jsx)(r.h1,{id:"lua-scripting",children:"Lua Scripting"})}),"\n",(0,l.jsx)(r.p,{children:"Without Lua scripts, HappyView's query endpoints return raw records and procedure endpoints proxy simple creates and updates. Lua scripts let you go much further:"}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsx)(r.li,{children:"Add filtering logic"}),"\n",(0,l.jsx)(r.li,{children:"Transform responses"}),"\n",(0,l.jsx)(r.li,{children:"Validate input"}),"\n",(0,l.jsx)(r.li,{children:"Compose multi-record operations"}),"\n",(0,l.jsx)(r.li,{children:"Build entirely custom behavior"}),"\n"]}),"\n",(0,l.jsxs)(r.p,{children:["Scripts are attached to query and procedure lexicons and run in a sandboxed Lua VM with access to the ",(0,l.jsx)(r.a,{href:"#record-api",children:"Record API"}),", a ",(0,l.jsx)(r.a,{href:"#database-api",children:"database API"}),", an ",(0,l.jsx)(r.a,{href:"#http-api",children:"HTTP client API"}),", a ",(0,l.jsx)(r.a,{href:"#json-api",children:"JSON API"}),", and a set of ",(0,l.jsx)(r.a,{href:"#context-globals",children:"context globals"}),"."]}),"\n",(0,l.jsxs)(r.p,{children:["For scripts that react to record changes from the network (rather than XRPC requests), see ",(0,l.jsx)(r.a,{href:"/guides/index-hooks",children:"Index Hooks"}),"."]}),"\n",(0,l.jsx)(r.h2,{id:"script-structure",children:"Script structure"}),"\n",(0,l.jsxs)(r.p,{children:["Every script must define a ",(0,l.jsx)(r.code,{children:"handle()"})," function. HappyView calls it when the XRPC endpoint is hit and returns its result as JSON to the client."]}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'function handle()\n -- your logic here\n return { key = "value" }\nend\n'})}),"\n",(0,l.jsxs)(r.p,{children:["You can define helper functions and variables outside ",(0,l.jsx)(r.code,{children:"handle()"}),". They're evaluated once when the script loads, then ",(0,l.jsx)(r.code,{children:"handle()"})," is called per request."]}),"\n",(0,l.jsx)(r.h2,{id:"sandbox",children:"Sandbox"}),"\n",(0,l.jsxs)(r.p,{children:["Scripts run in a restricted environment. The following standard Lua modules are ",(0,l.jsx)(r.strong,{children:"removed"})," and unavailable:"]}),"\n",(0,l.jsxs)(r.p,{children:[(0,l.jsx)(r.code,{children:"os"}),", ",(0,l.jsx)(r.code,{children:"io"}),", ",(0,l.jsx)(r.code,{children:"debug"}),", ",(0,l.jsx)(r.code,{children:"package"}),", ",(0,l.jsx)(r.code,{children:"require"}),", ",(0,l.jsx)(r.code,{children:"dofile"}),", ",(0,l.jsx)(r.code,{children:"loadfile"}),", ",(0,l.jsx)(r.code,{children:"load"}),", ",(0,l.jsx)(r.code,{children:"collectgarbage"})]}),"\n",(0,l.jsx)(r.p,{children:"An instruction limit of 1,000,000 prevents infinite loops. Exceeding it terminates the script with an error."}),"\n",(0,l.jsx)(r.h2,{id:"context-globals",children:"Context globals"}),"\n",(0,l.jsxs)(r.p,{children:["These globals are set automatically before ",(0,l.jsx)(r.code,{children:"handle()"})," is called."]}),"\n",(0,l.jsx)(r.h3,{id:"procedure-globals",children:"Procedure globals"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Global"}),(0,l.jsx)(r.th,{children:"Type"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"method"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsxs)(r.td,{children:["The XRPC method name (e.g. ",(0,l.jsx)(r.code,{children:"xyz.statusphere.setStatus"}),")"]})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"input"})}),(0,l.jsx)(r.td,{children:"table"}),(0,l.jsx)(r.td,{children:"Parsed JSON request body"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"caller_did"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"DID of the authenticated user"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"collection"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Target collection NSID"})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"query-globals",children:"Query globals"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Global"}),(0,l.jsx)(r.th,{children:"Type"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"method"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"The XRPC method name"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"params"})}),(0,l.jsx)(r.td,{children:"table"}),(0,l.jsx)(r.td,{children:"Query string parameters (all values are strings)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"collection"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Target collection NSID"})]})]})]}),"\n",(0,l.jsxs)(r.p,{children:["Queries are unauthenticated: there is no ",(0,l.jsx)(r.code,{children:"caller_did"})," or ",(0,l.jsx)(r.code,{children:"input"}),"."]}),"\n",(0,l.jsx)(r.h2,{id:"utility-globals",children:"Utility globals"}),"\n",(0,l.jsx)(r.p,{children:"Available in both queries and procedures:"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Function"}),(0,l.jsx)(r.th,{children:"Returns"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"now()"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Current UTC timestamp in ISO 8601 format"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"log(message)"})}),(0,l.jsx)(r.td,{children:"\u2014"}),(0,l.jsx)(r.td,{children:"Log a message (appears in server logs at debug level)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"TID()"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Generate a fresh AT Protocol TID (13-character sortable identifier)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"toarray(table)"})}),(0,l.jsx)(r.td,{children:"table"}),(0,l.jsxs)(r.td,{children:["Mark a table as a JSON array for serialization (see ",(0,l.jsx)(r.a,{href:"#toarray",children:"below"}),")"]})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"toarray",children:"toarray"}),"\n",(0,l.jsxs)(r.p,{children:["Lua tables don't distinguish between arrays and objects. When a table is serialized to JSON, an empty table ",(0,l.jsx)(r.code,{children:"{}"})," becomes a JSON object ",(0,l.jsx)(r.code,{children:"{}"})," instead of an array ",(0,l.jsx)(r.code,{children:"[]"}),". The ",(0,l.jsx)(r.code,{children:"toarray()"})," function marks a table so it always serializes as a JSON array \u2014 even when empty."]}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'return { items = toarray(results) }\n-- With results: [{"name": "a"}, {"name": "b"}]\n-- Without results: {"items": []} (not {"items": {}})\n'})}),"\n",(0,l.jsxs)(r.p,{children:["You don't need ",(0,l.jsx)(r.code,{children:"toarray()"})," on results from ",(0,l.jsx)(r.code,{children:"db.query"}),", ",(0,l.jsx)(r.code,{children:"db.search"}),", ",(0,l.jsx)(r.code,{children:"db.backlinks"}),", or ",(0,l.jsx)(r.code,{children:"db.raw"})," \u2014 those already return properly marked arrays. Use it when you build a table yourself with ",(0,l.jsx)(r.code,{children:"table.insert()"}),"."]}),"\n",(0,l.jsx)(r.h2,{id:"record-api",children:"Record API"}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.code,{children:"Record"})," API is only available in ",(0,l.jsx)(r.strong,{children:"procedure"})," scripts. It handles creating, updating, loading, and deleting AT Protocol records. Writes are proxied to the caller's PDS and indexed locally."]}),"\n",(0,l.jsx)(r.h3,{id:"constructor",children:"Constructor"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local r = Record("xyz.statusphere.status", { status = "\\ud83d\\ude0a", createdAt = now() })\n'})}),"\n",(0,l.jsxs)(r.p,{children:["Creates a new record instance for the given collection. The optional second argument sets initial field values. The record's ",(0,l.jsx)(r.code,{children:"_key_type"})," is automatically set from the lexicon's ",(0,l.jsx)(r.code,{children:"key"})," definition. Default values from the schema are populated for any missing fields."]}),"\n",(0,l.jsx)(r.h3,{id:"static-methods",children:"Static methods"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'-- Save multiple records in parallel\nRecord.save_all({ record1, record2, record3 })\n\n-- Load a record from the local database by AT URI\nlocal r = Record.load("at://did:plc:abc/xyz.statusphere.status/abc123")\n-- Returns nil if not found\n\n-- Load multiple records in parallel\nlocal records = Record.load_all({ uri1, uri2 })\n-- Returns nil entries for URIs not found\n'})}),"\n",(0,l.jsx)(r.h3,{id:"instance-methods",children:"Instance methods"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'-- Save (creates or updates depending on whether _uri is set)\nr:save()\n\n-- Delete from PDS and local database\nr:delete()\n\n-- Set the record key type (tid, any, nsid, or literal:*)\nr:set_key_type("tid")\n\n-- Set a specific record key\nr:set_rkey("my-key")\n\n-- Auto-generate a record key based on _key_type\nlocal key = r:generate_rkey()\n'})}),"\n",(0,l.jsx)(r.p,{children:(0,l.jsxs)(r.strong,{children:["Key type behavior for ",(0,l.jsx)(r.code,{children:"generate_rkey()"}),":"]})}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Key type"}),(0,l.jsx)(r.th,{children:"Generated rkey"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"tid"})}),(0,l.jsx)(r.td,{children:"Sortable timestamp-based ID"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"any"})}),(0,l.jsxs)(r.td,{children:["Same as ",(0,l.jsx)(r.code,{children:"tid"})]})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"literal:value"})}),(0,l.jsx)(r.td,{children:"The literal value after the colon"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"nsid"})}),(0,l.jsxs)(r.td,{children:["Error \u2014 use ",(0,l.jsx)(r.code,{children:"set_rkey()"})," instead"]})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"instance-fields",children:"Instance fields"}),"\n",(0,l.jsx)(r.p,{children:"These fields are set automatically and are read-only (writes raise an error):"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Field"}),(0,l.jsx)(r.th,{children:"Type"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_uri"})}),(0,l.jsx)(r.td,{children:"string?"}),(0,l.jsxs)(r.td,{children:["AT URI \u2014 set after ",(0,l.jsx)(r.code,{children:"save()"}),", cleared after ",(0,l.jsx)(r.code,{children:"delete()"})]})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_cid"})}),(0,l.jsx)(r.td,{children:"string?"}),(0,l.jsxs)(r.td,{children:["Content hash \u2014 set after ",(0,l.jsx)(r.code,{children:"save()"}),", cleared after ",(0,l.jsx)(r.code,{children:"delete()"})]})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_key_type"})}),(0,l.jsx)(r.td,{children:"string?"}),(0,l.jsx)(r.td,{children:"Record key type from the lexicon definition"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_rkey"})}),(0,l.jsx)(r.td,{children:"string?"}),(0,l.jsxs)(r.td,{children:["Record key \u2014 set via ",(0,l.jsx)(r.code,{children:"set_rkey()"})," or ",(0,l.jsx)(r.code,{children:"generate_rkey()"})]})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_collection"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Collection NSID (always set)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"_schema"})}),(0,l.jsx)(r.td,{children:"table?"}),(0,l.jsx)(r.td,{children:"Schema definition from the lexicon (used for validation)"})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"schema-validation",children:"Schema validation"}),"\n",(0,l.jsx)(r.p,{children:"When a record has a schema (loaded from the lexicon):"}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.strong,{children:"On save:"})," required fields are checked, and missing required fields raise an error"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.strong,{children:"On construction:"})," default values from schema properties are auto-populated"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.strong,{children:"On save:"})," only fields defined in the schema's ",(0,l.jsx)(r.code,{children:"properties"})," are sent to the PDS"]}),"\n"]}),"\n",(0,l.jsx)(r.h3,{id:"save-behavior",children:"Save behavior"}),"\n",(0,l.jsxs)(r.p,{children:[(0,l.jsx)(r.code,{children:"r:save()"})," auto-detects create vs update:"]}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:["If ",(0,l.jsx)(r.code,{children:"_uri"})," is nil \u2192 calls ",(0,l.jsx)(r.code,{children:"createRecord"})," on the PDS"]}),"\n",(0,l.jsxs)(r.li,{children:["If ",(0,l.jsx)(r.code,{children:"_uri"})," is set \u2192 calls ",(0,l.jsx)(r.code,{children:"putRecord"})," on the PDS"]}),"\n"]}),"\n",(0,l.jsxs)(r.p,{children:["After a successful save, ",(0,l.jsx)(r.code,{children:"_uri"})," and ",(0,l.jsx)(r.code,{children:"_cid"})," are updated on the record instance."]}),"\n",(0,l.jsx)(r.h2,{id:"database-api",children:"Database API"}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.code,{children:"db"})," table provides access to the database. Available in both queries and procedures."]}),"\n",(0,l.jsx)(r.h3,{id:"dbquery",children:"db.query"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local result = db.query({\n collection = "xyz.statusphere.status", -- required\n did = "did:plc:abc", -- optional: filter by DID\n limit = 20, -- optional: max 100, default 20\n offset = 0, -- optional: for pagination\n sort = "name", -- optional: field to sort by, default "indexed_at"\n sortDirection = "asc", -- optional: "asc" or "desc", default "desc"\n})\n\n-- result.records \u2014 array of record tables (each includes a "uri" field)\n-- result.cursor \u2014 present when more records exist\n'})}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.code,{children:"sort"})," field can be a top-level column (",(0,l.jsx)(r.code,{children:"indexed_at"}),", ",(0,l.jsx)(r.code,{children:"did"}),", ",(0,l.jsx)(r.code,{children:"uri"}),") or any field inside the record's ",(0,l.jsx)(r.code,{children:"value"})," object (e.g. ",(0,l.jsx)(r.code,{children:"name"}),", ",(0,l.jsx)(r.code,{children:"createdAt"}),"). Field names must contain only alphanumeric characters and underscores."]}),"\n",(0,l.jsx)(r.h3,{id:"dbget",children:"db.get"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local record = db.get("at://did:plc:abc/xyz.statusphere.status/abc123")\n-- Returns the record table or nil\n-- The returned table includes a "uri" field\n'})}),"\n",(0,l.jsx)(r.h3,{id:"dbsearch",children:"db.search"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local result = db.search({\n collection = "xyz.statusphere.status", -- required\n field = "displayName", -- required: record field to search\n query = "alice", -- required: search term\n limit = 10, -- optional: max 100, default 10\n})\n\n-- result.records \u2014 array of matching records, ranked by relevance:\n-- exact match > prefix match > contains match, then alphabetical\n'})}),"\n",(0,l.jsx)(r.h3,{id:"dbbacklinks",children:"db.backlinks"}),"\n",(0,l.jsx)(r.p,{children:"Find records that reference a given AT URI anywhere in their data. Useful for finding likes on a post, replies to a thread, or any record that links to another."}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local result = db.backlinks({\n collection = "xyz.statusphere.status", -- required\n uri = "at://did:plc:abc/xyz.statusphere.status/foo", -- required: the URI to find references to\n did = "did:plc:abc", -- optional: filter by DID\n limit = 20, -- optional: max 100, default 20\n offset = 0, -- optional: for pagination\n})\n\n-- result.records \u2014 array of records whose data contains the given URI\n-- result.cursor \u2014 present when more records exist\n'})}),"\n",(0,l.jsxs)(r.p,{children:["The search checks the full record data, so it works regardless of which field holds the reference (",(0,l.jsx)(r.code,{children:"subject"}),", ",(0,l.jsx)(r.code,{children:"parent"}),", ",(0,l.jsx)(r.code,{children:"reply.root"}),", etc.)."]}),"\n",(0,l.jsx)(r.h3,{id:"dbcount",children:"db.count"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local n = db.count("xyz.statusphere.status")\nlocal n = db.count("xyz.statusphere.status", "did:plc:abc") -- filter by DID\n'})}),"\n",(0,l.jsx)(r.h3,{id:"dbraw",children:"db.raw"}),"\n",(0,l.jsxs)(r.p,{children:["Run a raw SQL query against the database. Supports ",(0,l.jsx)(r.code,{children:"SELECT"}),", ",(0,l.jsx)(r.code,{children:"INSERT"}),", ",(0,l.jsx)(r.code,{children:"UPDATE"}),", ",(0,l.jsx)(r.code,{children:"DELETE"}),", and ",(0,l.jsx)(r.code,{children:"CREATE TABLE"})," statements."]}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'-- Read query\nlocal rows = db.raw(\n "SELECT uri, did, record FROM records WHERE collection = $1 AND did = $2 LIMIT $3",\n { "xyz.statusphere.status", "did:plc:abc", 10 }\n)\n\nfor _, row in ipairs(rows) do\n -- row.uri, row.did, row.record (JSONB is returned as a Lua table)\nend\n\n-- Write query (returns affected rows, if any)\ndb.raw("CREATE TABLE IF NOT EXISTS my_table (id TEXT PRIMARY KEY, value TEXT NOT NULL)")\ndb.raw("INSERT INTO my_table (id, value) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET value = $2",\n { "key1", "hello" })\n'})}),"\n",(0,l.jsxs)(r.p,{children:["Parameters are passed as an array and bound to ",(0,l.jsx)(r.code,{children:"$1"}),", ",(0,l.jsx)(r.code,{children:"$2"}),", etc. Supported parameter types: strings, integers, numbers, booleans, and nil."]}),"\n",(0,l.jsx)(r.p,{children:"Column types are mapped automatically:"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Postgres type"}),(0,l.jsx)(r.th,{children:"Lua type"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsxs)(r.td,{children:[(0,l.jsx)(r.code,{children:"TEXT"}),", ",(0,l.jsx)(r.code,{children:"VARCHAR"})]}),(0,l.jsx)(r.td,{children:"string"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsxs)(r.td,{children:[(0,l.jsx)(r.code,{children:"INT4"}),", ",(0,l.jsx)(r.code,{children:"INT8"})]}),(0,l.jsx)(r.td,{children:"integer"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsxs)(r.td,{children:[(0,l.jsx)(r.code,{children:"FLOAT4"}),", ",(0,l.jsx)(r.code,{children:"FLOAT8"})]}),(0,l.jsx)(r.td,{children:"number"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"BOOL"})}),(0,l.jsx)(r.td,{children:"boolean"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsxs)(r.td,{children:[(0,l.jsx)(r.code,{children:"JSON"}),", ",(0,l.jsx)(r.code,{children:"JSONB"})]}),(0,l.jsx)(r.td,{children:"table"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"TIMESTAMPTZ"})}),(0,l.jsx)(r.td,{children:"string (ISO 8601)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:"Other"}),(0,l.jsx)(r.td,{children:"string (fallback)"})]})]})]}),"\n",(0,l.jsx)(r.h2,{id:"http-api",children:"HTTP API"}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.code,{children:"http"})," table provides async HTTP client functions. Available in both queries and procedures."]}),"\n",(0,l.jsx)(r.h3,{id:"methods",children:"Methods"}),"\n",(0,l.jsxs)(r.p,{children:["All methods take a URL and an optional options table, and return a ",(0,l.jsx)(r.a,{href:"#response",children:"response table"}),"."]}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:"http.get(url, opts?)\nhttp.post(url, opts?)\nhttp.put(url, opts?)\nhttp.patch(url, opts?)\nhttp.delete(url, opts?)\nhttp.head(url, opts?)\n"})}),"\n",(0,l.jsx)(r.h3,{id:"options",children:"Options"}),"\n",(0,l.jsx)(r.p,{children:"The optional second argument is a table with:"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Field"}),(0,l.jsx)(r.th,{children:"Type"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"headers"})}),(0,l.jsx)(r.td,{children:"table"}),(0,l.jsx)(r.td,{children:"Request headers as key-value string pairs"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"body"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Request body (ignored for GET and HEAD)"})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"response",children:"Response"}),"\n",(0,l.jsx)(r.p,{children:"Every method returns a table with:"}),"\n",(0,l.jsxs)(r.table,{children:[(0,l.jsx)(r.thead,{children:(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.th,{children:"Field"}),(0,l.jsx)(r.th,{children:"Type"}),(0,l.jsx)(r.th,{children:"Description"})]})}),(0,l.jsxs)(r.tbody,{children:[(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"status"})}),(0,l.jsx)(r.td,{children:"integer"}),(0,l.jsx)(r.td,{children:"HTTP status code"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"body"})}),(0,l.jsx)(r.td,{children:"string"}),(0,l.jsx)(r.td,{children:"Response body text (empty string for HEAD)"})]}),(0,l.jsxs)(r.tr,{children:[(0,l.jsx)(r.td,{children:(0,l.jsx)(r.code,{children:"headers"})}),(0,l.jsx)(r.td,{children:"table"}),(0,l.jsx)(r.td,{children:"Response headers as key-value pairs (lowercase keys)"})]})]})]}),"\n",(0,l.jsx)(r.h3,{id:"examples",children:"Examples"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'-- Simple GET\nlocal resp = http.get("https://api.example.com/data")\n-- resp.status = 200, resp.body = "...", resp.headers["content-type"] = "application/json"\n\n-- GET with custom headers\nlocal resp = http.get("https://api.example.com/data", {\n headers = { ["authorization"] = "Bearer token123" }\n})\n\n-- POST with JSON body\nlocal resp = http.post("https://api.example.com/hook", {\n body = \'{"key": "value"}\',\n headers = { ["content-type"] = "application/json" }\n})\n\n-- PUT, PATCH, DELETE, HEAD follow the same pattern\nlocal resp = http.put(url, { body = data, headers = { ... } })\nlocal resp = http.patch(url, { body = data, headers = { ... } })\nlocal resp = http.delete(url, { headers = { ... } })\nlocal resp = http.head(url)\n'})}),"\n",(0,l.jsx)(r.h2,{id:"json-api",children:"JSON API"}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.code,{children:"json"})," global provides JSON serialization and deserialization. Available in queries, procedures, and ",(0,l.jsx)(r.a,{href:"/guides/index-hooks",children:"index hooks"}),"."]}),"\n",(0,l.jsx)(r.h3,{id:"jsonencode",children:"json.encode"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local str = json.encode({ key = "value", items = { 1, 2, 3 } })\n-- \'{"key":"value","items":[1,2,3]}\'\n'})}),"\n",(0,l.jsx)(r.p,{children:"Converts a Lua table to a JSON string."}),"\n",(0,l.jsx)(r.h3,{id:"jsondecode",children:"json.decode"}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'local tbl = json.decode(\'{"key": "value"}\')\n-- tbl.key == "value"\n'})}),"\n",(0,l.jsx)(r.p,{children:"Parses a JSON string into a Lua table. Returns an error if the input is not valid JSON."}),"\n",(0,l.jsx)(r.h2,{id:"standard-libraries",children:"Standard libraries"}),"\n",(0,l.jsx)(r.p,{children:"The following Lua 5.4 standard library modules are available:"}),"\n",(0,l.jsxs)(n,{children:[(0,l.jsx)("summary",{children:(0,l.jsx)(r.p,{children:(0,l.jsx)(r.code,{children:"string"})})}),(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.byte",children:(0,l.jsx)(r.code,{children:"byte"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.char",children:(0,l.jsx)(r.code,{children:"char"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.find",children:(0,l.jsx)(r.code,{children:"find"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.format",children:(0,l.jsx)(r.code,{children:"format"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.gmatch",children:(0,l.jsx)(r.code,{children:"gmatch"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.gsub",children:(0,l.jsx)(r.code,{children:"gsub"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.len",children:(0,l.jsx)(r.code,{children:"len"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.lower",children:(0,l.jsx)(r.code,{children:"lower"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.match",children:(0,l.jsx)(r.code,{children:"match"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.rep",children:(0,l.jsx)(r.code,{children:"rep"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.reverse",children:(0,l.jsx)(r.code,{children:"reverse"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.sub",children:(0,l.jsx)(r.code,{children:"sub"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-string.upper",children:(0,l.jsx)(r.code,{children:"upper"})})}),"\n"]})]}),"\n",(0,l.jsxs)(n,{children:[(0,l.jsx)("summary",{children:(0,l.jsx)(r.p,{children:(0,l.jsx)(r.code,{children:"table"})})}),(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.concat",children:(0,l.jsx)(r.code,{children:"concat"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.insert",children:(0,l.jsx)(r.code,{children:"insert"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.remove",children:(0,l.jsx)(r.code,{children:"remove"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.sort",children:(0,l.jsx)(r.code,{children:"sort"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.unpack",children:(0,l.jsx)(r.code,{children:"unpack"})})}),"\n"]})]}),"\n",(0,l.jsxs)(n,{children:[(0,l.jsx)("summary",{children:(0,l.jsx)(r.p,{children:(0,l.jsx)(r.code,{children:"math"})})}),(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.abs",children:(0,l.jsx)(r.code,{children:"abs"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.ceil",children:(0,l.jsx)(r.code,{children:"ceil"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.floor",children:(0,l.jsx)(r.code,{children:"floor"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.max",children:(0,l.jsx)(r.code,{children:"max"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.min",children:(0,l.jsx)(r.code,{children:"min"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.random",children:(0,l.jsx)(r.code,{children:"random"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.sqrt",children:(0,l.jsx)(r.code,{children:"sqrt"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.huge",children:(0,l.jsx)(r.code,{children:"huge"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-math.pi",children:(0,l.jsx)(r.code,{children:"pi"})})}),"\n"]})]}),"\n",(0,l.jsxs)(n,{children:[(0,l.jsx)("summary",{children:(0,l.jsx)(r.p,{children:"Standard builtins"})}),(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-print",children:(0,l.jsx)(r.code,{children:"print"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-tostring",children:(0,l.jsx)(r.code,{children:"tostring"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-tonumber",children:(0,l.jsx)(r.code,{children:"tonumber"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-type",children:(0,l.jsx)(r.code,{children:"type"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-pairs",children:(0,l.jsx)(r.code,{children:"pairs"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-ipairs",children:(0,l.jsx)(r.code,{children:"ipairs"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-next",children:(0,l.jsx)(r.code,{children:"next"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-select",children:(0,l.jsx)(r.code,{children:"select"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-table.unpack",children:(0,l.jsx)(r.code,{children:"unpack"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-error",children:(0,l.jsx)(r.code,{children:"error"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-pcall",children:(0,l.jsx)(r.code,{children:"pcall"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-xpcall",children:(0,l.jsx)(r.code,{children:"xpcall"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-assert",children:(0,l.jsx)(r.code,{children:"assert"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-setmetatable",children:(0,l.jsx)(r.code,{children:"setmetatable"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-getmetatable",children:(0,l.jsx)(r.code,{children:"getmetatable"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-rawget",children:(0,l.jsx)(r.code,{children:"rawget"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-rawset",children:(0,l.jsx)(r.code,{children:"rawset"})})}),"\n",(0,l.jsx)(r.li,{children:(0,l.jsx)(r.a,{href:"https://lua.org/manual/5.4/manual.html#pdf-rawequal",children:(0,l.jsx)(r.code,{children:"rawequal"})})}),"\n"]})]}),"\n",(0,l.jsx)(r.h2,{id:"debugging",children:"Debugging"}),"\n",(0,l.jsx)(r.h3,{id:"logging",children:"Logging"}),"\n",(0,l.jsxs)(r.p,{children:["Use ",(0,l.jsx)(r.code,{children:"log()"})," to trace script execution. Output appears in the server logs at ",(0,l.jsx)(r.strong,{children:"debug"})," level with the field ",(0,l.jsx)(r.code,{children:"lua_log"}),":"]}),"\n",(0,l.jsx)(r.pre,{children:(0,l.jsx)(r.code,{className:"language-lua",children:'function handle()\n log("handle called with params: " .. tostring(params.limit))\n local result = db.query({ collection = collection, limit = params.limit })\n log("query returned " .. #result.records .. " records")\n return result\nend\n'})}),"\n",(0,l.jsxs)(r.p,{children:["To see log output, make sure your ",(0,l.jsx)(r.code,{children:"RUST_LOG"})," environment variable includes debug level for HappyView (the default ",(0,l.jsx)(r.code,{children:"happyview=debug"})," works). See ",(0,l.jsx)(r.a,{href:"/getting-started/configuration",children:"Configuration"}),"."]}),"\n",(0,l.jsx)(r.h3,{id:"error-messages",children:"Error messages"}),"\n",(0,l.jsxs)(r.p,{children:["When a script fails, the client receives a generic ",(0,l.jsx)(r.code,{children:"500"})," response:"]}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.code,{children:'{"error": "script execution failed"}'}),": covers syntax errors, runtime errors, missing ",(0,l.jsx)(r.code,{children:"handle()"})," function, and errors raised with ",(0,l.jsx)(r.code,{children:"error()"})]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.code,{children:'{"error": "script exceeded execution time limit"}'}),": the script hit the 1,000,000 instruction limit"]}),"\n"]}),"\n",(0,l.jsxs)(r.p,{children:["The ",(0,l.jsx)(r.strong,{children:"full error message"})," is logged server-side at error level. Check the server logs to see the actual Lua error, including line numbers and stack traces."]}),"\n",(0,l.jsx)(r.h3,{id:"common-mistakes",children:"Common mistakes"}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsxs)(r.strong,{children:["Missing ",(0,l.jsx)(r.code,{children:"handle()"})," function"]}),": Every script must define a global ",(0,l.jsx)(r.code,{children:"handle()"}),' function. If it\'s missing or misspelled, the script fails silently with "script execution failed".']}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsxs)(r.strong,{children:["Calling ",(0,l.jsx)(r.code,{children:"error()"})," for expected conditions"]}),": Lua's ",(0,l.jsx)(r.code,{children:"error()"}),' triggers a 500 response. For expected conditions like "record not found", return a structured error response instead: ',(0,l.jsx)(r.code,{children:'return { error = "not found" }'}),"."]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.strong,{children:"Infinite loops"}),": The sandbox enforces a 1,000,000 instruction limit. If your script processes large data sets, paginate with ",(0,l.jsx)(r.code,{children:"db.query()"})," limits instead of loading everything at once."]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsxs)(r.strong,{children:["Forgetting ",(0,l.jsx)(r.code,{children:"params"})," values are strings"]}),": All query string parameters arrive as strings. Use ",(0,l.jsx)(r.code,{children:"tonumber(params.limit)"})," if you need a number."]}),"\n"]}),"\n",(0,l.jsx)(r.h2,{id:"example-scripts",children:"Example scripts"}),"\n",(0,l.jsx)(r.p,{children:"See the example script references for complete, ready-to-use scripts:"}),"\n",(0,l.jsx)(r.p,{children:(0,l.jsx)(r.strong,{children:"Queries:"})}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/get-record",children:"Get a record"})," \u2014 fetch a single record by AT URI"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/paginated-list",children:"Paginated list"})," \u2014 list records with cursor-based pagination and DID filtering"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/list-or-fetch",children:"List or fetch"})," \u2014 combined single-record lookup and paginated listing"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/expanded-query",children:"Expanded query"})," \u2014 list statuses with user profiles in a single response"]}),"\n"]}),"\n",(0,l.jsx)(r.p,{children:(0,l.jsx)(r.strong,{children:"Procedures:"})}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/create-record",children:"Create a record"})," \u2014 simple write that saves input as a record"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/upsert-record",children:"Upsert a record"})," \u2014 create or update using a deterministic rkey"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/update-or-delete",children:"Update or delete"})," \u2014 single endpoint handling create, update, and delete"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/batch-save",children:"Batch save"})," \u2014 create multiple records in parallel with ",(0,l.jsx)(r.code,{children:"Record.save_all()"})]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/sidecar-records",children:"Sidecar records"})," \u2014 create linked records across collections with a shared rkey"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/cascading-delete",children:"Cascading delete"})," \u2014 delete a record and all related records"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/complex-mutations",children:"Complex mutations"})," \u2014 load, transform, and save a record with multiple field changes"]}),"\n"]}),"\n",(0,l.jsx)(r.p,{children:(0,l.jsx)(r.strong,{children:"Index Hooks:"})}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/scripts/algolia-sync",children:"Algolia sync"})," \u2014 push records to an Algolia search index on create/update/delete"]}),"\n"]}),"\n",(0,l.jsx)(r.h2,{id:"next-steps",children:"Next steps"}),"\n",(0,l.jsxs)(r.ul,{children:["\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/guides/index-hooks",children:"Index Hooks"}),": React to record changes from the network in real time"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/guides/lexicons",children:"Lexicons"}),": Understand how record, query, and procedure lexicons work together"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/reference/xrpc-api",children:"XRPC API"}),": See how endpoints behave with and without Lua scripts"]}),"\n",(0,l.jsxs)(r.li,{children:[(0,l.jsx)(r.a,{href:"/getting-started/dashboard#lua-editor",children:"Dashboard"}),": Use the web editor with context-aware completions"]}),"\n"]})]})}function o(e={}){const{wrapper:r}={...(0,d.R)(),...e.components};return r?(0,l.jsx)(r,{...e,children:(0,l.jsx)(h,{...e})}):h(e)}},8453(e,r,n){n.d(r,{R:()=>i,x:()=>t});var s=n(6540);const l={},d=s.createContext(l);function i(e){const r=s.useContext(d);return s.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function t(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(l):e.components||l:i(e.components),s.createElement(d.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/e7f37855.5017c79c.js b/assets/js/e7f37855.5017c79c.js deleted file mode 100644 index cfaa552..0000000 --- a/assets/js/e7f37855.5017c79c.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkhappyview_docs=self.webpackChunkhappyview_docs||[]).push([[9097],{2371(e,n,s){s.r(n),s.d(n,{assets:()=>a,contentTitle:()=>d,default:()=>h,frontMatter:()=>c,metadata:()=>r,toc:()=>o});const r=JSON.parse('{"id":"reference/scripts/meilisearch-sync","title":"Index Hook: Meilisearch Sync","description":"Push records to a Meilisearch search index whenever they are created, updated, or deleted on the network.","source":"@site/docs/reference/scripts/meilisearch-sync.md","sourceDirName":"reference/scripts","slug":"/reference/scripts/meilisearch-sync","permalink":"/reference/scripts/meilisearch-sync","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{},"sidebar":"docs","previous":{"title":"Algolia Sync","permalink":"/reference/scripts/algolia-sync"},"next":{"title":"Glossary","permalink":"/reference/glossary"}}');var i=s(4848),t=s(8453);const c={},d="Index Hook: Meilisearch Sync",a={},o=[{value:"How it works",id:"how-it-works",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Use case",id:"use-case",level:2}];function l(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"index-hook-meilisearch-sync",children:"Index Hook: Meilisearch Sync"})}),"\n",(0,i.jsx)(n.p,{children:"Push records to a Meilisearch search index whenever they are created, updated, or deleted on the network."}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Lexicon type:"})," record (index hook)"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-lua",children:'function handle()\n local headers = {\n ["Authorization"] = "Bearer " .. env.MEILISEARCH_API_KEY,\n ["Content-Type"] = "application/json"\n }\n\n if action == "delete" then\n http.delete(env.MEILISEARCH_URL .. "/indexes/records/documents/" .. uri, {\n headers = headers\n })\n else\n http.post(env.MEILISEARCH_URL .. "/indexes/records/documents", {\n headers = headers,\n body = json.encode(toarray({\n {\n id = uri,\n collection = collection,\n did = did,\n record = record\n }\n }))\n })\n end\nend\n'})}),"\n",(0,i.jsx)(n.h2,{id:"how-it-works",children:"How it works"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["On ",(0,i.jsx)(n.strong,{children:"create"})," or ",(0,i.jsx)(n.strong,{children:"update"}),": sends a ",(0,i.jsx)(n.code,{children:"POST"})," request to Meilisearch's document API with the record data wrapped in an array. Meilisearch upserts by ",(0,i.jsx)(n.code,{children:"id"})," \u2014 if a document with the same AT URI already exists, it's replaced."]}),"\n",(0,i.jsxs)(n.li,{children:["On ",(0,i.jsx)(n.strong,{children:"delete"}),": sends a ",(0,i.jsx)(n.code,{children:"DELETE"})," request to remove the document from the index by its AT URI."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"toarray()"})," function ensures the table is encoded as a JSON array (Meilisearch expects an array of documents). See ",(0,i.jsx)(n.a,{href:"/guides/index-hooks#json-api",children:"JSON API"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["This script uses ",(0,i.jsx)(n.a,{href:"/guides/scripting",children:"script variables"})," instead of hardcoded values. Set these via the ",(0,i.jsx)(n.a,{href:"/reference/admin-api",children:"admin API"})," or dashboard:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Variable"}),(0,i.jsx)(n.th,{children:"Value"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"MEILISEARCH_URL"})}),(0,i.jsxs)(n.td,{children:["Your Meilisearch instance URL (e.g. ",(0,i.jsx)(n.code,{children:"http://meilisearch.railway.internal:7700"}),")"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"MEILISEARCH_API_KEY"})}),(0,i.jsx)(n.td,{children:"A Meilisearch API key with write permissions"})]})]})]}),"\n",(0,i.jsxs)(n.p,{children:["Script variables are stored in the ",(0,i.jsx)(n.code,{children:"script_variables"})," table and accessible as ",(0,i.jsx)(n.code,{children:"env.*"})," in Lua."]}),"\n",(0,i.jsx)(n.h2,{id:"use-case",children:"Use case"}),"\n",(0,i.jsx)(n.p,{children:"This hook keeps an external search index in sync with your indexed records in real time. Users searching through Meilisearch get results that reflect the latest state of the network without polling or scheduled jobs."}),"\n",(0,i.jsx)(n.p,{children:"Meilisearch is a good fit for self-hosted deployments \u2014 colocate it alongside HappyView (e.g. on the same Railway project) for sub-millisecond network latency."}),"\n",(0,i.jsxs)(n.p,{children:["Combine this with a ",(0,i.jsx)(n.a,{href:"/guides/scripting",children:"query script"})," that searches Meilisearch instead of the local database for a full-text search experience that goes beyond what ",(0,i.jsx)(n.code,{children:"db.search"})," offers."]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(l,{...e})}):l(e)}},8453(e,n,s){s.d(n,{R:()=>c,x:()=>d});var r=s(6540);const i={},t=r.createContext(i);function c(e){const n=r.useContext(t);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:c(e.components),r.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/e7f37855.6b9a8f63.js b/assets/js/e7f37855.6b9a8f63.js new file mode 100644 index 0000000..5816998 --- /dev/null +++ b/assets/js/e7f37855.6b9a8f63.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkhappyview_docs=self.webpackChunkhappyview_docs||[]).push([[9097],{2371(e,n,r){r.r(n),r.d(n,{assets:()=>a,contentTitle:()=>d,default:()=>h,frontMatter:()=>c,metadata:()=>s,toc:()=>o});const s=JSON.parse('{"id":"reference/scripts/meilisearch-sync","title":"Index Hook: Meilisearch Sync","description":"Push records to a Meilisearch search index whenever they are created, updated, or deleted on the network.","source":"@site/docs/reference/scripts/meilisearch-sync.md","sourceDirName":"reference/scripts","slug":"/reference/scripts/meilisearch-sync","permalink":"/reference/scripts/meilisearch-sync","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{},"sidebar":"docs","previous":{"title":"Algolia Sync","permalink":"/reference/scripts/algolia-sync"},"next":{"title":"Glossary","permalink":"/reference/glossary"}}');var i=r(4848),t=r(8453);const c={},d="Index Hook: Meilisearch Sync",a={},o=[{value:"How it works",id:"how-it-works",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Use case",id:"use-case",level:2}];function l(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"index-hook-meilisearch-sync",children:"Index Hook: Meilisearch Sync"})}),"\n",(0,i.jsx)(n.p,{children:"Push records to a Meilisearch search index whenever they are created, updated, or deleted on the network."}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Lexicon type:"})," record (index hook)"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-lua",children:'function handle()\n local headers = {\n ["Authorization"] = "Bearer " .. env.MEILISEARCH_API_KEY,\n ["Content-Type"] = "application/json"\n }\n\n if action == "delete" then\n http.delete(env.MEILISEARCH_URL .. "/indexes/records/documents/" .. uri, {\n headers = headers\n })\n else\n http.post(env.MEILISEARCH_URL .. "/indexes/records/documents", {\n headers = headers,\n body = json.encode(toarray({\n {\n id = uri,\n collection = collection,\n did = did,\n record = record\n }\n }))\n })\n end\n\n return record\nend\n'})}),"\n",(0,i.jsx)(n.h2,{id:"how-it-works",children:"How it works"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["On ",(0,i.jsx)(n.strong,{children:"create"})," or ",(0,i.jsx)(n.strong,{children:"update"}),": sends a ",(0,i.jsx)(n.code,{children:"POST"})," request to Meilisearch's document API with the record data wrapped in an array. Meilisearch upserts by ",(0,i.jsx)(n.code,{children:"id"})," \u2014 if a document with the same AT URI already exists, it's replaced."]}),"\n",(0,i.jsxs)(n.li,{children:["On ",(0,i.jsx)(n.strong,{children:"delete"}),": sends a ",(0,i.jsx)(n.code,{children:"DELETE"})," request to remove the document from the index by its AT URI."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"toarray()"})," function ensures the table is encoded as a JSON array (Meilisearch expects an array of documents). See ",(0,i.jsx)(n.a,{href:"/guides/index-hooks#json-api",children:"JSON API"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["This script uses ",(0,i.jsx)(n.a,{href:"/guides/scripting",children:"script variables"})," instead of hardcoded values. Set these via the ",(0,i.jsx)(n.a,{href:"/reference/admin-api",children:"admin API"})," or dashboard:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Variable"}),(0,i.jsx)(n.th,{children:"Value"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"MEILISEARCH_URL"})}),(0,i.jsxs)(n.td,{children:["Your Meilisearch instance URL (e.g. ",(0,i.jsx)(n.code,{children:"http://meilisearch.railway.internal:7700"}),")"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"MEILISEARCH_API_KEY"})}),(0,i.jsx)(n.td,{children:"A Meilisearch API key with write permissions"})]})]})]}),"\n",(0,i.jsxs)(n.p,{children:["Script variables are stored in the ",(0,i.jsx)(n.code,{children:"script_variables"})," table and accessible as ",(0,i.jsx)(n.code,{children:"env.*"})," in Lua."]}),"\n",(0,i.jsx)(n.h2,{id:"use-case",children:"Use case"}),"\n",(0,i.jsx)(n.p,{children:"This hook keeps an external search index in sync with your indexed records in real time. Users searching through Meilisearch get results that reflect the latest state of the network without polling or scheduled jobs."}),"\n",(0,i.jsx)(n.p,{children:"Meilisearch is a good fit for self-hosted deployments \u2014 colocate it alongside HappyView (e.g. on the same Railway project) for sub-millisecond network latency."}),"\n",(0,i.jsxs)(n.p,{children:["Combine this with a ",(0,i.jsx)(n.a,{href:"/guides/scripting",children:"query script"})," that searches Meilisearch instead of the local database for a full-text search experience that goes beyond what ",(0,i.jsx)(n.code,{children:"db.search"})," offers."]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(l,{...e})}):l(e)}},8453(e,n,r){r.d(n,{R:()=>c,x:()=>d});var s=r(6540);const i={},t=s.createContext(i);function c(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:c(e.components),s.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.3142db48.js b/assets/js/runtime~main.d4fb769f.js similarity index 67% rename from assets/js/runtime~main.3142db48.js rename to assets/js/runtime~main.d4fb769f.js index b8b2210..13ca912 100644 --- a/assets/js/runtime~main.3142db48.js +++ b/assets/js/runtime~main.d4fb769f.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,d,t,r,c={},f={};function b(e){var a=f[e];if(void 0!==a)return a.exports;var d=f[e]={exports:{}};return c[e].call(d.exports,d,d.exports,b),d.exports}b.m=c,e=[],b.O=(a,d,t,r)=>{if(!d){var c=1/0;for(i=0;i=r)&&Object.keys(b.O).every(e=>b.O[e](d[o]))?d.splice(o--,1):(f=!1,r0&&e[i-1][2]>r;i--)e[i]=e[i-1];e[i]=[d,t,r]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},d=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,t){if(1&t&&(e=this(e)),8&t)return e;if("object"==typeof e&&e){if(4&t&&e.__esModule)return e;if(16&t&&"function"==typeof e.then)return e}var r=Object.create(null);b.r(r);var c={};a=a||[null,d({}),d([]),d(d)];for(var f=2&t&&e;("object"==typeof f||"function"==typeof f)&&!~a.indexOf(f);f=d(f))Object.getOwnPropertyNames(f).forEach(a=>c[a]=()=>e[a]);return c.default=()=>e,b.d(r,c),r},b.d=(e,a)=>{for(var d in a)b.o(a,d)&&!b.o(e,d)&&Object.defineProperty(e,d,{enumerable:!0,get:a[d]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce((a,d)=>(b.f[d](e,a),a),[])),b.u=e=>"assets/js/"+({20:"919abb94",203:"9985d6d5",787:"280f48ec",1361:"fbd7a87c",1567:"22dd74f7",1952:"2d1b556d",1977:"2df7fd65",2007:"c985d368",2278:"23374ca6",2431:"bccc5e9b",2618:"b27c7406",3197:"5d3a5990",3993:"d2e94323",4440:"5fd2dbdb",5011:"121e86a6",5219:"3d80a749",5563:"539fb2af",5742:"aba21aa0",5953:"920ba7ef",5986:"fac68122",6087:"a77ba7fa",6438:"bde15bec",6495:"6acefe75",6590:"43880f7a",6803:"33ec3d65",7098:"a7bd4aaa",7194:"a4086a70",7520:"89877b86",7715:"009f1e98",8401:"17896441",8779:"111eee19",8865:"774696b9",8897:"a51dfc20",9048:"a94703ab",9097:"e7f37855",9178:"e8644baa",9301:"d26b9010",9492:"846656e5",9647:"5e95c892",9659:"ec125a10",9957:"f142c65c"}[e]||e)+"."+{20:"3ec12698",165:"4c5708a1",203:"2071e97d",225:"04246537",291:"e48169fe",787:"8ce2f590",1203:"4c8aeaab",1361:"e3e74927",1567:"132c49f2",1741:"e6304f1e",1746:"afdbc2a0",1903:"c19e32df",1952:"316fda02",1977:"5f144859",2007:"9be5144d",2130:"6aca0963",2217:"7fbe9dae",2237:"02886161",2278:"a9405a6f",2279:"14e9bbca",2291:"742b3f1e",2334:"0d54c30f",2431:"c3f06d4f",2492:"04a5c13d",2618:"d8754146",2821:"25ddbeb7",3197:"a75f4a7b",3356:"916f8094",3624:"3861afa5",3815:"62684417",3993:"dec694d1",4312:"3fc60254",4440:"3f63380d",4616:"c38d0c4b",4732:"37c818cd",4802:"c2bb0f45",4981:"a26c6f29",5011:"4a23b8e6",5149:"dc032f86",5219:"4b7483e6",5480:"0ac51d82",5563:"ab94867b",5734:"a0ed7a40",5742:"dfe711f3",5953:"2e58966a",5955:"5524a460",5986:"66f4e9f8",5996:"973aad9b",6087:"be66bc37",6241:"ddb16fb6",6438:"3fe537d8",6495:"bf671ca4",6567:"ba0bae2c",6590:"a0c8f815",6803:"9c357e99",6992:"6dfc4d99",7098:"1619d051",7194:"58f7d6fe",7520:"b544ad88",7592:"a68b31ac",7715:"4381c598",7873:"c42f29b1",7928:"08dd813c",8142:"f23a31c7",8249:"c71477f6",8401:"0e3115cb",8731:"da0fb09f",8756:"2c2f045b",8779:"94292821",8795:"1c60dc80",8865:"aaf7ca2b",8897:"c793654e",9032:"6c8ca03c",9048:"df97c324",9097:"5017c79c",9178:"350e6ac9",9301:"6c09a47c",9412:"2ce5e84d",9492:"7a01fd59",9510:"2071eba3",9620:"117206fb",9647:"ac259cea",9659:"3fe16f05",9717:"bdeb521f",9957:"366ae717"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),t={},r="happyview-docs:",b.l=(e,a,d,c)=>{if(t[e])t[e].push(a);else{var f,o;if(void 0!==d)for(var n=document.getElementsByTagName("script"),i=0;i{f.onerror=f.onload=null,clearTimeout(s);var r=t[e];if(delete t[e],f.parentNode&&f.parentNode.removeChild(f),r&&r.forEach(e=>e(d)),a)return a(d)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:f}),12e4);f.onerror=l.bind(null,f.onerror),f.onload=l.bind(null,f.onload),o&&document.head.appendChild(f)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/",b.gca=function(e){return e={17896441:"8401","919abb94":"20","9985d6d5":"203","280f48ec":"787",fbd7a87c:"1361","22dd74f7":"1567","2d1b556d":"1952","2df7fd65":"1977",c985d368:"2007","23374ca6":"2278",bccc5e9b:"2431",b27c7406:"2618","5d3a5990":"3197",d2e94323:"3993","5fd2dbdb":"4440","121e86a6":"5011","3d80a749":"5219","539fb2af":"5563",aba21aa0:"5742","920ba7ef":"5953",fac68122:"5986",a77ba7fa:"6087",bde15bec:"6438","6acefe75":"6495","43880f7a":"6590","33ec3d65":"6803",a7bd4aaa:"7098",a4086a70:"7194","89877b86":"7520","009f1e98":"7715","111eee19":"8779","774696b9":"8865",a51dfc20:"8897",a94703ab:"9048",e7f37855:"9097",e8644baa:"9178",d26b9010:"9301","846656e5":"9492","5e95c892":"9647",ec125a10:"9659",f142c65c:"9957"}[e]||e,b.p+b.u(e)},(()=>{var e={5354:0,1869:0};b.f.j=(a,d)=>{var t=b.o(e,a)?e[a]:void 0;if(0!==t)if(t)d.push(t[2]);else if(/^(1869|5354)$/.test(a))e[a]=0;else{var r=new Promise((d,r)=>t=e[a]=[d,r]);d.push(t[2]=r);var c=b.p+b.u(a),f=new Error;b.l(c,d=>{if(b.o(e,a)&&(0!==(t=e[a])&&(e[a]=void 0),t)){var r=d&&("load"===d.type?"missing":d.type),c=d&&d.target&&d.target.src;f.message="Loading chunk "+a+" failed.\n("+r+": "+c+")",f.name="ChunkLoadError",f.type=r,f.request=c,t[1](f)}},"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,d)=>{var t,r,[c,f,o]=d,n=0;if(c.some(a=>0!==e[a])){for(t in f)b.o(f,t)&&(b.m[t]=f[t]);if(o)var i=o(b)}for(a&&a(d);n{"use strict";var e,a,d,t,r,f={},c={};function b(e){var a=c[e];if(void 0!==a)return a.exports;var d=c[e]={exports:{}};return f[e].call(d.exports,d,d.exports,b),d.exports}b.m=f,e=[],b.O=(a,d,t,r)=>{if(!d){var f=1/0;for(i=0;i=r)&&Object.keys(b.O).every(e=>b.O[e](d[o]))?d.splice(o--,1):(c=!1,r0&&e[i-1][2]>r;i--)e[i]=e[i-1];e[i]=[d,t,r]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},d=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,t){if(1&t&&(e=this(e)),8&t)return e;if("object"==typeof e&&e){if(4&t&&e.__esModule)return e;if(16&t&&"function"==typeof e.then)return e}var r=Object.create(null);b.r(r);var f={};a=a||[null,d({}),d([]),d(d)];for(var c=2&t&&e;("object"==typeof c||"function"==typeof c)&&!~a.indexOf(c);c=d(c))Object.getOwnPropertyNames(c).forEach(a=>f[a]=()=>e[a]);return f.default=()=>e,b.d(r,f),r},b.d=(e,a)=>{for(var d in a)b.o(a,d)&&!b.o(e,d)&&Object.defineProperty(e,d,{enumerable:!0,get:a[d]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce((a,d)=>(b.f[d](e,a),a),[])),b.u=e=>"assets/js/"+({20:"919abb94",203:"9985d6d5",787:"280f48ec",1361:"fbd7a87c",1567:"22dd74f7",1952:"2d1b556d",1977:"2df7fd65",2007:"c985d368",2278:"23374ca6",2431:"bccc5e9b",2618:"b27c7406",3197:"5d3a5990",3993:"d2e94323",4440:"5fd2dbdb",5011:"121e86a6",5219:"3d80a749",5563:"539fb2af",5742:"aba21aa0",5953:"920ba7ef",5986:"fac68122",6087:"a77ba7fa",6438:"bde15bec",6495:"6acefe75",6590:"43880f7a",6803:"33ec3d65",7098:"a7bd4aaa",7194:"a4086a70",7520:"89877b86",7715:"009f1e98",8401:"17896441",8779:"111eee19",8865:"774696b9",8897:"a51dfc20",9048:"a94703ab",9097:"e7f37855",9178:"e8644baa",9301:"d26b9010",9492:"846656e5",9647:"5e95c892",9659:"ec125a10",9957:"f142c65c"}[e]||e)+"."+{20:"3ec12698",165:"4c5708a1",203:"2071e97d",225:"04246537",291:"e48169fe",787:"8ce2f590",1203:"4c8aeaab",1361:"e3e74927",1567:"d275db6e",1741:"e6304f1e",1746:"afdbc2a0",1903:"c19e32df",1952:"cb991d70",1977:"5f144859",2007:"9be5144d",2130:"6aca0963",2217:"7fbe9dae",2237:"02886161",2278:"a9405a6f",2279:"14e9bbca",2291:"742b3f1e",2334:"0d54c30f",2431:"c3f06d4f",2492:"04a5c13d",2618:"d8754146",2821:"25ddbeb7",3197:"a75f4a7b",3356:"916f8094",3624:"3861afa5",3815:"62684417",3993:"dec694d1",4312:"3fc60254",4440:"3f63380d",4616:"c38d0c4b",4732:"37c818cd",4802:"c2bb0f45",4981:"a26c6f29",5011:"4a23b8e6",5149:"dc032f86",5219:"4b7483e6",5480:"0ac51d82",5563:"ab94867b",5734:"a0ed7a40",5742:"dfe711f3",5953:"b802a833",5955:"5524a460",5986:"66f4e9f8",5996:"973aad9b",6087:"be66bc37",6241:"ddb16fb6",6438:"3fe537d8",6495:"bf671ca4",6567:"ba0bae2c",6590:"a0c8f815",6803:"9c357e99",6992:"6dfc4d99",7098:"1619d051",7194:"58f7d6fe",7520:"b544ad88",7592:"a68b31ac",7715:"4381c598",7873:"c42f29b1",7928:"08dd813c",8142:"f23a31c7",8249:"c71477f6",8401:"0e3115cb",8731:"da0fb09f",8756:"2c2f045b",8779:"94292821",8795:"1c60dc80",8865:"08c2c2c4",8897:"c793654e",9032:"6c8ca03c",9048:"df97c324",9097:"6b9a8f63",9178:"350e6ac9",9301:"6c09a47c",9412:"2ce5e84d",9492:"7a01fd59",9510:"2071eba3",9620:"117206fb",9647:"ac259cea",9659:"3fe16f05",9717:"bdeb521f",9957:"366ae717"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),t={},r="happyview-docs:",b.l=(e,a,d,f)=>{if(t[e])t[e].push(a);else{var c,o;if(void 0!==d)for(var n=document.getElementsByTagName("script"),i=0;i{c.onerror=c.onload=null,clearTimeout(s);var r=t[e];if(delete t[e],c.parentNode&&c.parentNode.removeChild(c),r&&r.forEach(e=>e(d)),a)return a(d)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:c}),12e4);c.onerror=l.bind(null,c.onerror),c.onload=l.bind(null,c.onload),o&&document.head.appendChild(c)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/",b.gca=function(e){return e={17896441:"8401","919abb94":"20","9985d6d5":"203","280f48ec":"787",fbd7a87c:"1361","22dd74f7":"1567","2d1b556d":"1952","2df7fd65":"1977",c985d368:"2007","23374ca6":"2278",bccc5e9b:"2431",b27c7406:"2618","5d3a5990":"3197",d2e94323:"3993","5fd2dbdb":"4440","121e86a6":"5011","3d80a749":"5219","539fb2af":"5563",aba21aa0:"5742","920ba7ef":"5953",fac68122:"5986",a77ba7fa:"6087",bde15bec:"6438","6acefe75":"6495","43880f7a":"6590","33ec3d65":"6803",a7bd4aaa:"7098",a4086a70:"7194","89877b86":"7520","009f1e98":"7715","111eee19":"8779","774696b9":"8865",a51dfc20:"8897",a94703ab:"9048",e7f37855:"9097",e8644baa:"9178",d26b9010:"9301","846656e5":"9492","5e95c892":"9647",ec125a10:"9659",f142c65c:"9957"}[e]||e,b.p+b.u(e)},(()=>{var e={5354:0,1869:0};b.f.j=(a,d)=>{var t=b.o(e,a)?e[a]:void 0;if(0!==t)if(t)d.push(t[2]);else if(/^(1869|5354)$/.test(a))e[a]=0;else{var r=new Promise((d,r)=>t=e[a]=[d,r]);d.push(t[2]=r);var f=b.p+b.u(a),c=new Error;b.l(f,d=>{if(b.o(e,a)&&(0!==(t=e[a])&&(e[a]=void 0),t)){var r=d&&("load"===d.type?"missing":d.type),f=d&&d.target&&d.target.src;c.message="Loading chunk "+a+" failed.\n("+r+": "+f+")",c.name="ChunkLoadError",c.type=r,c.request=f,t[1](c)}},"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,d)=>{var t,r,[f,c,o]=d,n=0;if(f.some(a=>0!==e[a])){for(t in c)b.o(c,t)&&(b.m[t]=c[t]);if(o)var i=o(b)}for(a&&a(d);n Authentication | HappyView - + diff --git a/getting-started/configuration/index.html b/getting-started/configuration/index.html index 67814bf..6c03e34 100644 --- a/getting-started/configuration/index.html +++ b/getting-started/configuration/index.html @@ -4,7 +4,7 @@ Configuration | HappyView - + diff --git a/getting-started/dashboard/index.html b/getting-started/dashboard/index.html index 8d33e99..1bcde15 100644 --- a/getting-started/dashboard/index.html +++ b/getting-started/dashboard/index.html @@ -4,7 +4,7 @@ Dashboard | HappyView - + diff --git a/getting-started/deployment/docker/index.html b/getting-started/deployment/docker/index.html index 2c21bc4..3704e49 100644 --- a/getting-started/deployment/docker/index.html +++ b/getting-started/deployment/docker/index.html @@ -4,7 +4,7 @@ Local Development with Docker | HappyView - + diff --git a/getting-started/deployment/other/index.html b/getting-started/deployment/other/index.html index 09699a9..546cb9c 100644 --- a/getting-started/deployment/other/index.html +++ b/getting-started/deployment/other/index.html @@ -4,7 +4,7 @@ Local Development from Source | HappyView - + diff --git a/getting-started/deployment/railway/index.html b/getting-started/deployment/railway/index.html index 4bcbe5d..61c6956 100644 --- a/getting-started/deployment/railway/index.html +++ b/getting-started/deployment/railway/index.html @@ -4,7 +4,7 @@ Deploy on Railway | HappyView - + diff --git a/getting-started/quickstart/index.html b/getting-started/quickstart/index.html index 8ae53ae..e79829c 100644 --- a/getting-started/quickstart/index.html +++ b/getting-started/quickstart/index.html @@ -4,7 +4,7 @@ Quickstart | HappyView - + diff --git a/guides/api-keys/index.html b/guides/api-keys/index.html index 31d75c6..e1cc595 100644 --- a/guides/api-keys/index.html +++ b/guides/api-keys/index.html @@ -4,7 +4,7 @@ API Keys | HappyView - + diff --git a/guides/backfill/index.html b/guides/backfill/index.html index 7e4b43b..6bb343a 100644 --- a/guides/backfill/index.html +++ b/guides/backfill/index.html @@ -4,7 +4,7 @@ Backfill | HappyView - + diff --git a/guides/event-logs/index.html b/guides/event-logs/index.html index d51125d..e45f178 100644 --- a/guides/event-logs/index.html +++ b/guides/event-logs/index.html @@ -4,7 +4,7 @@ Event Logs | HappyView - + diff --git a/guides/index-hooks/index.html b/guides/index-hooks/index.html index 0dea71f..ba79ebf 100644 --- a/guides/index-hooks/index.html +++ b/guides/index-hooks/index.html @@ -3,8 +3,8 @@ -Index Hooks | HappyView - +Index Hooks | HappyView + @@ -12,14 +12,18 @@

Index Hooks

-

Index hooks are Lua scripts that run automatically whenever a record in a collection is created, updated, or deleted on the network. They let you react to record changes in real time — push data to search engines, sync with external APIs, send notifications, or build materialized views.

-

Unlike query and procedure scripts that run in response to XRPC requests, index hooks are triggered by the firehose. They run asynchronously and never block record indexing.

+

Index hooks are Lua scripts that run automatically whenever a record in a collection is created, updated, or deleted on the network. They run before the record is indexed, giving you the ability to filter out unwanted records, transform record data before storage, or trigger side effects like syncing with external services.

+

Unlike query and procedure scripts that run in response to XRPC requests, index hooks are triggered by the firehose.

Attaching a hook​

Each record-type lexicon can have one index hook. You can add it through the dashboard (click "Add Index Hook" on any record lexicon's detail page) or via the admin API by including the index_hook field when uploading a lexicon.

Script structure​

Like query and procedure scripts, index hooks must define a handle() function:

function handle()
if action == "delete" then
log("deleted " .. uri)
else
log(action .. " " .. uri)
end
end
-

The function is called once per record event. There is no return value — index hooks are fire-and-forget from the caller's perspective.

+

The function is called once per record event. The return value controls what happens next:

+
Return valueEffect
nilThe record is not indexed (skipped entirely)
A tableThat table is stored as the record instead
trueThe original record is stored as-is
(no hook)The original record is stored as-is
+

On delete events, returning nil skips the delete (the record stays in the database).

+

Important: If your hook has side effects (e.g. syncing to a search index) but you want normal indexing to proceed, return record or true — not nothing. A missing return statement returns nil, which skips indexing.

+

If the hook errors after all retries, the system fails open — the original record is stored and the failed event is dead-lettered for later inspection.

Context globals​

These globals are set before handle() is called:

GlobalTypeDescription
actionstring"create", "update", or "delete"
uristringThe full AT URI (e.g. at://did:plc:abc/col/rkey)
didstringThe repo DID
collectionstringThe collection NSID
rkeystringThe record key
recordtable?The full record as a Lua table (nil on delete)
@@ -37,28 +41,36 @@
  1. If a hook fails, it retries up to 3 times with exponential backoff (1s, 2s, 4s delays).
  2. If all retries are exhausted, the failed event is inserted into the dead_letter_hooks table for later inspection.
  3. -
  4. Hook failures never block record indexing — the record is always indexed regardless of whether the hook succeeds.
  5. +
  6. On failure the system fails open — the original record is stored as-is so indexing is not permanently blocked.

Failed hooks are logged as errors. Check the event logs or query the dead_letter_hooks table directly to find and replay failures.

+

Performance considerations​

+

Because hooks run synchronously before indexing, they block the firehose consumer while executing. With retry logic (1s + 2s + 4s backoff), a persistently failing hook could block for ~7 seconds per record. Keep hook scripts fast and ensure external services they depend on are reliable.

Dead letter table​

The dead_letter_hooks table stores events that failed all retry attempts:

ColumnTypeDescription
idUUIDPrimary key
lexicon_idtextThe lexicon NSID
uritextThe AT URI of the record
didtextThe repo DID
collectiontextThe collection NSID
rkeytextThe record key
actiontextcreate, update, or delete
recordjsonbThe record data (null on delete)
errortextThe error message from the last attempt
attemptsintTotal number of attempts made
created_attimestamptzWhen the failure was recorded

Examples​

+

Filter out records missing a required field​

+

Skip indexing any record that doesn't have a title field:

+
function handle()
if action == "delete" then
return record -- allow deletes to proceed
end

if record.title == nil or record.title == "" then
return nil -- skip: no title
end

return record
end
+

Transform a record before storage​

+

Enrich a record with a computed field before it is stored:

+
function handle()
if action == "delete" then
return record
end

record.slug = string.lower(string.gsub(record.title or "", "%s+", "-"))
return record
end

Post to a webhook​

-
function handle()
http.post("https://hooks.example.com/records", {
headers = { ["Content-Type"] = "application/json" },
body = json.encode({
action = action,
uri = uri,
did = did,
record = record
})
})
end
+
function handle()
http.post("https://hooks.example.com/records", {
headers = { ["Content-Type"] = "application/json" },
body = json.encode({
action = action,
uri = uri,
did = did,
record = record
})
})
return record
end

Sync to Algolia​

Push records to an Algolia search index on create/update, and remove them on delete:

-
function handle()
local headers = {
["X-Algolia-API-Key"] = "your-api-key",
["X-Algolia-Application-Id"] = "your-app-id",
["Content-Type"] = "application/json"
}

if action == "delete" then
http.delete("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {
headers = headers
})
else
http.put("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {
headers = headers,
body = json.encode({
objectID = uri,
collection = collection,
did = did,
record = record
})
})
end
end
+
function handle()
local headers = {
["X-Algolia-API-Key"] = "your-api-key",
["X-Algolia-Application-Id"] = "your-app-id",
["Content-Type"] = "application/json"
}

if action == "delete" then
http.delete("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {
headers = headers
})
else
http.put("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {
headers = headers,
body = json.encode({
objectID = uri,
collection = collection,
did = did,
record = record
})
})
end

return record
end

See the full Algolia sync reference for more detail.

Sync to Meilisearch​

Push records to a self-hosted Meilisearch index on create/update, and remove them on delete:

-
function handle()
local headers = {
["Authorization"] = "Bearer " .. env.MEILISEARCH_API_KEY,
["Content-Type"] = "application/json"
}

if action == "delete" then
http.delete(env.MEILISEARCH_URL .. "/indexes/records/documents/" .. uri, {
headers = headers
})
else
http.post(env.MEILISEARCH_URL .. "/indexes/records/documents", {
headers = headers,
body = json.encode(toarray({
{
id = uri,
collection = collection,
did = did,
record = record
}
}))
})
end
end
+
function handle()
local headers = {
["Authorization"] = "Bearer " .. env.MEILISEARCH_API_KEY,
["Content-Type"] = "application/json"
}

if action == "delete" then
http.delete(env.MEILISEARCH_URL .. "/indexes/records/documents/" .. uri, {
headers = headers
})
else
http.post(env.MEILISEARCH_URL .. "/indexes/records/documents", {
headers = headers,
body = json.encode(toarray({
{
id = uri,
collection = collection,
did = did,
record = record
}
}))
})
end

return record
end

See the full Meilisearch sync reference for more detail.

Next steps​

  • Lua Scripting: Full reference for the sandbox, APIs, and debugging
  • Lexicons: Understand how record, query, and procedure lexicons work together
  • Admin API: Upload lexicons with index hooks via the API
  • -
+
\ No newline at end of file diff --git a/guides/lexicons/index.html b/guides/lexicons/index.html index 564511a..0916965 100644 --- a/guides/lexicons/index.html +++ b/guides/lexicons/index.html @@ -4,7 +4,7 @@ Lexicons | HappyView - + diff --git a/guides/scripting/index.html b/guides/scripting/index.html index b12e790..de9a707 100644 --- a/guides/scripting/index.html +++ b/guides/scripting/index.html @@ -4,7 +4,7 @@ Lua Scripting | HappyView - + @@ -20,7 +20,7 @@
  • Compose multi-record operations
  • Build entirely custom behavior
  • -

    Scripts are attached to query and procedure lexicons and run in a sandboxed Lua VM with access to the Record API, a read-only database API, an HTTP client API, a JSON API, and a set of context globals.

    +

    Scripts are attached to query and procedure lexicons and run in a sandboxed Lua VM with access to the Record API, a database API, an HTTP client API, a JSON API, and a set of context globals.

    For scripts that react to record changes from the network (rather than XRPC requests), see Index Hooks.

    Script structure​

    Every script must define a handle() function. HappyView calls it when the XRPC endpoint is hit and returns its result as JSON to the client.

    @@ -73,7 +73,7 @@

    After a successful save, _uri and _cid are updated on the record instance.

    Database API​

    -

    The db table provides read-only access to indexed records. Available in both queries and procedures.

    +

    The db table provides access to the database. Available in both queries and procedures.

    db.query​

    local result = db.query({
    collection = "xyz.statusphere.status", -- required
    did = "did:plc:abc", -- optional: filter by DID
    limit = 20, -- optional: max 100, default 20
    offset = 0, -- optional: for pagination
    sort = "name", -- optional: field to sort by, default "indexed_at"
    sortDirection = "asc", -- optional: "asc" or "desc", default "desc"
    })

    -- result.records — array of record tables (each includes a "uri" field)
    -- result.cursor — present when more records exist

    The sort field can be a top-level column (indexed_at, did, uri) or any field inside the record's value object (e.g. name, createdAt). Field names must contain only alphanumeric characters and underscores.

    @@ -88,8 +88,8 @@

    db.count​

    local n = db.count("xyz.statusphere.status")
    local n = db.count("xyz.statusphere.status", "did:plc:abc") -- filter by DID

    db.raw​

    -

    Run a raw SQL query against the database. Only SELECT statements are allowed.

    -
    local rows = db.raw(
    "SELECT uri, did, record FROM records WHERE collection = $1 AND did = $2 LIMIT $3",
    { "xyz.statusphere.status", "did:plc:abc", 10 }
    )

    for _, row in ipairs(rows) do
    -- row.uri, row.did, row.record (JSONB is returned as a Lua table)
    end
    +

    Run a raw SQL query against the database. Supports SELECT, INSERT, UPDATE, DELETE, and CREATE TABLE statements.

    +
    -- Read query
    local rows = db.raw(
    "SELECT uri, did, record FROM records WHERE collection = $1 AND did = $2 LIMIT $3",
    { "xyz.statusphere.status", "did:plc:abc", 10 }
    )

    for _, row in ipairs(rows) do
    -- row.uri, row.did, row.record (JSONB is returned as a Lua table)
    end

    -- Write query (returns affected rows, if any)
    db.raw("CREATE TABLE IF NOT EXISTS my_table (id TEXT PRIMARY KEY, value TEXT NOT NULL)")
    db.raw("INSERT INTO my_table (id, value) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET value = $2",
    { "key1", "hello" })

    Parameters are passed as an array and bound to $1, $2, etc. Supported parameter types: strings, integers, numbers, booleans, and nil.

    Column types are mapped automatically:

    Postgres typeLua type
    TEXT, VARCHARstring
    INT4, INT8integer
    FLOAT4, FLOAT8number
    BOOLboolean
    JSON, JSONBtable
    TIMESTAMPTZstring (ISO 8601)
    Otherstring (fallback)
    diff --git a/index.html b/index.html index eafc64f..4f0e853 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ HappyView - + diff --git a/reference/admin-api/index.html b/reference/admin-api/index.html index f639c42..f266e86 100644 --- a/reference/admin-api/index.html +++ b/reference/admin-api/index.html @@ -4,7 +4,7 @@ Admin API | HappyView - + diff --git a/reference/architecture/index.html b/reference/architecture/index.html index 76a328e..e2251be 100644 --- a/reference/architecture/index.html +++ b/reference/architecture/index.html @@ -4,7 +4,7 @@ Architecture | HappyView - + diff --git a/reference/changelog/index.html b/reference/changelog/index.html index 4579a9a..57c04d4 100644 --- a/reference/changelog/index.html +++ b/reference/changelog/index.html @@ -4,7 +4,7 @@ Changelog | HappyView - + diff --git a/reference/glossary/index.html b/reference/glossary/index.html index 9a90a2e..e0069e9 100644 --- a/reference/glossary/index.html +++ b/reference/glossary/index.html @@ -4,7 +4,7 @@ Glossary | HappyView - + diff --git a/reference/production-deployment/index.html b/reference/production-deployment/index.html index bf9b859..1e317e3 100644 --- a/reference/production-deployment/index.html +++ b/reference/production-deployment/index.html @@ -4,7 +4,7 @@ Deployment | HappyView - + diff --git a/reference/scripts/algolia-sync/index.html b/reference/scripts/algolia-sync/index.html index c75e388..95b3142 100644 --- a/reference/scripts/algolia-sync/index.html +++ b/reference/scripts/algolia-sync/index.html @@ -4,7 +4,7 @@ Index Hook: Algolia Sync | HappyView - + @@ -14,7 +14,7 @@

    Index Hook: Algolia Sync

    Push records to an Algolia search index whenever they are created, updated, or deleted on the network.

    Lexicon type: record (index hook)

    -
    function handle()
    local headers = {
    ["X-Algolia-API-Key"] = "your-api-key",
    ["X-Algolia-Application-Id"] = "your-app-id",
    ["Content-Type"] = "application/json"
    }

    if action == "delete" then
    http.delete("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {
    headers = headers
    })
    else
    http.put("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {
    headers = headers,
    body = json.encode({
    objectID = uri,
    collection = collection,
    did = did,
    record = record
    })
    })
    end
    end
    +
    function handle()
    local headers = {
    ["X-Algolia-API-Key"] = "your-api-key",
    ["X-Algolia-Application-Id"] = "your-app-id",
    ["Content-Type"] = "application/json"
    }

    if action == "delete" then
    http.delete("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {
    headers = headers
    })
    else
    http.put("https://YOUR-APP.algolia.net/1/indexes/records/" .. uri, {
    headers = headers,
    body = json.encode({
    objectID = uri,
    collection = collection,
    did = did,
    record = record
    })
    })
    end

    return record
    end

    How it works​

    1. On create or update: sends a PUT request to Algolia's index API with the record data, using the AT URI as the objectID. Algolia upserts the object — if it already exists, it's replaced.
    2. diff --git a/reference/scripts/batch-save/index.html b/reference/scripts/batch-save/index.html index fe9133b..f63a114 100644 --- a/reference/scripts/batch-save/index.html +++ b/reference/scripts/batch-save/index.html @@ -4,7 +4,7 @@ Procedure: Batch Save | HappyView - + diff --git a/reference/scripts/cascading-delete/index.html b/reference/scripts/cascading-delete/index.html index 9251829..7127ea6 100644 --- a/reference/scripts/cascading-delete/index.html +++ b/reference/scripts/cascading-delete/index.html @@ -4,7 +4,7 @@ Procedure: Cascading Delete | HappyView - + diff --git a/reference/scripts/complex-mutations/index.html b/reference/scripts/complex-mutations/index.html index dd6e0b7..ea80ddb 100644 --- a/reference/scripts/complex-mutations/index.html +++ b/reference/scripts/complex-mutations/index.html @@ -4,7 +4,7 @@ Procedure: Complex Mutations | HappyView - + diff --git a/reference/scripts/create-record/index.html b/reference/scripts/create-record/index.html index 6995acd..e5544c8 100644 --- a/reference/scripts/create-record/index.html +++ b/reference/scripts/create-record/index.html @@ -4,7 +4,7 @@ Procedure: Create a Record | HappyView - + diff --git a/reference/scripts/expanded-query/index.html b/reference/scripts/expanded-query/index.html index 853e512..d7f8485 100644 --- a/reference/scripts/expanded-query/index.html +++ b/reference/scripts/expanded-query/index.html @@ -4,7 +4,7 @@ Query: Expanded Query with Profiles | HappyView - + diff --git a/reference/scripts/get-record/index.html b/reference/scripts/get-record/index.html index a0f0334..05bf9d7 100644 --- a/reference/scripts/get-record/index.html +++ b/reference/scripts/get-record/index.html @@ -4,7 +4,7 @@ Query: Get a Single Record | HappyView - + diff --git a/reference/scripts/list-or-fetch/index.html b/reference/scripts/list-or-fetch/index.html index db42826..b7e7e0c 100644 --- a/reference/scripts/list-or-fetch/index.html +++ b/reference/scripts/list-or-fetch/index.html @@ -4,7 +4,7 @@ Query: List or Fetch Records | HappyView - + diff --git a/reference/scripts/meilisearch-sync/index.html b/reference/scripts/meilisearch-sync/index.html index 0e1862b..000f389 100644 --- a/reference/scripts/meilisearch-sync/index.html +++ b/reference/scripts/meilisearch-sync/index.html @@ -4,7 +4,7 @@ Index Hook: Meilisearch Sync | HappyView - + @@ -14,7 +14,7 @@

      Index Hook: Meilisearch Sync

      Push records to a Meilisearch search index whenever they are created, updated, or deleted on the network.

      Lexicon type: record (index hook)

      -
      function handle()
      local headers = {
      ["Authorization"] = "Bearer " .. env.MEILISEARCH_API_KEY,
      ["Content-Type"] = "application/json"
      }

      if action == "delete" then
      http.delete(env.MEILISEARCH_URL .. "/indexes/records/documents/" .. uri, {
      headers = headers
      })
      else
      http.post(env.MEILISEARCH_URL .. "/indexes/records/documents", {
      headers = headers,
      body = json.encode(toarray({
      {
      id = uri,
      collection = collection,
      did = did,
      record = record
      }
      }))
      })
      end
      end
      +
      function handle()
      local headers = {
      ["Authorization"] = "Bearer " .. env.MEILISEARCH_API_KEY,
      ["Content-Type"] = "application/json"
      }

      if action == "delete" then
      http.delete(env.MEILISEARCH_URL .. "/indexes/records/documents/" .. uri, {
      headers = headers
      })
      else
      http.post(env.MEILISEARCH_URL .. "/indexes/records/documents", {
      headers = headers,
      body = json.encode(toarray({
      {
      id = uri,
      collection = collection,
      did = did,
      record = record
      }
      }))
      })
      end

      return record
      end

      How it works​

      1. On create or update: sends a POST request to Meilisearch's document API with the record data wrapped in an array. Meilisearch upserts by id — if a document with the same AT URI already exists, it's replaced.
      2. diff --git a/reference/scripts/paginated-list/index.html b/reference/scripts/paginated-list/index.html index 1033fa4..8b7244b 100644 --- a/reference/scripts/paginated-list/index.html +++ b/reference/scripts/paginated-list/index.html @@ -4,7 +4,7 @@ Query: Paginated List | HappyView - + diff --git a/reference/scripts/sidecar-records/index.html b/reference/scripts/sidecar-records/index.html index cfb3d93..e15be55 100644 --- a/reference/scripts/sidecar-records/index.html +++ b/reference/scripts/sidecar-records/index.html @@ -4,7 +4,7 @@ Procedure: Create Sidecar Records | HappyView - + diff --git a/reference/scripts/update-or-delete/index.html b/reference/scripts/update-or-delete/index.html index aadd984..f15c880 100644 --- a/reference/scripts/update-or-delete/index.html +++ b/reference/scripts/update-or-delete/index.html @@ -4,7 +4,7 @@ Procedure: Update or Delete | HappyView - + diff --git a/reference/scripts/upsert-record/index.html b/reference/scripts/upsert-record/index.html index 015b80a..400d183 100644 --- a/reference/scripts/upsert-record/index.html +++ b/reference/scripts/upsert-record/index.html @@ -4,7 +4,7 @@ Procedure: Upsert a Record | HappyView - + diff --git a/reference/troubleshooting/index.html b/reference/troubleshooting/index.html index c710b75..d70f930 100644 --- a/reference/troubleshooting/index.html +++ b/reference/troubleshooting/index.html @@ -4,7 +4,7 @@ Troubleshooting | HappyView - + diff --git a/reference/xrpc-api/index.html b/reference/xrpc-api/index.html index 4f7529e..dea127b 100644 --- a/reference/xrpc-api/index.html +++ b/reference/xrpc-api/index.html @@ -4,7 +4,7 @@ XRPC API | HappyView - + diff --git a/tutorials/statusphere/index.html b/tutorials/statusphere/index.html index bc5b18a..a47aa84 100644 --- a/tutorials/statusphere/index.html +++ b/tutorials/statusphere/index.html @@ -4,7 +4,7 @@ Tutorial: Statusphere with HappyView | HappyView - + -- 2.51.2