refactor: migrate db tauri commands to drizzle

This commit is contained in:
Huakun Shen 2025-04-01 05:31:13 -04:00
parent 92c69430ff
commit 8fd3223b66
No known key found for this signature in database
30 changed files with 636 additions and 844 deletions

View File

@ -7,10 +7,11 @@ import { decideKkrpcSerialization } from "@/utils/kkrpc"
import { sleep } from "@/utils/time" import { sleep } from "@/utils/time"
import { trimSlash } from "@/utils/url" import { trimSlash } from "@/utils/url"
import { constructExtensionSupportDir } from "@kksh/api" import { constructExtensionSupportDir } from "@kksh/api"
import { db, spawnExtensionFileServer } from "@kksh/api/commands" import { spawnExtensionFileServer } from "@kksh/api/commands"
import type { HeadlessCommand } from "@kksh/api/headless" import type { HeadlessCommand } from "@kksh/api/headless"
import { CustomUiCmd, ExtPackageJsonExtra, HeadlessCmd, TemplateUiCmd } from "@kksh/api/models" import { CustomUiCmd, ExtPackageJsonExtra, HeadlessCmd, TemplateUiCmd } from "@kksh/api/models"
import { constructJarvisServerAPIWithPermissions, type IApp } from "@kksh/api/ui" import { constructJarvisServerAPIWithPermissions, type IApp } from "@kksh/api/ui"
import { db } from "@kksh/drizzle"
import { launchNewExtWindow, loadExtensionManifestFromDisk } from "@kksh/extension" import { launchNewExtWindow, loadExtensionManifestFromDisk } from "@kksh/extension"
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server" import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server"
import { convertFileSrc } from "@tauri-apps/api/core" import { convertFileSrc } from "@tauri-apps/api/core"

View File

@ -1,5 +1,5 @@
import { db } from "@kksh/api/commands"
import type { CustomUiCmd, ExtPackageJsonExtra, HeadlessCmd, TemplateUiCmd } from "@kksh/api/models" import type { CustomUiCmd, ExtPackageJsonExtra, HeadlessCmd, TemplateUiCmd } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import * as extAPI from "@kksh/extension" import * as extAPI from "@kksh/extension"
import * as path from "@tauri-apps/api/path" import * as path from "@tauri-apps/api/path"
import Fuse from "fuse.js" import Fuse from "fuse.js"

View File

@ -12,6 +12,7 @@
<svelte:window on:keydown={handleKeyDown} /> <svelte:window on:keydown={handleKeyDown} />
<div class="fixed h-12 w-full" data-tauri-drag-region></div>
<Layouts.Center class="min-h-screen py-5"> <Layouts.Center class="min-h-screen py-5">
<Error.RawErrorJSONPreset <Error.RawErrorJSONPreset
title="Error" title="Error"

View File

@ -5,8 +5,6 @@
import { systemCommands, systemCommandsFiltered } from "@/cmds/system" import { systemCommands, systemCommandsFiltered } from "@/cmds/system"
import AppsCmds from "@/components/main/AppsCmds.svelte" import AppsCmds from "@/components/main/AppsCmds.svelte"
import { i18n } from "@/i18n" import { i18n } from "@/i18n"
import { getUniqueExtensionByIdentifier } from "@/orm/cmds"
import { db } from "@/orm/database"
import * as m from "@/paraglide/messages" import * as m from "@/paraglide/messages"
import { import {
appConfig, appConfig,

View File

@ -3,8 +3,9 @@
import { goHome } from "@/utils/route" import { goHome } from "@/utils/route"
import { listenToNewClipboardItem, listenToWindowFocus } from "@/utils/tauri-events" import { listenToNewClipboardItem, listenToWindowFocus } from "@/utils/tauri-events"
import Icon from "@iconify/svelte" import Icon from "@iconify/svelte"
import { ClipboardContentType, db } from "@kksh/api/commands" import { ClipboardContentType } from "@kksh/api/commands"
import { SearchModeEnum, SQLSortOrderEnum, type ExtData } from "@kksh/api/models" import { SearchModeEnum, SQLSortOrderEnum, type ExtData } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { Button, Command, Resizable } from "@kksh/svelte5" import { Button, Command, Resizable } from "@kksh/svelte5"
import { Constants } from "@kksh/ui" import { Constants } from "@kksh/ui"
import { CustomCommandInput, GlobalCommandPaletteFooter } from "@kksh/ui/main" import { CustomCommandInput, GlobalCommandPaletteFooter } from "@kksh/ui/main"

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { cn } from "@/utils" import { cn } from "@/utils"
import { db } from "@kksh/api/commands"
import type { ExtData } from "@kksh/api/models" import type { ExtData } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { Resizable, Separator } from "@kksh/svelte5" import { Resizable, Separator } from "@kksh/svelte5"
import { convertFileSrc } from "@tauri-apps/api/core" import { convertFileSrc } from "@tauri-apps/api/core"
import DOMPurify from "dompurify" import DOMPurify from "dompurify"

View File

@ -10,7 +10,6 @@
import { positionToCssStyleString, positionToTailwindClasses } from "@/utils/style" import { positionToCssStyleString, positionToTailwindClasses } from "@/utils/style"
import { sleep } from "@/utils/time" import { sleep } from "@/utils/time"
import { isInMainWindow } from "@/utils/window" import { isInMainWindow } from "@/utils/window"
import { db } from "@kksh/api/commands"
import { CustomPosition, ThemeColor, type Position } from "@kksh/api/models" import { CustomPosition, ThemeColor, type Position } from "@kksh/api/models"
import { import {
constructJarvisServerAPIWithPermissions, constructJarvisServerAPIWithPermissions,
@ -19,6 +18,7 @@
type IUiCustom type IUiCustom
} from "@kksh/api/ui" } from "@kksh/api/ui"
import { toast, type IUiCustomServer1, type IUiCustomServer2 } from "@kksh/api/ui/custom" import { toast, type IUiCustomServer1, type IUiCustomServer2 } from "@kksh/api/ui/custom"
import { db } from "@kksh/drizzle"
import { Button } from "@kksh/svelte5" import { Button } from "@kksh/svelte5"
import { cn } from "@kksh/ui/utils" import { cn } from "@kksh/ui/utils"
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server" import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server"

View File

@ -1,8 +1,7 @@
import { KunkunIframeExtParams } from "@/cmds/ext" import { KunkunIframeExtParams } from "@/cmds/ext"
import { i18n } from "@/i18n" import { i18n } from "@/i18n"
import { appState } from "@/stores/appState"
import { db, unregisterExtensionWindow } from "@kksh/api/commands"
import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models" import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { loadExtensionManifestFromDisk } from "@kksh/extension" import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { error as svError } from "@sveltejs/kit" import { error as svError } from "@sveltejs/kit"
import { join } from "@tauri-apps/api/path" import { join } from "@tauri-apps/api/path"

View File

@ -13,7 +13,6 @@
} from "@/utils/tauri-events.js" } from "@/utils/tauri-events.js"
import { sleep } from "@/utils/time.js" import { sleep } from "@/utils/time.js"
import { isInMainWindow } from "@/utils/window.js" import { isInMainWindow } from "@/utils/window.js"
import { db } from "@kksh/api/commands"
import { import {
constructJarvisServerAPIWithPermissions, constructJarvisServerAPIWithPermissions,
type IApp, type IApp,
@ -29,6 +28,7 @@
type IComponent, type IComponent,
type TemplateUiCommand type TemplateUiCommand
} from "@kksh/api/ui/template" } from "@kksh/api/ui/template"
import { db } from "@kksh/drizzle"
import { Button, Form } from "@kksh/svelte5" import { Button, Form } from "@kksh/svelte5"
import { LoadingBar } from "@kksh/ui" import { LoadingBar } from "@kksh/ui"
import { Templates } from "@kksh/ui/extension" import { Templates } from "@kksh/ui/extension"

