2024-08-24 18:35:09 +08:00
|
|
|
import { datastore, system } from "@silverbulletmd/silverbullet/syscalls";
|
2024-07-30 23:33:33 +08:00
|
|
|
import type {
|
2024-02-09 04:00:45 +08:00
|
|
|
KV,
|
|
|
|
KvKey,
|
|
|
|
KvQuery,
|
|
|
|
ObjectQuery,
|
|
|
|
ObjectValue,
|
2024-02-29 22:23:05 +08:00
|
|
|
} from "../../plug-api/types.ts";
|
2024-07-30 23:33:33 +08:00
|
|
|
import type { QueryProviderEvent } from "../../plug-api/types.ts";
|
2024-08-24 18:35:09 +08:00
|
|
|
import { determineType, type SimpleJSONType } from "./attributes.ts";
|
2024-02-09 04:00:45 +08:00
|
|
|
import { ttlCache } from "$lib/memory_cache.ts";
|
2023-10-03 20:16:33 +08:00
|
|
|
|
|
|
|
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<void> {
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-05-28 02:33:41 +08:00
|
|
|
* Clears all keys for a given file
|
|
|
|
* @param file
|
2023-10-03 20:16:33 +08:00
|
|
|
*/
|
2024-05-28 02:33:41 +08:00
|
|
|
export async function clearFileIndex(file: string): Promise<void> {
|
|
|
|
if (file.endsWith(".md")) {
|
|
|
|
file = file.replace(/\.md$/, "");
|
|
|
|
}
|
2023-10-03 20:16:33 +08:00
|
|
|
const allKeys: KvKey[] = [];
|
|
|
|
for (
|
|
|
|
const { key } of await datastore.query({
|
2024-05-28 02:33:41 +08:00
|
|
|
prefix: [pageKey, file],
|
2023-10-03 20:16:33 +08:00
|
|
|
})
|
|
|
|
) {
|
|
|
|
allKeys.push(key);
|
2024-05-28 02:33:41 +08:00
|
|
|
allKeys.push([indexKey, ...key.slice(2), file]);
|
2023-10-03 20:16:33 +08:00
|
|
|
}
|
|
|
|
await datastore.batchDel(allKeys);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-05-28 02:33:41 +08:00
|
|
|
* Clears the entire index
|
2023-10-03 20:16:33 +08:00
|
|
|
*/
|
|
|
|
export async function clearIndex(): Promise<void> {
|
|
|
|
const allKeys: KvKey[] = [];
|
|
|
|
for (
|
2024-01-27 00:05:10 +08:00
|
|
|
const { key } of await datastore.query({ prefix: [indexKey] })
|
|
|
|
) {
|
|
|
|
allKeys.push(key);
|
|
|
|
}
|
|
|
|
for (
|
|
|
|
const { key } of await datastore.query({ prefix: [pageKey] })
|
2023-10-03 20:16:33 +08:00
|
|
|
) {
|
|
|
|
allKeys.push(key);
|
|
|
|
}
|
|
|
|
await datastore.batchDel(allKeys);
|
|
|
|
console.log("Deleted", allKeys.length, "keys from the index");
|
|
|
|
}
|
|
|
|
|
2024-01-21 02:16:07 +08:00
|
|
|
// OBJECTS API
|
2023-10-03 20:16:33 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Indexes entities in the data store
|
|
|
|
*/
|
2024-08-24 18:35:09 +08:00
|
|
|
export async function indexObjects<T>(
|
2023-10-03 20:16:33 +08:00
|
|
|
page: string,
|
|
|
|
objects: ObjectValue<T>[],
|
|
|
|
): Promise<void> {
|
|
|
|
const kvs: KV<T>[] = [];
|
2024-08-24 18:35:09 +08:00
|
|
|
const schema = await system.getSpaceConfig("schema");
|
|
|
|
const allAttributes = new Map<string, SimpleJSONType>();
|
2023-10-03 20:16:33 +08:00
|
|
|
for (const obj of objects) {
|
2024-01-11 20:20:50 +08:00
|
|
|
if (!obj.tag) {
|
|
|
|
console.error("Object has no tag", obj, "this shouldn't happen");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Index as all the tag + any additional tags specified
|
|
|
|
const allTags = [obj.tag, ...obj.tags || []];
|
2024-08-24 18:35:09 +08:00
|
|
|
const tagSchemaProperties =
|
|
|
|
schema.tag[obj.tag] && schema.tag[obj.tag].properties || {};
|
2024-01-11 20:20:50 +08:00
|
|
|
for (const tag of allTags) {
|
2023-12-22 01:37:50 +08:00
|
|
|
// The object itself
|
2023-10-03 20:16:33 +08:00
|
|
|
kvs.push({
|
|
|
|
key: [tag, cleanKey(obj.ref, page)],
|
|
|
|
value: obj,
|
|
|
|
});
|
|
|
|
// Index attributes
|
2024-08-24 18:35:09 +08:00
|
|
|
const schemaAttributes = schema.tag[tag] && schema.tag[tag].properties;
|
|
|
|
if (!schemaAttributes) {
|
|
|
|
// There is no schema definition for this tag, so we index all attributes
|
|
|
|
for (
|
2023-10-03 20:16:33 +08:00
|
|
|
const [attrName, attrValue] of Object.entries(
|
|
|
|
obj as Record<string, any>,
|
|
|
|
)
|
|
|
|
) {
|
2024-08-24 18:35:09 +08:00
|
|
|
if (attrName.startsWith("$") || tagSchemaProperties[attrName]) {
|
2023-10-03 20:16:33 +08:00
|
|
|
continue;
|
|
|
|
}
|
2024-08-24 18:35:09 +08:00
|
|
|
|
2023-10-03 20:16:33 +08:00
|
|
|
allAttributes.set(`${tag}:${attrName}`, determineType(attrValue));
|
|
|
|
}
|
2024-08-24 18:35:09 +08:00
|
|
|
} else {
|
|
|
|
// For tags with schemas, only index attributes that are not in the schema
|
2023-10-10 02:39:03 +08:00
|
|
|
for (
|
|
|
|
const [attrName, attrValue] of Object.entries(
|
|
|
|
obj as Record<string, any>,
|
|
|
|
)
|
|
|
|
) {
|
2024-08-24 18:35:09 +08:00
|
|
|
// Skip schema-defined and internal attributes
|
|
|
|
if (
|
|
|
|
schemaAttributes[attrName] || tagSchemaProperties[attrName] ||
|
|
|
|
attrName.startsWith("$")
|
|
|
|
) {
|
2023-10-10 02:39:03 +08:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
allAttributes.set(`${tag}:${attrName}`, determineType(attrValue));
|
|
|
|
}
|
2023-10-03 20:16:33 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (allAttributes.size > 0) {
|
2024-01-14 00:30:15 +08:00
|
|
|
[...allAttributes].forEach(([key, value]) => {
|
|
|
|
const [tagName, name] = key.split(":");
|
|
|
|
kvs.push({
|
2024-08-24 18:35:09 +08:00
|
|
|
key: ["ah-attr", cleanKey(key, page)],
|
2024-01-14 00:30:15 +08:00
|
|
|
value: {
|
2023-10-03 20:16:33 +08:00
|
|
|
ref: key,
|
2024-08-24 18:35:09 +08:00
|
|
|
tag: "ah-attr",
|
2024-01-11 20:20:50 +08:00
|
|
|
tagName,
|
2023-10-03 20:16:33 +08:00
|
|
|
name,
|
|
|
|
page,
|
2024-08-24 18:35:09 +08:00
|
|
|
schema: value,
|
2024-01-14 00:30:15 +08:00
|
|
|
} as T,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (kvs.length > 0) {
|
|
|
|
return batchSet(page, kvs);
|
|
|
|
} else {
|
|
|
|
return Promise.resolve();
|
2023-10-03 20:16:33 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function cleanKey(ref: string, page: string) {
|
|
|
|
if (ref.startsWith(`${page}@`)) {
|
|
|
|
return ref.substring(page.length + 1);
|
|
|
|
} else {
|
|
|
|
return ref;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-15 23:43:12 +08:00
|
|
|
export function queryObjects<T>(
|
2023-10-03 20:16:33 +08:00
|
|
|
tag: string,
|
|
|
|
query: ObjectQuery,
|
2024-01-15 23:43:12 +08:00
|
|
|
ttlSecs?: number,
|
2023-10-03 20:16:33 +08:00
|
|
|
): Promise<ObjectValue<T>[]> {
|
2024-01-15 23:43:12 +08:00
|
|
|
return ttlCache(query, async () => {
|
|
|
|
return (await datastore.query({
|
|
|
|
...query,
|
|
|
|
prefix: [indexKey, tag],
|
|
|
|
distinct: true,
|
|
|
|
})).map(({ value }) => value);
|
|
|
|
}, ttlSecs);
|
2023-10-03 20:16:33 +08:00
|
|
|
}
|
|
|
|
|
2024-08-24 18:35:09 +08:00
|
|
|
export function queryDeleteObjects<T>(
|
|
|
|
tag: string,
|
|
|
|
query: ObjectQuery,
|
|
|
|
): Promise<void> {
|
|
|
|
return datastore.queryDelete({
|
|
|
|
...query,
|
|
|
|
prefix: [indexKey, tag],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-10-03 21:24:07 +08:00
|
|
|
export async function query(
|
|
|
|
query: KvQuery,
|
2024-02-03 02:19:07 +08:00
|
|
|
variables?: Record<string, any>,
|
2023-10-03 21:24:07 +08:00
|
|
|
): Promise<KV[]> {
|
|
|
|
return (await datastore.query({
|
|
|
|
...query,
|
|
|
|
prefix: [indexKey, ...query.prefix ? query.prefix : []],
|
2024-02-03 02:19:07 +08:00
|
|
|
}, variables)).map(({ key, value }) => ({ key: key.slice(1), value }));
|
2023-10-03 21:24:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export function getObjectByRef<T>(
|
2023-10-03 20:16:33 +08:00
|
|
|
page: string,
|
|
|
|
tag: string,
|
|
|
|
ref: string,
|
|
|
|
): Promise<ObjectValue<T> | undefined> {
|
2023-10-03 21:24:07 +08:00
|
|
|
return datastore.get([indexKey, tag, cleanKey(ref, page), page]);
|
2023-10-03 20:16:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function objectSourceProvider({
|
|
|
|
query,
|
2024-02-03 02:19:07 +08:00
|
|
|
variables,
|
2023-10-03 20:16:33 +08:00
|
|
|
}: QueryProviderEvent): Promise<any[]> {
|
|
|
|
const tag = query.querySource!;
|
|
|
|
const results = await datastore.query({
|
|
|
|
...query,
|
|
|
|
prefix: [indexKey, tag],
|
2023-10-03 21:24:07 +08:00
|
|
|
distinct: true,
|
2024-02-03 02:19:07 +08:00
|
|
|
}, variables);
|
2023-10-03 20:16:33 +08:00
|
|
|
return results.map((r) => r.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function discoverSources() {
|
2024-08-24 18:35:09 +08:00
|
|
|
const schema = await system.getSpaceConfig("schema");
|
|
|
|
// Query all tags we indexed
|
2023-12-22 18:27:07 +08:00
|
|
|
return (await datastore.query({
|
|
|
|
prefix: [indexKey, "tag"],
|
|
|
|
select: [{ name: "name" }],
|
|
|
|
distinct: true,
|
|
|
|
})).map((
|
2023-10-13 22:33:37 +08:00
|
|
|
{ value },
|
2024-08-24 18:35:09 +08:00
|
|
|
) => value.name)
|
|
|
|
// And concatenate all the tags from the schema
|
|
|
|
.concat(Object.keys(schema.tag));
|
2023-10-03 20:16:33 +08:00
|
|
|
}
|