import * as relations from "@kksh/drizzle/relations" import * as schema from "@kksh/drizzle/schema" import { CmdType, Ext, ExtCmd, ExtData, SearchMode, SearchModeEnum, SQLSortOrder, SQLSortOrderEnum } from "@kunkunapi/src/models" import * as orm from "drizzle-orm" import type { SelectedFields } from "drizzle-orm/sqlite-core" import * as v from "valibot" import { db } from "./database" /* -------------------------------------------------------------------------- */ /* Built-in Extensions */ /* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */ /* Extension CRUD */ /* -------------------------------------------------------------------------- */ export async function getUniqueExtensionByIdentifier(identifier: string): Promise { const ext = await db .select() .from(schema.extensions) .where(orm.eq(schema.extensions.identifier, identifier)) .get() return v.parse(v.optional(Ext), ext) } /** * Use this function when you expect the extension to exist. Such as builtin extensions. * @param identifier * @returns */ export function getExtensionByIdentifierExpectExists(identifier: string): Promise { return getUniqueExtensionByIdentifier(identifier).then((ext) => { if (!ext) { throw new Error(`Unexpexted Error: Extension ${identifier} not found`) } return ext }) } export async function getAllExtensions(): Promise { const exts = await db.select().from(schema.extensions).all() return v.parse(v.array(Ext), exts) } /** * There can be duplicate extensions with the same identifier. Store and Dev extensions can have the same identifier. * But install path must be unique. * @param path */ export async function getUniqueExtensionByPath(path: string) { const ext = await db .select() .from(schema.extensions) .where(orm.eq(schema.extensions.path, path)) .get() return v.parse(Ext, ext) } export function getAllExtensionsByIdentifier(identifier: string): Promise { return db .select() .from(schema.extensions) .where(orm.eq(schema.extensions.identifier, identifier)) .all() .then((exts) => v.parse(v.array(Ext), exts)) } export function deleteExtensionByPath(path: string): Promise { return db .delete(schema.extensions) .where(orm.eq(schema.extensions.path, path)) .run() .then(() => undefined) } export function deleteExtensionByExtId(extId: number): Promise { return db .delete(schema.extensions) .where(orm.eq(schema.extensions.extId, extId)) .run() .then(() => undefined) } /* -------------------------------------------------------------------------- */ /* Extension Command CRUD */ /* -------------------------------------------------------------------------- */ // export async function getExtensionWithCmdsByIdentifier(identifier: string): Promise { // const ext = await db // .select({ // ...schema.extensions, // commands: relations.commandsRelations // }) // .from(schema.extensions) // .leftJoin(schema.commands, orm.eq(schema.extensions.extId, schema.commands.extId)) // .where(orm.eq(schema.extensions.identifier, identifier)) // .get() // // return v.parse(v.nullable(ExtWithCmds), ext); // } export async function getCmdById(cmdId: number): Promise { const cmd = await db .select() .from(schema.commands) .where(orm.eq(schema.commands.cmdId, cmdId)) .get() return v.parse(ExtCmd, cmd) } export async function getAllCmds(): Promise { const cmds = await db.select().from(schema.commands).all() return v.parse(v.array(ExtCmd), cmds) } export function getCommandsByExtId(extId: number) { return db .select() .from(schema.commands) .where(orm.eq(schema.commands.extId, extId)) .all() .then((cmds) => v.parse(v.array(ExtCmd), cmds)) } export function deleteCmdById(cmdId: number) { return db .delete(schema.commands) .where(orm.eq(schema.commands.cmdId, cmdId)) .run() .then(() => undefined) } export function updateCmdByID(data: { cmdId: number name: string cmdType: CmdType data: string alias?: string hotkey?: string enabled: boolean }) { return db .update(schema.commands) .set({ name: data.name, type: data.cmdType, data: data.data, alias: data.alias, // optional hotkey: data.hotkey, // optional enabled: data.enabled // in drizzle schema, use integer({ mode: 'boolean' }) for boolean sqlite // enabled: data.enabled ? String(data.enabled) : undefined }) .where(orm.eq(schema.commands.cmdId, data.cmdId)) .run() .then(() => undefined) } /* -------------------------------------------------------------------------- */ /* Extension Data CRUD */ /* -------------------------------------------------------------------------- */ export const ExtDataField = v.union([v.literal("data"), v.literal("search_text")]) export type ExtDataField = v.InferOutput function convertRawExtDataToExtData(rawData?: { createdAt: string updatedAt: string data: null | string searchText?: null | string dataId: number extId: number dataType: string }): ExtData | undefined { if (!rawData) { return rawData } const parsedRes = v.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 db.insert(schema.extensionData).values(data).run() } export function getExtensionDataById(dataId: number, fields?: ExtDataField[]) { const _fields = fields ?? [] const selectQuery: SelectedFields = { dataId: schema.extensionData.dataId, extId: schema.extensionData.extId, dataType: schema.extensionData.dataType, metadata: schema.extensionData.metadata, createdAt: schema.extensionData.createdAt, updatedAt: schema.extensionData.updatedAt // data: schema.extensionData.data, // searchText: schema.extensionData.searchText } if (_fields.includes("data")) { selectQuery["data"] = schema.extensionData.data } if (_fields.includes("search_text")) { selectQuery["searchText"] = schema.extensionData.searchText } return db .select(selectQuery) .from(schema.extensionData) .where(orm.eq(schema.extensionData.dataId, dataId)) .get() .then((rawData) => { console.log("Raw Data", rawData) // @ts-expect-error - rawData is unknown, but will be safe parsed with valibot return convertRawExtDataToExtData(rawData) }) } 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 { const fields = v.parse(v.optional(v.array(ExtDataField), []), searchParams.fields) const _fields = fields ?? [] // Build the select query based on fields const selectQuery: SelectedFields = { dataId: schema.extensionData.dataId, extId: schema.extensionData.extId, dataType: schema.extensionData.dataType, createdAt: schema.extensionData.createdAt, updatedAt: schema.extensionData.updatedAt } if (_fields.includes("data")) { selectQuery["data"] = schema.extensionData.data } if (_fields.includes("search_text")) { selectQuery["searchText"] = schema.extensionData.searchText } // Build the query const query = db.select(selectQuery).from(schema.extensionData) // Add conditions const conditions = [orm.eq(schema.extensionData.extId, searchParams.extId)] if (searchParams.dataId) { conditions.push(orm.eq(schema.extensionData.dataId, searchParams.dataId)) } if (searchParams.dataType) { conditions.push(orm.eq(schema.extensionData.dataType, searchParams.dataType)) } if (searchParams.searchText) { switch (searchParams.searchMode) { case SearchModeEnum.ExactMatch: conditions.push(orm.eq(schema.extensionData.searchText, searchParams.searchText)) break case SearchModeEnum.Like: conditions.push(orm.like(schema.extensionData.searchText, `%${searchParams.searchText}%`)) break case SearchModeEnum.FTS: // For FTS, we need to use a raw SQL query since Drizzle doesn't support MATCH directly conditions.push(orm.sql`${schema.extensionDataFts.searchText} MATCH ${searchParams.searchText}`) break } } if (searchParams.afterCreatedAt) { conditions.push(orm.gt(schema.extensionData.createdAt, searchParams.afterCreatedAt)) } if (searchParams.beforeCreatedAt) { conditions.push(orm.lt(schema.extensionData.createdAt, searchParams.beforeCreatedAt)) } // Add ordering if (searchParams.orderByCreatedAt) { query.orderBy( searchParams.orderByCreatedAt === SQLSortOrderEnum.Asc ? orm.asc(schema.extensionData.createdAt) : orm.desc(schema.extensionData.createdAt) ) } if (searchParams.orderByUpdatedAt) { query.orderBy( searchParams.orderByUpdatedAt === SQLSortOrderEnum.Asc ? orm.asc(schema.extensionData.updatedAt) : orm.desc(schema.extensionData.updatedAt) ) } // Add limit and offset if (searchParams.limit) { query.limit(searchParams.limit) } if (searchParams.offset) { query.offset(searchParams.offset) } // Execute query and convert results const results = await query.where(orm.and(...conditions)).all() return results.map((rawData) => { // @ts-expect-error - rawData is unknown, but will be safe parsed with valibot return convertRawExtDataToExtData(rawData) }).filter((item): item is ExtData => item !== undefined) } // export async function getNCommands(n: number): // export function createExtension(ext: { // identifier: string // version: string // enabled?: boolean // path?: string // data?: any // }) { // return invoke(generateJarvisPluginCommand("create_extension"), ext) // }