From 436b291be1658e40571db643073262ec8ca8e033 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 7 Jan 2026 17:47:13 +0100 Subject: [PATCH 01/19] fix(db): update default search engine URL template in seed migration --- packages/db/migrations/seed.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts index 407c03746b..d7c2e5bb24 100644 --- a/packages/db/migrations/seed.ts +++ b/packages/db/migrations/seed.ts @@ -1,6 +1,5 @@ import { createId, objectKeys } from "@homarr/common"; import { - createDocumentationLink, everyoneGroup, getIntegrationDefaultUrl, getIntegrationName, @@ -95,7 +94,7 @@ const seedDefaultSearchEnginesAsync = async (db: Database) => { 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" }), + urlTemplate: "https://homarr.dev/search?q=%s", type: "generic" as const, integrationId: null, }, From 21ce992368eb96cb034d0189cbe381c10f37c63a Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 7 Jan 2026 17:51:45 +0100 Subject: [PATCH 02/19] feat(api): add DuckDuckGo bangs search endpoint --- packages/api/src/root.ts | 2 + packages/api/src/router/bangs/bangs-router.ts | 19 +++ packages/api/src/services/duckduckgo-bangs.ts | 127 ++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 packages/api/src/router/bangs/bangs-router.ts create mode 100644 packages/api/src/services/duckduckgo-bangs.ts diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index 6efc10ca5b..205190741d 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -16,6 +16,7 @@ import { locationRouter } from "./router/location"; import { logRouter } from "./router/log"; import { mediaRouter } from "./router/medias/media-router"; import { onboardRouter } from "./router/onboard/onboard-router"; +import { bangsRouter } from "./router/bangs/bangs-router"; import { searchEngineRouter } from "./router/search-engine/search-engine-router"; import { sectionRouter } from "./router/section/section-router"; import { serverSettingsRouter } from "./router/serverSettings"; @@ -25,6 +26,7 @@ import { widgetRouter } from "./router/widgets"; import { createTRPCRouter } from "./trpc"; export const appRouter = createTRPCRouter({ + bangs: bangsRouter, user: userRouter, group: groupRouter, invite: inviteRouter, diff --git a/packages/api/src/router/bangs/bangs-router.ts b/packages/api/src/router/bangs/bangs-router.ts new file mode 100644 index 0000000000..49f7b955cb --- /dev/null +++ b/packages/api/src/router/bangs/bangs-router.ts @@ -0,0 +1,19 @@ +import { z } from "zod/v4"; + +import { searchDuckDuckGoBangsAsync } from "../../services/duckduckgo-bangs"; +import { createTRPCRouter, publicProcedure } from "../../trpc"; + +export const bangsRouter = createTRPCRouter({ + search: publicProcedure + .input( + z.object({ + query: z.string(), + limit: z.number().int().min(1).max(50).default(20), + }), + ) + .query(async ({ input }) => { + return await searchDuckDuckGoBangsAsync({ query: input.query, limit: input.limit }); + }), +}); + + diff --git a/packages/api/src/services/duckduckgo-bangs.ts b/packages/api/src/services/duckduckgo-bangs.ts new file mode 100644 index 0000000000..54ac776f75 --- /dev/null +++ b/packages/api/src/services/duckduckgo-bangs.ts @@ -0,0 +1,127 @@ +const DUCKDUCKGO_BANGS_URL = "https://duckduckgo.com/bang.js"; + +export type DuckDuckGoBang = { + /** Bang token (e.g. "yt") */ + t: string; + /** Display name */ + s: string; + /** Domain */ + d?: string; + /** Url template (contains {{{s}}}) */ + u: string; + /** Category */ + c?: string; + /** Subcategory */ + sc?: string; + /** Rank */ + r?: number; +}; + +type CachedBangs = { + fetchedAtMs: number; + bangs: DuckDuckGoBang[]; +}; + +const DAY_MS = 24 * 60 * 60 * 1000; +const cache: { value: CachedBangs | null; inFlight: Promise | null } = { value: null, inFlight: null }; + +const normalizeToken = (token: string) => token.toLowerCase().trim(); + +const byTokenAsc = (a: DuckDuckGoBang, b: DuckDuckGoBang) => a.t.localeCompare(b.t); + +const isCacheValid = (cached: CachedBangs, nowMs: number) => nowMs - cached.fetchedAtMs < DAY_MS; + +const fetchBangsAsync = async (): Promise => { + const res = await fetch(DUCKDUCKGO_BANGS_URL, { + headers: { + accept: "application/json,text/plain,*/*", + }, + }); + + if (!res.ok) { + throw new Error(`Failed to fetch DuckDuckGo bangs: ${res.status} ${res.statusText}`); + } + + const json = (await res.json()) as unknown; + if (!Array.isArray(json)) { + throw new Error("Invalid DuckDuckGo bangs payload: expected array"); + } + + const parsed: DuckDuckGoBang[] = []; + for (const item of json) { + if (!item || typeof item !== "object") continue; + const t = (item as Record).t; + const s = (item as Record).s; + const u = (item as Record).u; + if (typeof t !== "string" || typeof s !== "string" || typeof u !== "string") continue; + + const token = normalizeToken(t); + if (!token) continue; + + parsed.push({ + t: token, + s, + u, + d: typeof (item as Record).d === "string" ? ((item as Record).d as string) : undefined, + c: typeof (item as Record).c === "string" ? ((item as Record).c as string) : undefined, + sc: typeof (item as Record).sc === "string" ? ((item as Record).sc as string) : undefined, + r: typeof (item as Record).r === "number" ? ((item as Record).r as number) : undefined, + }); + } + + parsed.sort(byTokenAsc); + return { fetchedAtMs: Date.now(), bangs: parsed }; +}; + +export const getDuckDuckGoBangsAsync = async (nowMs = Date.now()): Promise => { + if (cache.value && isCacheValid(cache.value, nowMs)) { + return cache.value; + } + + if (!cache.inFlight) { + cache.inFlight = fetchBangsAsync() + .then((value) => { + cache.value = value; + return value; + }) + .finally(() => { + cache.inFlight = null; + }); + } + + return await cache.inFlight; +}; + +const lowerBound = (arr: DuckDuckGoBang[], tokenPrefix: string) => { + let lo = 0; + let hi = arr.length; + while (lo < hi) { + const mid = (lo + hi) >> 1; + if (arr[mid]!.t < tokenPrefix) lo = mid + 1; + else hi = mid; + } + return lo; +}; + +export const searchDuckDuckGoBangsAsync = async (input: { + query: string; + limit: number; +}): Promise => { + const q = normalizeToken(input.query); + if (!q) return []; + + const { bangs } = await getDuckDuckGoBangsAsync(); + const start = lowerBound(bangs, q); + const out: DuckDuckGoBang[] = []; + + for (let i = start; i < bangs.length; i++) { + const bang = bangs[i]!; + if (!bang.t.startsWith(q)) break; + out.push(bang); + if (out.length >= input.limit) break; + } + + return out; +}; + + From f380f4a22633f9a4d26a0ec386829745963b4ac2 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 7 Jan 2026 17:52:07 +0100 Subject: [PATCH 03/19] feat(spotlight): make launcher default and support in-place query updates --- .../src/components/actions/group-actions.tsx | 4 + .../actions/groups/action-group.tsx | 6 +- .../actions/items/group-action-item.tsx | 16 +- .../spotlight/src/components/spotlight.tsx | 46 +++--- packages/spotlight/src/lib/interaction.ts | 1 + .../src/modes/help/home-empty-groups.tsx | 140 ++++++++++++++++++ packages/spotlight/src/modes/index.tsx | 80 +--------- 7 files changed, 189 insertions(+), 104 deletions(-) create mode 100644 packages/spotlight/src/modes/help/home-empty-groups.tsx diff --git a/packages/spotlight/src/components/actions/group-actions.tsx b/packages/spotlight/src/components/actions/group-actions.tsx index 0477e0ce2e..cc71591b90 100644 --- a/packages/spotlight/src/components/actions/group-actions.tsx +++ b/packages/spotlight/src/components/actions/group-actions.tsx @@ -12,6 +12,7 @@ import { SpotlightGroupActionItem } from "./items/group-action-item"; interface GroupActionsProps> { group: SearchGroup; query: string; + setQuery: (query: string) => void; setMode: (mode: keyof TranslationObject["search"]["mode"]) => void; setChildrenOptions: (options: inferSearchInteractionOptions<"children">) => void; } @@ -19,6 +20,7 @@ interface GroupActionsProps> { export const SpotlightGroupActions = >({ group, query, + setQuery, setMode, setChildrenOptions, }: GroupActionsProps) => { @@ -58,6 +60,7 @@ export const SpotlightGroupActions = >({ option={option} group={group} query={query} + setQuery={setQuery} setMode={setMode} setChildrenOptions={setChildrenOptions} /> @@ -90,6 +93,7 @@ export const SpotlightGroupActions = >({ option={option} group={group} query={query} + setQuery={setQuery} setMode={setMode} setChildrenOptions={setChildrenOptions} /> diff --git a/packages/spotlight/src/components/actions/groups/action-group.tsx b/packages/spotlight/src/components/actions/groups/action-group.tsx index 300e0f10a8..2b3e282ecf 100644 --- a/packages/spotlight/src/components/actions/groups/action-group.tsx +++ b/packages/spotlight/src/components/actions/groups/action-group.tsx @@ -11,6 +11,7 @@ import { SpotlightGroupActions } from "../group-actions"; interface SpotlightActionGroupsProps { groups: SearchGroup[]; query: string; + setQuery: (query: string) => void; setMode: (mode: keyof TranslationObject["search"]["mode"]) => void; setChildrenOptions: (options: inferSearchInteractionOptions<"children">) => void; } @@ -19,7 +20,10 @@ export const SpotlightActionGroups = ({ groups, ...others }: SpotlightActionGrou const t = useI18n(); return groups.map((group) => ( - + {/*eslint-disable-next-line @typescript-eslint/no-explicit-any */} group={group} {...others} /> diff --git a/packages/spotlight/src/components/actions/items/group-action-item.tsx b/packages/spotlight/src/components/actions/items/group-action-item.tsx index d5f85f3be9..60399adc7b 100644 --- a/packages/spotlight/src/components/actions/items/group-action-item.tsx +++ b/packages/spotlight/src/components/actions/items/group-action-item.tsx @@ -5,11 +5,13 @@ import { Link } from "@homarr/ui"; import type { SearchGroup } from "../../../lib/group"; import type { inferSearchInteractionOptions } from "../../../lib/interaction"; +import { selectAction, spotlightStore } from "../../../spotlight-store"; import classes from "./action-item.module.css"; interface SpotlightGroupActionItemProps> { option: TOption; query: string; + setQuery: (query: string) => void; setMode: (mode: keyof TranslationObject["search"]["mode"]) => void; setChildrenOptions: (options: inferSearchInteractionOptions<"children">) => void; group: SearchGroup; @@ -18,11 +20,15 @@ interface SpotlightGroupActionItemProps> export const SpotlightGroupActionItem = >({ group, query, + setQuery, setMode, setChildrenOptions, option, }: SpotlightGroupActionItemProps) => { const interaction = group.useInteraction(option, query); + // Avoid passing React's special `key` prop via spread + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key: _reactKey, ...optionProps } = option as unknown as { key?: unknown } & Record; const renderRoot = interaction.type === "link" @@ -34,6 +40,9 @@ export const SpotlightGroupActionItem = const handleClickAsync = async () => { if (interaction.type === "javaScript") { await interaction.onSelect(); + } else if (interaction.type === "setQuery") { + setQuery(interaction.query); + setTimeout(() => selectAction(0, spotlightStore)); } else if (interaction.type === "mode") { setMode(interaction.mode); } else if (interaction.type === "children") { @@ -46,11 +55,14 @@ export const SpotlightGroupActionItem = renderRoot={renderRoot} onClick={handleClickAsync} closeSpotlightOnTrigger={ - interaction.type !== "mode" && interaction.type !== "children" && interaction.type !== "none" + interaction.type !== "mode" && + interaction.type !== "children" && + interaction.type !== "none" && + interaction.type !== "setQuery" } className={classes.spotlightAction} > - + ); }; diff --git a/packages/spotlight/src/components/spotlight.tsx b/packages/spotlight/src/components/spotlight.tsx index c99a76973b..019d8453dc 100644 --- a/packages/spotlight/src/components/spotlight.tsx +++ b/packages/spotlight/src/components/spotlight.tsx @@ -4,7 +4,7 @@ import type { Dispatch, SetStateAction } from "react"; import { useMemo, useRef, useState } from "react"; import { ActionIcon, Center, Group, Kbd } from "@mantine/core"; import { Spotlight as MantineSpotlight } from "@mantine/spotlight"; -import { IconQuestionMark, IconSearch, IconX } from "@tabler/icons-react"; +import { IconSearch, IconX } from "@tabler/icons-react"; import { hotkeys } from "@homarr/definitions"; import type { TranslationObject } from "@homarr/translation"; @@ -13,6 +13,7 @@ import { useI18n } from "@homarr/translation/client"; import type { inferSearchInteractionOptions } from "../lib/interaction"; import type { SearchMode } from "../lib/mode"; import { searchModes } from "../modes"; +import { useHomeEmptyGroups } from "../modes/help/home-empty-groups"; import { selectAction, spotlightStore } from "../spotlight-store"; import { SpotlightChildrenActions } from "./actions/children-actions"; import { SpotlightActionGroups } from "./actions/groups/action-group"; @@ -22,6 +23,7 @@ type SearchModeKey = keyof TranslationObject["search"]["mode"]; const defaultMode = "home"; export const Spotlight = () => { const searchModeState = useState(defaultMode); + const queryState = useState(""); const mode = searchModeState[0]; const activeMode = useMemo(() => searchModes.find((searchMode) => searchMode.modeKey === mode), [mode]); @@ -30,16 +32,17 @@ export const Spotlight = () => { } // We use the "key" below to prevent the 'Different amounts of hooks' error - return ; + return ; }; interface SpotlightWithActiveModeProps { modeState: [SearchModeKey, Dispatch>]; + queryState: [string, Dispatch>]; activeMode: SearchMode; } -const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveModeProps) => { - const [query, setQuery] = useState(""); +const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: SpotlightWithActiveModeProps) => { + const [query, setQuery] = queryState; const [mode, setMode] = modeState; const [childrenOptions, setChildrenOptions] = useState | null>(null); const t = useI18n(); @@ -47,28 +50,36 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM // Works as always the same amount of hooks are executed const useGroups = "groups" in activeMode ? () => activeMode.groups : activeMode.useGroups; const groups = useGroups(); + const homeEmptyGroups = useHomeEmptyGroups(); return ( { setMode(defaultMode); setChildrenOptions(null); + setQuery(""); }} query={query} onQueryChange={(query) => { - if (mode !== "help" || query.length !== 1) { - setQuery(query); - } + setQuery(query); + + // Only switch modes when a single trigger character is typed + if (query.length !== 1) return; const modeToActivate = searchModes.find((mode) => mode.character === query); - if (!modeToActivate) { - return; - } + if (!modeToActivate) return; setMode(modeToActivate.modeKey); + setChildrenOptions(null); + + // Keep '!' in the input to allow !bang parsing in external mode + setQuery(""); + + setTimeout(() => selectAction(0, spotlightStore)); }} store={spotlightStore} @@ -91,17 +102,7 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM }, }} rightSection={ - mode === defaultMode ? ( - { - setMode("help"); - inputRef.current?.focus(); - }} - variant="subtle" - > - - - ) : ( + mode === defaultMode ? null : ( { setMode(defaultMode); @@ -138,6 +139,7 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM /> ) : ( { setMode(mode); setChildrenOptions(null); @@ -152,7 +154,7 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM }); }} query={query} - groups={groups} + groups={mode === defaultMode && query.length === 0 ? homeEmptyGroups : groups} /> )} diff --git a/packages/spotlight/src/lib/interaction.ts b/packages/spotlight/src/lib/interaction.ts index 1854c8c480..060a35c513 100644 --- a/packages/spotlight/src/lib/interaction.ts +++ b/packages/spotlight/src/lib/interaction.ts @@ -11,6 +11,7 @@ const createSearchInteraction = (type: TType) => ({ const searchInteractions = [ createSearchInteraction("link").optionsType<{ href: string; newTab?: boolean }>(), createSearchInteraction("javaScript").optionsType<{ onSelect: () => MaybePromise }>(), + createSearchInteraction("setQuery").optionsType<{ query: string }>(), createSearchInteraction("mode").optionsType<{ mode: keyof TranslationObject["search"]["mode"] }>(), createSearchInteraction("children").optionsType<{ // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/spotlight/src/modes/help/home-empty-groups.tsx b/packages/spotlight/src/modes/help/home-empty-groups.tsx new file mode 100644 index 0000000000..0640b3cbdf --- /dev/null +++ b/packages/spotlight/src/modes/help/home-empty-groups.tsx @@ -0,0 +1,140 @@ +import { Group, Kbd, Stack, Text } from "@mantine/core"; +import { + IconBook2, + IconBrandDiscord, + IconBrandGithub, + IconLayoutDashboard, + IconSearch, + IconSettings, +} from "@tabler/icons-react"; + +import { useSession } from "@homarr/auth/client"; +import { createDocumentationLink } from "@homarr/definitions"; +import { useScopedI18n } from "@homarr/translation/client"; +import type { TablerIcon } from "@homarr/ui"; + +import { createGroup } from "../../lib/group"; +import { interaction } from "../../lib/interaction"; +import type { SearchMode } from "../../lib/mode"; +import { appIntegrationBoardMode } from "../app-integration-board"; +import { commandMode } from "../command"; +import { externalMode } from "../external"; +import { pageMode } from "../page"; +import { userGroupMode } from "../user-group"; + +type QuickLinkOption = { + icon: TablerIcon; + name: string; + path: string; +}; + +export const useHomeEmptyGroups = () => { + const { data: session } = useSession(); + const tPages = useScopedI18n("search.mode.page.group.page.option"); + const visibleSearchModes: SearchMode[] = [appIntegrationBoardMode, externalMode, commandMode, pageMode]; + + if (session?.user.permissions.includes("admin")) { + visibleSearchModes.unshift(userGroupMode); + } + + return [ + createGroup({ + keyPath: "path", + title: () => "Quick links", + options: (() => { + const quickLinks: QuickLinkOption[] = []; + if (session?.user) { + quickLinks.push({ + icon: IconSettings, + path: `/manage/users/${session.user.id}/general`, + name: tPages("preferences.label"), + }); + } + + quickLinks.push({ + icon: IconLayoutDashboard, + path: "/manage/boards", + name: tPages("manageBoard.label"), + }); + + if (session?.user?.permissions.includes("admin")) { + quickLinks.push({ + icon: IconSettings, + path: "/manage/settings", + name: tPages("manageSettings.label"), + }); + } + + if (session) { + quickLinks.push({ + icon: IconSearch, + path: "/manage/search-engines", + name: tPages("manageSearchEngine.label"), + }); + } + + return quickLinks; + })(), + Component: ({ name, icon: Icon }) => ( + + + {name} + + ), + filter: () => true, + useInteraction: interaction.link(({ path }) => ({ href: path })), + }), + createGroup({ + keyPath: "character", + title: (t) => t("search.mode.help.group.mode.title"), + options: visibleSearchModes.map(({ character, modeKey }) => ({ character, modeKey })), + Component: ({ modeKey, character }) => { + const t = useScopedI18n(`search.mode.${modeKey}`); + + return ( + + {t("help")} + {character} + + ); + }, + filter: () => true, + useInteraction: interaction.mode(({ modeKey }) => ({ mode: modeKey })), + }), + createGroup({ + keyPath: "href", + title: (t) => t("search.mode.help.group.help.title"), + useOptions() { + const t = useScopedI18n("search.mode.help.group.help.option"); + + return [ + { + href: createDocumentationLink("/docs/getting-started"), + icon: IconBook2, + label: t("documentation.label"), + }, + { + href: "https://github.com/homarr-labs/homarr/issues/new/choose", + icon: IconBrandGithub, + label: t("submitIssue.label"), + }, + { + href: "https://discord.com/invite/aCsmEV5RgA", + icon: IconBrandDiscord, + label: t("discord.label"), + }, + ]; + }, + Component: (props) => ( + + + {props.label} + + ), + filter: () => true, + useInteraction: interaction.link(({ href }) => ({ href, newTab: true })), + }), + ] as const; +}; + + diff --git a/packages/spotlight/src/modes/index.tsx b/packages/spotlight/src/modes/index.tsx index 0371209f51..3bbd61955f 100644 --- a/packages/spotlight/src/modes/index.tsx +++ b/packages/spotlight/src/modes/index.tsx @@ -1,13 +1,3 @@ -import { Group, Kbd, Text } from "@mantine/core"; -import { IconBook2, IconBrandDiscord, IconBrandGithub } from "@tabler/icons-react"; - -import { useSession } from "@homarr/auth/client"; -import { createDocumentationLink } from "@homarr/definitions"; -import { useScopedI18n } from "@homarr/translation/client"; - -import { createGroup } from "../lib/group"; -import { interaction } from "../lib/interaction"; -import type { SearchMode } from "../lib/mode"; import { appIntegrationBoardMode } from "./app-integration-board"; import { commandMode } from "./command"; import { externalMode } from "./external"; @@ -15,72 +5,4 @@ import { homeMode } from "./home"; import { pageMode } from "./page"; import { userGroupMode } from "./user-group"; -const searchModesForHelp = [userGroupMode, appIntegrationBoardMode, externalMode, commandMode, pageMode] as const; - -const helpMode = { - modeKey: "help", - character: "?", - useGroups() { - const { data: session } = useSession(); - const visibleSearchModes: SearchMode[] = [appIntegrationBoardMode, externalMode, commandMode, pageMode]; - - if (session?.user.permissions.includes("admin")) { - visibleSearchModes.unshift(userGroupMode); - } - - return [ - createGroup({ - keyPath: "character", - title: (t) => t("search.mode.help.group.mode.title"), - options: visibleSearchModes.map(({ character, modeKey }) => ({ character, modeKey })), - Component: ({ modeKey, character }) => { - const t = useScopedI18n(`search.mode.${modeKey}`); - - return ( - - {t("help")} - {character} - - ); - }, - filter: () => true, - useInteraction: interaction.mode(({ modeKey }) => ({ mode: modeKey })), - }), - createGroup({ - keyPath: "href", - title: (t) => t("search.mode.help.group.help.title"), - useOptions() { - const t = useScopedI18n("search.mode.help.group.help.option"); - - return [ - { - label: t("documentation.label"), - icon: IconBook2, - href: createDocumentationLink("/docs/getting-started"), - }, - { - label: t("submitIssue.label"), - icon: IconBrandGithub, - href: "https://github.com/homarr-labs/homarr/issues/new/choose", - }, - { - label: t("discord.label"), - icon: IconBrandDiscord, - href: "https://discord.com/invite/aCsmEV5RgA", - }, - ]; - }, - Component: (props) => ( - - - {props.label} - - ), - filter: () => true, - useInteraction: interaction.link(({ href }) => ({ href, newTab: true })), - }), - ]; - }, -} satisfies SearchMode; - -export const searchModes = [...searchModesForHelp, helpMode, homeMode] as const; +export const searchModes = [userGroupMode, appIntegrationBoardMode, externalMode, commandMode, pageMode, homeMode] as const; From 005fe492b8d39e12dfd5a54d909b7d9bccd4969b Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 7 Jan 2026 17:52:28 +0100 Subject: [PATCH 04/19] feat(spotlight): support !bang search with DDG fallback --- .../external/search-engines-search-group.tsx | 234 +++++++++++++++--- packages/translation/src/lang/en.json | 3 + 2 files changed, 209 insertions(+), 28 deletions(-) diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx index 61abc2d6f4..63a844e80c 100644 --- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx +++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx @@ -17,6 +17,35 @@ import { interaction } from "../../lib/interaction"; type SearchEngine = RouterOutputs["searchEngine"]["search"][number]; type FromIntegrationSearchResult = RouterOutputs["integration"]["searchInIntegration"][number]; +type DuckDuckGoBang = RouterOutputs["bangs"]["search"][number]; + +type ExternalOption = + | { + key: string; + kind: "hint"; + label: string; + description?: string; + } + | { + key: string; + kind: "search"; + label: string; + description?: string; + iconUrl?: string; + bang: string; + urlTemplate: string; + searchText: string; + } + | { + key: string; + kind: "engine"; + engine: SearchEngine; + } + | { + key: string; + kind: "ddg"; + bang: DuckDuckGoBang; + }; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions type MediaRequestChildrenProps = { @@ -235,57 +264,206 @@ export const searchEnginesChildrenOptions = createChildrenOptions( }, }); -export const searchEnginesSearchGroups = createGroup({ - keyPath: "short", +const parseBangQuery = (query: string) => { + const withBangPrefix = query.startsWith("!") ? query.slice(1) : query; + const bangIdx = withBangPrefix.indexOf(" "); + const bangToken = (bangIdx === -1 ? withBangPrefix : withBangPrefix.slice(0, bangIdx)).toLowerCase().trim(); + const searchText = bangIdx === -1 ? "" : withBangPrefix.slice(bangIdx + 1); + const locked = bangIdx !== -1; + return { bangToken, searchText, locked }; +}; + +const buildSearchUrl = (template: string, query: string) => { + const encoded = encodeURIComponent(query); + if (template.includes("{{{s}}}")) { + return template.replaceAll("{{{s}}}", encoded); + } + + return template.replaceAll("%s", encoded); +}; + +export const searchEnginesSearchGroups = createGroup({ + keyPath: "key", title: (t) => t("search.mode.external.group.searchEngine.title"), - Component: ({ iconUrl, name, short, description }) => { + Component: (option) => { + if (option.kind === "hint") { + return ( + + + + {option.label} + {option.description ? ( + + {option.description} + + ) : null} + + + ); + } + + if (option.kind === "search") { + return ( + + + {option.iconUrl ? ( + {option.label} + ) : ( + + )} + + {option.label} + {option.description ? ( + + {option.description} + + ) : null} + + + + !{option.bang} + + ); + } + + if (option.kind === "engine") { + const { iconUrl, name, short, description } = option.engine; + return ( + + + {name} + + {name} + + {description} + + + + + !{short} + + ); + } + + const { s: name, t: short, d: domain } = option.bang; return ( - {name} + {name} - {description} + {domain ? domain : "DuckDuckGo bang"} - {short} + !{short} ); }, - onKeyDown(event, options, query, { setChildrenOptions }) { - if (event.code !== "Space") return; - - const engine = options.find((option) => option.short === query); - if (!engine) return; - - setChildrenOptions(searchEnginesChildrenOptions(engine)); - }, - useInteraction: (searchEngine, query) => { + useInteraction(option, query) { const { openSearchInNewTab } = useSettings(); - if (searchEngine.type === "generic" && searchEngine.urlTemplate) { + const { bangToken, searchText } = parseBangQuery(query); + + if (option.kind === "search") { return { - type: "link" as const, - href: searchEngine.urlTemplate.replace("%s", query), + type: "link", + href: buildSearchUrl(option.urlTemplate, option.searchText), newTab: openSearchInNewTab, }; } - if (searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null) { - return { - type: "children", - ...searchEnginesChildrenOptions(searchEngine), - }; + if (option.kind === "engine") { + const nextBang = option.engine.short; + const nextQuery = `!${nextBang} ${searchText}`.trimEnd() + " "; + return { type: "setQuery", query: bangToken === nextBang && query.endsWith(" ") ? query : nextQuery }; } - throw new Error(`Unable to process search engine with type ${searchEngine.type}`); + if (option.kind === "ddg") { + const nextBang = option.bang.t; + const nextQuery = `!${nextBang} ${searchText}`.trimEnd() + " "; + return { type: "setQuery", query: bangToken === nextBang && query.endsWith(" ") ? query : nextQuery }; + } + + return { type: "none" }; }, useQueryOptions(query) { - return clientApi.searchEngine.search.useQuery({ - query: query.trim(), - limit: 5, - }); + const tExternal = useScopedI18n("search.mode.external.group.searchEngine"); + const { bangToken, searchText, locked } = parseBangQuery(query); + const enginesQuery = clientApi.searchEngine.search.useQuery({ query: bangToken, limit: 10 }); + + const ddgQuery = clientApi.bangs.search.useQuery( + { query: bangToken, limit: 20 }, + { + enabled: bangToken.length > 0, + }, + ); + + const isLoading = enginesQuery.isLoading || ddgQuery.isLoading; + const isError = enginesQuery.isError || ddgQuery.isError; + + const engineOptions = (enginesQuery.data ?? []).map( + (engine): ExternalOption => ({ + key: `engine-${engine.short}`, + kind: "engine", + engine, + }), + ); + + const ddgOptions = (ddgQuery.data ?? []) + .filter((bang) => !engineOptions.some((o) => o.kind === "engine" && o.engine.short === bang.t)) + .map( + (bang): ExternalOption => ({ + key: `ddg-${bang.t}`, + kind: "ddg", + bang, + }), + ); + + const searchActions: ExternalOption[] = []; + if (locked && bangToken.length > 0) { + const matchedEngine = (enginesQuery.data ?? []).find((e) => e.short === bangToken); + const matchedDdg = (ddgQuery.data ?? []).find((b) => b.t === bangToken); + + const label = matchedEngine?.name ?? matchedDdg?.s; + const iconUrl = matchedEngine?.iconUrl; + const urlTemplate = matchedEngine?.type === "generic" ? matchedEngine.urlTemplate : matchedDdg?.u; + + if (label && urlTemplate) { + if (searchText.trim().length > 0) { + searchActions.push({ + key: "search-action", + kind: "search", + label: `Search "${searchText.trim()}" with ${label}`, + description: "Press Enter to open", + bang: bangToken, + iconUrl, + urlTemplate, + searchText: searchText.trim(), + }); + } else { + searchActions.push({ + key: "search-hint", + kind: "hint", + label: `${label} selected (!${bangToken})`, + description: "Type your search query to continue", + }); + } + } + } if (query.length === 0) { + searchActions.push({ + key: "hint", + kind: "hint", + label: "Type a bang, e.g. !yt, then press Space to select", + description: tExternal("tip.ddgBangs"), + }); + } + + return { + isLoading, + isError, + data: [...searchActions, ...engineOptions, ...ddgOptions], + }; }, }); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 517ecfea19..5835a1cd49 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -4159,6 +4159,9 @@ "group": { "searchEngine": { "title": "Search engines", + "tip": { + "ddgBangs": "Tip: You can use any DuckDuckGo bangs here as well, e.g. !something" + }, "children": { "action": { "search": { From 0375a1d4ebc27428612c2ae53a20804d9c933b2f Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 7 Jan 2026 17:52:42 +0100 Subject: [PATCH 05/19] feat(spotlight): remove redundant home search-engine switch action --- .../src/modes/home/home-search-engine-group.tsx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/spotlight/src/modes/home/home-search-engine-group.tsx b/packages/spotlight/src/modes/home/home-search-engine-group.tsx index 09fa49fbf8..40e40f1319 100644 --- a/packages/spotlight/src/modes/home/home-search-engine-group.tsx +++ b/packages/spotlight/src/modes/home/home-search-engine-group.tsx @@ -1,5 +1,5 @@ import { Box, Group, Stack, Text } from "@mantine/core"; -import { IconCaretUpDown, IconSearch, IconSearchOff } from "@tabler/icons-react"; +import { IconSearch, IconSearchOff } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; @@ -79,20 +79,7 @@ export const homeSearchEngineGroup = createGroup({ isLoading: defaultSearchEngineQuery.isLoading || (resultQuery.isLoading && fromIntegrationEnabled) || status === "loading", isError: defaultSearchEngineQuery.isError || (resultQuery.isError && fromIntegrationEnabled), - data: [ - ...createDefaultSearchEntries(defaultSearchEngine, results, session, query, t), - { - id: "other", - name: t("search.mode.home.group.search.option.other.label"), - icon: IconCaretUpDown, - useInteraction() { - return { - type: "mode", - mode: "external", - }; - }, - }, - ], + data: createDefaultSearchEntries(defaultSearchEngine, results, session, query, t), }; }, }); From 8dc898711be6779e3ee35941eafb60d6cbc49d5b Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 7 Jan 2026 18:00:14 +0100 Subject: [PATCH 06/19] perf(spotlight): debounce integration and bang searches --- .../src/modes/external/search-engines-search-group.tsx | 9 ++++++--- .../src/modes/home/home-search-engine-group.tsx | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx index 63a844e80c..46367ddd24 100644 --- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx +++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx @@ -1,4 +1,5 @@ import { Group, Image, Kbd, Stack, Text } from "@mantine/core"; +import { useDebouncedValue } from "@mantine/hooks"; import { IconDownload, IconSearch } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; @@ -391,12 +392,14 @@ export const searchEnginesSearchGroups = createGroup({ useQueryOptions(query) { const tExternal = useScopedI18n("search.mode.external.group.searchEngine"); const { bangToken, searchText, locked } = parseBangQuery(query); - const enginesQuery = clientApi.searchEngine.search.useQuery({ query: bangToken, limit: 10 }); + const [debouncedBangToken] = useDebouncedValue(bangToken, 100); + const enginesQueryToken = query === "!" ? "" : debouncedBangToken; + const enginesQuery = clientApi.searchEngine.search.useQuery({ query: enginesQueryToken, limit: 10 }); const ddgQuery = clientApi.bangs.search.useQuery( - { query: bangToken, limit: 20 }, + { query: debouncedBangToken, limit: 20 }, { - enabled: bangToken.length > 0, + enabled: debouncedBangToken.length > 0, }, ); diff --git a/packages/spotlight/src/modes/home/home-search-engine-group.tsx b/packages/spotlight/src/modes/home/home-search-engine-group.tsx index 40e40f1319..d105d7a207 100644 --- a/packages/spotlight/src/modes/home/home-search-engine-group.tsx +++ b/packages/spotlight/src/modes/home/home-search-engine-group.tsx @@ -1,5 +1,6 @@ import { Box, Group, Stack, Text } from "@mantine/core"; import { IconSearch, IconSearchOff } from "@tabler/icons-react"; +import { useDebouncedValue } from "@mantine/hooks"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; @@ -59,14 +60,15 @@ export const homeSearchEngineGroup = createGroup({ useQueryOptions(query) { const t = useI18n(); const { data: session, status } = useSession(); + const [debouncedQuery] = useDebouncedValue(query, 100); const { data: defaultSearchEngine, ...defaultSearchEngineQuery } = clientApi.searchEngine.getDefaultSearchEngine.useQuery(undefined, { enabled: status !== "loading", }); - const fromIntegrationEnabled = defaultSearchEngine?.type === "fromIntegration" && query.length > 0; + const fromIntegrationEnabled = defaultSearchEngine?.type === "fromIntegration" && debouncedQuery.length > 0; const { data: results, ...resultQuery } = clientApi.integration.searchInIntegration.useQuery( { - query, + query: debouncedQuery, integrationId: defaultSearchEngine?.integrationId ?? "", }, { From 802f791bbbc4eec03a6f1bd3ea2f9924f7ff2df6 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 7 Jan 2026 18:02:34 +0100 Subject: [PATCH 07/19] perf(spotlight): debounce integration search engine children results --- .../src/modes/external/search-engines-search-group.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx index 46367ddd24..949e80219e 100644 --- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx +++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx @@ -193,11 +193,13 @@ const mediaRequestsChildrenOptions = createChildrenOptions({ useActions: (searchEngine, query) => { + const [debouncedQuery] = useDebouncedValue(query, 100); const { data } = clientApi.integration.searchInIntegration.useQuery( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - { integrationId: searchEngine.integrationId!, query }, + { integrationId: searchEngine.integrationId!, query: debouncedQuery }, { - enabled: searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && query.length > 0, + enabled: + searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && debouncedQuery.length > 0, }, ); const { openSearchInNewTab } = useSettings(); From 7ed557ef2bad2ea81d9bbb9458a5167fc5d63234 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 7 Jan 2026 18:04:50 +0100 Subject: [PATCH 08/19] perf(spotlight): tune debounce timings --- .../src/modes/external/search-engines-search-group.tsx | 8 +++----- .../spotlight/src/modes/home/home-search-engine-group.tsx | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx index 949e80219e..f7945ed2c6 100644 --- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx +++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx @@ -193,13 +193,11 @@ const mediaRequestsChildrenOptions = createChildrenOptions({ useActions: (searchEngine, query) => { - const [debouncedQuery] = useDebouncedValue(query, 100); const { data } = clientApi.integration.searchInIntegration.useQuery( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - { integrationId: searchEngine.integrationId!, query: debouncedQuery }, + { integrationId: searchEngine.integrationId!, query }, { - enabled: - searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && debouncedQuery.length > 0, + enabled: searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && query.length > 0, }, ); const { openSearchInNewTab } = useSettings(); @@ -394,7 +392,7 @@ export const searchEnginesSearchGroups = createGroup({ useQueryOptions(query) { const tExternal = useScopedI18n("search.mode.external.group.searchEngine"); const { bangToken, searchText, locked } = parseBangQuery(query); - const [debouncedBangToken] = useDebouncedValue(bangToken, 100); + const [debouncedBangToken] = useDebouncedValue(bangToken, 250); const enginesQueryToken = query === "!" ? "" : debouncedBangToken; const enginesQuery = clientApi.searchEngine.search.useQuery({ query: enginesQueryToken, limit: 10 }); diff --git a/packages/spotlight/src/modes/home/home-search-engine-group.tsx b/packages/spotlight/src/modes/home/home-search-engine-group.tsx index d105d7a207..d90d153305 100644 --- a/packages/spotlight/src/modes/home/home-search-engine-group.tsx +++ b/packages/spotlight/src/modes/home/home-search-engine-group.tsx @@ -60,7 +60,7 @@ export const homeSearchEngineGroup = createGroup({ useQueryOptions(query) { const t = useI18n(); const { data: session, status } = useSession(); - const [debouncedQuery] = useDebouncedValue(query, 100); + const [debouncedQuery] = useDebouncedValue(query, 250); const { data: defaultSearchEngine, ...defaultSearchEngineQuery } = clientApi.searchEngine.getDefaultSearchEngine.useQuery(undefined, { enabled: status !== "loading", From 1640a09dbbe3e1f3b5abc1679d5e94110b9fae67 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 7 Jan 2026 18:06:07 +0100 Subject: [PATCH 09/19] Revert "perf(spotlight): tune debounce timings" This reverts commit 7ed557ef2bad2ea81d9bbb9458a5167fc5d63234. --- .../src/modes/external/search-engines-search-group.tsx | 8 +++++--- .../spotlight/src/modes/home/home-search-engine-group.tsx | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx index f7945ed2c6..949e80219e 100644 --- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx +++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx @@ -193,11 +193,13 @@ const mediaRequestsChildrenOptions = createChildrenOptions({ useActions: (searchEngine, query) => { + const [debouncedQuery] = useDebouncedValue(query, 100); const { data } = clientApi.integration.searchInIntegration.useQuery( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - { integrationId: searchEngine.integrationId!, query }, + { integrationId: searchEngine.integrationId!, query: debouncedQuery }, { - enabled: searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && query.length > 0, + enabled: + searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && debouncedQuery.length > 0, }, ); const { openSearchInNewTab } = useSettings(); @@ -392,7 +394,7 @@ export const searchEnginesSearchGroups = createGroup({ useQueryOptions(query) { const tExternal = useScopedI18n("search.mode.external.group.searchEngine"); const { bangToken, searchText, locked } = parseBangQuery(query); - const [debouncedBangToken] = useDebouncedValue(bangToken, 250); + const [debouncedBangToken] = useDebouncedValue(bangToken, 100); const enginesQueryToken = query === "!" ? "" : debouncedBangToken; const enginesQuery = clientApi.searchEngine.search.useQuery({ query: enginesQueryToken, limit: 10 }); diff --git a/packages/spotlight/src/modes/home/home-search-engine-group.tsx b/packages/spotlight/src/modes/home/home-search-engine-group.tsx index d90d153305..d105d7a207 100644 --- a/packages/spotlight/src/modes/home/home-search-engine-group.tsx +++ b/packages/spotlight/src/modes/home/home-search-engine-group.tsx @@ -60,7 +60,7 @@ export const homeSearchEngineGroup = createGroup({ useQueryOptions(query) { const t = useI18n(); const { data: session, status } = useSession(); - const [debouncedQuery] = useDebouncedValue(query, 250); + const [debouncedQuery] = useDebouncedValue(query, 100); const { data: defaultSearchEngine, ...defaultSearchEngineQuery } = clientApi.searchEngine.getDefaultSearchEngine.useQuery(undefined, { enabled: status !== "loading", From 7967fbc09e62e5f18c76c179717117e72194d1ad Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 7 Jan 2026 18:06:11 +0100 Subject: [PATCH 10/19] Revert "perf(spotlight): debounce integration search engine children results" This reverts commit 802f791bbbc4eec03a6f1bd3ea2f9924f7ff2df6. --- .../src/modes/external/search-engines-search-group.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx index 949e80219e..46367ddd24 100644 --- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx +++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx @@ -193,13 +193,11 @@ const mediaRequestsChildrenOptions = createChildrenOptions({ useActions: (searchEngine, query) => { - const [debouncedQuery] = useDebouncedValue(query, 100); const { data } = clientApi.integration.searchInIntegration.useQuery( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - { integrationId: searchEngine.integrationId!, query: debouncedQuery }, + { integrationId: searchEngine.integrationId!, query }, { - enabled: - searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && debouncedQuery.length > 0, + enabled: searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && query.length > 0, }, ); const { openSearchInNewTab } = useSettings(); From 074a219e6e4cfc40f5de5ce32a9cc431cc4d66a0 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 7 Jan 2026 18:06:29 +0100 Subject: [PATCH 11/19] Revert "perf(spotlight): debounce integration and bang searches" This reverts commit 8dc898711be6779e3ee35941eafb60d6cbc49d5b. --- .../src/modes/external/search-engines-search-group.tsx | 9 +++------ .../src/modes/home/home-search-engine-group.tsx | 6 ++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx index 46367ddd24..63a844e80c 100644 --- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx +++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx @@ -1,5 +1,4 @@ import { Group, Image, Kbd, Stack, Text } from "@mantine/core"; -import { useDebouncedValue } from "@mantine/hooks"; import { IconDownload, IconSearch } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; @@ -392,14 +391,12 @@ export const searchEnginesSearchGroups = createGroup({ useQueryOptions(query) { const tExternal = useScopedI18n("search.mode.external.group.searchEngine"); const { bangToken, searchText, locked } = parseBangQuery(query); - const [debouncedBangToken] = useDebouncedValue(bangToken, 100); - const enginesQueryToken = query === "!" ? "" : debouncedBangToken; - const enginesQuery = clientApi.searchEngine.search.useQuery({ query: enginesQueryToken, limit: 10 }); + const enginesQuery = clientApi.searchEngine.search.useQuery({ query: bangToken, limit: 10 }); const ddgQuery = clientApi.bangs.search.useQuery( - { query: debouncedBangToken, limit: 20 }, + { query: bangToken, limit: 20 }, { - enabled: debouncedBangToken.length > 0, + enabled: bangToken.length > 0, }, ); diff --git a/packages/spotlight/src/modes/home/home-search-engine-group.tsx b/packages/spotlight/src/modes/home/home-search-engine-group.tsx index d105d7a207..40e40f1319 100644 --- a/packages/spotlight/src/modes/home/home-search-engine-group.tsx +++ b/packages/spotlight/src/modes/home/home-search-engine-group.tsx @@ -1,6 +1,5 @@ import { Box, Group, Stack, Text } from "@mantine/core"; import { IconSearch, IconSearchOff } from "@tabler/icons-react"; -import { useDebouncedValue } from "@mantine/hooks"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; @@ -60,15 +59,14 @@ export const homeSearchEngineGroup = createGroup({ useQueryOptions(query) { const t = useI18n(); const { data: session, status } = useSession(); - const [debouncedQuery] = useDebouncedValue(query, 100); const { data: defaultSearchEngine, ...defaultSearchEngineQuery } = clientApi.searchEngine.getDefaultSearchEngine.useQuery(undefined, { enabled: status !== "loading", }); - const fromIntegrationEnabled = defaultSearchEngine?.type === "fromIntegration" && debouncedQuery.length > 0; + const fromIntegrationEnabled = defaultSearchEngine?.type === "fromIntegration" && query.length > 0; const { data: results, ...resultQuery } = clientApi.integration.searchInIntegration.useQuery( { - query: debouncedQuery, + query, integrationId: defaultSearchEngine?.integrationId ?? "", }, { From f35197bde7db21fed1e706eb378fbb5dc647c8c6 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 25 Jan 2026 14:54:53 +0100 Subject: [PATCH 12/19] feat(request-handler): cache DuckDuckGo bangs with schema parsing --- .../request-handler/src/duckduckgo-bangs.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 packages/request-handler/src/duckduckgo-bangs.ts diff --git a/packages/request-handler/src/duckduckgo-bangs.ts b/packages/request-handler/src/duckduckgo-bangs.ts new file mode 100644 index 0000000000..09c4b279fc --- /dev/null +++ b/packages/request-handler/src/duckduckgo-bangs.ts @@ -0,0 +1,78 @@ +import dayjs from "dayjs"; +import { z } from "zod"; + +import { env } from "@homarr/common/env"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; +import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; +import { createChannelWithLatestAndEvents } from "@homarr/redis"; +import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; + +export type DuckDuckGoBang = z.infer; + +const DUCKDUCKGO_BANGS_URL = "https://duckduckgo.com/bang.js"; + +/** + * DuckDuckGo `bang.js` uses short keys (to reduce the payload size): + * - `t`: bang token (e.g. `"yt"`) + * - `s`: display name + * - `d`: domain (optional) + * - `u`: URL template (contains `{{{s}}}`) + * - `c`: category (optional) + * - `sc`: subcategory (optional) + * - `r`: rank (optional) + */ +export const duckDuckGoBangSchema = z.object({ + t: z.string(), + s: z.string(), + d: z.string().optional(), + u: z.string(), + c: z.string().optional(), + sc: z.string().optional(), + r: z.number().optional(), +}); + +const duckDuckGoBangsResponseSchema = z.array(duckDuckGoBangSchema); + +const normalizeBangToken = (token: string) => token.toLowerCase().trim(); + +const compareBangTokenAsc = (left: DuckDuckGoBang, right: DuckDuckGoBang) => left.t.localeCompare(right.t); + +export const duckDuckGoBangsRequestHandler = createCachedRequestHandler({ + queryKey: "duckduckgo-bangs", + cacheDuration: dayjs.duration(1, "day"), + async requestAsync(_) { + if (env.NO_EXTERNAL_CONNECTION) { + return []; + } + + const res = await withTimeoutAsync(async (signal) => { + return await fetchWithTrustedCertificatesAsync(DUCKDUCKGO_BANGS_URL, { + signal, + headers: { + accept: "application/json,text/plain,*/*", + }, + }); + }); + + if (!res.ok) { + throw new Error(`Failed to fetch DuckDuckGo bangs: ${res.status} ${res.statusText}`); + } + + const json: unknown = await res.json(); + const bangs = await duckDuckGoBangsResponseSchema.parseAsync(json); + + const normalized = bangs + .map((bang) => ({ + ...bang, + t: normalizeBangToken(bang.t), + })) + .filter((bang) => bang.t.length > 0); + + normalized.sort(compareBangTokenAsc); + + return normalized; + }, + createRedisChannel() { + return createChannelWithLatestAndEvents("homarr:duckduckgo-bangs"); + }, +}); From 7df1ea3163590276918b780f3dfb0ec7562f2415 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 25 Jan 2026 14:54:59 +0100 Subject: [PATCH 13/19] refactor(api): serve DuckDuckGo bangs via cached request-handler --- packages/api/src/root.ts | 2 +- packages/api/src/router/bangs/bangs-router.ts | 2 - packages/api/src/services/duckduckgo-bangs.ts | 147 ++++-------------- 3 files changed, 35 insertions(+), 116 deletions(-) diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index 205190741d..aa9ca54791 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -1,5 +1,6 @@ import { apiKeysRouter } from "./router/apiKeys"; import { appRouter as innerAppRouter } from "./router/app"; +import { bangsRouter } from "./router/bangs/bangs-router"; import { boardRouter } from "./router/board"; import { certificateRouter } from "./router/certificates/certificate-router"; import { cronJobsRouter } from "./router/cron-jobs"; @@ -16,7 +17,6 @@ import { locationRouter } from "./router/location"; import { logRouter } from "./router/log"; import { mediaRouter } from "./router/medias/media-router"; import { onboardRouter } from "./router/onboard/onboard-router"; -import { bangsRouter } from "./router/bangs/bangs-router"; import { searchEngineRouter } from "./router/search-engine/search-engine-router"; import { sectionRouter } from "./router/section/section-router"; import { serverSettingsRouter } from "./router/serverSettings"; diff --git a/packages/api/src/router/bangs/bangs-router.ts b/packages/api/src/router/bangs/bangs-router.ts index 49f7b955cb..ae3a3f2145 100644 --- a/packages/api/src/router/bangs/bangs-router.ts +++ b/packages/api/src/router/bangs/bangs-router.ts @@ -15,5 +15,3 @@ export const bangsRouter = createTRPCRouter({ return await searchDuckDuckGoBangsAsync({ query: input.query, limit: input.limit }); }), }); - - diff --git a/packages/api/src/services/duckduckgo-bangs.ts b/packages/api/src/services/duckduckgo-bangs.ts index 54ac776f75..b5f8d685a4 100644 --- a/packages/api/src/services/duckduckgo-bangs.ts +++ b/packages/api/src/services/duckduckgo-bangs.ts @@ -1,127 +1,48 @@ -const DUCKDUCKGO_BANGS_URL = "https://duckduckgo.com/bang.js"; +import { duckDuckGoBangsRequestHandler } from "@homarr/request-handler/duckduckgo-bangs"; +import type { DuckDuckGoBang } from "@homarr/request-handler/duckduckgo-bangs"; -export type DuckDuckGoBang = { - /** Bang token (e.g. "yt") */ - t: string; - /** Display name */ - s: string; - /** Domain */ - d?: string; - /** Url template (contains {{{s}}}) */ - u: string; - /** Category */ - c?: string; - /** Subcategory */ - sc?: string; - /** Rank */ - r?: number; -}; - -type CachedBangs = { - fetchedAtMs: number; - bangs: DuckDuckGoBang[]; -}; - -const DAY_MS = 24 * 60 * 60 * 1000; -const cache: { value: CachedBangs | null; inFlight: Promise | null } = { value: null, inFlight: null }; - -const normalizeToken = (token: string) => token.toLowerCase().trim(); - -const byTokenAsc = (a: DuckDuckGoBang, b: DuckDuckGoBang) => a.t.localeCompare(b.t); - -const isCacheValid = (cached: CachedBangs, nowMs: number) => nowMs - cached.fetchedAtMs < DAY_MS; +// DuckDuckGo bang keys are intentionally short: +// - `t`: token (e.g. "yt"), `s`: display name, `u`: URL template (contains `{{{s}}}`) +// - `d`: domain, `c`: category, `sc`: subcategory, `r`: rank (optional) -const fetchBangsAsync = async (): Promise => { - const res = await fetch(DUCKDUCKGO_BANGS_URL, { - headers: { - accept: "application/json,text/plain,*/*", - }, - }); - - if (!res.ok) { - throw new Error(`Failed to fetch DuckDuckGo bangs: ${res.status} ${res.statusText}`); - } - - const json = (await res.json()) as unknown; - if (!Array.isArray(json)) { - throw new Error("Invalid DuckDuckGo bangs payload: expected array"); - } - - const parsed: DuckDuckGoBang[] = []; - for (const item of json) { - if (!item || typeof item !== "object") continue; - const t = (item as Record).t; - const s = (item as Record).s; - const u = (item as Record).u; - if (typeof t !== "string" || typeof s !== "string" || typeof u !== "string") continue; - - const token = normalizeToken(t); - if (!token) continue; - - parsed.push({ - t: token, - s, - u, - d: typeof (item as Record).d === "string" ? ((item as Record).d as string) : undefined, - c: typeof (item as Record).c === "string" ? ((item as Record).c as string) : undefined, - sc: typeof (item as Record).sc === "string" ? ((item as Record).sc as string) : undefined, - r: typeof (item as Record).r === "number" ? ((item as Record).r as number) : undefined, - }); - } - - parsed.sort(byTokenAsc); - return { fetchedAtMs: Date.now(), bangs: parsed }; -}; - -export const getDuckDuckGoBangsAsync = async (nowMs = Date.now()): Promise => { - if (cache.value && isCacheValid(cache.value, nowMs)) { - return cache.value; - } - - if (!cache.inFlight) { - cache.inFlight = fetchBangsAsync() - .then((value) => { - cache.value = value; - return value; - }) - .finally(() => { - cache.inFlight = null; - }); - } - - return await cache.inFlight; -}; +const normalizeBangToken = (token: string) => token.toLowerCase().trim(); const lowerBound = (arr: DuckDuckGoBang[], tokenPrefix: string) => { - let lo = 0; - let hi = arr.length; - while (lo < hi) { - const mid = (lo + hi) >> 1; - if (arr[mid]!.t < tokenPrefix) lo = mid + 1; - else hi = mid; + let low = 0; + let high = arr.length; + while (low < high) { + const mid = (low + high) >> 1; + const midBang = arr[mid]; + // Must use the same ordering as the source list sort (localeCompare), + // otherwise binary search can miss tokens with symbols like "&" or "_". + if (!midBang || midBang.t.localeCompare(tokenPrefix) >= 0) { + high = mid; + continue; + } + + low = mid + 1; } - return lo; + return low; }; export const searchDuckDuckGoBangsAsync = async (input: { query: string; limit: number; }): Promise => { - const q = normalizeToken(input.query); - if (!q) return []; - - const { bangs } = await getDuckDuckGoBangsAsync(); - const start = lowerBound(bangs, q); - const out: DuckDuckGoBang[] = []; - - for (let i = start; i < bangs.length; i++) { - const bang = bangs[i]!; - if (!bang.t.startsWith(q)) break; - out.push(bang); - if (out.length >= input.limit) break; + const queryTokenPrefix = normalizeBangToken(input.query); + if (!queryTokenPrefix) return []; + + const { data: allBangs } = await duckDuckGoBangsRequestHandler.handler({}).getCachedOrUpdatedDataAsync({}); + const startIndex = lowerBound(allBangs, queryTokenPrefix); + const matches: DuckDuckGoBang[] = []; + + for (let index = startIndex; index < allBangs.length; index++) { + const bang = allBangs[index]; + if (!bang) break; + if (!bang.t.startsWith(queryTokenPrefix)) break; + matches.push(bang); + if (matches.length >= input.limit) break; } - return out; + return matches; }; - - From 90bb7605001404fa8a4b0ff81af3f75fa3df13b8 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 25 Jan 2026 14:55:09 +0100 Subject: [PATCH 14/19] fix(spotlight): improve !bang UX and reduce query spam --- .../actions/items/group-action-item.tsx | 2 +- .../spotlight/src/components/spotlight.tsx | 18 +++++------ .../external/search-engines-search-group.tsx | 31 ++++++++++++------- .../src/modes/help/home-empty-groups.tsx | 10 +++--- packages/spotlight/src/modes/home/context.tsx | 1 - packages/spotlight/src/modes/index.tsx | 9 +++++- 6 files changed, 42 insertions(+), 29 deletions(-) diff --git a/packages/spotlight/src/components/actions/items/group-action-item.tsx b/packages/spotlight/src/components/actions/items/group-action-item.tsx index 60399adc7b..eb48e04b2b 100644 --- a/packages/spotlight/src/components/actions/items/group-action-item.tsx +++ b/packages/spotlight/src/components/actions/items/group-action-item.tsx @@ -27,7 +27,7 @@ export const SpotlightGroupActionItem = }: SpotlightGroupActionItemProps) => { const interaction = group.useInteraction(option, query); // Avoid passing React's special `key` prop via spread - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key: _reactKey, ...optionProps } = option as unknown as { key?: unknown } & Record; const renderRoot = diff --git a/packages/spotlight/src/components/spotlight.tsx b/packages/spotlight/src/components/spotlight.tsx index 019d8453dc..c90542f452 100644 --- a/packages/spotlight/src/components/spotlight.tsx +++ b/packages/spotlight/src/components/spotlight.tsx @@ -32,7 +32,9 @@ export const Spotlight = () => { } // We use the "key" below to prevent the 'Different amounts of hooks' error - return ; + return ( + + ); }; interface SpotlightWithActiveModeProps { @@ -63,22 +65,20 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh setQuery(""); }} query={query} - onQueryChange={(query) => { - setQuery(query); + onQueryChange={(nextQuery) => { + const sanitizedQuery = mode === "external" && nextQuery.startsWith("!") ? nextQuery.slice(1) : nextQuery; + setQuery(sanitizedQuery); // Only switch modes when a single trigger character is typed - if (query.length !== 1) return; + if (sanitizedQuery.length !== 1) return; - const modeToActivate = searchModes.find((mode) => mode.character === query); + const modeToActivate = searchModes.find((mode) => mode.character === sanitizedQuery); if (!modeToActivate) return; setMode(modeToActivate.modeKey); setChildrenOptions(null); - // Keep '!' in the input to allow !bang parsing in external mode - setQuery(""); - setTimeout(() => selectAction(0, spotlightStore)); }} @@ -154,7 +154,7 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh }); }} query={query} - groups={mode === defaultMode && query.length === 0 ? homeEmptyGroups : groups} + groups={mode === defaultMode && query.length === 0 ? [...homeEmptyGroups] : groups} /> )} diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx index 63a844e80c..134dd82d62 100644 --- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx +++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx @@ -1,5 +1,7 @@ import { Group, Image, Kbd, Stack, Text } from "@mantine/core"; +import { useDebouncedValue } from "@mantine/hooks"; import { IconDownload, IconSearch } from "@tabler/icons-react"; +import { keepPreviousData } from "@tanstack/react-query"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; @@ -353,7 +355,7 @@ export const searchEnginesSearchGroups = createGroup({ {name} - {domain ? domain : "DuckDuckGo bang"} + {domain ?? "DuckDuckGo bang"} @@ -376,13 +378,13 @@ export const searchEnginesSearchGroups = createGroup({ if (option.kind === "engine") { const nextBang = option.engine.short; - const nextQuery = `!${nextBang} ${searchText}`.trimEnd() + " "; + const nextQuery = `${nextBang} ${searchText}`.trimEnd() + " "; return { type: "setQuery", query: bangToken === nextBang && query.endsWith(" ") ? query : nextQuery }; } if (option.kind === "ddg") { const nextBang = option.bang.t; - const nextQuery = `!${nextBang} ${searchText}`.trimEnd() + " "; + const nextQuery = `${nextBang} ${searchText}`.trimEnd() + " "; return { type: "setQuery", query: bangToken === nextBang && query.endsWith(" ") ? query : nextQuery }; } @@ -391,12 +393,18 @@ export const searchEnginesSearchGroups = createGroup({ useQueryOptions(query) { const tExternal = useScopedI18n("search.mode.external.group.searchEngine"); const { bangToken, searchText, locked } = parseBangQuery(query); - const enginesQuery = clientApi.searchEngine.search.useQuery({ query: bangToken, limit: 10 }); + const [debouncedBangToken] = useDebouncedValue(bangToken, 150); + + const enginesQuery = clientApi.searchEngine.search.useQuery( + { query: debouncedBangToken, limit: 10 }, + { placeholderData: keepPreviousData }, + ); const ddgQuery = clientApi.bangs.search.useQuery( - { query: bangToken, limit: 20 }, + { query: debouncedBangToken, limit: 10 }, { - enabled: bangToken.length > 0, + enabled: debouncedBangToken.length >= 1, + placeholderData: keepPreviousData, }, ); @@ -412,7 +420,7 @@ export const searchEnginesSearchGroups = createGroup({ ); const ddgOptions = (ddgQuery.data ?? []) - .filter((bang) => !engineOptions.some((o) => o.kind === "engine" && o.engine.short === bang.t)) + .filter((bang) => !engineOptions.some((option) => option.kind === "engine" && option.engine.short === bang.t)) .map( (bang): ExternalOption => ({ key: `ddg-${bang.t}`, @@ -423,8 +431,8 @@ export const searchEnginesSearchGroups = createGroup({ const searchActions: ExternalOption[] = []; if (locked && bangToken.length > 0) { - const matchedEngine = (enginesQuery.data ?? []).find((e) => e.short === bangToken); - const matchedDdg = (ddgQuery.data ?? []).find((b) => b.t === bangToken); + const matchedEngine = (enginesQuery.data ?? []).find((engine) => engine.short === bangToken); + const matchedDdg = (ddgQuery.data ?? []).find((bang) => bang.t === bangToken); const label = matchedEngine?.name ?? matchedDdg?.s; const iconUrl = matchedEngine?.iconUrl; @@ -451,7 +459,8 @@ export const searchEnginesSearchGroups = createGroup({ }); } } - } if (query.length === 0) { + } + if (query.length === 0) { searchActions.push({ key: "hint", kind: "hint", @@ -463,7 +472,7 @@ export const searchEnginesSearchGroups = createGroup({ return { isLoading, isError, - data: [...searchActions, ...engineOptions, ...ddgOptions], + data: [...searchActions, ...engineOptions, ...ddgOptions].slice(0, 10), }; }, }); diff --git a/packages/spotlight/src/modes/help/home-empty-groups.tsx b/packages/spotlight/src/modes/help/home-empty-groups.tsx index 0640b3cbdf..1c8c480e30 100644 --- a/packages/spotlight/src/modes/help/home-empty-groups.tsx +++ b/packages/spotlight/src/modes/help/home-empty-groups.tsx @@ -1,4 +1,4 @@ -import { Group, Kbd, Stack, Text } from "@mantine/core"; +import { Group, Kbd, Text } from "@mantine/core"; import { IconBook2, IconBrandDiscord, @@ -22,11 +22,11 @@ import { externalMode } from "../external"; import { pageMode } from "../page"; import { userGroupMode } from "../user-group"; -type QuickLinkOption = { +interface QuickLinkOption { icon: TablerIcon; name: string; path: string; -}; +} export const useHomeEmptyGroups = () => { const { data: session } = useSession(); @@ -57,7 +57,7 @@ export const useHomeEmptyGroups = () => { name: tPages("manageBoard.label"), }); - if (session?.user?.permissions.includes("admin")) { + if (session?.user.permissions.includes("admin")) { quickLinks.push({ icon: IconSettings, path: "/manage/settings", @@ -136,5 +136,3 @@ export const useHomeEmptyGroups = () => { }), ] as const; }; - - diff --git a/packages/spotlight/src/modes/home/context.tsx b/packages/spotlight/src/modes/home/context.tsx index 3a681499b1..fced7fc069 100644 --- a/packages/spotlight/src/modes/home/context.tsx +++ b/packages/spotlight/src/modes/home/context.tsx @@ -94,7 +94,6 @@ const createSpotlightContext = (displayName: string) => { context.unregisterItems(key); }; // We ignore the results - // eslint-disable-next-line react-hooks/exhaustive-deps }, [...dependencyArray, key]); }; diff --git a/packages/spotlight/src/modes/index.tsx b/packages/spotlight/src/modes/index.tsx index 3bbd61955f..fa6e819166 100644 --- a/packages/spotlight/src/modes/index.tsx +++ b/packages/spotlight/src/modes/index.tsx @@ -5,4 +5,11 @@ import { homeMode } from "./home"; import { pageMode } from "./page"; import { userGroupMode } from "./user-group"; -export const searchModes = [userGroupMode, appIntegrationBoardMode, externalMode, commandMode, pageMode, homeMode] as const; +export const searchModes = [ + userGroupMode, + appIntegrationBoardMode, + externalMode, + commandMode, + pageMode, + homeMode, +] as const; From fb15e1e88d00282a0c9bcc10225534b9ecec2b74 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 25 Jan 2026 15:29:22 +0100 Subject: [PATCH 15/19] feat(user): add support for ddg bangs feature with schema validation and UI integration --- .../boards/(content)/_header-actions.tsx | 1 - .../boards/(content)/_ready-context.tsx | 1 - .../general/_components/_ddg-bangs.tsx | 67 + .../manage/users/[userId]/general/page.tsx | 2 + apps/nextjs/src/app/api/health/live/route.ts | 1 - .../board/sections/gridstack/use-gridstack.ts | 1 - packages/api/src/router/user.ts | 19 + .../migrations/sqlite/0035_chunky_zombie.sql | 1 + .../migrations/sqlite/meta/0035_snapshot.json | 2257 +++++++++++++++++ .../db/migrations/sqlite/meta/_journal.json | 9 +- packages/db/schema/mysql.ts | 1 + packages/db/schema/postgresql.ts | 1 + packages/db/schema/sqlite.ts | 1 + packages/settings/src/creator.ts | 3 + .../external/search-engines-search-group.tsx | 5 +- packages/translation/src/lang/en.json | 13 + packages/validation/src/user.ts | 4 + 17 files changed, 2380 insertions(+), 7 deletions(-) create mode 100644 apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_ddg-bangs.tsx create mode 100644 packages/db/migrations/sqlite/0035_chunky_zombie.sql create mode 100644 packages/db/migrations/sqlite/meta/0035_snapshot.json diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx index ce6866ef39..f1185430cf 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx @@ -264,6 +264,5 @@ const usePreventLeaveWithDirty = (isDirty: boolean) => { window.removeEventListener("popstate", handlePopState); window.removeEventListener("beforeunload", handleBeforeUnload); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDirty]); }; diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_ready-context.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_ready-context.tsx index dcd07a3112..606a296ea7 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_ready-context.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_ready-context.tsx @@ -27,7 +27,6 @@ export const BoardReadyProvider = ({ children }: PropsWithChildren) => { useEffect(() => { setReadySections((previous) => previous.filter((id) => board.sections.some((section) => section.id === id))); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [board.sections.length, setReadySections]); const markAsReady = useCallback((id: string) => { diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_ddg-bangs.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_ddg-bangs.tsx new file mode 100644 index 0000000000..a1ce7a8bba --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_ddg-bangs.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { Button, Group, Stack, Switch } from "@mantine/core"; +import type { z } from "zod/v4"; + +import type { RouterOutputs } from "@homarr/api"; +import { clientApi } from "@homarr/api/client"; +import { revalidatePathActionAsync } from "@homarr/common/client"; +import { useZodForm } from "@homarr/form"; +import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; +import { useI18n } from "@homarr/translation/client"; +import { userDdgBangsSchema } from "@homarr/validation/user"; + +interface DdgBangsFormProps { + user: RouterOutputs["user"]["getById"]; +} + +export const DdgBangsForm = ({ user }: DdgBangsFormProps) => { + const t = useI18n(); + const { mutate, isPending } = clientApi.user.changeDdgBangs.useMutation({ + async onSettled() { + await revalidatePathActionAsync(`/manage/users/${user.id}`); + }, + onSuccess(_, variables) { + form.setInitialValues({ + ddgBangs: variables.ddgBangs, + }); + showSuccessNotification({ + message: t("user.action.changeDdgBangs.notification.success.message"), + }); + }, + onError() { + showErrorNotification({ + message: t("user.action.changeDdgBangs.notification.error.message"), + }); + }, + }); + + const form = useZodForm(userDdgBangsSchema, { + initialValues: { + ddgBangs: user.ddgBangs, + }, + }); + + const handleSubmit = (values: FormType) => { + mutate({ + id: user.id, + ...values, + }); + }; + + return ( +
+ + + + + + + +
+ ); +}; + +type FormType = z.infer; diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx index ee960b0f5a..0a03370d1b 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx @@ -13,6 +13,7 @@ import { createMetaTitle } from "~/metadata"; import { canAccessUserEditPage } from "../access"; import { ChangeHomeBoardForm } from "./_components/_change-home-board"; import { ChangeSearchPreferencesForm } from "./_components/_change-search-preferences"; +import { DdgBangsForm } from "./_components/_ddg-bangs"; import { DeleteUserButton } from "./_components/_delete-user-button"; import { FirstDayOfWeek } from "./_components/_first-day-of-week"; import { PingIconsEnabled } from "./_components/_ping-icons-enabled"; @@ -106,6 +107,7 @@ export default async function EditUserPage(props: Props) { {tGeneral("item.search")} + diff --git a/apps/nextjs/src/app/api/health/live/route.ts b/apps/nextjs/src/app/api/health/live/route.ts index 2576b3f8e4..ceeb7dc15e 100644 --- a/apps/nextjs/src/app/api/health/live/route.ts +++ b/apps/nextjs/src/app/api/health/live/route.ts @@ -75,7 +75,6 @@ const executeHealthCheckSafelyAsync = async ( }, }; } catch (error) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions logger.error(new ErrorWithMetadata("Healthcheck failed", { name }, { cause: error })); return { status: "unhealthy", diff --git a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts index 4f2fef067b..eda89a8e68 100644 --- a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts +++ b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts @@ -206,7 +206,6 @@ export const useGridstack = (section: Omit, itemIds: string[]) } // Only run this effect when the section items change - // eslint-disable-next-line react-hooks/exhaustive-deps }, [itemIds.length, columnCount]); /** diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index 875ffe80b8..71db269f03 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -19,6 +19,7 @@ import { userChangePasswordApiSchema, userChangeSearchPreferencesSchema, userCreateSchema, + userDdgBangsSchema, userEditProfileSchema, userFirstDayOfWeekSchema, userInitSchema, @@ -228,6 +229,7 @@ export const userRouter = createTRPCRouter({ pingIconsEnabled: true, defaultSearchEngineId: true, openSearchInNewTab: true, + ddgBangs: true, }), ) .meta({ openapi: { method: "GET", path: "/api/users/{userId}", tags: ["users"], protect: true } }) @@ -253,6 +255,7 @@ export const userRouter = createTRPCRouter({ pingIconsEnabled: true, defaultSearchEngineId: true, openSearchInNewTab: true, + ddgBangs: true, }, where: eq(users.id, input.userId), }); @@ -496,6 +499,22 @@ export const userRouter = createTRPCRouter({ }) .where(eq(users.id, ctx.session.user.id)); }), + changeDdgBangs: protectedProcedure.input(userDdgBangsSchema.and(byIdSchema)).mutation(async ({ input, ctx }) => { + // Only admins can change other users DDG bang preference + if (!ctx.session.user.permissions.includes("admin") && ctx.session.user.id !== input.id) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + + await ctx.db + .update(users) + .set({ + ddgBangs: input.ddgBangs, + }) + .where(eq(users.id, input.id)); + }), changeFirstDayOfWeek: protectedProcedure .input(convertIntersectionToZodObject(userFirstDayOfWeekSchema.and(byIdSchema))) .output(z.void()) diff --git a/packages/db/migrations/sqlite/0035_chunky_zombie.sql b/packages/db/migrations/sqlite/0035_chunky_zombie.sql new file mode 100644 index 0000000000..e762ad58a6 --- /dev/null +++ b/packages/db/migrations/sqlite/0035_chunky_zombie.sql @@ -0,0 +1 @@ +ALTER TABLE `user` ADD `ddg_bangs` integer DEFAULT true NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/sqlite/meta/0035_snapshot.json b/packages/db/migrations/sqlite/meta/0035_snapshot.json new file mode 100644 index 0000000000..8c64fc063b --- /dev/null +++ b/packages/db/migrations/sqlite/meta/0035_snapshot.json @@ -0,0 +1,2257 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "ad0d96d4-7ba7-44f1-a2a7-40f4c94c99ed", + "prevId": "26cc52b8-505b-44af-9cbd-6f70f6f3bb6b", + "tables": { + "account": { + "name": "account", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "columns": [ + "provider", + "provider_account_id" + ], + "name": "account_provider_provider_account_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_user_id_user_id_fk": { + "name": "apiKey_user_id_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ping_url": { + "name": "ping_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "columns": [ + "board_id", + "group_id", + "permission" + ], + "name": "boardGroupPermission_board_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "columns": [ + "board_id", + "user_id", + "permission" + ], + "name": "boardUserPermission_board_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fixed'" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'no-repeat'" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cover'" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fa5252'" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fd7e14'" + }, + "opacity": { + "name": "opacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_color": { + "name": "icon_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_radius": { + "name": "item_radius", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'lg'" + }, + "disable_status": { + "name": "disable_status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "board_name_unique": { + "name": "board_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cron_job_configuration": { + "name": "cron_job_configuration", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groupMember": { + "name": "groupMember", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_group_id_group_id_fk": { + "name": "groupMember_group_id_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_user_id_user_id_fk": { + "name": "groupMember_user_id_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_group_id_user_id_pk": { + "columns": [ + "group_id", + "user_id" + ], + "name": "groupMember_group_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_group_id_group_id_fk": { + "name": "groupPermission_group_id_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "home_board_id": { + "name": "home_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "group_name_unique": { + "name": "group_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_home_board_id_board_id_fk": { + "name": "group_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_mobile_home_board_id_board_id_fk": { + "name": "group_mobile_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "icon": { + "name": "icon", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_repository_id": { + "name": "icon_repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_icon_repository_id_iconRepository_id_fk": { + "name": "icon_icon_repository_id_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": [ + "icon_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationGroupPermissions_integration_id_group_id_permission_pk": { + "columns": [ + "integration_id", + "group_id", + "permission" + ], + "name": "integrationGroupPermissions_integration_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "columns": [ + "item_id", + "integration_id" + ], + "name": "integration_item_item_id_integration_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": [ + "kind" + ], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "columns": [ + "integration_id", + "kind" + ], + "name": "integrationSecret_integration_id_kind_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "columns": [ + "integration_id", + "user_id", + "permission" + ], + "name": "integrationUserPermission_integration_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": [ + "kind" + ], + "isUnique": false + } + }, + "foreignKeys": { + "integration_app_id_app_id_fk": { + "name": "integration_app_id_app_id_fk", + "tableFrom": "integration", + "tableTo": "app", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "item_layout": { + "name": "item_layout", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout_id": { + "name": "layout_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "item_layout_item_id_item_id_fk": { + "name": "item_layout_item_id_item_id_fk", + "tableFrom": "item_layout", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_section_id_section_id_fk": { + "name": "item_layout_section_id_section_id_fk", + "tableFrom": "item_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_layout_id_layout_id_fk": { + "name": "item_layout_layout_id_layout_id_fk", + "tableFrom": "item_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_layout_item_id_section_id_layout_id_pk": { + "columns": [ + "item_id", + "section_id", + "layout_id" + ], + "name": "item_layout_item_id_section_id_layout_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "item_board_id_board_id_fk": { + "name": "item_board_id_board_id_fk", + "tableFrom": "item", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "layout": { + "name": "layout", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breakpoint": { + "name": "breakpoint", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "layout_board_id_board_id_fk": { + "name": "layout_board_id_board_id_fk", + "tableFrom": "layout", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "media": { + "name": "media", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "media_creator_id_user_id_fk": { + "name": "media_creator_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "onboarding": { + "name": "onboarding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_step": { + "name": "previous_step", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "search_engine": { + "name": "search_engine", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short": { + "name": "short", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'generic'" + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "search_engine_short_unique": { + "name": "search_engine_short_unique", + "columns": [ + "short" + ], + "isUnique": true + } + }, + "foreignKeys": { + "search_engine_integration_id_integration_id_fk": { + "name": "search_engine_integration_id_integration_id_fk", + "tableFrom": "search_engine", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "section_collapse_state": { + "name": "section_collapse_state", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "collapsed": { + "name": "collapsed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_collapse_state_user_id_user_id_fk": { + "name": "section_collapse_state_user_id_user_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_collapse_state_section_id_section_id_fk": { + "name": "section_collapse_state_section_id_section_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_collapse_state_user_id_section_id_pk": { + "columns": [ + "user_id", + "section_id" + ], + "name": "section_collapse_state_user_id_section_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "section_layout": { + "name": "section_layout", + "columns": { + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout_id": { + "name": "layout_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_layout_section_id_section_id_fk": { + "name": "section_layout_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_layout_id_layout_id_fk": { + "name": "section_layout_layout_id_layout_id_fk", + "tableFrom": "section_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_parent_section_id_section_id_fk": { + "name": "section_layout_parent_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "parent_section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_layout_section_id_layout_id_pk": { + "columns": [ + "section_id", + "layout_id" + ], + "name": "section_layout_section_id_layout_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "setting_key": { + "name": "setting_key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": { + "serverSetting_settingKey_unique": { + "name": "serverSetting_settingKey_unique", + "columns": [ + "setting_key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_certificate_hostname": { + "name": "trusted_certificate_hostname", + "columns": { + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thumbprint": { + "name": "thumbprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "certificate": { + "name": "certificate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trusted_certificate_hostname_hostname_thumbprint_pk": { + "columns": [ + "hostname", + "thumbprint" + ], + "name": "trusted_certificate_hostname_hostname_thumbprint_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "home_board_id": { + "name": "home_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_search_engine_id": { + "name": "default_search_engine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "open_search_in_new_tab": { + "name": "open_search_in_new_tab", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ddg_bangs": { + "name": "ddg_bangs", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "color_scheme": { + "name": "color_scheme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dark'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "ping_icons_enabled": { + "name": "ping_icons_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_home_board_id_board_id_fk": { + "name": "user_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_mobile_home_board_id_board_id_fk": { + "name": "user_mobile_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_default_search_engine_id_search_engine_id_fk": { + "name": "user_default_search_engine_id_search_engine_id_fk", + "tableFrom": "user", + "tableTo": "search_engine", + "columnsFrom": [ + "default_search_engine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/sqlite/meta/_journal.json b/packages/db/migrations/sqlite/meta/_journal.json index ef6bdbd499..d8238b12db 100644 --- a/packages/db/migrations/sqlite/meta/_journal.json +++ b/packages/db/migrations/sqlite/meta/_journal.json @@ -246,6 +246,13 @@ "when": 1760968503571, "tag": "0034_add_app_reference_to_integration", "breakpoints": true + }, + { + "idx": 35, + "version": "6", + "when": 1769351238802, + "tag": "0035_chunky_zombie", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts index edbcec2810..e310f21592 100644 --- a/packages/db/schema/mysql.ts +++ b/packages/db/schema/mysql.ts @@ -78,6 +78,7 @@ export const users = mysqlTable("user", { onDelete: "set null", }), openSearchInNewTab: boolean().default(false).notNull(), + ddgBangs: boolean().default(true).notNull(), colorScheme: varchar({ length: 5 }).$type().default("dark").notNull(), firstDayOfWeek: tinyint().$type().default(1).notNull(), // Defaults to Monday pingIconsEnabled: boolean().default(false).notNull(), diff --git a/packages/db/schema/postgresql.ts b/packages/db/schema/postgresql.ts index e41d72f752..0ff8c5eb90 100644 --- a/packages/db/schema/postgresql.ts +++ b/packages/db/schema/postgresql.ts @@ -77,6 +77,7 @@ export const users = pgTable("user", { onDelete: "set null", }), openSearchInNewTab: boolean().default(false).notNull(), + ddgBangs: boolean().default(true).notNull(), colorScheme: varchar({ length: 5 }).$type().default("dark").notNull(), firstDayOfWeek: smallint().$type().default(1).notNull(), // Defaults to Monday pingIconsEnabled: boolean().default(false).notNull(), diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index 78ec4ac9cc..c8d78c0fd5 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -60,6 +60,7 @@ export const users = sqliteTable("user", { onDelete: "set null", }), openSearchInNewTab: int({ mode: "boolean" }).default(true).notNull(), + ddgBangs: int({ mode: "boolean" }).default(true).notNull(), colorScheme: text().$type().default("dark").notNull(), firstDayOfWeek: int().$type().default(1).notNull(), // Defaults to Monday pingIconsEnabled: int({ mode: "boolean" }).default(false).notNull(), diff --git a/packages/settings/src/creator.ts b/packages/settings/src/creator.ts index 5eb348930c..43f410cefb 100644 --- a/packages/settings/src/creator.ts +++ b/packages/settings/src/creator.ts @@ -8,6 +8,7 @@ export type SettingsContextProps = Pick< | "homeBoardId" | "mobileHomeBoardId" | "openSearchInNewTab" + | "ddgBangs" | "pingIconsEnabled" > & Pick; @@ -27,6 +28,7 @@ export type UserSettings = Pick< | "homeBoardId" | "mobileHomeBoardId" | "openSearchInNewTab" + | "ddgBangs" | "pingIconsEnabled" >; @@ -39,6 +41,7 @@ export const createSettings = ({ }) => ({ defaultSearchEngineId: user?.defaultSearchEngineId ?? serverSettings.search.defaultSearchEngineId, openSearchInNewTab: user?.openSearchInNewTab ?? true, + ddgBangs: user?.ddgBangs ?? true, firstDayOfWeek: user?.firstDayOfWeek ?? (1 as const), homeBoardId: user?.homeBoardId ?? serverSettings.board.homeBoardId, mobileHomeBoardId: user?.mobileHomeBoardId ?? serverSettings.board.mobileHomeBoardId, diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx index 134dd82d62..14f7365e1a 100644 --- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx +++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx @@ -392,6 +392,7 @@ export const searchEnginesSearchGroups = createGroup({ }, useQueryOptions(query) { const tExternal = useScopedI18n("search.mode.external.group.searchEngine"); + const { ddgBangs } = useSettings(); const { bangToken, searchText, locked } = parseBangQuery(query); const [debouncedBangToken] = useDebouncedValue(bangToken, 150); @@ -403,7 +404,7 @@ export const searchEnginesSearchGroups = createGroup({ const ddgQuery = clientApi.bangs.search.useQuery( { query: debouncedBangToken, limit: 10 }, { - enabled: debouncedBangToken.length >= 1, + enabled: ddgBangs && debouncedBangToken.length >= 1, placeholderData: keepPreviousData, }, ); @@ -465,7 +466,7 @@ export const searchEnginesSearchGroups = createGroup({ key: "hint", kind: "hint", label: "Type a bang, e.g. !yt, then press Space to select", - description: tExternal("tip.ddgBangs"), + description: ddgBangs ? tExternal("tip.ddgBangs") : undefined, }); } diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 5835a1cd49..bc2d8ba7a1 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -157,6 +157,9 @@ }, "openSearchInNewTab": { "label": "Open search results in new tab" + }, + "ddgBangs": { + "label": "Enable DuckDuckGo bangs" } }, "error": { @@ -246,6 +249,16 @@ } } }, + "changeDdgBangs": { + "notification": { + "success": { + "message": "DuckDuckGo bangs toggled successfully" + }, + "error": { + "message": "Unable to toggle DuckDuckGo bangs" + } + } + }, "manageAvatar": { "changeImage": { "label": "Change image", diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts index c00d9226dc..00c43ddd7a 100644 --- a/packages/validation/src/user.ts +++ b/packages/validation/src/user.ts @@ -147,3 +147,7 @@ export const userFirstDayOfWeekSchema = z.object({ export const userPingIconsEnabledSchema = z.object({ pingIconsEnabled: z.boolean(), }); + +export const userDdgBangsSchema = z.object({ + ddgBangs: z.boolean(), +}); From 742f0e07200ee34b1a4680606cd55146982463e4 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 28 Jan 2026 12:04:36 +0100 Subject: [PATCH 16/19] fix(request-handler): use ResponseError instead of generic Error Replace generic Error with ResponseError from @homarr/common/server for better error handling consistency across the codebase. --- packages/request-handler/src/duckduckgo-bangs.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/request-handler/src/duckduckgo-bangs.ts b/packages/request-handler/src/duckduckgo-bangs.ts index 09c4b279fc..a51bf5327e 100644 --- a/packages/request-handler/src/duckduckgo-bangs.ts +++ b/packages/request-handler/src/duckduckgo-bangs.ts @@ -2,6 +2,7 @@ import dayjs from "dayjs"; import { z } from "zod"; import { env } from "@homarr/common/env"; +import { ResponseError } from "@homarr/common/server"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { createChannelWithLatestAndEvents } from "@homarr/redis"; @@ -55,7 +56,7 @@ export const duckDuckGoBangsRequestHandler = createCachedRequestHandler({ }); if (!res.ok) { - throw new Error(`Failed to fetch DuckDuckGo bangs: ${res.status} ${res.statusText}`); + throw new ResponseError(res); } const json: unknown = await res.json(); From d0569a2b8185f8ed06fe177a27807eda7779d24b Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 28 Jan 2026 12:04:44 +0100 Subject: [PATCH 17/19] docs(api): explain binary search benefit over findIndex Add JSDoc comment explaining why lowerBound uses binary search (O(log n)) instead of findIndex (O(n)) for searching ~13,000+ DuckDuckGo bangs. --- packages/api/src/services/duckduckgo-bangs.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/api/src/services/duckduckgo-bangs.ts b/packages/api/src/services/duckduckgo-bangs.ts index b5f8d685a4..297f5c8ed8 100644 --- a/packages/api/src/services/duckduckgo-bangs.ts +++ b/packages/api/src/services/duckduckgo-bangs.ts @@ -7,6 +7,13 @@ import type { DuckDuckGoBang } from "@homarr/request-handler/duckduckgo-bangs"; const normalizeBangToken = (token: string) => token.toLowerCase().trim(); +/** + * Binary search to find the first index where bang.t >= tokenPrefix. + * This is O(log n) vs O(n) for findIndex, which matters because DuckDuckGo + * has ~13,000+ bangs. Combined with the pre-sorted data, this allows + * efficient prefix matching by finding the start position and iterating + * only through consecutive matches. + */ const lowerBound = (arr: DuckDuckGoBang[], tokenPrefix: string) => { let low = 0; let high = arr.length; From c640a058bb0a6a904913c394687ed7efbdb94566 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 28 Jan 2026 12:04:53 +0100 Subject: [PATCH 18/19] refactor(user): move ddgBangs toggle into search preferences Consolidate the DuckDuckGo bangs toggle into the existing search preferences form instead of having a separate form section. - Add ddgBangsEnabled to userChangeSearchPreferencesSchema - Update changeSearchPreferencesAsync to handle ddgBangs - Add toggle to ChangeSearchPreferencesForm component - Remove standalone DdgBangsForm component - Deprecate standalone changeDdgBangs endpoint (kept for API compat) --- .../_change-search-preferences.tsx | 6 +++ .../manage/users/[userId]/general/page.tsx | 2 - packages/api/src/router/user.ts | 43 ++++++++++++------- .../router/user/change-search-preferences.ts | 8 +++- packages/validation/src/user.ts | 1 + 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-search-preferences.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-search-preferences.tsx index 5bf23a5fb3..afae60dc15 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-search-preferences.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-search-preferences.tsx @@ -26,6 +26,7 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS form.setInitialValues({ defaultSearchEngineId: variables.defaultSearchEngineId, openInNewTab: variables.openInNewTab, + ddgBangsEnabled: variables.ddgBangsEnabled, }); showSuccessNotification({ message: t("user.action.changeSearchPreferences.notification.success.message"), @@ -41,6 +42,7 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS initialValues: { defaultSearchEngineId: user.defaultSearchEngineId, openInNewTab: user.openSearchInNewTab, + ddgBangsEnabled: user.ddgBangs, }, }); @@ -64,6 +66,10 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS label={t("user.field.openSearchInNewTab.label")} {...form.getInputProps("openInNewTab", { type: "checkbox" })} /> + - - - - ); -}; - -type FormType = z.infer; diff --git a/packages/db/migrations/mysql/0037_lying_electro.sql b/packages/db/migrations/mysql/0037_lying_electro.sql new file mode 100644 index 0000000000..71bf798142 --- /dev/null +++ b/packages/db/migrations/mysql/0037_lying_electro.sql @@ -0,0 +1 @@ +ALTER TABLE `user` ADD `ddg_bangs` boolean DEFAULT true NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/mysql/meta/0037_snapshot.json b/packages/db/migrations/mysql/meta/0037_snapshot.json new file mode 100644 index 0000000000..fb1fbfc5d5 --- /dev/null +++ b/packages/db/migrations/mysql/meta/0037_snapshot.json @@ -0,0 +1,2378 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "abed83bb-11b6-4c0d-ab04-a4bc1bac0eff", + "prevId": "90ea3b87-abde-433b-9cd4-0aaff72311a9", + "tables": { + "account": { + "name": "account", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "name": "account_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_user_id_user_id_fk": { + "name": "apiKey_user_id_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "apiKey_id": { + "name": "apiKey_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ping_url": { + "name": "ping_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "app_id": { + "name": "app_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "name": "boardGroupPermission_board_id_group_id_permission_pk", + "columns": [ + "board_id", + "group_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "name": "boardUserPermission_board_id_user_id_permission_pk", + "columns": [ + "board_id", + "user_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('fixed')" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('no-repeat')" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('cover')" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fa5252')" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fd7e14')" + }, + "opacity": { + "name": "opacity", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_color": { + "name": "icon_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_radius": { + "name": "item_radius", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('lg')" + }, + "disable_status": { + "name": "disable_status", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "board_id": { + "name": "board_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "board_name_unique": { + "name": "board_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "cron_job_configuration": { + "name": "cron_job_configuration", + "columns": { + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "cron_job_configuration_name": { + "name": "cron_job_configuration_name", + "columns": [ + "name" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "groupMember": { + "name": "groupMember", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_group_id_group_id_fk": { + "name": "groupMember_group_id_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_user_id_user_id_fk": { + "name": "groupMember_user_id_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_group_id_user_id_pk": { + "name": "groupMember_group_id_user_id_pk", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_group_id_group_id_fk": { + "name": "groupPermission_group_id_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_home_board_id_board_id_fk": { + "name": "group_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_mobile_home_board_id_board_id_fk": { + "name": "group_mobile_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_id": { + "name": "group_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "group_name_unique": { + "name": "group_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(150)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "iconRepository_id": { + "name": "iconRepository_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "icon": { + "name": "icon", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(250)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_repository_id": { + "name": "icon_repository_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_icon_repository_id_iconRepository_id_fk": { + "name": "icon_icon_repository_id_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": [ + "icon_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "icon_id": { + "name": "icon_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_group_permission__pk": { + "name": "integration_group_permission__pk", + "columns": [ + "integration_id", + "group_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "name": "integration_item_item_id_integration_id_pk", + "columns": [ + "item_id", + "integration_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": [ + "kind" + ], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "name": "integrationSecret_integration_id_kind_pk", + "columns": [ + "integration_id", + "kind" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "name": "integrationUserPermission_integration_id_user_id_permission_pk", + "columns": [ + "integration_id", + "user_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "app_id": { + "name": "app_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": [ + "kind" + ], + "isUnique": false + } + }, + "foreignKeys": { + "integration_app_id_app_id_fk": { + "name": "integration_app_id_app_id_fk", + "tableFrom": "integration", + "tableTo": "app", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_id": { + "name": "integration_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "invite_id": { + "name": "invite_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": [ + "token" + ] + } + }, + "checkConstraint": {} + }, + "item_layout": { + "name": "item_layout", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout_id": { + "name": "layout_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "item_layout_item_id_item_id_fk": { + "name": "item_layout_item_id_item_id_fk", + "tableFrom": "item_layout", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_section_id_section_id_fk": { + "name": "item_layout_section_id_section_id_fk", + "tableFrom": "item_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_layout_id_layout_id_fk": { + "name": "item_layout_layout_id_layout_id_fk", + "tableFrom": "item_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_layout_item_id_section_id_layout_id_pk": { + "name": "item_layout_item_id_section_id_layout_id_pk", + "columns": [ + "item_id", + "section_id", + "layout_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": { + "item_board_id_board_id_fk": { + "name": "item_board_id_board_id_fk", + "tableFrom": "item", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_id": { + "name": "item_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "layout": { + "name": "layout", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breakpoint": { + "name": "breakpoint", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "layout_board_id_board_id_fk": { + "name": "layout_board_id_board_id_fk", + "tableFrom": "layout", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "layout_id": { + "name": "layout_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "media": { + "name": "media", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "LONGBLOB", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "media_creator_id_user_id_fk": { + "name": "media_creator_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "media_id": { + "name": "media_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "onboarding": { + "name": "onboarding", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_step": { + "name": "previous_step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "onboarding_id": { + "name": "onboarding_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "search_engine": { + "name": "search_engine", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short": { + "name": "short", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'generic'" + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "search_engine_integration_id_integration_id_fk": { + "name": "search_engine_integration_id_integration_id_fk", + "tableFrom": "search_engine", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "search_engine_id": { + "name": "search_engine_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "search_engine_short_unique": { + "name": "search_engine_short_unique", + "columns": [ + "short" + ] + } + }, + "checkConstraint": {} + }, + "section_collapse_state": { + "name": "section_collapse_state", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "collapsed": { + "name": "collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_collapse_state_user_id_user_id_fk": { + "name": "section_collapse_state_user_id_user_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_collapse_state_section_id_section_id_fk": { + "name": "section_collapse_state_section_id_section_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_collapse_state_user_id_section_id_pk": { + "name": "section_collapse_state_user_id_section_id_pk", + "columns": [ + "user_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "section_layout": { + "name": "section_layout", + "columns": { + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout_id": { + "name": "layout_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_layout_section_id_section_id_fk": { + "name": "section_layout_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_layout_id_layout_id_fk": { + "name": "section_layout_layout_id_layout_id_fk", + "tableFrom": "section_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_parent_section_id_section_id_fk": { + "name": "section_layout_parent_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "parent_section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_layout_section_id_layout_id_pk": { + "name": "section_layout_section_id_layout_id_pk", + "columns": [ + "section_id", + "layout_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_id": { + "name": "section_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "setting_key": { + "name": "setting_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "serverSetting_setting_key": { + "name": "serverSetting_setting_key", + "columns": [ + "setting_key" + ] + } + }, + "uniqueConstraints": { + "serverSetting_settingKey_unique": { + "name": "serverSetting_settingKey_unique", + "columns": [ + "setting_key" + ] + } + }, + "checkConstraint": {} + }, + "session": { + "name": "session", + "columns": { + "session_token": { + "name": "session_token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_session_token": { + "name": "session_session_token", + "columns": [ + "session_token" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_search_engine_id": { + "name": "default_search_engine_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "open_search_in_new_tab": { + "name": "open_search_in_new_tab", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ddg_bangs": { + "name": "ddg_bangs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "color_scheme": { + "name": "color_scheme", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dark'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "ping_icons_enabled": { + "name": "ping_icons_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_home_board_id_board_id_fk": { + "name": "user_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_mobile_home_board_id_board_id_fk": { + "name": "user_mobile_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_default_search_engine_id_search_engine_id_fk": { + "name": "user_default_search_engine_id_search_engine_id_fk", + "tableFrom": "user", + "tableTo": "search_engine", + "columnsFrom": [ + "default_search_engine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_certificate_hostname": { + "name": "trusted_certificate_hostname", + "columns": { + "hostname": { + "name": "hostname", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thumbprint": { + "name": "thumbprint", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "certificate": { + "name": "certificate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trusted_certificate_hostname_hostname_thumbprint_pk": { + "name": "trusted_certificate_hostname_hostname_thumbprint_pk", + "columns": [ + "hostname", + "thumbprint" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/mysql/meta/_journal.json b/packages/db/migrations/mysql/meta/_journal.json index 284af5f2a9..c8e70749ea 100644 --- a/packages/db/migrations/mysql/meta/_journal.json +++ b/packages/db/migrations/mysql/meta/_journal.json @@ -260,6 +260,13 @@ "when": 1760968518445, "tag": "0036_add_app_reference_to_integration", "breakpoints": true + }, + { + "idx": 37, + "version": "5", + "when": 1769597497728, + "tag": "0037_lying_electro", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/db/migrations/postgresql/0003_needy_meggan.sql b/packages/db/migrations/postgresql/0003_needy_meggan.sql new file mode 100644 index 0000000000..90755a9b37 --- /dev/null +++ b/packages/db/migrations/postgresql/0003_needy_meggan.sql @@ -0,0 +1 @@ +ALTER TABLE "user" ADD COLUMN "ddg_bangs" boolean DEFAULT true NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/postgresql/meta/0003_snapshot.json b/packages/db/migrations/postgresql/meta/0003_snapshot.json new file mode 100644 index 0000000000..9986db3305 --- /dev/null +++ b/packages/db/migrations/postgresql/meta/0003_snapshot.json @@ -0,0 +1,2228 @@ +{ + "id": "15ab3206-c918-4f4b-b11e-0e023f3e7d85", + "prevId": "66a8193e-c82f-4b41-99da-8d94f5737c62", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "name": "account_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apiKey": { + "name": "apiKey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_user_id_user_id_fk": { + "name": "apiKey_user_id_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app": { + "name": "app", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ping_url": { + "name": "ping_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.boardGroupPermission": { + "name": "boardGroupPermission", + "schema": "", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "name": "boardGroupPermission_board_id_group_id_permission_pk", + "columns": [ + "board_id", + "group_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.boardUserPermission": { + "name": "boardUserPermission", + "schema": "", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "name": "boardUserPermission_board_id_user_id_permission_pk", + "columns": [ + "board_id", + "user_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board": { + "name": "board", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'no-repeat'" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cover'" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#fa5252'" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#fd7e14'" + }, + "opacity": { + "name": "opacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_color": { + "name": "icon_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "item_radius": { + "name": "item_radius", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'lg'" + }, + "disable_status": { + "name": "disable_status", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "board_name_unique": { + "name": "board_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cron_job_configuration": { + "name": "cron_job_configuration", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": true, + "notNull": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groupMember": { + "name": "groupMember", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_group_id_group_id_fk": { + "name": "groupMember_group_id_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_user_id_user_id_fk": { + "name": "groupMember_user_id_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_group_id_user_id_pk": { + "name": "groupMember_group_id_user_id_pk", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groupPermission": { + "name": "groupPermission", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_group_id_group_id_fk": { + "name": "groupPermission_group_id_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group": { + "name": "group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "smallint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_home_board_id_board_id_fk": { + "name": "group_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_mobile_home_board_id_board_id_fk": { + "name": "group_mobile_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "group_name_unique": { + "name": "group_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.iconRepository": { + "name": "iconRepository", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(150)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.icon": { + "name": "icon", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(250)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon_repository_id": { + "name": "icon_repository_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "icon_icon_repository_id_iconRepository_id_fk": { + "name": "icon_icon_repository_id_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": [ + "icon_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "schema": "", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_group_permission__pk": { + "name": "integration_group_permission__pk", + "columns": [ + "integration_id", + "group_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_item": { + "name": "integration_item", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "name": "integration_item_item_id_integration_id_pk", + "columns": [ + "item_id", + "integration_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrationSecret": { + "name": "integrationSecret", + "schema": "", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "name": "integrationSecret_integration_id_kind_pk", + "columns": [ + "integration_id", + "kind" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrationUserPermission": { + "name": "integrationUserPermission", + "schema": "", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "name": "integrationUserPermission_integration_id_user_id_permission_pk", + "columns": [ + "integration_id", + "user_id", + "permission" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_app_id_app_id_fk": { + "name": "integration_app_id_app_id_fk", + "tableFrom": "integration", + "tableTo": "app", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite": { + "name": "invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item_layout": { + "name": "item_layout", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "item_layout_item_id_item_id_fk": { + "name": "item_layout_item_id_item_id_fk", + "tableFrom": "item_layout", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_section_id_section_id_fk": { + "name": "item_layout_section_id_section_id_fk", + "tableFrom": "item_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "item_layout_layout_id_layout_id_fk": { + "name": "item_layout_layout_id_layout_id_fk", + "tableFrom": "item_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_layout_item_id_section_id_layout_id_pk": { + "name": "item_layout_item_id_section_id_layout_id_pk", + "columns": [ + "item_id", + "section_id", + "layout_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item": { + "name": "item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"json\": {}}'" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "item_board_id_board_id_fk": { + "name": "item_board_id_board_id_fk", + "tableFrom": "item", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.layout": { + "name": "layout", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "column_count": { + "name": "column_count", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "breakpoint": { + "name": "breakpoint", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "layout_board_id_board_id_fk": { + "name": "layout_board_id_board_id_fk", + "tableFrom": "layout", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "media_creator_id_user_id_fk": { + "name": "media_creator_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.onboarding": { + "name": "onboarding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "step": { + "name": "step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "previous_step": { + "name": "previous_step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.search_engine": { + "name": "search_engine", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "short": { + "name": "short", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "'generic'" + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "search_engine_integration_id_integration_id_fk": { + "name": "search_engine_integration_id_integration_id_fk", + "tableFrom": "search_engine", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "search_engine_short_unique": { + "name": "search_engine_short_unique", + "nullsNotDistinct": false, + "columns": [ + "short" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.section_collapse_state": { + "name": "section_collapse_state", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "collapsed": { + "name": "collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_collapse_state_user_id_user_id_fk": { + "name": "section_collapse_state_user_id_user_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_collapse_state_section_id_section_id_fk": { + "name": "section_collapse_state_section_id_section_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_collapse_state_user_id_section_id_pk": { + "name": "section_collapse_state_user_id_section_id_pk", + "columns": [ + "user_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.section_layout": { + "name": "section_layout", + "schema": "", + "columns": { + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "section_layout_section_id_section_id_fk": { + "name": "section_layout_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_layout_id_layout_id_fk": { + "name": "section_layout_layout_id_layout_id_fk", + "tableFrom": "section_layout", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_layout_parent_section_id_section_id_fk": { + "name": "section_layout_parent_section_id_section_id_fk", + "tableFrom": "section_layout", + "tableTo": "section", + "columnsFrom": [ + "parent_section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_layout_section_id_layout_id_pk": { + "name": "section_layout_section_id_layout_id_pk", + "columns": [ + "section_id", + "layout_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.section": { + "name": "section", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.serverSetting": { + "name": "serverSetting", + "schema": "", + "columns": { + "setting_key": { + "name": "setting_key", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "serverSetting_settingKey_unique": { + "name": "serverSetting_settingKey_unique", + "nullsNotDistinct": false, + "columns": [ + "setting_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "session_token": { + "name": "session_token", + "type": "varchar(512)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "'credentials'" + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "default_search_engine_id": { + "name": "default_search_engine_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "open_search_in_new_tab": { + "name": "open_search_in_new_tab", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ddg_bangs": { + "name": "ddg_bangs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "color_scheme": { + "name": "color_scheme", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'dark'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "ping_icons_enabled": { + "name": "ping_icons_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_home_board_id_board_id_fk": { + "name": "user_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_mobile_home_board_id_board_id_fk": { + "name": "user_mobile_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": [ + "mobile_home_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_default_search_engine_id_search_engine_id_fk": { + "name": "user_default_search_engine_id_search_engine_id_fk", + "tableFrom": "user", + "tableTo": "search_engine", + "columnsFrom": [ + "default_search_engine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_certificate_hostname": { + "name": "trusted_certificate_hostname", + "schema": "", + "columns": { + "hostname": { + "name": "hostname", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "thumbprint": { + "name": "thumbprint", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "certificate": { + "name": "certificate", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "trusted_certificate_hostname_hostname_thumbprint_pk": { + "name": "trusted_certificate_hostname_hostname_thumbprint_pk", + "columns": [ + "hostname", + "thumbprint" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/postgresql/meta/_journal.json b/packages/db/migrations/postgresql/meta/_journal.json index 01f3a047c8..c7c8c5a06d 100644 --- a/packages/db/migrations/postgresql/meta/_journal.json +++ b/packages/db/migrations/postgresql/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1760968530084, "tag": "0002_add_app_reference_to_integration", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1769597472063, + "tag": "0003_needy_meggan", + "breakpoints": true } ] -} +} \ No newline at end of file