From 3e1c000d515c0edc6f49241fc381187e1829ca7e Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 26 Mar 2025 21:25:13 +0100 Subject: [PATCH] feat: add default search engines seeding / set homarr docs as global server search engine (#2663) * feat: add default search engines seeding * feat: set Homarr Docs as the default search Set the default search engine in server settings during seeding * refactor: use typed methods to define settings * feat: add insertServerSettingByKeyAsync * feat: update seeding logic for server settings * fix: format file using prettier * fix: disable eslint for `urlTemplate` * refactor: remove never happning else * feat: enhance createDocumentationLink - Updated createDocumentationLink to accept query parameters * test: add unit tests for createDocumentationLink * fix: update urlTemplate for Homarr documentation --- packages/db/migrations/seed.ts | 83 +++++++++++++++++----- packages/db/queries/server-setting.ts | 11 +++ packages/definitions/src/docs/index.ts | 11 ++- packages/definitions/src/test/docs.spec.ts | 47 ++++++++++++ 4 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 packages/definitions/src/test/docs.spec.ts diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts index fa86e77a3..cb4fac094 100644 --- a/packages/db/migrations/seed.ts +++ b/packages/db/migrations/seed.ts @@ -1,18 +1,22 @@ -import SuperJSON from "superjson"; - import { objectKeys } from "@homarr/common"; -import { everyoneGroup } from "@homarr/definitions"; +import { createDocumentationLink, everyoneGroup } from "@homarr/definitions"; import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings"; -import { createId, eq } from ".."; import type { Database } from ".."; -import { onboarding, serverSettings } from "../schema"; +import { createId, eq } from ".."; +import { + getServerSettingByKeyAsync, + insertServerSettingByKeyAsync, + updateServerSettingByKeyAsync, +} from "../queries/server-setting"; +import { onboarding, searchEngines } from "../schema"; import { groups } from "../schema/mysql"; export const seedDataAsync = async (db: Database) => { await seedEveryoneGroupAsync(db); await seedOnboardingAsync(db); await seedServerSettingsAsync(db); + await seedDefaultSearchEnginesAsync(db); }; const seedEveryoneGroupAsync = async (db: Database) => { @@ -48,21 +52,73 @@ const seedOnboardingAsync = async (db: Database) => { console.log("Created onboarding step through seed"); }; +const seedDefaultSearchEnginesAsync = async (db: Database) => { + const existingSearchEngines = await db.$count(searchEngines); + + if (existingSearchEngines > 0) { + console.log("Skipping seeding of default search engines as some already exists"); + return; + } + + const homarrId = createId(); + const defaultSearchEngines = [ + { + id: createId(), + name: "Google", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/google.svg", + short: "g", + description: "Search the web with Google", + urlTemplate: "https://www.google.com/search?q=%s", + type: "generic" as const, + integrationId: null, + }, + { + id: createId(), + name: "YouTube", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/youtube.svg", + short: "yt", + description: "Search for videos on YouTube", + urlTemplate: "https://www.youtube.com/results?search_query=%s", + type: "generic" as const, + integrationId: null, + }, + { + id: homarrId, + name: "Homarr Docs", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg", + short: "docs", + description: "Search the Homarr documentation", + urlTemplate: createDocumentationLink("/search", undefined, { q: "%s" }), + type: "generic" as const, + integrationId: null, + }, + ]; + + await db.insert(searchEngines).values(defaultSearchEngines); + console.log(`Created ${defaultSearchEngines.length} default search engines through seeding process`); + + // Set Homarr docs as the default search engine in server settings + const searchSettings = await getServerSettingByKeyAsync(db, "search"); + + await updateServerSettingByKeyAsync(db, "search", { + ...searchSettings, + defaultSearchEngineId: homarrId, + }); + console.log("Set Homarr docs as the default search engine"); +}; + const seedServerSettingsAsync = async (db: Database) => { const serverSettingsData = await db.query.serverSettings.findMany(); for (const settingsKey of defaultServerSettingsKeys) { const currentDbEntry = serverSettingsData.find((setting) => setting.settingKey === settingsKey); if (!currentDbEntry) { - await db.insert(serverSettings).values({ - settingKey: settingsKey, - value: SuperJSON.stringify(defaultServerSettings[settingsKey]), - }); + await insertServerSettingByKeyAsync(db, settingsKey, defaultServerSettings[settingsKey]); console.log(`Created serverSetting through seed key=${settingsKey}`); continue; } - const currentSettings = SuperJSON.parse>(currentDbEntry.value); + const currentSettings = await getServerSettingByKeyAsync(db, settingsKey); const defaultSettings = defaultServerSettings[settingsKey]; const missingKeys = objectKeys(defaultSettings).filter((key) => !(key in currentSettings)); @@ -71,12 +127,7 @@ const seedServerSettingsAsync = async (db: Database) => { continue; } - await db - .update(serverSettings) - .set({ - value: SuperJSON.stringify({ ...defaultSettings, ...currentSettings }), // Add missing keys - }) - .where(eq(serverSettings.settingKey, settingsKey)); + await updateServerSettingByKeyAsync(db, settingsKey, { ...defaultSettings, ...currentSettings }); console.log(`Updated serverSetting through seed key=${settingsKey}`); } }; diff --git a/packages/db/queries/server-setting.ts b/packages/db/queries/server-setting.ts index 73f5156bc..371b18f29 100644 --- a/packages/db/queries/server-setting.ts +++ b/packages/db/queries/server-setting.ts @@ -50,3 +50,14 @@ export const updateServerSettingByKeyAsync = async ( + db: Database, + key: TKey, + value: ServerSettings[TKey], +) => { + await db.insert(serverSettings).values({ + settingKey: key, + value: SuperJSON.stringify(value), + }); +}; diff --git a/packages/definitions/src/docs/index.ts b/packages/definitions/src/docs/index.ts index 5d3c11f5c..af6731239 100644 --- a/packages/definitions/src/docs/index.ts +++ b/packages/definitions/src/docs/index.ts @@ -3,5 +3,12 @@ import type { HomarrDocumentationPath } from "./homarr-docs-sitemap"; const documentationBaseUrl = "https://homarr.dev"; // Please use the method so the path can be checked! -export const createDocumentationLink = (path: HomarrDocumentationPath, hashTag?: `#${string}`) => - `${documentationBaseUrl}${path}${hashTag ?? ""}`; +export const createDocumentationLink = ( + path: HomarrDocumentationPath, + hashTag?: `#${string}`, + queryParams?: Record, +) => { + const url = `${documentationBaseUrl}${path}`; + const params = queryParams ? `?${new URLSearchParams(queryParams)}` : ""; + return `${url}${params}${hashTag ?? ""}`; +}; diff --git a/packages/definitions/src/test/docs.spec.ts b/packages/definitions/src/test/docs.spec.ts new file mode 100644 index 000000000..d7690f22e --- /dev/null +++ b/packages/definitions/src/test/docs.spec.ts @@ -0,0 +1,47 @@ +/* eslint-disable no-restricted-syntax */ +import { describe, expect, test } from "vitest"; + +import { createDocumentationLink } from "../docs"; +import type { HomarrDocumentationPath } from "../docs/homarr-docs-sitemap"; + +describe("createDocumentationLink should generate correct URLs", () => { + test.each([ + ["/docs/getting-started", undefined, undefined, "https://homarr.dev/docs/getting-started"], + ["/blog", undefined, undefined, "https://homarr.dev/blog"], + ["/docs/widgets/weather", "#configuration", undefined, "https://homarr.dev/docs/widgets/weather#configuration"], + [ + "/docs/advanced/environment-variables", + undefined, + { lang: "en" }, + "https://homarr.dev/docs/advanced/environment-variables?lang=en", + ], + [ + "/docs/widgets/bookmarks", + "#sorting", + { lang: "fr", theme: "dark" }, + "https://homarr.dev/docs/widgets/bookmarks?lang=fr&theme=dark#sorting", + ], + ] satisfies [HomarrDocumentationPath, `#${string}` | undefined, Record | undefined, string][])( + "should create correct URL for path %s with hash %s and params %o", + (path, hashTag, queryParams, expected) => { + expect(createDocumentationLink(path, hashTag, queryParams)).toBe(expected); + }, + ); +}); + +describe("createDocumentationLink parameter validation", () => { + test("should work with only path parameter", () => { + const result = createDocumentationLink("/docs/getting-started"); + expect(result).toBe("https://homarr.dev/docs/getting-started"); + }); + + test("should work with path and hashtag", () => { + const result = createDocumentationLink("/docs/getting-started", "#installation"); + expect(result).toBe("https://homarr.dev/docs/getting-started#installation"); + }); + + test("should work with path and query params", () => { + const result = createDocumentationLink("/docs/getting-started", undefined, { version: "1.0" }); + expect(result).toBe("https://homarr.dev/docs/getting-started?version=1.0"); + }); +});