|
| 1 | +import * as relations from "@kksh/drizzle/relations" |
| 2 | +import * as schema from "@kksh/drizzle/schema" |
| 3 | +import { CmdType, Ext, ExtCmd, ExtData, SearchMode, SearchModeEnum, SQLSortOrder, SQLSortOrderEnum } from "@kunkunapi/src/models" |
| 4 | +import * as orm from "drizzle-orm" |
| 5 | +import type { SelectedFields } from "drizzle-orm/sqlite-core" |
| 6 | +import * as v from "valibot" |
| 7 | +import { db } from "./database" |
| 8 | + |
| 9 | +/* -------------------------------------------------------------------------- */ |
| 10 | +/* Built-in Extensions */ |
| 11 | +/* -------------------------------------------------------------------------- */ |
| 12 | + |
| 13 | +/* -------------------------------------------------------------------------- */ |
| 14 | +/* Extension CRUD */ |
| 15 | +/* -------------------------------------------------------------------------- */ |
| 16 | +export async function getUniqueExtensionByIdentifier(identifier: string): Promise<Ext | undefined> { |
| 17 | + const ext = await db |
| 18 | + .select() |
| 19 | + .from(schema.extensions) |
| 20 | + .where(orm.eq(schema.extensions.identifier, identifier)) |
| 21 | + .get() |
| 22 | + return v.parse(v.optional(Ext), ext) |
| 23 | +} |
| 24 | + |
| 25 | +/** |
| 26 | + * Use this function when you expect the extension to exist. Such as builtin extensions. |
| 27 | + * @param identifier |
| 28 | + * @returns |
| 29 | + */ |
| 30 | +export function getExtensionByIdentifierExpectExists(identifier: string): Promise<Ext> { |
| 31 | + return getUniqueExtensionByIdentifier(identifier).then((ext) => { |
| 32 | + if (!ext) { |
| 33 | + throw new Error(`Unexpexted Error: Extension ${identifier} not found`) |
| 34 | + } |
| 35 | + return ext |
| 36 | + }) |
| 37 | +} |
| 38 | + |
| 39 | +export async function getAllExtensions(): Promise<Ext[]> { |
| 40 | + const exts = await db.select().from(schema.extensions).all() |
| 41 | + return v.parse(v.array(Ext), exts) |
| 42 | +} |
| 43 | + |
| 44 | +/** |
| 45 | + * There can be duplicate extensions with the same identifier. Store and Dev extensions can have the same identifier. |
| 46 | + * But install path must be unique. |
| 47 | + * @param path |
| 48 | + */ |
| 49 | +export async function getUniqueExtensionByPath(path: string) { |
| 50 | + const ext = await db |
| 51 | + .select() |
| 52 | + .from(schema.extensions) |
| 53 | + .where(orm.eq(schema.extensions.path, path)) |
| 54 | + .get() |
| 55 | + return v.parse(Ext, ext) |
| 56 | +} |
| 57 | + |
| 58 | +export function getAllExtensionsByIdentifier(identifier: string): Promise<Ext[]> { |
| 59 | + return db |
| 60 | + .select() |
| 61 | + .from(schema.extensions) |
| 62 | + .where(orm.eq(schema.extensions.identifier, identifier)) |
| 63 | + .all() |
| 64 | + .then((exts) => v.parse(v.array(Ext), exts)) |
| 65 | +} |
| 66 | + |
| 67 | +export function deleteExtensionByPath(path: string): Promise<void> { |
| 68 | + return db |
| 69 | + .delete(schema.extensions) |
| 70 | + .where(orm.eq(schema.extensions.path, path)) |
| 71 | + .run() |
| 72 | + .then(() => undefined) |
| 73 | +} |
| 74 | + |
| 75 | +export function deleteExtensionByExtId(extId: number): Promise<void> { |
| 76 | + return db |
| 77 | + .delete(schema.extensions) |
| 78 | + .where(orm.eq(schema.extensions.extId, extId)) |
| 79 | + .run() |
| 80 | + .then(() => undefined) |
| 81 | +} |
| 82 | + |
| 83 | +/* -------------------------------------------------------------------------- */ |
| 84 | +/* Extension Command CRUD */ |
| 85 | +/* -------------------------------------------------------------------------- */ |
| 86 | + |
| 87 | +// export async function getExtensionWithCmdsByIdentifier(identifier: string): Promise<ExtWithCmds> { |
| 88 | +// const ext = await db |
| 89 | +// .select({ |
| 90 | +// ...schema.extensions, |
| 91 | +// commands: relations.commandsRelations |
| 92 | +// }) |
| 93 | +// .from(schema.extensions) |
| 94 | +// .leftJoin(schema.commands, orm.eq(schema.extensions.extId, schema.commands.extId)) |
| 95 | +// .where(orm.eq(schema.extensions.identifier, identifier)) |
| 96 | +// .get() |
| 97 | + |
| 98 | +// // return v.parse(v.nullable(ExtWithCmds), ext); |
| 99 | +// } |
| 100 | + |
| 101 | +export async function getCmdById(cmdId: number): Promise<ExtCmd> { |
| 102 | + const cmd = await db |
| 103 | + .select() |
| 104 | + .from(schema.commands) |
| 105 | + .where(orm.eq(schema.commands.cmdId, cmdId)) |
| 106 | + .get() |
| 107 | + return v.parse(ExtCmd, cmd) |
| 108 | +} |
| 109 | + |
| 110 | +export async function getAllCmds(): Promise<ExtCmd[]> { |
| 111 | + const cmds = await db.select().from(schema.commands).all() |
| 112 | + return v.parse(v.array(ExtCmd), cmds) |
| 113 | +} |
| 114 | + |
| 115 | +export function getCommandsByExtId(extId: number) { |
| 116 | + return db |
| 117 | + .select() |
| 118 | + .from(schema.commands) |
| 119 | + .where(orm.eq(schema.commands.extId, extId)) |
| 120 | + .all() |
| 121 | + .then((cmds) => v.parse(v.array(ExtCmd), cmds)) |
| 122 | +} |
| 123 | + |
| 124 | +export function deleteCmdById(cmdId: number) { |
| 125 | + return db |
| 126 | + .delete(schema.commands) |
| 127 | + .where(orm.eq(schema.commands.cmdId, cmdId)) |
| 128 | + .run() |
| 129 | + .then(() => undefined) |
| 130 | +} |
| 131 | + |
| 132 | +export function updateCmdByID(data: { |
| 133 | + cmdId: number |
| 134 | + name: string |
| 135 | + cmdType: CmdType |
| 136 | + data: string |
| 137 | + alias?: string |
| 138 | + hotkey?: string |
| 139 | + enabled: boolean |
| 140 | +}) { |
| 141 | + return db |
| 142 | + .update(schema.commands) |
| 143 | + .set({ |
| 144 | + name: data.name, |
| 145 | + type: data.cmdType, |
| 146 | + data: data.data, |
| 147 | + alias: data.alias, // optional |
| 148 | + hotkey: data.hotkey, // optional |
| 149 | + enabled: data.enabled |
| 150 | + // in drizzle schema, use integer({ mode: 'boolean' }) for boolean sqlite |
| 151 | + // enabled: data.enabled ? String(data.enabled) : undefined |
| 152 | + }) |
| 153 | + .where(orm.eq(schema.commands.cmdId, data.cmdId)) |
| 154 | + .run() |
| 155 | + .then(() => undefined) |
| 156 | +} |
| 157 | + |
| 158 | +/* -------------------------------------------------------------------------- */ |
| 159 | +/* Extension Data CRUD */ |
| 160 | +/* -------------------------------------------------------------------------- */ |
| 161 | +export const ExtDataField = v.union([v.literal("data"), v.literal("search_text")]) |
| 162 | +export type ExtDataField = v.InferOutput<typeof ExtDataField> |
| 163 | + |
| 164 | +function convertRawExtDataToExtData(rawData?: { |
| 165 | + createdAt: string |
| 166 | + updatedAt: string |
| 167 | + data: null | string |
| 168 | + searchText?: null | string |
| 169 | + dataId: number |
| 170 | + extId: number |
| 171 | + dataType: string |
| 172 | +}): ExtData | undefined { |
| 173 | + if (!rawData) { |
| 174 | + return rawData |
| 175 | + } |
| 176 | + const parsedRes = v.safeParse(ExtData, { |
| 177 | + ...rawData, |
| 178 | + createdAt: new Date(rawData.createdAt), |
| 179 | + updatedAt: new Date(rawData.updatedAt), |
| 180 | + data: rawData.data ?? undefined, |
| 181 | + searchText: rawData.searchText ?? undefined |
| 182 | + }) |
| 183 | + if (parsedRes.success) { |
| 184 | + return parsedRes.output |
| 185 | + } else { |
| 186 | + console.error("Extension Data Parse Failure", parsedRes.issues) |
| 187 | + throw new Error("Fail to parse extension data") |
| 188 | + } |
| 189 | +} |
| 190 | + |
| 191 | +export function createExtensionData(data: { |
| 192 | + extId: number |
| 193 | + dataType: string |
| 194 | + data: string |
| 195 | + searchText?: string |
| 196 | +}) { |
| 197 | + return db.insert(schema.extensionData).values(data).run() |
| 198 | +} |
| 199 | + |
| 200 | +export function getExtensionDataById(dataId: number, fields?: ExtDataField[]) { |
| 201 | + const _fields = fields ?? [] |
| 202 | + const selectQuery: SelectedFields = { |
| 203 | + dataId: schema.extensionData.dataId, |
| 204 | + extId: schema.extensionData.extId, |
| 205 | + dataType: schema.extensionData.dataType, |
| 206 | + metadata: schema.extensionData.metadata, |
| 207 | + createdAt: schema.extensionData.createdAt, |
| 208 | + updatedAt: schema.extensionData.updatedAt |
| 209 | + // data: schema.extensionData.data, |
| 210 | + // searchText: schema.extensionData.searchText |
| 211 | + } |
| 212 | + if (_fields.includes("data")) { |
| 213 | + selectQuery["data"] = schema.extensionData.data |
| 214 | + } |
| 215 | + if (_fields.includes("search_text")) { |
| 216 | + selectQuery["searchText"] = schema.extensionData.searchText |
| 217 | + } |
| 218 | + return db |
| 219 | + .select(selectQuery) |
| 220 | + .from(schema.extensionData) |
| 221 | + .where(orm.eq(schema.extensionData.dataId, dataId)) |
| 222 | + .get() |
| 223 | + .then((rawData) => { |
| 224 | + console.log("Raw Data", rawData) |
| 225 | + // @ts-expect-error - rawData is unknown, but will be safe parsed with valibot |
| 226 | + return convertRawExtDataToExtData(rawData) |
| 227 | + }) |
| 228 | +} |
| 229 | + |
| 230 | +export async function searchExtensionData(searchParams: { |
| 231 | + extId: number |
| 232 | + searchMode: SearchMode |
| 233 | + dataId?: number |
| 234 | + dataType?: string |
| 235 | + searchText?: string |
| 236 | + afterCreatedAt?: string |
| 237 | + beforeCreatedAt?: string |
| 238 | + limit?: number |
| 239 | + offset?: number |
| 240 | + orderByCreatedAt?: SQLSortOrder |
| 241 | + orderByUpdatedAt?: SQLSortOrder |
| 242 | + fields?: ExtDataField[] |
| 243 | +}): Promise<ExtData[]> { |
| 244 | + const fields = v.parse(v.optional(v.array(ExtDataField), []), searchParams.fields) |
| 245 | + const _fields = fields ?? [] |
| 246 | + |
| 247 | + // Build the select query based on fields |
| 248 | + const selectQuery: SelectedFields = { |
| 249 | + dataId: schema.extensionData.dataId, |
| 250 | + extId: schema.extensionData.extId, |
| 251 | + dataType: schema.extensionData.dataType, |
| 252 | + createdAt: schema.extensionData.createdAt, |
| 253 | + updatedAt: schema.extensionData.updatedAt |
| 254 | + } |
| 255 | + |
| 256 | + if (_fields.includes("data")) { |
| 257 | + selectQuery["data"] = schema.extensionData.data |
| 258 | + } |
| 259 | + if (_fields.includes("search_text")) { |
| 260 | + selectQuery["searchText"] = schema.extensionData.searchText |
| 261 | + } |
| 262 | + |
| 263 | + // Build the query |
| 264 | + const query = db.select(selectQuery).from(schema.extensionData) |
| 265 | + |
| 266 | + // Add conditions |
| 267 | + const conditions = [orm.eq(schema.extensionData.extId, searchParams.extId)] |
| 268 | + |
| 269 | + if (searchParams.dataId) { |
| 270 | + conditions.push(orm.eq(schema.extensionData.dataId, searchParams.dataId)) |
| 271 | + } |
| 272 | + |
| 273 | + if (searchParams.dataType) { |
| 274 | + conditions.push(orm.eq(schema.extensionData.dataType, searchParams.dataType)) |
| 275 | + } |
| 276 | + |
| 277 | + if (searchParams.searchText) { |
| 278 | + switch (searchParams.searchMode) { |
| 279 | + case SearchModeEnum.ExactMatch: |
| 280 | + conditions.push(orm.eq(schema.extensionData.searchText, searchParams.searchText)) |
| 281 | + break |
| 282 | + case SearchModeEnum.Like: |
| 283 | + conditions.push(orm.like(schema.extensionData.searchText, `%${searchParams.searchText}%`)) |
| 284 | + break |
| 285 | + case SearchModeEnum.FTS: |
| 286 | + // For FTS, we need to use a raw SQL query since Drizzle doesn't support MATCH directly |
| 287 | + conditions.push(orm.sql`${schema.extensionDataFts.searchText} MATCH ${searchParams.searchText}`) |
| 288 | + break |
| 289 | + } |
| 290 | + } |
| 291 | + |
| 292 | + if (searchParams.afterCreatedAt) { |
| 293 | + conditions.push(orm.gt(schema.extensionData.createdAt, searchParams.afterCreatedAt)) |
| 294 | + } |
| 295 | + |
| 296 | + if (searchParams.beforeCreatedAt) { |
| 297 | + conditions.push(orm.lt(schema.extensionData.createdAt, searchParams.beforeCreatedAt)) |
| 298 | + } |
| 299 | + |
| 300 | + // Add ordering |
| 301 | + if (searchParams.orderByCreatedAt) { |
| 302 | + query.orderBy( |
| 303 | + searchParams.orderByCreatedAt === SQLSortOrderEnum.Asc |
| 304 | + ? orm.asc(schema.extensionData.createdAt) |
| 305 | + : orm.desc(schema.extensionData.createdAt) |
| 306 | + ) |
| 307 | + } |
| 308 | + |
| 309 | + if (searchParams.orderByUpdatedAt) { |
| 310 | + query.orderBy( |
| 311 | + searchParams.orderByUpdatedAt === SQLSortOrderEnum.Asc |
| 312 | + ? orm.asc(schema.extensionData.updatedAt) |
| 313 | + : orm.desc(schema.extensionData.updatedAt) |
| 314 | + ) |
| 315 | + } |
| 316 | + |
| 317 | + // Add limit and offset |
| 318 | + if (searchParams.limit) { |
| 319 | + query.limit(searchParams.limit) |
| 320 | + } |
| 321 | + |
| 322 | + if (searchParams.offset) { |
| 323 | + query.offset(searchParams.offset) |
| 324 | + } |
| 325 | + |
| 326 | + // Execute query and convert results |
| 327 | + const results = await query.where(orm.and(...conditions)).all() |
| 328 | + return results.map((rawData) => { |
| 329 | + // @ts-expect-error - rawData is unknown, but will be safe parsed with valibot |
| 330 | + return convertRawExtDataToExtData(rawData) |
| 331 | + }).filter((item): item is ExtData => item !== undefined) |
| 332 | +} |
| 333 | + |
| 334 | +// export async function getNCommands(n: number): |
| 335 | +// export function createExtension(ext: { |
| 336 | +// identifier: string |
| 337 | +// version: string |
| 338 | +// enabled?: boolean |
| 339 | +// path?: string |
| 340 | +// data?: any |
| 341 | +// }) { |
| 342 | +// return invoke<void>(generateJarvisPluginCommand("create_extension"), ext) |
| 343 | +// } |
0 commit comments