Exposing Objects indexing as syscalls

pull/1220/head
Zef Hemel 2025-01-17 10:41:02 +01:00
parent 66433d27cc
commit 83550c1623
14 changed files with 269 additions and 89 deletions

49
common/syscalls/index.ts Normal file
View File

@ -0,0 +1,49 @@
import type {
KvQuery,
ObjectQuery,
ObjectValue,
} from "@silverbulletmd/silverbullet/types";
import type { SysCallMapping, System } from "$lib/plugos/system.ts";
import type { LuaCollectionQuery } from "$common/space_lua/query_collection.ts";
// These are just wrappers around the system.invokeFunction calls, but they make it easier to use the index
export function indexSyscalls(system: System<any>): SysCallMapping {
return {
"index.indexObjects": (_ctx, page: string, objects: ObjectValue<any>[]) => {
return system.invokeFunction("index.indexObjects", [page, objects]);
},
"index.queryObjects": (
_ctx,
tag: string,
query: ObjectQuery,
ttlSecs?: number,
) => {
return system.invokeFunction("index.queryObjects", [
tag,
query,
ttlSecs,
]);
},
"index.queryLuaObjects": (
_ctx,
tag: string,
query: LuaCollectionQuery,
scopedVariables?: Record<string, any>,
) => {
return system.invokeFunction(
"index.queryLuaObjects",
[tag, query, scopedVariables],
);
},
"index.queryDeleteObjects": (_ctx, tag: string, query: ObjectQuery) => {
return system.invokeFunction("index.queryDeleteObjects", [tag, query]);
},
"index.query": (_ctx, query: KvQuery, variables?: Record<string, any>) => {
return system.invokeFunction("index.query", [query, variables]);
},
"index.getObjectByRef": (_ctx, page: string, tag: string, ref: string) => {
return system.invokeFunction("index.getObjectByRef", [page, tag, ref]);
},
};
}

View File

