From 02f3139f46d687b2e1d318e046a57983cdf87a79 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sun, 7 Jun 2026 22:23:36 +0200 Subject: [PATCH] refactor(search): read OPENSEARCH_* env per call Tests can't override values captured at module load because ES imports hoist before any Deno.env.set call. Reading env on each helper invocation makes the module trivially testable and has no production cost (env doesn't change at runtime). --- src/search/opensearch.ts | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/search/opensearch.ts b/src/search/opensearch.ts index 3462b73..20acc5a 100644 --- a/src/search/opensearch.ts +++ b/src/search/opensearch.ts @@ -1,21 +1,19 @@ import type { API } from "@opensearch-project/opensearch"; import { log } from "../log.ts"; -const OPENSEARCH_URL = Deno.env.get("OPENSEARCH_URL"); -const OPENSEARCH_USERNAME = Deno.env.get("OPENSEARCH_USERNAME"); -const OPENSEARCH_PASSWORD = Deno.env.get("OPENSEARCH_PASSWORD"); -const OPENSEARCH_INDEX = Deno.env.get("OPENSEARCH_INDEX") ?? "notes"; - const MAX_LIMIT = 100; -const enabled = (): boolean => { - if (!OPENSEARCH_URL) return false; - return true; -}; +const openSearchUrl = (): string | undefined => Deno.env.get("OPENSEARCH_URL"); +const openSearchIndex = (): string => + Deno.env.get("OPENSEARCH_INDEX") ?? "notes"; + +const enabled = (): boolean => Boolean(openSearchUrl()); const authHeaders = (): Record => { - if (OPENSEARCH_USERNAME && OPENSEARCH_PASSWORD) { - const creds = btoa(`${OPENSEARCH_USERNAME}:${OPENSEARCH_PASSWORD}`); + const username = Deno.env.get("OPENSEARCH_USERNAME"); + const password = Deno.env.get("OPENSEARCH_PASSWORD"); + if (username && password) { + const creds = btoa(`${username}:${password}`); return { Authorization: `Basic ${creds}` }; } return {}; @@ -29,7 +27,7 @@ const request = async ( path: string, body?: unknown, ): Promise => { - const url = `${OPENSEARCH_URL}${path}`; + const url = `${openSearchUrl()}${path}`; const headers: Record = { ...authHeaders() }; if (body !== undefined) headers["Content-Type"] = "application/json"; return await fetch(url, { @@ -59,11 +57,12 @@ const indexMappings = { export const ensureIndex = async (): Promise => { if (!enabled()) return; - const head = await request("HEAD", `/${OPENSEARCH_INDEX}`); + const index = openSearchIndex(); + const head = await request("HEAD", `/${index}`); if (head.status === 200) return; - const res = await request("PUT", `/${OPENSEARCH_INDEX}`, indexMappings); + const res = await request("PUT", `/${index}`, indexMappings); if (res.ok) { - log(`[opensearch] created index ${OPENSEARCH_INDEX}`); + log(`[opensearch] created index ${index}`); return; } const text = await res.text(); @@ -96,7 +95,7 @@ export const indexNote = async (note: IndexedNote): Promise => { }; const res = await request( "PUT", - `/${OPENSEARCH_INDEX}/_doc/${docId(note.did, note.rkey)}`, + `/${openSearchIndex()}/_doc/${docId(note.did, note.rkey)}`, doc, ); if (!res.ok) { @@ -114,7 +113,7 @@ export const removeNote = async ( if (!enabled()) return; const res = await request( "DELETE", - `/${OPENSEARCH_INDEX}/_doc/${docId(did, rkey)}`, + `/${openSearchIndex()}/_doc/${docId(did, rkey)}`, ); if (!res.ok && res.status !== 404) { const text = await res.text(); @@ -219,7 +218,7 @@ export const searchNotes = async ( ...(searchAfter ? { search_after: searchAfter } : {}), }; - const res = await request("POST", `/${OPENSEARCH_INDEX}/_search`, body); + const res = await request("POST", `/${openSearchIndex()}/_search`, body); if (!res.ok) { const text = await res.text(); throw new Error(`Search failed: ${res.status} ${text}`);