import { datastore } from "$sb/syscalls.ts"; import { KV, KvKey, KvQuery, ObjectQuery, ObjectValue } from "$sb/types.ts"; import { QueryProviderEvent } from "$sb/app_event.ts"; import { builtins } from "./builtins.ts"; import { AttributeObject, determineType } from "./attributes.ts"; const indexKey = "idx"; const pageKey = "ridx"; /* * Key namespace: * [indexKey, type, ...key, page] -> value * [pageKey, page, ...key] -> true // for fast page clearing * ["type", type] -> true // for fast type listing */ export function batchSet(page: string, kvs: KV[]): Promise { const finalBatch: KV[] = []; for (const { key, value } of kvs) { finalBatch.push({ key: [indexKey, ...key, page], value, }, { key: [pageKey, page, ...key], value: true, }); } return datastore.batchSet(finalBatch); } /** * Clears all keys for a given page * @param page */ export async function clearPageIndex(page: string): Promise { const allKeys: KvKey[] = []; for ( const { key } of await datastore.query({ prefix: [pageKey, page], }) ) { allKeys.push(key); allKeys.push([indexKey, ...key.slice(2), page]); } await datastore.batchDel(allKeys); } /** * Clears the entire datastore for this indexKey plug */ export async function clearIndex(): Promise { const allKeys: KvKey[] = []; for ( const { key } of await datastore.query({ prefix: [] }) ) { allKeys.push(key); } await datastore.batchDel(allKeys); console.log("Deleted", allKeys.length, "keys from the index"); } // ENTITIES API /** * Indexes entities in the data store */ export async function indexObjects( page: string, objects: ObjectValue[], ): Promise { const kvs: KV[] = []; const allAttributes = new Map(); // tag:name -> attributeType for (const obj of objects) { for (const tag of obj.tags) { // The object itself kvs.push({ key: [tag, cleanKey(obj.ref, page)], value: obj, }); // Index attributes const builtinAttributes = builtins[tag]; if (!builtinAttributes) { // This is not a builtin tag, so we index all attributes (almost, see below) attributeLabel: for ( const [attrName, attrValue] of Object.entries( obj as Record, ) ) { if (attrName.startsWith("$")) { continue; } // Check for all tags attached to this object if they're builtins // If so: if `attrName` is defined in the builtin, use the attributeType from there (mostly to preserve readOnly aspects) for (const otherTag of obj.tags) { const builtinAttributes = builtins[otherTag]; if (builtinAttributes && builtinAttributes[attrName]) { allAttributes.set( `${tag}:${attrName}`, builtinAttributes[attrName], ); continue attributeLabel; } } allAttributes.set(`${tag}:${attrName}`, determineType(attrValue)); } } else if (tag !== "attribute") { // For builtin tags, only index custom ones for ( const [attrName, attrValue] of Object.entries( obj as Record, ) ) { // console.log("Indexing", tag, attrName, attrValue); // Skip builtins and internal attributes if (builtinAttributes[attrName] || attrName.startsWith("$")) { continue; } allAttributes.set(`${tag}:${attrName}`, determineType(attrValue)); } } } } if (allAttributes.size > 0) { await indexObjects( page, [...allAttributes].map(([key, value]) => { const [tag, name] = key.split(":"); const attributeType = value.startsWith("!") ? value.substring(1) : value; return { ref: key, tags: ["attribute"], tag, name, attributeType, readOnly: value.startsWith("!"), page, }; }), ); } return batchSet(page, kvs); } function cleanKey(ref: string, page: string) { if (ref.startsWith(`${page}@`)) { return ref.substring(page.length + 1); } else { return ref; } } export async function queryObjects( tag: string, query: ObjectQuery, ): Promise[]> { return (await datastore.query({ ...query, prefix: [indexKey, tag], distinct: true, })).map(({ value }) => value); } export async function query( query: KvQuery, ): Promise { return (await datastore.query({ ...query, prefix: [indexKey, ...query.prefix ? query.prefix : []], })).map(({ key, value }) => ({ key: key.slice(1), value })); } export function getObjectByRef( page: string, tag: string, ref: string, ): Promise | undefined> { return datastore.get([indexKey, tag, cleanKey(ref, page), page]); } export async function objectSourceProvider({ query, }: QueryProviderEvent): Promise { const tag = query.querySource!; const results = await datastore.query({ ...query, prefix: [indexKey, tag], distinct: true, }); return results.map((r) => r.value); } export async function discoverSources() { return (await datastore.query({ prefix: [indexKey, "tag"], select: [{ name: "name" }], distinct: true, })).map(( { value }, ) => value.name); }