@ -47,7 +47,7 @@ export function dataStoreReadSyscalls(
prefix: string[], prefix: string[],
query: LuaCollectionQuery, query: LuaCollectionQuery,
scopeVariables: Record<string, any> = {}, scopeVariables: Record<string, any> = {},
): Promise<KV[]> => { ): Promise<any[]> => {
const dsQueryCollection = new DataStoreQueryCollection(ds, prefix); const dsQueryCollection = new DataStoreQueryCollection(ds, prefix);
const env = new LuaEnv(commonSystem.spaceLuaEnv.env); const env = new LuaEnv(commonSystem.spaceLuaEnv.env);
for (const [key, value] of Object.entries(scopeVariables)) { for (const [key, value] of Object.entries(scopeVariables)) {

View File

@ -17,4 +17,7 @@ export * as datastore from "./syscalls/datastore.ts";
export * as jsonschema from "./syscalls/jsonschema.ts"; export * as jsonschema from "./syscalls/jsonschema.ts";
export * as lua from "./syscalls/lua.ts"; export * as lua from "./syscalls/lua.ts";
// Not technically syscalls, but we want to export them for convenience
export * as index from "./syscalls/index.ts";
export * from "./syscall.ts"; export * from "./syscall.ts";

View File

@ -75,7 +75,7 @@ export function queryLua(
prefix: string[], prefix: string[],
query: LuaCollectionQuery, query: LuaCollectionQuery,
scopeVariables: Record<string, any>, scopeVariables: Record<string, any>,
): Promise<KV[]> { ): Promise<any[]> {
return syscall("datastore.queryLua", prefix, query, scopeVariables); return syscall("datastore.queryLua", prefix, query, scopeVariables);
} }

View File

@ -0,0 +1,82 @@
import type {
ObjectQuery,
ObjectValue,
} from "@silverbulletmd/silverbullet/types";
import type { LuaCollectionQuery } from "$common/space_lua/query_collection.ts";
import { syscall } from "@silverbulletmd/silverbullet/syscall";
/**
* Exposes the SilverBullet object indexing system
* @module
*/
/**
* Indexes objects for a specific page
* @param page - The page identifier where objects will be indexed
* @param objects - Array of objects to be indexed
* @returns Promise that resolves when indexing is complete
*/
export function indexObjects<T>(
page: string,
objects: ObjectValue<T>[],
): Promise<void> {
return syscall("index.indexObjects", page, objects);
}
/**
* Queries objects based on specified criteria
* @param tag - The tag to filter objects by
* @param query - Query parameters to filter objects
* @param ttlSecs - Optional time-to-live in seconds for the query cache
* @returns Promise that resolves with an array of matching objects
*/
export function queryObjects<T>(
tag: string,
query: ObjectQuery,
ttlSecs?: number,
): Promise<ObjectValue<T>[]> {
return syscall("index.queryObjects", tag, query, ttlSecs);
}
/**
* Queries objects using a Lua-based collection query
* @param tag - The tag to filter objects by
* @param query - Lua query parameters to filter objects
* @param scopedVariables - Optional variables to be used in the Lua query
* @returns Promise that resolves with an array of matching objects
*/
export function queryLuaObjects<T>(
tag: string,
query: LuaCollectionQuery,
scopedVariables?: Record<string, any>,
): Promise<ObjectValue<T>[]> {
return syscall("index.queryLuaObjects", tag, query, scopedVariables);
}
/**
* Deletes objects that match the specified query criteria
* @param tag - The tag of objects to be deleted
* @param query - Query parameters to identify objects for deletion
* @returns Promise that resolves when deletion is complete
*/
export function queryDeleteObjects(
tag: string,
query: ObjectQuery,
): Promise<void> {
return syscall("index.queryDeleteObjects", tag, query);
}
/**
* Retrieves a specific object by its reference
* @param page - The page identifier where the object is located
* @param tag - The tag of the object
* @param ref - The reference identifier of the object
* @returns Promise that resolves with the matching object or undefined if not found
*/
export function getObjectByRef<T>(
page: string,
tag: string,
ref: string,
): Promise<ObjectValue<T> | undefined> {
return syscall("index.getObjectByRef", page, tag, ref);
}

View File

@ -9,6 +9,7 @@ import type {
import type { QueryProviderEvent } from "../../plug-api/types.ts"; import type { QueryProviderEvent } from "../../plug-api/types.ts";
import { determineType, type SimpleJSONType } from "./attributes.ts"; import { determineType, type SimpleJSONType } from "./attributes.ts";
import { ttlCache } from "$lib/memory_cache.ts"; import { ttlCache } from "$lib/memory_cache.ts";
import type { LuaCollectionQuery } from "$common/space_lua/query_collection.ts";
const indexKey = "idx"; const indexKey = "idx";
const pageKey = "ridx"; const pageKey = "ridx";
@ -179,6 +180,17 @@ export function queryObjects<T>(
}, ttlSecs); }, ttlSecs);
} }
export function queryLuaObjects<T>(
tag: string,
query: LuaCollectionQuery,
scopedVariables: Record<string, any> = {},
ttlSecs?: number,
): Promise<ObjectValue<T>[]> {
return ttlCache(query, () => {
return datastore.queryLua([indexKey, tag], query, scopedVariables);
}, ttlSecs);
}
export function queryDeleteObjects<T>( export function queryDeleteObjects<T>(
tag: string, tag: string,
query: ObjectQuery, query: ObjectQuery,

View File

@ -12,6 +12,9 @@ functions:
queryObjects: queryObjects:
path: api.ts:queryObjects path: api.ts:queryObjects
# Note: not setting env: server to allow for client-side datastore query caching # Note: not setting env: server to allow for client-side datastore query caching
queryLuaObjects:
path: api.ts:queryLuaObjects
# Note: not setting env: server to allow for client-side datastore query caching
getObjectByRef: getObjectByRef:
path: api.ts:getObjectByRef path: api.ts:getObjectByRef
env: server env: server

View File

@ -41,6 +41,7 @@ import type { DataStoreMQ } from "$lib/data/mq.datastore.ts";
import { plugPrefix } from "$common/spaces/constants.ts"; import { plugPrefix } from "$common/spaces/constants.ts";
import { base64EncodedDataUrl } from "$lib/crypto.ts"; import { base64EncodedDataUrl } from "$lib/crypto.ts";
import type { ConfigContainer } from "../type/config.ts"; import type { ConfigContainer } from "../type/config.ts";
import { indexSyscalls } from "$common/syscalls/index.ts";
const fileListInterval = 30 * 1000; // 30s const fileListInterval = 30 * 1000; // 30s
@ -133,6 +134,7 @@ export class ServerSystem extends CommonSystem {
mqSyscalls(this.mq), mqSyscalls(this.mq),
languageSyscalls(), languageSyscalls(),
jsonschemaSyscalls(), jsonschemaSyscalls(),
indexSyscalls(this.system),
luaSyscalls(), luaSyscalls(),
templateSyscalls(this.ds), templateSyscalls(this.ds),
dataStoreReadSyscalls(this.ds, this), dataStoreReadSyscalls(this.ds, this),

View File

@ -44,6 +44,7 @@ import type { DataStoreMQ } from "$lib/data/mq.datastore.ts";
import { plugPrefix } from "$common/spaces/constants.ts"; import { plugPrefix } from "$common/spaces/constants.ts";
import { jsonschemaSyscalls } from "$common/syscalls/jsonschema.ts"; import { jsonschemaSyscalls } from "$common/syscalls/jsonschema.ts";
import { luaSyscalls } from "$common/syscalls/lua.ts"; import { luaSyscalls } from "$common/syscalls/lua.ts";
import { indexSyscalls } from "$common/syscalls/index.ts";
const plugNameExtractRegex = /\/(.+)\.plug\.js$/; const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
@ -162,6 +163,7 @@ export class ClientSystem extends CommonSystem {
clientCodeWidgetSyscalls(), clientCodeWidgetSyscalls(),
languageSyscalls(), languageSyscalls(),
jsonschemaSyscalls(), jsonschemaSyscalls(),
indexSyscalls(this.system),
luaSyscalls(), luaSyscalls(),
this.client.syncMode this.client.syncMode
// In sync mode handle locally // In sync mode handle locally

View File

@ -1,6 +1,5 @@
The Client Store API provides a simple key-value store for client-specific states and preferences. The Client Store API provides a simple key-value store for client-specific states and preferences.
# Client Store API
## clientStore.set(key, value) ## clientStore.set(key, value)
Sets a value in the client store. Sets a value in the client store.

View File

@ -1,10 +1,9 @@
# Datastore API
The Datastore API provides functions for interacting with a key-value store that has query capabilities. The Datastore API provides functions for interacting with a key-value store that has query capabilities.
## Key-Value Operations # Key-Value Operations
### datastore.set(key, value) ## datastore.set(key, value)
Sets a value in the key-value store. Sets a value in the key-value store.
Example: Example:
@ -12,7 +11,7 @@ Example:
datastore.set("user:123", {name = "John", age = 30}) datastore.set("user:123", {name = "John", age = 30})
``` ```
### datastore.get(key) ## datastore.get(key)
Gets a value from the key-value store. Gets a value from the key-value store.
Example: Example:
@ -21,7 +20,7 @@ local user = datastore.get("user:123")
print(user.name) -- prints "John" print(user.name) -- prints "John"
``` ```
### datastore.del(key) ## datastore.del(key)
Deletes a value from the key-value store. Deletes a value from the key-value store.
Example: Example:
@ -29,9 +28,9 @@ Example:
datastore.del("user:123") datastore.del("user:123")
``` ```
## Batch Operations # Batch Operations
### datastore.batch_set(kvs) ## datastore.batch_set(kvs)
Sets multiple key-value pairs in a single operation. Sets multiple key-value pairs in a single operation.
Example: Example:
@ -43,7 +42,7 @@ local kvs = {
datastore.batch_set(kvs) datastore.batch_set(kvs)
``` ```
### datastore.batch_get(keys) ## datastore.batch_get(keys)
Gets multiple values in a single operation. Gets multiple values in a single operation.
Example: Example:
@ -55,7 +54,7 @@ for _, value in ipairs(values) do
end end
``` ```
### datastore.batch_del(keys) ## datastore.batch_del(keys)
Deletes multiple values in a single operation. Deletes multiple values in a single operation.
Example: Example:

34
website/API/index.md Normal file
View File

@ -0,0 +1,34 @@
The `index` API provides functions for interacting with SilverBullet's [[Objects]], allowing you to store and query page-associated data.
## Object Operations
### index.index_objects(page, objects)
Indexes an array of objects for a specific page.
Example:
```lua
local objects = {
{tag = "mytask", ref="task1", content = "Buy groceries"},
{tag = "mytask", ref="task2", content = "Write docs"}
}
index.index_objects("my page", objects)
```
### index.query_lua_objects(tag, query, scoped_variables?)
Queries objects using a Lua-based collection query.
Example:
```lua
local tasks = index.query_lua_objects("mytask", {limit=3})
```
### index.get_object_by_ref(page, tag, ref)
Retrieves a specific object by its reference.
Example:
```lua
local task = index.get_object_by_ref("my page", "mytask", "task1")
if task then
print("Found task: " .. task.content)
end
```

View File

@ -1,10 +1,8 @@
# Space API
The Space API provides functions for interacting with pages, attachments, and files in the space. The Space API provides functions for interacting with pages, attachments, and files in the space.
## Page Operations # Page Operations
### space.list_pages() ## space.list_pages()
Returns a list of all pages in the space. Returns a list of all pages in the space.
Example: Example:
@ -15,7 +13,7 @@ for page in each(pages) do
end end
``` ```
### space.read_page(name) ## space.read_page(name)
Reads the content of a page. Reads the content of a page.
Example: Example:
@ -24,7 +22,7 @@ local content = space.read_page("welcome")
print(content) -- prints the content of the "welcome" page print(content) -- prints the content of the "welcome" page
``` ```
### space.get_page_meta(name) ## space.get_page_meta(name)
Gets metadata for a specific page. Gets metadata for a specific page.
Example: Example:
@ -33,7 +31,7 @@ local meta = space.get_page_meta("welcome")
print(meta.name, meta.lastModified) -- prints page name and last modified date print(meta.name, meta.lastModified) -- prints page name and last modified date
``` ```
### space.write_page(name, text) ## space.write_page(name, text)
Writes content to a page. Writes content to a page.
Example: Example:
@ -42,7 +40,7 @@ local meta = space.write_page("notes", "My new note content")
print("Page updated at: " .. meta.lastModified) print("Page updated at: " .. meta.lastModified)
``` ```
### space.delete_page(name) ## space.delete_page(name)
Deletes a page from the space. Deletes a page from the space.
Example: Example:
@ -50,9 +48,9 @@ Example:
space.delete_page("old-notes") space.delete_page("old-notes")
``` ```
## Attachment Operations # Attachment Operations
### space.list_attachments() ## space.list_attachments()
Returns a list of all attachments in the space. Returns a list of all attachments in the space.
Example: Example:
@ -63,7 +61,7 @@ for att in each(attachments) do
end end
``` ```
### space.read_attachment(name) ## space.read_attachment(name)
Reads the content of an attachment. Reads the content of an attachment.
Example: Example:
@ -72,7 +70,7 @@ local data = space.read_attachment("image.png")
print("Attachment size: " .. #data .. " bytes") print("Attachment size: " .. #data .. " bytes")
``` ```
### space.write_attachment(name, data) ## space.write_attachment(name, data)
Writes binary data to an attachment. Writes binary data to an attachment.
Example: Example:
@ -82,7 +80,7 @@ local meta = space.write_attachment("test.bin", binary_data)
print("Attachment saved with size: " .. meta.size) print("Attachment saved with size: " .. meta.size)
``` ```
### space.delete_attachment(name) ## space.delete_attachment(name)
Deletes an attachment from the space. Deletes an attachment from the space.
Example: Example:
@ -90,9 +88,9 @@ Example:
space.delete_attachment("old-image.png") space.delete_attachment("old-image.png")
``` ```
## File Operations # File Operations
### space.list_files() ## space.list_files()
Returns a list of all files in the space. Returns a list of all files in the space.
Example: Example:
@ -103,7 +101,7 @@ for _, file in ipairs(files) do
end end
``` ```
### space.get_file_meta(name) ## space.get_file_meta(name)
Gets metadata for a specific file. Gets metadata for a specific file.
Example: Example:
@ -112,7 +110,7 @@ local meta = space.get_file_meta("document.txt")
print(meta.name, meta.modified, meta.size) print(meta.name, meta.modified, meta.size)
``` ```
### space.read_file(name) ## space.read_file(name)
Reads the content of a file. Reads the content of a file.
Example: Example:
@ -121,7 +119,7 @@ local content = space.read_file("document.txt")
print("File size: " .. #content .. " bytes") print("File size: " .. #content .. " bytes")
``` ```
### space.write_file(name, data) ## space.write_file(name, data)
Writes binary data to a file. Writes binary data to a file.
Example: Example:
@ -131,7 +129,7 @@ local meta = space.write_file("greeting.txt", text)
print("File written with size: " .. meta.size) print("File written with size: " .. meta.size)
``` ```
### space.delete_file(name) ## space.delete_file(name)
Deletes a file from the space. Deletes a file from the space.
Example: Example:
@ -139,7 +137,7 @@ Example:
space.delete_file("old-document.txt") space.delete_file("old-document.txt")
``` ```
### space.file_exists(name) ## space.file_exists(name)
Checks if a file exists in the space. Checks if a file exists in the space.
Example: Example:

View File

@ -9,65 +9,62 @@ OPENAI_API_KEY: yourapikeyhere
# Implementation # Implementation
```space-lua ```space-lua
openai = {} openai = {
Client = {}
}
openai.Client.__index = openai.Client
-- Initialize OpenAI, optionally OPENAI_API_KEY from your SECRETS page if not supplied directly -- Create a new OpenAI client instance
function openai.init(openaiApiKey) function openai.Client.new(apiKey)
if openai.client then -- Read SECRETS if no API key provided
-- Already initialized if not apiKey then
return local secretsPage = space.readPage("SECRETS")
end apiKey = string.match(secretsPage, "OPENAI_API_KEY: (%S+)")
if not openaiApiKey then end
-- Read SECRETS if not apiKey then
local secretsPage = space.readPage("SECRETS") error("No OpenAI API key supplied")
-- Find the line with the pattern OPENAI_API_KEY: <key> and extract the key end
openaiApiKey = string.match(secretsPage, "OPENAI_API_KEY: (%S+)")
end local openai_lib = js.import("https://esm.sh/openai")
if not openaiApiKey then local client = js.new(openai_lib.OpenAI, {
error("No OpenAI API key supplied") apiKey = apiKey,
end dangerouslyAllowBrowser = true
})
local openai_lib = js.import("https://esm.sh/openai")
openai.client = js.new(openai_lib.OpenAI, { local self = setmetatable({
apiKey = openaiApiKey, client = client
dangerouslyAllowBrowser = true }, OpenAIClient)
})
end return self
end
function openai.ensure_inited()
if not openai.client then -- Chat completion method
error("OpenAI not yet initialized") function openai.Client:chat(message)
end local r = self.client.chat.completions.create({
end model = "gpt-4o-mini",
messages = {
function openai.chat(message) { role = "user", content = message },
openai.ensure_inited() },
local r = openai.client.chat.completions.create({ })
model = "gpt-4o-mini", return r.choices[1].message.content
messages = { end
{ role = "user", content = message },
}, -- Streaming chat completion method
}) function openai.Client:stream_chat(message)
return r.choices[1].message.content local r = self.client.chat.completions.create({
end model = "gpt-4o-mini",
messages = {
{ role = "user", content = message },
function openai.stream_chat(message) },
openai.ensure_inited() stream = true,
local r = openai.client.chat.completions.create({ })
model = "gpt-4o-mini", local iterator = js.each_iterable(r)
messages = { return function()
{ role = "user", content = message }, local el = iterator()
}, if el then
stream = true, return el.choices[1].delta.content
}) end
local iterator = js.each_iterable(r)
return function()
local el = iterator()
if el then
return el.choices[1].delta.content
end end
end
end end
``` ```