View File

@ -1,7 +1,7 @@
import { KunkunTemplateExtParams } from "@/cmds/ext" import { KunkunTemplateExtParams } from "@/cmds/ext"
import { i18n } from "@/i18n" import { i18n } from "@/i18n"
import { db, unregisterExtensionWindow } from "@kksh/api/commands" import type { ExtPackageJsonExtra } from "@kksh/api/models"
import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models" import { db } from "@kksh/drizzle"
import { loadExtensionManifestFromDisk } from "@kksh/extension" import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { error as sbError, error as svError } from "@sveltejs/kit" import { error as sbError, error as svError } from "@sveltejs/kit"
import { join } from "@tauri-apps/api/path" import { join } from "@tauri-apps/api/path"

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import * as m from "@/paraglide/messages" import * as m from "@/paraglide/messages"
import { db } from "@kksh/api/commands" import { db } from "@kksh/drizzle"
import { loadExtensionManifestFromDisk } from "@kksh/extension" import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { Button, Dialog, Table } from "@kksh/svelte5" import { Button, Dialog, Table } from "@kksh/svelte5"
import { join } from "@tauri-apps/api/path" import { join } from "@tauri-apps/api/path"

View File

@ -7,13 +7,12 @@
getUniqueExtensionByPath, getUniqueExtensionByPath,
searchExtensionData, searchExtensionData,
updateCmdByID updateCmdByID
} from "@/orm/cmds" } from "@kksh/drizzle/api"
import * as schema from "@kksh/drizzle/schema" import * as schema from "@kksh/drizzle/schema"
import { Button, Input } from "@kksh/svelte5" import { Button, Input } from "@kksh/svelte5"
import { CmdTypeEnum, Ext } from "@kunkunapi/src/models/extension" import { CmdTypeEnum, Ext } from "@kunkunapi/src/models/extension"
import { SearchModeEnum, SQLSortOrderEnum } from "@kunkunapi/src/models/sql" import { SearchModeEnum, SQLSortOrderEnum } from "@kunkunapi/src/models/sql"
import { db } from "$lib/orm/database" // import * as orm from "drizzle-orm"
import * as orm from "drizzle-orm"
import { Inspect } from "svelte-inspect-value" import { Inspect } from "svelte-inspect-value"
import { toast } from "svelte-sonner" import { toast } from "svelte-sonner"
import * as v from "valibot" import * as v from "valibot"

View File

