-
Notifications
You must be signed in to change notification settings - Fork 86
feat(new-webui): Port search services from old-webui to Fastify plugins. #912
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
206f755
8a00cc7
592f5e4
c93574e
f22304e
14c2f00
4a74063
3be40ca
4ba04b3
a4548c5
50e2ce7
45e623c
c9dea3b
8dc3883
440b674
8dd1341
99b8d04
215bdf4
0126404
cd43194
7394d32
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,170 @@ | ||||||||||||||||||||||||||||||||||||||||||
| import {setTimeout} from "node:timers/promises"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| import type {MySQLPromisePool} from "@fastify/mysql"; | ||||||||||||||||||||||||||||||||||||||||||
| import {encode} from "@msgpack/msgpack"; | ||||||||||||||||||||||||||||||||||||||||||
| import {FastifyInstance} from "fastify"; | ||||||||||||||||||||||||||||||||||||||||||
| import fp from "fastify-plugin"; | ||||||||||||||||||||||||||||||||||||||||||
| import {ResultSetHeader} from "mysql2"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| import settings from "../../../../../../settings.json" with {type: "json"}; | ||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||
| QUERY_JOB_STATUS, | ||||||||||||||||||||||||||||||||||||||||||
| QUERY_JOB_STATUS_WAITING_STATES, | ||||||||||||||||||||||||||||||||||||||||||
| QUERY_JOB_TYPE, | ||||||||||||||||||||||||||||||||||||||||||
| QUERY_JOBS_TABLE_COLUMN_NAMES, | ||||||||||||||||||||||||||||||||||||||||||
| QueryJob, | ||||||||||||||||||||||||||||||||||||||||||
| } from "../../../../../typings/query.js"; | ||||||||||||||||||||||||||||||||||||||||||
| import {JOB_COMPLETION_STATUS_POLL_INTERVAL_MILLIS} from "./typings.js"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Class for submitting and monitoring query jobs in the database. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| class QueryJobsDbManager { | ||||||||||||||||||||||||||||||||||||||||||
| #sqlDbConnPool: MySQLPromisePool; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private constructor (sqlDbConnPool: MySQLPromisePool) { | ||||||||||||||||||||||||||||||||||||||||||
| this.#sqlDbConnPool = sqlDbConnPool; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Creates a new QueryJobsDbManager. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @param fastify | ||||||||||||||||||||||||||||||||||||||||||
| * @return | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| static create (fastify: FastifyInstance): QueryJobsDbManager { | ||||||||||||||||||||||||||||||||||||||||||
| const sqlDbConnPool = fastify.mysql; | ||||||||||||||||||||||||||||||||||||||||||
| return new QueryJobsDbManager(sqlDbConnPool); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Submits a search job to the database. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @param searchConfig The arguments for the query. | ||||||||||||||||||||||||||||||||||||||||||
| * @return The job's ID. | ||||||||||||||||||||||||||||||||||||||||||
| * @throws {Error} on error. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| async submitSearchJob (searchConfig: object): Promise<number> { | ||||||||||||||||||||||||||||||||||||||||||
| const [queryInsertResults] = await this.#sqlDbConnPool.query<ResultSetHeader>( | ||||||||||||||||||||||||||||||||||||||||||
| ` | ||||||||||||||||||||||||||||||||||||||||||
| INSERT INTO ${settings.SqlDbQueryJobsTableName} ( | ||||||||||||||||||||||||||||||||||||||||||
| ${QUERY_JOBS_TABLE_COLUMN_NAMES.JOB_CONFIG}, | ||||||||||||||||||||||||||||||||||||||||||
| ${QUERY_JOBS_TABLE_COLUMN_NAMES.TYPE} | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| VALUES (?, ?) | ||||||||||||||||||||||||||||||||||||||||||
| `, | ||||||||||||||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||||||||||||||
| Buffer.from(encode(searchConfig)), | ||||||||||||||||||||||||||||||||||||||||||
| QUERY_JOB_TYPE.SEARCH_OR_AGGREGATION, | ||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return queryInsertResults.insertId; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Submits an aggregation job to the database. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @param searchConfig The arguments for the query. | ||||||||||||||||||||||||||||||||||||||||||
| * @param timeRangeBucketSizeMillis | ||||||||||||||||||||||||||||||||||||||||||
| * @return The aggregation job's ID. | ||||||||||||||||||||||||||||||||||||||||||
| * @throws {Error} on error. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| async submitAggregationJob ( | ||||||||||||||||||||||||||||||||||||||||||
| searchConfig: object, | ||||||||||||||||||||||||||||||||||||||||||
| timeRangeBucketSizeMillis: number | ||||||||||||||||||||||||||||||||||||||||||
| ): Promise<number> { | ||||||||||||||||||||||||||||||||||||||||||
| const searchAggregationConfig = { | ||||||||||||||||||||||||||||||||||||||||||
| ...searchConfig, | ||||||||||||||||||||||||||||||||||||||||||
| aggregation_config: { | ||||||||||||||||||||||||||||||||||||||||||
| count_by_time_bucket_size: timeRangeBucketSizeMillis, | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return this.submitSearchJob(searchAggregationConfig); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Submits a query cancellation request to the database. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @param jobId ID of the job to cancel. | ||||||||||||||||||||||||||||||||||||||||||
| * @return | ||||||||||||||||||||||||||||||||||||||||||
| * @throws {Error} on error. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| async submitQueryCancellation (jobId: number): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||
| await this.#sqlDbConnPool.query( | ||||||||||||||||||||||||||||||||||||||||||
| ` | ||||||||||||||||||||||||||||||||||||||||||
| UPDATE ${settings.SqlDbQueryJobsTableName} | ||||||||||||||||||||||||||||||||||||||||||
| SET ${QUERY_JOBS_TABLE_COLUMN_NAMES.STATUS} = ${QUERY_JOB_STATUS.CANCELLING} | ||||||||||||||||||||||||||||||||||||||||||
| WHERE ${QUERY_JOBS_TABLE_COLUMN_NAMES.ID} = ? | ||||||||||||||||||||||||||||||||||||||||||
| AND ${QUERY_JOBS_TABLE_COLUMN_NAMES.STATUS} | ||||||||||||||||||||||||||||||||||||||||||
| IN (${QUERY_JOB_STATUS.PENDING}, ${QUERY_JOB_STATUS.RUNNING}) | ||||||||||||||||||||||||||||||||||||||||||
| `, | ||||||||||||||||||||||||||||||||||||||||||
| jobId, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+96
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Prefer array wrapper for parameter binding
- `,
- jobId,
+ `,
+ [jobId],Sticking to the documented array form keeps the call site consistent with the rest of the codebase and avoids surprises. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Waits for the job to complete. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @param jobId | ||||||||||||||||||||||||||||||||||||||||||
| * @return | ||||||||||||||||||||||||||||||||||||||||||
| * @throws {Error} on MySQL error, if the job wasn't found in the database, if the job was | ||||||||||||||||||||||||||||||||||||||||||
| * cancelled, or if the job completed in an unexpected state. | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| async awaitJobCompletion (jobId: number): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||||||||||||||||||||||||||||||||||||||||||
| while (true) { | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+117
to
+118
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Consider using a condition in the while loop. The infinite loop with a break statement could be replaced with a conditional while loop for better readability. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- while (true) {
+ let jobCompleted = false;
+ while (!jobCompleted) {And then replace the - break;
+ jobCompleted = true;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| let queryJob: QueryJob | undefined; | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| const [queryRows] = await this.#sqlDbConnPool.query<QueryJob[]>( | ||||||||||||||||||||||||||||||||||||||||||
| ` | ||||||||||||||||||||||||||||||||||||||||||
| SELECT ${QUERY_JOBS_TABLE_COLUMN_NAMES.STATUS} | ||||||||||||||||||||||||||||||||||||||||||
| FROM ${settings.SqlDbQueryJobsTableName} | ||||||||||||||||||||||||||||||||||||||||||
| WHERE ${QUERY_JOBS_TABLE_COLUMN_NAMES.ID} = ? | ||||||||||||||||||||||||||||||||||||||||||
| `, | ||||||||||||||||||||||||||||||||||||||||||
| jobId | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| [queryJob] = queryRows; | ||||||||||||||||||||||||||||||||||||||||||
| } catch (e: unknown) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Failed to query status for job ${jobId}`, {cause: e}); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| if ("undefined" === typeof queryJob) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Job ${jobId} not found in the database.`); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const status = queryJob[QUERY_JOBS_TABLE_COLUMN_NAMES.STATUS]; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (false === QUERY_JOB_STATUS_WAITING_STATES.has(status)) { | ||||||||||||||||||||||||||||||||||||||||||
| if (QUERY_JOB_STATUS.CANCELLED === status) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Job ${jobId} was cancelled.`); | ||||||||||||||||||||||||||||||||||||||||||
| } else if (QUERY_JOB_STATUS.SUCCEEDED !== status) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||
| `Job ${jobId} exited with unexpected status=${status}: ` + | ||||||||||||||||||||||||||||||||||||||||||
| `${Object.keys(QUERY_JOB_STATUS)[status]}.` | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| await setTimeout(JOB_COMPLETION_STATUS_POLL_INTERVAL_MILLIS); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| declare module "fastify" { | ||||||||||||||||||||||||||||||||||||||||||
| export interface FastifyInstance { | ||||||||||||||||||||||||||||||||||||||||||
| QueryJobsDbManager: QueryJobsDbManager; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export default fp( | ||||||||||||||||||||||||||||||||||||||||||
| (fastify) => { | ||||||||||||||||||||||||||||||||||||||||||
| fastify.decorate("QueryJobsDbManager", QueryJobsDbManager.create(fastify)); | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| name: "QueryJobsDbManager", | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| /** | ||
| * Interval in milliseconds for polling the completion status of a job. | ||
| */ | ||
| export const JOB_COMPLETION_STATUS_POLL_INTERVAL_MILLIS = 500; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import {FastifyInstance} from "fastify"; | ||
| import fp from "fastify-plugin"; | ||
|
|
||
| import settings from "../../../../../../settings.json" with {type: "json"}; | ||
| import type {SearchResultsMetadataDocument} from "./typings.js"; | ||
|
|
||
|
|
||
| /** | ||
| * Creates a MongoDB collection for search results metadata. | ||
| * | ||
| * @param fastify | ||
| * @return MongoDB collection | ||
| * @throws {Error} if the MongoDB database is not found. | ||
|
Comment on lines
+12
to
+13
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Enhance JSDoc return type documentation. The return type in the JSDoc comment could be more specific to help developers understand what's being returned. - * @return MongoDB collection
+ * @return {Collection<SearchResultsMetadataDocument>} MongoDB collection for search results metadata🤖 Prompt for AI Agents |
||
| */ | ||
| const createSearchResultsMetadataCollection = (fastify: FastifyInstance) => { | ||
| if ("undefined" === typeof fastify.mongo.db) { | ||
| throw new Error("MongoDB database not found"); | ||
| } | ||
|
|
||
| return fastify.mongo.db.collection<SearchResultsMetadataDocument>( | ||
| settings.MongoDbSearchResultsMetadataCollectionName | ||
| ); | ||
| }; | ||
|
|
||
| declare module "fastify" { | ||
| export interface FastifyInstance { | ||
| SearchResultsMetadataCollection: ReturnType<typeof createSearchResultsMetadataCollection>; | ||
| } | ||
| } | ||
|
|
||
| export default fp( | ||
| (fastify) => { | ||
| fastify.decorate( | ||
| "SearchResultsMetadataCollection", | ||
| createSearchResultsMetadataCollection(fastify) | ||
| ); | ||
| }, | ||
| { | ||
| name: "SearchResultsMetadataCollection", | ||
| } | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import {Nullable} from "../../../../../typings/common.js"; | ||
|
|
||
|
|
||
| /** | ||
| * Enum of search-related signals. | ||
| * | ||
| * This includes request and response signals for various search operations and their respective | ||
| * states. | ||
| */ | ||
| enum SEARCH_SIGNAL { | ||
| NONE = "none", | ||
|
|
||
| REQ_CANCELLING = "req-cancelling", | ||
| REQ_CLEARING = "req-clearing", | ||
| REQ_QUERYING = "req-querying", | ||
|
|
||
| RESP_DONE = "resp-done", | ||
| RESP_QUERYING = "resp-querying", | ||
| } | ||
|
|
||
| /** | ||
| * MongoDB document for search results metadata. `numTotalResults` is optional | ||
| * since it is only set when the search job is completed. | ||
| */ | ||
| interface SearchResultsMetadataDocument { | ||
| _id: string; | ||
| errorMsg: Nullable<string>; | ||
| lastSignal: SEARCH_SIGNAL; | ||
| numTotalResults?: number; | ||
| } | ||
|
|
||
| export type {SearchResultsMetadataDocument}; | ||
| export {SEARCH_SIGNAL}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as plugins
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also creates nice routes like api/search/query. Like it will add the folder structure to the route, so i like it