/** * Meilisearch client configuration */ import { MeiliSearch } from 'meilisearch'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __dirname = dirname(fileURLToPath(import.meta.url)); const MEILISEARCH_HOST = process.env.MEILISEARCH_HOST || 'http://127.0.0.1:7700'; const MEILISEARCH_MASTER_KEY = process.env.MEILISEARCH_MASTER_KEY || 'changeme123'; const INDEX_NAME = process.env.MEILISEARCH_INDEX_NAME || 'navidocs-pages'; let client = null; let index = null; let tenantKeyUid = null; export function getMeilisearchClient() { if (!client) { console.log(`[Meilisearch] Initializing client with host=${MEILISEARCH_HOST}, apiKey=${MEILISEARCH_MASTER_KEY}`); client = new MeiliSearch({ host: MEILISEARCH_HOST, apiKey: MEILISEARCH_MASTER_KEY }); } return client; } export async function getMeilisearchIndex() { if (!index) { const client = getMeilisearchClient(); try { index = await client.getIndex(INDEX_NAME); } catch (error) { // Index doesn't exist, create it console.log('Creating Meilisearch index:', INDEX_NAME); await client.createIndex(INDEX_NAME, { primaryKey: 'id' }); index = await client.getIndex(INDEX_NAME); // Configure index settings await configureIndex(index); } } return index; } async function configureIndex(index) { // Load config from docs const configPath = join(__dirname, '../../docs/architecture/meilisearch-config.json'); const config = JSON.parse(readFileSync(configPath, 'utf8')); await index.updateSettings({ searchableAttributes: config.settings.searchableAttributes, filterableAttributes: config.settings.filterableAttributes, sortableAttributes: config.settings.sortableAttributes, displayedAttributes: config.settings.displayedAttributes, synonyms: config.settings.synonyms, stopWords: config.settings.stopWords, rankingRules: config.settings.rankingRules, typoTolerance: config.settings.typoTolerance, faceting: config.settings.faceting, pagination: config.settings.pagination, separatorTokens: config.settings.separatorTokens, nonSeparatorTokens: config.settings.nonSeparatorTokens }); console.log('Meilisearch index configured'); } async function ensureTenantKeyUid() { if (tenantKeyUid) return tenantKeyUid; const client = getMeilisearchClient(); try { const keys = await client.getKeys(); const byName = keys.results?.find(k => k.name === 'navidocs-tenant-key' && k.actions?.includes('search')); if (byName) { tenantKeyUid = byName.uid; return tenantKeyUid; } // Fallback: use any search key that covers our index const anySearchKey = keys.results?.find(k => k.actions?.includes('search') && (k.indexes?.includes('*') || k.indexes?.includes(INDEX_NAME))); if (anySearchKey) { tenantKeyUid = anySearchKey.uid; return tenantKeyUid; } } catch (e) { // proceed to create } // Create a search-only key to act as parent for tenant tokens const created = await client.createKey({ name: 'navidocs-tenant-key', description: 'Parent key for NaviDocs tenant tokens', actions: ['search'], indexes: [INDEX_NAME] }); tenantKeyUid = created.uid; return tenantKeyUid; } export async function generateTenantToken(userId, organizationIds, expiresIn = 3600) { const client = getMeilisearchClient(); // Quote string values for Meilisearch filter syntax const orgList = (organizationIds || []).map((id) => `"${id}"`).join(', '); const filter = `userId = "${userId}" OR organizationId IN [${orgList}]`; const searchRules = { [INDEX_NAME]: { filter } }; const expiresAt = new Date(Date.now() + expiresIn * 1000); // Ensure a string is returned across client versions const parentUid = await ensureTenantKeyUid(); const token = await client.generateTenantToken(parentUid, searchRules, { expiresAt }); return typeof token === 'string' ? token : String(token); }