@ -20,9 +20,10 @@ import type {
IPath as ITauriPath IPath as ITauriPath
} from "tauri-api-adapter" } from "tauri-api-adapter"
import * as v from "valibot" import * as v from "valibot"
import { KV, type JarvisExtDB } from "../commands/db"
import type { fileSearch } from "../commands/fileSearch" import type { fileSearch } from "../commands/fileSearch"
import { type AppInfo } from "../models/apps" import { type AppInfo } from "../models/apps"
import { type ExtData } from "../models/extension"
import { ExtDataField, SearchMode, SQLSortOrder } from "../models/sql"
import type { LightMode, Position, Radius, ThemeColor } from "../models/styles" import type { LightMode, Position, Radius, ThemeColor } from "../models/styles"
import type { DenoSysOptions } from "../permissions/schema" import type { DenoSysOptions } from "../permissions/schema"
@ -154,23 +155,34 @@ export interface IUiCustom {
} }
export interface IDb { export interface IDb {
add: typeof JarvisExtDB.prototype.add add: (data: { data: string; dataType?: string; searchText?: string }) => Promise<void>
delete: typeof JarvisExtDB.prototype.delete delete: (dataId: number) => Promise<void>
search: typeof JarvisExtDB.prototype.search search: (searchParams: {
retrieveAll: typeof JarvisExtDB.prototype.retrieveAll dataId?: number
retrieveAllByType: typeof JarvisExtDB.prototype.retrieveAllByType searchMode?: SearchMode
deleteAll: typeof JarvisExtDB.prototype.deleteAll dataType?: string
update: typeof JarvisExtDB.prototype.update searchText?: string
afterCreatedAt?: Date
beforeCreatedAt?: Date
limit?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}) => Promise<ExtData[]>
retrieveAll: (options: { fields?: ExtDataField[] }) => Promise<ExtData[]>
retrieveAllByType: (dataType: string) => Promise<ExtData[]>
deleteAll: () => Promise<void>
update: (data: { dataId: number; data: string; searchText?: string }) => Promise<void>
} }
/** /**
* A key-value store built on top of the Database API (based on sqlite) * A key-value store built on top of the Database API (based on sqlite)
*/ */
export interface IKV { export interface IKV {
get: typeof KV.prototype.get get: <T = string>(key: string) => Promise<T | null | undefined>
set: typeof KV.prototype.set set: (key: string, value: string) => Promise<void>
exists: typeof KV.prototype.exists exists: (key: string) => Promise<boolean>
delete: typeof KV.prototype.delete delete: (key: string) => Promise<void>
} }
export interface IFs { export interface IFs {

View File

@ -1,465 +0,0 @@
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"
export interface QueryResult {
/** The number of rows affected by the query. */
rowsAffected: number
/**
* The last inserted `id`.
*
* This value is not set for Postgres databases. If the
* last inserted id is required on Postgres, the `select` function
* must be used, with a `RETURNING` clause
* (`INSERT INTO todos (title) VALUES ($1) RETURNING id`).
*/
lastInsertId?: number
}
export function select(query: string, values: any[]) {
return invoke<any[]>(generateJarvisPluginCommand("select"), {
query,
values
})
}
export function execute(query: string, values: any[]) {
return invoke<QueryResult>(generateJarvisPluginCommand("execute"), {
query,
values
})
}
/* -------------------------------------------------------------------------- */
/* 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
})
}
}

View File

@ -5,8 +5,7 @@ export * from "./tools"
export * from "./extension" export * from "./extension"
export * from "./system" export * from "./system"
export * from "./store" export * from "./store"
export * as db from "./db" export * as sql from "./sql"
export { JarvisExtDB } from "./db"
export * from "./clipboard" export * from "./clipboard"
export * from "./fileSearch" export * from "./fileSearch"
export * from "./utils" export * from "./utils"

View File

@ -0,0 +1,30 @@
import { invoke } from "@tauri-apps/api/core"
import { generateJarvisPluginCommand } from "./common"
export interface QueryResult {
/** The number of rows affected by the query. */
rowsAffected: number
/**
* The last inserted `id`.
*
* This value is not set for Postgres databases. If the
* last inserted id is required on Postgres, the `select` function
* must be used, with a `RETURNING` clause
* (`INSERT INTO todos (title) VALUES ($1) RETURNING id`).
*/
lastInsertId?: number
}
export function select(query: string, values: any[]) {
return invoke<any[]>(generateJarvisPluginCommand("select"), {
query,
values
})
}
export function execute(query: string, values: any[]) {
return invoke<QueryResult>(generateJarvisPluginCommand("execute"), {
query,
values
})
}

View File

@ -18,10 +18,7 @@ export const Ext = v.object({
extId: v.number(), extId: v.number(),
identifier: v.string(), identifier: v.string(),
version: v.string(), version: v.string(),
enabled: v.pipe( enabled: v.boolean(),
v.number(),
v.transform((input) => Boolean(input))
),
installedAt: v.string(), installedAt: v.string(),
path: v.optional(v.nullable(v.string())), path: v.optional(v.nullable(v.string())),
data: v.optional(v.any()) data: v.optional(v.any())
@ -49,11 +46,9 @@ export const ExtCmd = v.object({
data: v.string(), data: v.string(),
alias: v.optional(v.nullable(v.string())), alias: v.optional(v.nullable(v.string())),
hotkey: v.optional(v.nullable(v.string())), hotkey: v.optional(v.nullable(v.string())),
enabled: v.pipe( enabled: v.boolean()
v.number(),
v.transform((input) => Boolean(input))
)
}) })
export type ExtCmd = v.InferOutput<typeof ExtCmd> export type ExtCmd = v.InferOutput<typeof ExtCmd>
export const QuickLinkCmd = v.object({ export const QuickLinkCmd = v.object({

View File

@ -1,12 +1,12 @@
import { enum_, type InferOutput } from "valibot" import * as v from "valibot"
export enum SQLSortOrderEnum { export enum SQLSortOrderEnum {
Asc = "ASC", Asc = "ASC",
Desc = "DESC" Desc = "DESC"
} }
export const SQLSortOrder = enum_(SQLSortOrderEnum) export const SQLSortOrder = v.enum_(SQLSortOrderEnum)
export type SQLSortOrder = InferOutput<typeof SQLSortOrder> export type SQLSortOrder = v.InferOutput<typeof SQLSortOrder>
export enum SearchModeEnum { export enum SearchModeEnum {
ExactMatch = "exact_match", ExactMatch = "exact_match",
@ -14,8 +14,8 @@ export enum SearchModeEnum {
FTS = "fts" FTS = "fts"
} }
export const SearchMode = enum_(SearchModeEnum) export const SearchMode = v.enum_(SearchModeEnum)
export type SearchMode = InferOutput<typeof SearchMode> export type SearchMode = v.InferOutput<typeof SearchMode>
export function convertDateToSqliteString(date: Date) { export function convertDateToSqliteString(date: Date) {
const pad = (num: number) => num.toString().padStart(2, "0") const pad = (num: number) => num.toString().padStart(2, "0")
@ -29,3 +29,6 @@ export function convertDateToSqliteString(date: Date) {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
} }
export const ExtDataField = v.union([v.literal("data"), v.literal("search_text")])
export type ExtDataField = v.InferOutput<typeof ExtDataField>

View File

@ -17,7 +17,8 @@ export const extensions = sqliteTable("extensions", {
extId: integer("ext_id").primaryKey({ autoIncrement: true }), extId: integer("ext_id").primaryKey({ autoIncrement: true }),
identifier: text().notNull(), identifier: text().notNull(),
version: text().notNull(), version: text().notNull(),
enabled: numeric().default(sql`(TRUE)`), // enabled: numeric().default(sql`(TRUE)`),
enabled: integer({ mode: "boolean" }),
path: text(), path: text(),
data: numeric(), data: numeric(),
installedAt: numeric("installed_at").default(sql`(CURRENT_TIMESTAMP)`) installedAt: numeric("installed_at").default(sql`(CURRENT_TIMESTAMP)`)

View File

@ -1,2 +1,3 @@
export * as schema from "./drizzle/schema" export * as schema from "./drizzle/schema"
export * as relations from "./drizzle/relations" export * as relations from "./drizzle/relations"
export * as db from "./src/apis"

View File

@ -4,6 +4,7 @@
"private": true, "private": true,
"exports": { "exports": {
".": "./index.ts", ".": "./index.ts",
"./api": "./src/apis.ts",
"./schema": "./drizzle/schema.ts", "./schema": "./drizzle/schema.ts",
"./relations": "./drizzle/relations.ts" "./relations": "./drizzle/relations.ts"
}, },

View File

@ -1,53 +1,80 @@
import * as relations from "@kksh/drizzle/relations" import { KUNKUN_EXT_IDENTIFIER, type IDb, type IKV } from "@kksh/api"
import * as schema from "../drizzle/schema"
import { import {
CmdType, CmdType,
Ext, convertDateToSqliteString,
ExtCmd, Ext,
ExtData, ExtCmd,
SearchMode, ExtData,
SearchModeEnum, SearchMode,
SQLSortOrder, SearchModeEnum,
SQLSortOrderEnum SQLSortOrder,
SQLSortOrderEnum
} from "@kksh/api/models" } from "@kksh/api/models"
import * as relations from "@kksh/drizzle/relations"
import * as orm from "drizzle-orm" import * as orm from "drizzle-orm"
import type { SelectedFields } from "drizzle-orm/sqlite-core" import type { SelectedFields } from "drizzle-orm/sqlite-core"
import * as v from "valibot" import * as v from "valibot"
import * as schema from "../drizzle/schema"
import { db } from "./proxy" import { db } from "./proxy"
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Built-in Extensions */ /* Built-in Extensions */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
export function getExtClipboard() { export function getExtClipboard(): Promise<Ext> {
// return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_CLIPBOARD_EXT_IDENTIFIER) return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_CLIPBOARD_EXT_IDENTIFIER)
} }
export function getExtQuickLinks() { export function getExtQuickLinks(): Promise<Ext> {
// return getExtensionByIdentifierExpectExists( return getExtensionByIdentifierExpectExists(
// KUNKUN_EXT_IDENTIFIER.KUNKUN_QUICK_LINKS_EXT_IDENTIFIER KUNKUN_EXT_IDENTIFIER.KUNKUN_QUICK_LINKS_EXT_IDENTIFIER
// ) )
} }
export function getExtRemote() { export function getExtRemote(): Promise<Ext> {
// return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_REMOTE_EXT_IDENTIFIER) return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_REMOTE_EXT_IDENTIFIER)
} }
export function getExtScriptCmd() { export function getExtScriptCmd(): Promise<Ext> {
// return getExtensionByIdentifierExpectExists( return getExtensionByIdentifierExpectExists(
// KUNKUN_EXT_IDENTIFIER.KUNKUN_SCRIPT_CMD_EXT_IDENTIFIER KUNKUN_EXT_IDENTIFIER.KUNKUN_SCRIPT_CMD_EXT_IDENTIFIER
// ) )
} }
export function getExtDev() { export function getExtDev(): Promise<Ext> {
// return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_DEV_EXT_IDENTIFIER) return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_DEV_EXT_IDENTIFIER)
} }
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Extension CRUD */ /* Extension CRUD */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
export function createExtension(ext: {
identifier: string
version: string
enabled?: boolean
path?: string
data?: any
}) {
return db
.insert(schema.extensions)
.values({
identifier: ext.identifier,
version: ext.version,
enabled: ext.enabled,
path: ext.path,
data: ext.data
})
.run()
}
export async function getUniqueExtensionByIdentifier(identifier: string): Promise<Ext | undefined> { export async function getUniqueExtensionByIdentifier(identifier: string): Promise<Ext | undefined> {
const ext = await db const ext = await db
.select() .select()
.from(schema.extensions) .from(schema.extensions)
.where(orm.eq(schema.extensions.identifier, identifier)) .where(orm.eq(schema.extensions.identifier, identifier))
.get() .get()
return v.parse(v.optional(Ext), ext) const result = v.safeParse(v.optional(Ext), ext)
if (!result.success) {
console.error("Failed to parse extension:", v.flatten(result.issues))
return undefined
}
const parsed = result.output
return parsed
} }
/** /**
@ -56,17 +83,17 @@ export async function getUniqueExtensionByIdentifier(identifier: string): Promis
* @returns * @returns
*/ */
export function getExtensionByIdentifierExpectExists(identifier: string): Promise<Ext> { export function getExtensionByIdentifierExpectExists(identifier: string): Promise<Ext> {
return getUniqueExtensionByIdentifier(identifier).then((ext) => { return getUniqueExtensionByIdentifier(identifier).then((ext) => {
if (!ext) { if (!ext) {
throw new Error(`Unexpexted Error: Extension ${identifier} not found`) throw new Error(`Unexpexted Error: Extension ${identifier} not found`)
} }
return ext return ext
}) })
} }
export async function getAllExtensions(): Promise<Ext[]> { export async function getAllExtensions(): Promise<Ext[]> {
const exts = await db.select().from(schema.extensions).all() const exts = await db.select().from(schema.extensions).all()
return v.parse(v.array(Ext), exts) return v.parse(v.array(Ext), exts)
} }
/** /**
@ -75,112 +102,121 @@ export async function getAllExtensions(): Promise<Ext[]> {
* @param path * @param path
*/ */
export async function getUniqueExtensionByPath(path: string) { export async function getUniqueExtensionByPath(path: string) {
const ext = await db const ext = await db
.select() .select()
.from(schema.extensions) .from(schema.extensions)
.where(orm.eq(schema.extensions.path, path)) .where(orm.eq(schema.extensions.path, path))
.get() .get()
return v.parse(Ext, ext) return v.parse(Ext, ext)
} }
export function getAllExtensionsByIdentifier(identifier: string): Promise<Ext[]> { export function getAllExtensionsByIdentifier(identifier: string): Promise<Ext[]> {
return db return db
.select() .select()
.from(schema.extensions) .from(schema.extensions)
.where(orm.eq(schema.extensions.identifier, identifier)) .where(orm.eq(schema.extensions.identifier, identifier))
.all() .all()
.then((exts) => v.parse(v.array(Ext), exts)) .then((exts) => v.parse(v.array(Ext), exts))
} }
export function deleteExtensionByPath(path: string): Promise<void> { export function deleteExtensionByPath(path: string): Promise<void> {
return db return db
.delete(schema.extensions) .delete(schema.extensions)
.where(orm.eq(schema.extensions.path, path)) .where(orm.eq(schema.extensions.path, path))
.run() .run()
.then(() => undefined) .then(() => undefined)
} }
export function deleteExtensionByExtId(extId: number): Promise<void> { export function deleteExtensionByExtId(extId: number): Promise<void> {
return db return db
.delete(schema.extensions) .delete(schema.extensions)
.where(orm.eq(schema.extensions.extId, extId)) .where(orm.eq(schema.extensions.extId, extId))
.run() .run()
.then(() => undefined) .then(() => undefined)
} }
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Extension Command CRUD */ /* Extension Command CRUD */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
export function createCommand(data: {
// export async function getExtensionWithCmdsByIdentifier(identifier: string): Promise<ExtWithCmds> { extId: number
// const ext = await db name: string
// .select({ cmdType: CmdType
// ...schema.extensions, data: string
// commands: relations.commandsRelations alias?: string
// }) hotkey?: string
// .from(schema.extensions) enabled?: boolean
// .leftJoin(schema.commands, orm.eq(schema.extensions.extId, schema.commands.extId)) }) {
// .where(orm.eq(schema.extensions.identifier, identifier)) return db
// .get() .insert(schema.commands)
.values({
// // return v.parse(v.nullable(ExtWithCmds), ext); extId: data.extId,
// } name: data.name,
type: data.cmdType,
data: data.data,
alias: data.alias,
hotkey: data.hotkey,
enabled: data.enabled ?? true
})
.run()
.then(() => undefined)
}
export async function getCmdById(cmdId: number): Promise<ExtCmd> { export async function getCmdById(cmdId: number): Promise<ExtCmd> {
const cmd = await db const cmd = await db
.select() .select()
.from(schema.commands) .from(schema.commands)
.where(orm.eq(schema.commands.cmdId, cmdId)) .where(orm.eq(schema.commands.cmdId, cmdId))
.get() .get()
return v.parse(ExtCmd, cmd) return v.parse(ExtCmd, cmd)
} }
export async function getAllCmds(): Promise<ExtCmd[]> { export async function getAllCmds(): Promise<ExtCmd[]> {
const cmds = await db.select().from(schema.commands).all() const cmds = await db.select().from(schema.commands).all()
return v.parse(v.array(ExtCmd), cmds) return v.parse(v.array(ExtCmd), cmds)
} }
export function getCommandsByExtId(extId: number) { export function getCommandsByExtId(extId: number) {
return db return db
.select() .select()
.from(schema.commands) .from(schema.commands)
.where(orm.eq(schema.commands.extId, extId)) .where(orm.eq(schema.commands.extId, extId))
.all() .all()
.then((cmds) => v.parse(v.array(ExtCmd), cmds)) .then((cmds) => v.parse(v.array(ExtCmd), cmds))
} }
export function deleteCmdById(cmdId: number) { export function deleteCmdById(cmdId: number) {
return db return db
.delete(schema.commands) .delete(schema.commands)
.where(orm.eq(schema.commands.cmdId, cmdId)) .where(orm.eq(schema.commands.cmdId, cmdId))
.run() .run()
.then(() => undefined) .then(() => undefined)
} }
export function updateCmdByID(data: { export function updateCmdByID(data: {
cmdId: number cmdId: number
name: string name: string
cmdType: CmdType cmdType: CmdType
data: string data: string
alias?: string alias?: string
hotkey?: string hotkey?: string
enabled: boolean enabled: boolean
}) { }) {
return db return db
.update(schema.commands) .update(schema.commands)
.set({ .set({
name: data.name, name: data.name,
type: data.cmdType, type: data.cmdType,
data: data.data, data: data.data,
alias: data.alias, // optional alias: data.alias, // optional
hotkey: data.hotkey, // optional hotkey: data.hotkey, // optional
enabled: data.enabled enabled: data.enabled
// in drizzle schema, use integer({ mode: 'boolean' }) for boolean sqlite // in drizzle schema, use integer({ mode: 'boolean' }) for boolean sqlite
// enabled: data.enabled ? String(data.enabled) : undefined // enabled: data.enabled ? String(data.enabled) : undefined
}) })
.where(orm.eq(schema.commands.cmdId, data.cmdId)) .where(orm.eq(schema.commands.cmdId, data.cmdId))
.run() .run()
.then(() => undefined) .then(() => undefined)
} }
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
@ -190,200 +226,391 @@ export const ExtDataField = v.union([v.literal("data"), v.literal("search_text")
export type ExtDataField = v.InferOutput<typeof ExtDataField> export type ExtDataField = v.InferOutput<typeof ExtDataField>
function convertRawExtDataToExtData(rawData?: { function convertRawExtDataToExtData(rawData?: {
createdAt: string createdAt: string
updatedAt: string updatedAt: string
data: null | string data: null | string
searchText?: null | string searchText?: null | string
dataId: number dataId: number
extId: number extId: number
dataType: string dataType: string
}): ExtData | undefined { }): ExtData | undefined {
if (!rawData) { if (!rawData) {
return rawData return rawData
} }
const parsedRes = v.safeParse(ExtData, { const parsedRes = v.safeParse(ExtData, {
...rawData, ...rawData,
createdAt: new Date(rawData.createdAt), createdAt: new Date(rawData.createdAt),
updatedAt: new Date(rawData.updatedAt), updatedAt: new Date(rawData.updatedAt),
data: rawData.data ?? undefined, data: rawData.data ?? undefined,
searchText: rawData.searchText ?? undefined searchText: rawData.searchText ?? undefined
}) })
if (parsedRes.success) { if (parsedRes.success) {
return parsedRes.output return parsedRes.output
} else { } else {
console.error("Extension Data Parse Failure", parsedRes.issues) console.error("Extension Data Parse Failure", parsedRes.issues)
throw new Error("Fail to parse extension data") throw new Error("Fail to parse extension data")
} }
} }
export function createExtensionData(data: { export function createExtensionData(data: {
extId: number extId: number
dataType: string dataType: string
data: string data: string
searchText?: string searchText?: string
}) { }) {
return db.insert(schema.extensionData).values(data).run() return db.insert(schema.extensionData).values(data).run()
} }
export function getExtensionDataById(dataId: number, fields?: ExtDataField[]) { export function getExtensionDataById(dataId: number, fields?: ExtDataField[]) {
const _fields = fields ?? [] const _fields = fields ?? []
const selectQuery: SelectedFields = { const selectQuery: SelectedFields = {
dataId: schema.extensionData.dataId, dataId: schema.extensionData.dataId,
extId: schema.extensionData.extId, extId: schema.extensionData.extId,
dataType: schema.extensionData.dataType, dataType: schema.extensionData.dataType,
metadata: schema.extensionData.metadata, metadata: schema.extensionData.metadata,
createdAt: schema.extensionData.createdAt, createdAt: schema.extensionData.createdAt,
updatedAt: schema.extensionData.updatedAt updatedAt: schema.extensionData.updatedAt
// data: schema.extensionData.data, // data: schema.extensionData.data,
// searchText: schema.extensionData.searchText // searchText: schema.extensionData.searchText
} }
if (_fields.includes("data")) { if (_fields.includes("data")) {
selectQuery["data"] = schema.extensionData.data selectQuery["data"] = schema.extensionData.data
} }
if (_fields.includes("search_text")) { if (_fields.includes("search_text")) {
selectQuery["searchText"] = schema.extensionData.searchText selectQuery["searchText"] = schema.extensionData.searchText
} }
return db return db
.select(selectQuery) .select(selectQuery)
.from(schema.extensionData) .from(schema.extensionData)
.where(orm.eq(schema.extensionData.dataId, dataId)) .where(orm.eq(schema.extensionData.dataId, dataId))
.get() .get()
.then((rawData) => { .then((rawData) => {
console.log("Raw Data", rawData) console.log("Raw Data", rawData)
// @ts-expect-error - rawData is unknown, but will be safe parsed with valibot // @ts-expect-error - rawData is unknown, but will be safe parsed with valibot
return convertRawExtDataToExtData(rawData) return convertRawExtDataToExtData(rawData)
}) })
} }
export async function searchExtensionData(searchParams: { export async function searchExtensionData(searchParams: {
extId: number extId: number
searchMode: SearchMode searchMode: SearchMode
dataId?: number dataId?: number
dataType?: string dataType?: string
searchText?: string searchText?: string
afterCreatedAt?: string afterCreatedAt?: string
beforeCreatedAt?: string beforeCreatedAt?: string
limit?: number limit?: number
offset?: number offset?: number
orderByCreatedAt?: SQLSortOrder orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[] fields?: ExtDataField[]
}): Promise<ExtData[]> { }): Promise<ExtData[]> {
const fields = v.parse(v.optional(v.array(ExtDataField), []), searchParams.fields) const fields = v.parse(v.optional(v.array(ExtDataField), []), searchParams.fields)
const _fields = fields ?? [] const _fields = fields ?? []
// Build the select query based on fields // Build the select query based on fields
const selectQuery: SelectedFields = { const selectQuery: SelectedFields = {
dataId: schema.extensionData.dataId, dataId: schema.extensionData.dataId,
extId: schema.extensionData.extId, extId: schema.extensionData.extId,
dataType: schema.extensionData.dataType, dataType: schema.extensionData.dataType,
createdAt: schema.extensionData.createdAt, createdAt: schema.extensionData.createdAt,
updatedAt: schema.extensionData.updatedAt updatedAt: schema.extensionData.updatedAt
} }
if (_fields.includes("data")) { if (_fields.includes("data")) {
selectQuery["data"] = schema.extensionData.data selectQuery["data"] = schema.extensionData.data
} }
if (_fields.includes("search_text")) { if (_fields.includes("search_text")) {
selectQuery["searchText"] = schema.extensionData.searchText selectQuery["searchText"] = schema.extensionData.searchText
} }
// Build the query // Build the query
let baseQuery = db.select(selectQuery).from(schema.extensionData) let baseQuery = db.select(selectQuery).from(schema.extensionData)
// Add FTS join if needed // Add FTS join if needed
if (searchParams.searchMode === SearchModeEnum.FTS && searchParams.searchText) { if (searchParams.searchMode === SearchModeEnum.FTS && searchParams.searchText) {
// @ts-expect-error - The join type is correct but TypeScript can't infer it properly // @ts-expect-error - The join type is correct but TypeScript can't infer it properly
baseQuery = baseQuery.innerJoin( baseQuery = baseQuery.innerJoin(
schema.extensionDataFts, schema.extensionDataFts,
orm.eq(schema.extensionData.dataId, schema.extensionDataFts.dataId) orm.eq(schema.extensionData.dataId, schema.extensionDataFts.dataId)
) )
} }
// Add conditions // Add conditions
const conditions = [orm.eq(schema.extensionData.extId, searchParams.extId)] const conditions = [orm.eq(schema.extensionData.extId, searchParams.extId)]
if (searchParams.dataId) { if (searchParams.dataId) {
conditions.push(orm.eq(schema.extensionData.dataId, searchParams.dataId)) conditions.push(orm.eq(schema.extensionData.dataId, searchParams.dataId))
} }
if (searchParams.dataType) { if (searchParams.dataType) {
conditions.push(orm.eq(schema.extensionData.dataType, searchParams.dataType)) conditions.push(orm.eq(schema.extensionData.dataType, searchParams.dataType))
} }
if (searchParams.searchText) { if (searchParams.searchText) {
switch (searchParams.searchMode) { switch (searchParams.searchMode) {
case SearchModeEnum.ExactMatch: case SearchModeEnum.ExactMatch:
conditions.push(orm.eq(schema.extensionData.searchText, searchParams.searchText)) conditions.push(orm.eq(schema.extensionData.searchText, searchParams.searchText))
break break
case SearchModeEnum.Like: case SearchModeEnum.Like:
conditions.push(orm.like(schema.extensionData.searchText, `%${searchParams.searchText}%`)) conditions.push(orm.like(schema.extensionData.searchText, `%${searchParams.searchText}%`))
break break
case SearchModeEnum.FTS: case SearchModeEnum.FTS:
conditions.push( conditions.push(
orm.sql`${schema.extensionDataFts.searchText} MATCH ${searchParams.searchText}` orm.sql`${schema.extensionDataFts.searchText} MATCH ${searchParams.searchText}`
) )
break break
} }
} }
if (searchParams.afterCreatedAt) { if (searchParams.afterCreatedAt) {
conditions.push(orm.gt(schema.extensionData.createdAt, searchParams.afterCreatedAt)) conditions.push(orm.gt(schema.extensionData.createdAt, searchParams.afterCreatedAt))
} }
if (searchParams.beforeCreatedAt) { if (searchParams.beforeCreatedAt) {
conditions.push(orm.lt(schema.extensionData.createdAt, searchParams.beforeCreatedAt)) conditions.push(orm.lt(schema.extensionData.createdAt, searchParams.beforeCreatedAt))
} }
// Build the final query with all conditions and modifiers // Build the final query with all conditions and modifiers
const query = baseQuery const query = baseQuery
.where(orm.and(...conditions)) .where(orm.and(...conditions))
.orderBy( .orderBy(
searchParams.orderByCreatedAt searchParams.orderByCreatedAt
? searchParams.orderByCreatedAt === SQLSortOrderEnum.Asc ? searchParams.orderByCreatedAt === SQLSortOrderEnum.Asc
? orm.asc(schema.extensionData.createdAt) ? orm.asc(schema.extensionData.createdAt)
: orm.desc(schema.extensionData.createdAt) : orm.desc(schema.extensionData.createdAt)
: searchParams.orderByUpdatedAt : searchParams.orderByUpdatedAt
? searchParams.orderByUpdatedAt === SQLSortOrderEnum.Asc ? searchParams.orderByUpdatedAt === SQLSortOrderEnum.Asc
? orm.asc(schema.extensionData.updatedAt) ? orm.asc(schema.extensionData.updatedAt)
: orm.desc(schema.extensionData.updatedAt) : orm.desc(schema.extensionData.updatedAt)
: orm.asc(schema.extensionData.createdAt) // Default ordering : orm.asc(schema.extensionData.createdAt) // Default ordering
) )
.limit(searchParams.limit ?? 100) // Default limit .limit(searchParams.limit ?? 100) // Default limit
.offset(searchParams.offset ?? 0) // Default offset .offset(searchParams.offset ?? 0) // Default offset
// Execute query and convert results // Execute query and convert results
const results = await query.all() const results = await query.all()
return results return results
.map((rawData) => { .map((rawData) => {
// @ts-expect-error - rawData is unknown, but will be safe parsed with valibot // @ts-expect-error - rawData is unknown, but will be safe parsed with valibot
return convertRawExtDataToExtData(rawData) return convertRawExtDataToExtData(rawData)
}) })
.filter((item): item is ExtData => item !== undefined) .filter((item): item is ExtData => item !== undefined)
} }
export function deleteExtensionDataById(dataId: number) { export function deleteExtensionDataById(dataId: number) {
return db
// return invoke<void>(generateJarvisPluginCommand("delete_extension_data_by_id"), { dataId }) .delete(schema.extensionData)
.where(orm.eq(schema.extensionData.dataId, dataId))
.run()
.then(() => undefined)
} }
export function updateExtensionDataById(data: { export function updateExtensionDataById(data: {
dataId: number dataId: number
data: string data: string
searchText?: string searchText?: string
}) { }): Promise<void> {
// return invoke<void>(generateJarvisPluginCommand("update_extension_data_by_id"), data) return db
.update(schema.extensionData)
.set({
data: data.data,
searchText: data.searchText
})
.where(orm.eq(schema.extensionData.dataId, data.dataId))
.run()
.then(() => undefined)
} }
/**
* 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 implements IDb {
extId: number
// export async function getNCommands(n: number): constructor(extId: number) {
// export function createExtension(ext: { this.extId = extId
// identifier: string }
// version: string
// enabled?: boolean async add(data: { data: string; dataType?: string; searchText?: string }): Promise<void> {
// path?: string return createExtensionData({
// data?: any data: data.data,
// }) { dataType: data.dataType ?? "default",
// return invoke<void>(generateJarvisPluginCommand("create_extension"), ext) searchText: data.searchText,
// } extId: this.extId
}).then(() => undefined)
}
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 implements IKV {
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
})
}
}

View File

@ -1,52 +1,47 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { db as dbCmd } from "@kksh/api/commands" import { sql } from "@kksh/api/commands"
import * as schema from "../drizzle/schema"
import { error } from "@tauri-apps/plugin-log" import { error } from "@tauri-apps/plugin-log"
import { drizzle } from "drizzle-orm/sqlite-proxy" import { drizzle } from "drizzle-orm/sqlite-proxy"
import * as schema from "../drizzle/schema"
/**
* Loads the sqlite database via the Tauri Proxy.
*/
// export const sqlite = await Database.load("sqlite:test.db");
/** /**
* The drizzle database instance. * The drizzle database instance.
*/ */
export const db = drizzle<typeof schema>( export const db = drizzle<typeof schema>(
async (sql, params, method) => { async (sqlQuery, params, method) => {
let rows: any = [] let rows: any = []
let results = [] let results = []
console.log({ console.log({
sql, sql: sqlQuery,
params, params,
method method
}) })
console.log(sql) console.log(sqlQuery)
// If the query is a SELECT, use the select method // If the query is a SELECT, use the select method
if (isSelectQuery(sql)) { if (isSelectQuery(sqlQuery)) {
rows = await dbCmd.select(sql, params).catch((e) => { rows = await sql.select(sqlQuery, params).catch((e) => {
error("SQL Error:", e) error("SQL Error:", e)
return [] return []
}) })
} else { } else {
// Otherwise, use the execute method // Otherwise, use the execute method
rows = await dbCmd.execute(sql, params).catch((e) => { rows = await sql.execute(sqlQuery, params).catch((e) => {
error("SQL Error:", e) error("SQL Error:", e)
return [] return []
}) })
return { rows: [] } return { rows: [] }
} }
rows = rows.map((row: any) => { rows = rows.map((row: any) => {
return Object.values(row) return Object.values(row)
}) })
// If the method is "all", return all rows // If the method is "all", return all rows
results = method === "all" ? rows : rows[0] results = method === "all" ? rows : rows[0]
return { rows: results } return { rows: results }
}, },
// Pass the schema to the drizzle instance // Pass the schema to the drizzle instance
{ schema: schema, logger: true } { schema: schema, logger: true }
) )
/** /**
@ -55,6 +50,6 @@ export const db = drizzle<typeof schema>(
* @returns True if the query is a SELECT query, false otherwise. * @returns True if the query is a SELECT query, false otherwise.
*/ */
function isSelectQuery(sql: string): boolean { function isSelectQuery(sql: string): boolean {
const selectRegex = /^\s*SELECT\b/i const selectRegex = /^\s*SELECT\b/i
return selectRegex.test(sql) return selectRegex.test(sql)
} }

View File

@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@kksh/api": "workspace:*", "@kksh/api": "workspace:*",
"@kksh/drizzle": "workspace:*",
"@std/semver": "npm:@jsr/std__semver@^1.0.4", "@std/semver": "npm:@jsr/std__semver@^1.0.4",
"@tauri-apps/plugin-upload": "^2.2.1", "@tauri-apps/plugin-upload": "^2.2.1",
"semver": "^7.7.1", "semver": "^7.7.1",

View File

@ -1,14 +1,6 @@
import { db } from "@kksh/api/commands" import { CmdTypeEnum, ExtPackageJson, Icon, QuickLinkCmd } from "@kksh/api/models"
import { import { db } from "@kksh/drizzle"
CmdTypeEnum,
ExtCmd,
ExtPackageJson,
ExtPackageJsonExtra,
Icon,
QuickLinkCmd
} from "@kksh/api/models"
import * as v from "valibot" import * as v from "valibot"
import { isExtPathInDev } from "./utils"
export async function upsertExtension(extPkgJson: ExtPackageJson, extFullPath: string) { export async function upsertExtension(extPkgJson: ExtPackageJson, extFullPath: string) {
const extInDb = await db.getUniqueExtensionByIdentifier(extPkgJson.kunkun.identifier) const extInDb = await db.getUniqueExtensionByIdentifier(extPkgJson.kunkun.identifier)
@ -39,7 +31,7 @@ export async function createQuickLinkCommand(name: string, link: string, icon: I
export async function getAllQuickLinkCommands(): Promise<QuickLinkCmd[]> { export async function getAllQuickLinkCommands(): Promise<QuickLinkCmd[]> {
const extension = await db.getExtQuickLinks() const extension = await db.getExtQuickLinks()
const cmds = await db.getCommandsByExtId(extension.extId) const cmds = await db.getCommandsByExtId(extension.extId)
return cmds const parsedCmds = cmds
.map((cmd) => { .map((cmd) => {
try { try {
cmd.data = JSON.parse(cmd.data) cmd.data = JSON.parse(cmd.data)
@ -55,4 +47,5 @@ export async function getAllQuickLinkCommands(): Promise<QuickLinkCmd[]> {
} }
}) })
.filter((cmd) => cmd !== null) .filter((cmd) => cmd !== null)
return parsedCmds
} }

View File

@ -3,8 +3,9 @@
* including install, uninstall, upgrade, check app-extension compatibility, etc. * including install, uninstall, upgrade, check app-extension compatibility, etc.
*/ */
import { isCompatible } from "@kksh/api" import { isCompatible } from "@kksh/api"
import { copy_dir_all, db, decompressTarball } from "@kksh/api/commands" import { copy_dir_all, decompressTarball } from "@kksh/api/commands"
import type { ExtensionStoreListItem, ExtPackageJsonExtra } from "@kksh/api/models" import type { ExtensionStoreListItem, ExtPackageJsonExtra } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { greaterThan, parse as parseSemver } from "@std/semver" import { greaterThan, parse as parseSemver } from "@std/semver"
import * as path from "@tauri-apps/api/path" import * as path from "@tauri-apps/api/path"
import * as dialog from "@tauri-apps/plugin-dialog" import * as dialog from "@tauri-apps/plugin-dialog"

View File

@ -1,5 +1,5 @@
import { db } from "@kksh/api/commands"
import { ExtPackageJson, ExtPackageJsonExtra, License } from "@kksh/api/models" import { ExtPackageJson, ExtPackageJsonExtra, License } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { basename, dirname, join } from "@tauri-apps/api/path" import { basename, dirname, join } from "@tauri-apps/api/path"
import { readDir, readTextFile } from "@tauri-apps/plugin-fs" import { readDir, readTextFile } from "@tauri-apps/plugin-fs"
import { debug, error } from "@tauri-apps/plugin-log" import { debug, error } from "@tauri-apps/plugin-log"
@ -77,7 +77,7 @@ export function loadAllExtensionsFromDisk(
* @returns loaded extensions * @returns loaded extensions
*/ */
export async function loadAllExtensionsFromDb(): Promise<ExtPackageJsonExtra[]> { export async function loadAllExtensionsFromDb(): Promise<ExtPackageJsonExtra[]> {
const allDbExts = await (await db.getAllExtensions()).filter((ext) => ext.path) const allDbExts = (await db.getAllExtensions()).filter((ext) => ext.path)
const results: ExtPackageJsonExtra[] = [] const results: ExtPackageJsonExtra[] = []
for (const ext of allDbExts) { for (const ext of allDbExts) {
if (!ext.path) continue if (!ext.path) continue

View File

@ -1,4 +1,3 @@
## Permission Table ## Permission Table
<table> <table>
@ -7,7 +6,6 @@
<th>Description</th> <th>Description</th>
</tr> </tr>
<tr> <tr>
<td> <td>

View File

@ -1,11 +1,9 @@
<script lang="ts"> <script lang="ts">
import autoAnimate from "@formkit/auto-animate"
import Icon from "@iconify/svelte" import Icon from "@iconify/svelte"
import { Button, ButtonModule, Collapsible, ScrollArea } from "@kksh/svelte5" import { Button, ButtonModule, Collapsible, ScrollArea } from "@kksh/svelte5"
import { Error, Layouts, Shiki } from "@kksh/ui" import { Error, Layouts, Shiki } from "@kksh/ui"
import { ChevronsUpDown } from "lucide-svelte" import { ChevronsUpDown } from "lucide-svelte"
import { type Snippet } from "svelte" import { type Snippet } from "svelte"
import { fade, slide } from "svelte/transition"
const { const {
title, title,

3
pnpm-lock.yaml generated
View File

@ -588,6 +588,9 @@ importers:
'@kksh/api': '@kksh/api':
specifier: workspace:* specifier: workspace:*
version: link:../api version: link:../api
'@kksh/drizzle':
specifier: workspace:*
version: link:../drizzle
'@std/semver': '@std/semver':
specifier: npm:@jsr/std__semver@^1.0.4 specifier: npm:@jsr/std__semver@^1.0.4
version: '@jsr/std__semver@1.0.3' version: '@jsr/std__semver@1.0.3'