From cb4b1c1a20bf11ab6810acbc43e95556ce8cd6e5 Mon Sep 17 00:00:00 2001 From: Trezy Date: Fri, 17 Apr 2026 16:03:30 -0500 Subject: [PATCH] feat: add missing HappyView Lua APIs to autocomplete and hover docs --- web/src/components/monaco-editor.tsx | 11 ++-- web/src/lib/lua-completions.ts | 75 +++++++++++++++++++++++++++- web/src/lib/lua-hover.ts | 37 +++++++++++++- 3 files changed, 115 insertions(+), 8 deletions(-) diff --git a/web/src/components/monaco-editor.tsx b/web/src/components/monaco-editor.tsx index 6574e00..5e82c95 100644 --- a/web/src/components/monaco-editor.tsx +++ b/web/src/components/monaco-editor.tsx @@ -317,10 +317,13 @@ export function MonacoEditor({ position.column - 1, ); - // Inside db.query({ ... }) — suggest option keys - if (/db\.query\(\s*\{[^}]*$/.test(textBeforeCursor)) { + // Inside db.query/search/backlinks({ ... }) — suggest option keys + const dbOptionsMatch = textBeforeCursor.match( + /db\.(query|search|backlinks)\(\s*\{[^}]*$/, + ); + if (dbOptionsMatch) { const optionEntries = - completionsRef.current?.["db.query"] ?? []; + completionsRef.current?.[`db.${dbOptionsMatch[1]}`] ?? []; if (optionEntries.length) { // Check for collection = " inside db.query — offer NSID completions const collectionQuoteMatch = textBeforeCursor.match( @@ -366,7 +369,7 @@ export function MonacoEditor({ } } - // Collection NSID completions for collection = " (outside db.query) and db.count + // Collection NSID completions for collection = " (outside db options) and db.count const collectionAssignMatch = textBeforeCursor.match( /(?:db\.count\(\s*"([^"]*)$|collection\s*=\s*"([^"]*)$)/, ); diff --git a/web/src/lib/lua-completions.ts b/web/src/lib/lua-completions.ts index 6bd87fe..b6d1b51 100644 --- a/web/src/lib/lua-completions.ts +++ b/web/src/lib/lua-completions.ts @@ -26,7 +26,11 @@ export const LUA_BUILTINS = [ "string", "table", "math", "coroutine", "utf8", // HappyView sandbox globals "input", "params", "caller_did", "collection", "method", - "now", "log", "TID", + "now", "log", "TID", "toarray", "env", + // Hook-specific globals + "action", "uri", "did", "rkey", "record", + // HappyView API modules + "http", "xrpc", "atproto", ]; export const LUA_SNIPPETS: LuaSnippetEntry[] = [ @@ -111,6 +115,7 @@ const STATIC_COMPLETIONS: LuaCompletions = { { label: "delete", detail: "method", description: "Delete this record from PDS and database — r:delete()" }, { label: "set_key_type", detail: "method", description: "Set the record key type (tid, any, nsid, literal:*) — r:set_key_type(type)" }, { label: "set_rkey", detail: "method", description: "Set a specific rkey for this record — r:set_rkey(key)" }, + { label: "set_repo", detail: "method", description: "Override the repo DID (instead of caller_did) — r:set_repo(did)" }, { label: "generate_rkey", detail: "method", description: "Generate an rkey based on _key_type — r:generate_rkey()" }, { label: "_uri", detail: "string?", description: "AT URI of the record (set after save)" }, { label: "_cid", detail: "string?", description: "CID of the record (set after save)" }, @@ -136,12 +141,52 @@ const STATIC_COMPLETIONS: LuaCompletions = { description: "Count records — db.count(collection, did?) → integer", insertText: "count(${1:collection})", }, + { + label: "search", + detail: "function", + description: "Search records by field value — db.search({ collection, field, query, limit? }) → { records }", + insertText: "search({\n\tcollection = ${1:collection},\n\tfield = ${2:\"field\"},\n\tquery = ${3:\"search term\"},\n})", + }, + { + label: "backlinks", + detail: "function", + description: "Find records referencing a URI — db.backlinks({ collection, uri, did?, limit?, cursor? }) → { records, cursor? }", + insertText: "backlinks({\n\tcollection = ${1:collection},\n\turi = ${2:uri},\n})", + }, + { + label: "raw", + detail: "function", + description: "Execute a raw SQL query — db.raw(sql, params?) → rows[]", + insertText: "raw(${1:\"SELECT * FROM records WHERE collection = ?\"}${2:, \\{${3}\\}})", + }, + { + label: "backend", + detail: "function", + description: "Returns the database backend — db.backend() → \"sqlite\" or \"postgres\"", + insertText: "backend()", + }, ], "db.query": [ { label: "collection", detail: "string", description: "Collection NSID (required)" }, { label: "did", detail: "string?", description: "Filter records by DID" }, { label: "limit", detail: "integer?", description: "Max records to return (max 100, default 20)" }, - { label: "offset", detail: "integer?", description: "Pagination offset (default 0)" }, + { label: "offset", detail: "integer?", description: "Pagination offset (default 0, used with custom sort)" }, + { label: "cursor", detail: "string?", description: "Pagination cursor from previous query" }, + { label: "sort", detail: "string?", description: "Field name to sort by" }, + { label: "sortDirection", detail: "string?", description: "Sort direction — \"asc\" or \"desc\" (default \"desc\")" }, + ], + "db.search": [ + { label: "collection", detail: "string", description: "Collection NSID (required)" }, + { label: "field", detail: "string", description: "JSON field to search (required)" }, + { label: "query", detail: "string", description: "Search term (required)" }, + { label: "limit", detail: "integer?", description: "Max records to return (max 100, default 10)" }, + ], + "db.backlinks": [ + { label: "collection", detail: "string", description: "Collection NSID (required)" }, + { label: "uri", detail: "string", description: "Target URI to find references to (required)" }, + { label: "did", detail: "string?", description: "Filter by DID" }, + { label: "limit", detail: "integer?", description: "Max records to return (max 100, default 20)" }, + { label: "cursor", detail: "string?", description: "Pagination cursor" }, ], "db.query_result": [ { label: "records", detail: "table[]", description: "Array of record tables (each includes uri)" }, @@ -219,6 +264,32 @@ const STATIC_COMPLETIONS: LuaCompletions = { { label: "len", detail: "function", description: "UTF-8 string length — utf8.len(s [, i [, j [, lax]]])" }, { label: "offset", detail: "function", description: "Byte offset of nth character — utf8.offset(s, n [, i])" }, ], + // HappyView HTTP API + http: [ + { label: "get", detail: "function", description: "HTTP GET request — http.get(url, options?) → {status, body, headers}", insertText: "get(${1:url})" }, + { label: "post", detail: "function", description: "HTTP POST request — http.post(url, options?) → {status, body, headers}", insertText: "post(${1:url}, {\n\theaders = { [\"content-type\"] = \"application/json\" },\n\tbody = ${2:body},\n})" }, + { label: "put", detail: "function", description: "HTTP PUT request — http.put(url, options?) → {status, body, headers}", insertText: "put(${1:url}, {\n\tbody = ${2:body},\n})" }, + { label: "patch", detail: "function", description: "HTTP PATCH request — http.patch(url, options?) → {status, body, headers}", insertText: "patch(${1:url}, {\n\tbody = ${2:body},\n})" }, + { label: "delete", detail: "function", description: "HTTP DELETE request — http.delete(url, options?) → {status, body, headers}", insertText: "delete(${1:url})" }, + { label: "head", detail: "function", description: "HTTP HEAD request — http.head(url, options?) → {status, headers}", insertText: "head(${1:url})" }, + ], + "http.options": [ + { label: "headers", detail: "table?", description: "Request headers as key-value pairs" }, + { label: "body", detail: "string?", description: "Request body (for POST, PUT, PATCH, DELETE)" }, + ], + // HappyView XRPC API + xrpc: [ + { label: "query", detail: "function", description: "XRPC query — xrpc.query(method, params?) → {status, body}", insertText: "query(${1:\"com.atproto.repo.describeRepo\"}, {\n\t${2}\n})" }, + { label: "procedure", detail: "function", description: "XRPC procedure (requires caller_did) — xrpc.procedure(method, input, params?) → {status, body}", insertText: "procedure(${1:\"method\"}, {\n\t${2}\n})" }, + ], + // HappyView AT Protocol API + atproto: [ + { label: "resolve_service_endpoint", detail: "function", description: "Resolve a DID to its PDS endpoint URL — atproto.resolve_service_endpoint(did) → string or nil", insertText: "resolve_service_endpoint(${1:did})" }, + { label: "get_labels", detail: "function", description: "Get labels for a URI — atproto.get_labels(uri) → label[]", insertText: "get_labels(${1:uri})" }, + { label: "get_labels_batch", detail: "function", description: "Get labels for multiple URIs — atproto.get_labels_batch({uri1, uri2}) → {[uri]: label[]}", insertText: "get_labels_batch(${1:uris})" }, + { label: "sign", detail: "function", description: "Sign a record (requires attestation signer) — atproto.sign(record) → signature", insertText: "sign(${1:record})" }, + { label: "verify_signature", detail: "function", description: "Verify a record signature — atproto.verify_signature(record, sig, repo_did) → boolean", insertText: "verify_signature(${1:record}, ${2:sig}, ${3:repo_did})" }, + ], }; /** Extract property completions from a record schema object (`defs.main.record`). */ diff --git a/web/src/lib/lua-hover.ts b/web/src/lib/lua-hover.ts index 0b7eae2..6f85916 100644 --- a/web/src/lib/lua-hover.ts +++ b/web/src/lib/lua-hover.ts @@ -52,12 +52,21 @@ export const HOVER_DOCS = new Map([ // ── HappyView sandbox globals ───────────────────────────────────────── ["input", { signature: "input", description: "Procedure input table (from request body)" }], ["params", { signature: "params", description: "Query parameters table (from URL query string)" }], - ["caller_did", { signature: "caller_did", description: "DID of the authenticated caller" }], + ["caller_did", { signature: "caller_did", description: "DID of the authenticated caller (nil for unauthenticated queries)" }], ["collection", { signature: "collection", description: "Target collection NSID for this lexicon" }], ["method", { signature: "method", description: "XRPC method name being called" }], + ["env", { signature: "env", description: "Script variables table (configured per-lexicon in the dashboard)" }], ["now", { signature: "now()", description: "Current UTC timestamp as ISO 8601 string" }], ["log", { signature: "log(···)", description: "Log values to server console" }], ["TID", { signature: "TID()", description: "Generate a new TID (timestamp identifier)" }], + ["toarray", { signature: "toarray(table)", description: "Mark a table as a JSON array for serialization (ensures empty tables serialize as [] not {})" }], + + // ── Hook-specific globals ──────────────────────────────────────────── + ["action", { signature: "action", description: "Hook action type — \"create\", \"update\", or \"delete\"" }], + ["uri", { signature: "uri", description: "AT URI of the record being processed" }], + ["did", { signature: "did", description: "DID of the repo owner" }], + ["rkey", { signature: "rkey", description: "Record key of the record being processed" }], + ["record", { signature: "record", description: "Record data table (nil on delete actions)" }], // ── string module ───────────────────────────────────────────────────── ["string.byte", { signature: "string.byte(s [, i [, j]])", description: "Returns internal numeric codes of characters", module: "string" }], @@ -139,10 +148,34 @@ export const HOVER_DOCS = new Map([ ["Record:delete", { signature: "record:delete()", description: "Delete this record from PDS and database" }], ["Record:set_key_type", { signature: "record:set_key_type(type)", description: "Set the record key type (tid, any, nsid, literal:*)" }], ["Record:set_rkey", { signature: "record:set_rkey(key)", description: "Set a specific rkey for this record" }], + ["Record:set_repo", { signature: "record:set_repo(did)", description: "Override the repo DID (write to a different user's repo instead of caller_did)" }], ["Record:generate_rkey", { signature: "record:generate_rkey()", description: "Generate an rkey based on the record's _key_type" }], // ── HappyView db API ────────────────────────────────────────────────── - ["db.query", { signature: "db.query({collection, did?, limit?, offset?})", description: "Query records — returns {records, cursor?}", module: "db" }], + ["db.query", { signature: "db.query({collection, did?, limit?, offset?, cursor?, sort?, sortDirection?})", description: "Query records — returns {records, cursor?}", module: "db" }], ["db.get", { signature: "db.get(uri)", description: "Get a single record by AT URI — returns record or nil", module: "db" }], ["db.count", { signature: "db.count(collection [, did])", description: "Count records in a collection", module: "db" }], + ["db.search", { signature: "db.search({collection, field, query, limit?})", description: "Search records by field value — returns {records}", module: "db" }], + ["db.backlinks", { signature: "db.backlinks({collection, uri, did?, limit?, cursor?})", description: "Find records that reference a URI via record_refs — returns {records, cursor?}", module: "db" }], + ["db.raw", { signature: "db.raw(sql [, params])", description: "Execute a raw SQL query — returns array of row tables", module: "db" }], + ["db.backend", { signature: "db.backend()", description: "Returns the database backend — \"sqlite\" or \"postgres\"", module: "db" }], + + // ── HappyView HTTP API ─────────────────────────────────────────────── + ["http.get", { signature: "http.get(url [, options])", description: "HTTP GET request — returns {status, body, headers}", module: "http" }], + ["http.post", { signature: "http.post(url [, options])", description: "HTTP POST request — returns {status, body, headers}", module: "http" }], + ["http.put", { signature: "http.put(url [, options])", description: "HTTP PUT request — returns {status, body, headers}", module: "http" }], + ["http.patch", { signature: "http.patch(url [, options])", description: "HTTP PATCH request — returns {status, body, headers}", module: "http" }], + ["http.delete", { signature: "http.delete(url [, options])", description: "HTTP DELETE request — returns {status, body, headers}", module: "http" }], + ["http.head", { signature: "http.head(url [, options])", description: "HTTP HEAD request — returns {status, headers}", module: "http" }], + + // ── HappyView XRPC API ────────────────────────────────────────────── + ["xrpc.query", { signature: "xrpc.query(method [, params])", description: "Make an XRPC query to a PDS — returns {status, body} where body is JSON string", module: "xrpc" }], + ["xrpc.procedure", { signature: "xrpc.procedure(method, input [, params])", description: "Make an XRPC procedure call (requires caller_did) — returns {status, body}", module: "xrpc" }], + + // ── HappyView AT Protocol API ──────────────────────────────────────── + ["atproto.resolve_service_endpoint", { signature: "atproto.resolve_service_endpoint(did)", description: "Resolve a DID to its PDS endpoint URL — returns string or nil", module: "atproto" }], + ["atproto.get_labels", { signature: "atproto.get_labels(uri)", description: "Get labels for a URI — returns array of {src, uri, val, cts}", module: "atproto" }], + ["atproto.get_labels_batch", { signature: "atproto.get_labels_batch({uri1, uri2, ...})", description: "Get labels for multiple URIs — returns table keyed by URI", module: "atproto" }], + ["atproto.sign", { signature: "atproto.sign(record)", description: "Sign a record (requires attestation signer, procedure scripts only) — returns signature object", module: "atproto" }], + ["atproto.verify_signature", { signature: "atproto.verify_signature(record, sig, repo_did)", description: "Verify a record signature — returns boolean", module: "atproto" }], ]); -- 2.51.2