mirror of
https://github.com/kunkunsh/kunkun.git
synced 2025-04-12 09:49:42 +00:00

* feat: add kv store API for extensions * feat: add kv api to @kksh/api package * bump: @kksh/api to 0.0.47 * feat: add IKV type export to UI module * feat: add delete api for KV API
438 lines
12 KiB
TypeScript
438 lines
12 KiB
TypeScript
import { invoke } from "@tauri-apps/api/core"
|
|
import { array, literal, optional, parse, safeParse, union, type InferOutput } from "valibot"
|
|
import { KUNKUN_EXT_IDENTIFIER } from "../constants"
|
|
import { CmdType, Ext, ExtCmd, ExtData } from "../models/extension"
|
|
import { convertDateToSqliteString, SearchMode, SearchModeEnum, SQLSortOrder } from "../models/sql"
|
|
import { generateJarvisPluginCommand } from "./common"
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Extension CRUD */
|
|
/* -------------------------------------------------------------------------- */
|
|
export function createExtension(ext: {
|
|
identifier: string
|
|
version: string
|
|
enabled?: boolean
|
|
path?: string
|
|
data?: any
|
|
}) {
|
|
return invoke<void>(generateJarvisPluginCommand("create_extension"), ext)
|
|
}
|
|
|
|
export function getAllExtensions() {
|
|
return invoke<Ext[]>(generateJarvisPluginCommand("get_all_extensions"))
|
|
}
|
|
|
|
export function getUniqueExtensionByIdentifier(identifier: string) {
|
|
return invoke<Ext | undefined>(
|
|
generateJarvisPluginCommand("get_unique_extension_by_identifier"),
|
|
{
|
|
identifier
|
|
}
|
|
)
|
|
}
|
|
|
|
export function getUniqueExtensionByPath(path: string) {
|
|
return invoke<Ext | undefined>(generateJarvisPluginCommand("get_unique_extension_by_path"), {
|
|
path
|
|
})
|
|
}
|
|
|
|
export function getAllExtensionsByIdentifier(identifier: string) {
|
|
return invoke<Ext[]>(generateJarvisPluginCommand("get_all_extensions_by_identifier"), {
|
|
identifier
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Use this function when you expect the extension to exist. Such as builtin extensions.
|
|
* @param identifier
|
|
* @returns
|
|
*/
|
|
export function getExtensionByIdentifierExpectExists(identifier: string): Promise<Ext> {
|
|
return getUniqueExtensionByIdentifier(identifier).then((ext) => {
|
|
if (!ext) {
|
|
throw new Error(`Unexpexted Error: Extension ${identifier} not found`)
|
|
}
|
|
return ext
|
|
})
|
|
}
|
|
|
|
// TODO: clean this up
|
|
// export function deleteExtensionByIdentifier(identifier: string) {
|
|
// return invoke<void>(generateJarvisPluginCommand("delete_extension_by_identifier"), { identifier })
|
|
// }
|
|
|
|
export function deleteExtensionByPath(path: string) {
|
|
return invoke<void>(generateJarvisPluginCommand("delete_extension_by_path"), {
|
|
path
|
|
})
|
|
}
|
|
|
|
export function deleteExtensionByExtId(extId: string) {
|
|
return invoke<void>(generateJarvisPluginCommand("delete_extension_by_ext_id"), { extId })
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Extension Command CRUD */
|
|
/* -------------------------------------------------------------------------- */
|
|
export function createCommand(data: {
|
|
extId: number
|
|
name: string
|
|
cmdType: CmdType
|
|
data: string
|
|
alias?: string
|
|
hotkey?: string
|
|
enabled?: boolean
|
|
}) {
|
|
return invoke<void>(generateJarvisPluginCommand("create_command"), {
|
|
...data,
|
|
enabled: data.enabled ?? false
|
|
})
|
|
}
|
|
|
|
export function getCommandById(cmdId: number) {
|
|
return invoke<ExtCmd | undefined>(generateJarvisPluginCommand("get_command_by_id"), { cmdId })
|
|
}
|
|
|
|
export function getCommandsByExtId(extId: number) {
|
|
return invoke<ExtCmd[]>(generateJarvisPluginCommand("get_commands_by_ext_id"), { extId })
|
|
}
|
|
|
|
export function deleteCommandById(cmdId: number) {
|
|
return invoke<void>(generateJarvisPluginCommand("delete_command_by_id"), {
|
|
cmdId
|
|
})
|
|
}
|
|
|
|
export function updateCommandById(data: {
|
|
cmdId: number
|
|
name: string
|
|
cmdType: CmdType
|
|
data: string
|
|
alias?: string
|
|
hotkey?: string
|
|
enabled: boolean
|
|
}) {
|
|
return invoke<void>(generateJarvisPluginCommand("update_command_by_id"), data)
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Extension Data CRUD */
|
|
/* -------------------------------------------------------------------------- */
|
|
export const ExtDataField = union([literal("data"), literal("search_text")])
|
|
export type ExtDataField = InferOutput<typeof ExtDataField>
|
|
|
|
function convertRawExtDataToExtData(rawData?: {
|
|
createdAt: string
|
|
updatedAt: string
|
|
data: null | string
|
|
searchText: null | string
|
|
}): ExtData | undefined {
|
|
if (!rawData) {
|
|
return rawData
|
|
}
|
|
const parsedRes = safeParse(ExtData, {
|
|
...rawData,
|
|
createdAt: new Date(rawData.createdAt),
|
|
updatedAt: new Date(rawData.updatedAt),
|
|
data: rawData.data ?? undefined,
|
|
searchText: rawData.searchText ?? undefined
|
|
})
|
|
if (parsedRes.success) {
|
|
return parsedRes.output
|
|
} else {
|
|
console.error("Extension Data Parse Failure", parsedRes.issues)
|
|
throw new Error("Fail to parse extension data")
|
|
}
|
|
}
|
|
|
|
export function createExtensionData(data: {
|
|
extId: number
|
|
dataType: string
|
|
data: string
|
|
searchText?: string
|
|
}) {
|
|
return invoke<void>(generateJarvisPluginCommand("create_extension_data"), data)
|
|
}
|
|
|
|
export function getExtensionDataById(dataId: number, fields?: ExtDataField[]) {
|
|
return invoke<
|
|
| (ExtData & {
|
|
createdAt: string
|
|
updatedAt: string
|
|
data: null | string
|
|
searchText: null | string
|
|
})
|
|
| undefined
|
|
>(generateJarvisPluginCommand("get_extension_data_by_id"), {
|
|
dataId,
|
|
fields
|
|
}).then(convertRawExtDataToExtData)
|
|
}
|
|
|
|
/**
|
|
* Fields option can be used to select optional fields. By default, if left empty, data and searchText are not returned.
|
|
* This is because data and searchText can be large and we don't want to return them by default.
|
|
* If you just want to get data ids in order to delete them, retrieving all data is not necessary.
|
|
* @param searchParams
|
|
*/
|
|
export async function searchExtensionData(searchParams: {
|
|
extId: number
|
|
searchMode: SearchMode
|
|
dataId?: number
|
|
dataType?: string
|
|
searchText?: string
|
|
afterCreatedAt?: string
|
|
beforeCreatedAt?: string
|
|
limit?: number
|
|
offset?: number
|
|
orderByCreatedAt?: SQLSortOrder
|
|
orderByUpdatedAt?: SQLSortOrder
|
|
fields?: ExtDataField[]
|
|
}): Promise<ExtData[]> {
|
|
const fields = parse(optional(array(ExtDataField), []), searchParams.fields)
|
|
let items = await invoke<
|
|
(ExtData & {
|
|
createdAt: string
|
|
updatedAt: string
|
|
data: null | string
|
|
searchText: null | string
|
|
})[]
|
|
>(generateJarvisPluginCommand("search_extension_data"), {
|
|
searchQuery: {
|
|
...searchParams,
|
|
fields
|
|
}
|
|
})
|
|
|
|
return items.map(convertRawExtDataToExtData).filter((item) => item) as ExtData[]
|
|
}
|
|
|
|
export function deleteExtensionDataById(dataId: number) {
|
|
return invoke<void>(generateJarvisPluginCommand("delete_extension_data_by_id"), { dataId })
|
|
}
|
|
|
|
export function updateExtensionDataById(data: {
|
|
dataId: number
|
|
data: string
|
|
searchText?: string
|
|
}) {
|
|
return invoke<void>(generateJarvisPluginCommand("update_extension_data_by_id"), data)
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Built-in Extensions */
|
|
/* -------------------------------------------------------------------------- */
|
|
export function getExtClipboard() {
|
|
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_CLIPBOARD_EXT_IDENTIFIER)
|
|
}
|
|
export function getExtQuickLinks() {
|
|
return getExtensionByIdentifierExpectExists(
|
|
KUNKUN_EXT_IDENTIFIER.KUNKUN_QUICK_LINKS_EXT_IDENTIFIER
|
|
)
|
|
}
|
|
export function getExtRemote() {
|
|
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_REMOTE_EXT_IDENTIFIER)
|
|
}
|
|
export function getExtScriptCmd() {
|
|
return getExtensionByIdentifierExpectExists(
|
|
KUNKUN_EXT_IDENTIFIER.KUNKUN_SCRIPT_CMD_EXT_IDENTIFIER
|
|
)
|
|
}
|
|
export function getExtDev() {
|
|
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_DEV_EXT_IDENTIFIER)
|
|
}
|
|
|
|
/**
|
|
* Database API for extensions.
|
|
* Extensions shouldn't have full access to the database, they can only access their own data.
|
|
* When an extension is loaded, the main thread will create an instance of this class and
|
|
* expose it to the extension.
|
|
*/
|
|
export class JarvisExtDB {
|
|
extId: number
|
|
|
|
constructor(extId: number) {
|
|
this.extId = extId
|
|
}
|
|
|
|
async add(data: { data: string; dataType?: string; searchText?: string }) {
|
|
return createExtensionData({
|
|
data: data.data,
|
|
dataType: data.dataType ?? "default",
|
|
searchText: data.searchText,
|
|
extId: this.extId
|
|
})
|
|
}
|
|
|
|
async delete(dataId: number): Promise<void> {
|
|
// Verify if this data belongs to this extension
|
|
const d = await getExtensionDataById(dataId)
|
|
if (!d || d.extId !== this.extId) {
|
|
throw new Error("Extension Data not found")
|
|
}
|
|
return await deleteExtensionDataById(dataId)
|
|
}
|
|
|
|
async search(searchParams: {
|
|
dataId?: number
|
|
searchMode?: SearchMode
|
|
dataType?: string
|
|
searchText?: string
|
|
afterCreatedAt?: Date
|
|
beforeCreatedAt?: Date
|
|
limit?: number
|
|
orderByCreatedAt?: SQLSortOrder
|
|
orderByUpdatedAt?: SQLSortOrder
|
|
fields?: ExtDataField[]
|
|
}): Promise<ExtData[]> {
|
|
const beforeCreatedAt = searchParams.beforeCreatedAt
|
|
? convertDateToSqliteString(searchParams.beforeCreatedAt)
|
|
: undefined
|
|
const afterCreatedAt = searchParams.afterCreatedAt
|
|
? convertDateToSqliteString(searchParams.afterCreatedAt)
|
|
: undefined
|
|
return searchExtensionData({
|
|
...searchParams,
|
|
searchMode: searchParams.searchMode ?? SearchModeEnum.FTS,
|
|
extId: this.extId,
|
|
beforeCreatedAt,
|
|
afterCreatedAt
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Retrieve all data of this extension.
|
|
* Use `search()` method for more advanced search.
|
|
* @param options optional fields to retrieve. By default, data and searchText are not returned.
|
|
* @returns
|
|
*/
|
|
retrieveAll(options: { fields?: ExtDataField[] }): Promise<ExtData[]> {
|
|
return this.search({ fields: options.fields })
|
|
}
|
|
|
|
/**
|
|
* Retrieve all data of this extension by type.
|
|
* Use `search()` method for more advanced search.
|
|
* @param dataType
|
|
* @returns
|
|
*/
|
|
retrieveAllByType(dataType: string): Promise<ExtData[]> {
|
|
return this.search({ dataType })
|
|
}
|
|
|
|
/**
|
|
* Delete all data of this extension.
|
|
*/
|
|
deleteAll(): Promise<void> {
|
|
return this.search({})
|
|
.then((items) => {
|
|
return Promise.all(items.map((item) => this.delete(item.dataId)))
|
|
})
|
|
.then(() => {})
|
|
}
|
|
|
|
/**
|
|
* Update data and searchText of this extension.
|
|
* @param dataId unique id of the data
|
|
* @param data
|
|
* @param searchText
|
|
* @returns
|
|
*/
|
|
async update(data: { dataId: number; data: string; searchText?: string }): Promise<void> {
|
|
const d = await getExtensionDataById(data.dataId)
|
|
if (!d || d.extId !== this.extId) {
|
|
throw new Error("Extension Data not found")
|
|
}
|
|
return updateExtensionDataById(data)
|
|
}
|
|
}
|
|
|
|
export class KV {
|
|
extId: number
|
|
db: JarvisExtDB
|
|
private DataType: string = "kunkun_kv"
|
|
|
|
constructor(extId: number) {
|
|
this.extId = extId
|
|
this.db = new JarvisExtDB(extId)
|
|
}
|
|
|
|
get<T = string>(key: string): Promise<T | null | undefined> {
|
|
return this.db
|
|
.search({
|
|
dataType: this.DataType,
|
|
searchText: key,
|
|
searchMode: SearchModeEnum.ExactMatch,
|
|
fields: ["search_text", "data"]
|
|
})
|
|
.then((items) => {
|
|
if (items.length === 0) {
|
|
return null
|
|
} else if (items.length > 1) {
|
|
throw new Error("Multiple KVs with the same key")
|
|
}
|
|
return items[0].data ? (JSON.parse(items[0].data).value as T) : null
|
|
})
|
|
.catch((err) => {
|
|
console.warn(err)
|
|
return null
|
|
})
|
|
}
|
|
|
|
set(key: string, value: string): Promise<void> {
|
|
return this.db
|
|
.search({
|
|
dataType: this.DataType,
|
|
searchText: key,
|
|
searchMode: SearchModeEnum.ExactMatch
|
|
})
|
|
.then((items) => {
|
|
if (items.length === 0) {
|
|
return this.db.add({
|
|
data: JSON.stringify({ value: value }),
|
|
dataType: this.DataType,
|
|
searchText: key
|
|
})
|
|
} else if (items.length === 1) {
|
|
return this.db.update({
|
|
dataId: items[0].dataId,
|
|
data: JSON.stringify({ value: value }),
|
|
searchText: key
|
|
})
|
|
} else {
|
|
return Promise.all(items.map((item) => this.db.delete(item.dataId))).then(() =>
|
|
Promise.resolve()
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
delete(key: string): Promise<void> {
|
|
return this.db
|
|
.search({
|
|
dataType: this.DataType,
|
|
searchText: key,
|
|
searchMode: SearchModeEnum.ExactMatch
|
|
})
|
|
.then((items) => {
|
|
return Promise.all(items.map((item) => this.db.delete(item.dataId))).then(() =>
|
|
Promise.resolve()
|
|
)
|
|
})
|
|
}
|
|
|
|
exists(key: string): Promise<boolean> {
|
|
return this.db
|
|
.search({
|
|
dataType: this.DataType,
|
|
searchText: key,
|
|
searchMode: SearchModeEnum.ExactMatch,
|
|
fields: []
|
|
})
|
|
.then((items) => {
|
|
return items.length > 0
|
|
})
|
|
}
|
|
}
|