elasticsearch-based configurable generic appview for prototyping ideas
at main 3.9 kB view raw
1import { AppConfig } from "./config.ts"; 2 3function atUriParts(uri: string) { 4 const parts = uri.split("/"); 5 return { 6 did: parts[2], 7 collection: parts[3], 8 rkey: parts[4], 9 }; 10} 11 12function flattenValues(input: unknown): unknown[] { 13 const result: unknown[] = []; 14 15 function recurse(value: unknown) { 16 if (value === null || typeof value !== "object") { 17 result.push(value); 18 } else if (Array.isArray(value)) { 19 for (const item of value) recurse(item); 20 } else if (value) { 21 for (const key in value) { 22 recurse((value as Record<string, unknown>)[key]); 23 } 24 } 25 } 26 27 recurse(input); 28 return result; 29} 30 31export async function ensureIndexMapping(config: AppConfig) { 32 const properties: Record<string, any> = {}; 33 34 for (const fields of Object.values(config.index_fields)) { 35 for (const { id, type } of Object.values(fields)) { 36 properties[id] = { 37 type: type === "text" ? "text" : "keyword" 38 }; 39 } 40 } 41 42 for (const key of [ 43 "$metadata.uri", 44 "$metadata.cid", 45 "$metadata.indexedAt", 46 "$metadata.did", 47 "$metadata.collection", 48 "$metadata.rkey", 49 ]) { 50 properties[key] = { type: "keyword" }; 51 } 52 53 await fetch(`${config.es_url}/${config.index_name}`, { 54 method: "PUT", 55 headers: { "Content-Type": "application/json" }, 56 body: JSON.stringify({ 57 mappings: { properties } 58 }), 59 }); 60} 61 62function extractIndexedFields( 63 record: any, 64 indexSpec: Record<string, { id: string; type: string }> 65): Record<string, unknown> { 66 const result: Record<string, unknown> = {}; 67 68 for (const [rawPath, { id }] of Object.entries(indexSpec)) { 69 const [path, modifier] = rawPath.split("#"); 70 const pathSegments = path.split("."); 71 72 let value = record; 73 for (const segment of pathSegments) { 74 if (typeof value !== "object" || value === null) { 75 value = undefined; 76 break; 77 } 78 value = value[segment]; 79 } 80 81 if (value === undefined) continue; 82 83 if (modifier === "flatten") { 84 const flattened = flattenValues(value); 85 if (flattened.length > 0) { 86 result[id] = flattened; 87 } 88 } else { 89 result[id] = value; 90 } 91 } 92 93 return result; 94} 95 96export type IndexerEvent = 97 | { type: 'index'; uri: string; data: Record<string, unknown> } 98 | { type: 'delete'; uri: string }; 99 100export type OnEventCallback = (event: IndexerEvent) => Promise<void> | void; 101 102export async function indexDocument( 103 config: AppConfig, 104 onEvent: OnEventCallback, 105 uri: string, 106 record: Record<string, unknown>, 107 cid: string, 108 indexedAt: string 109) { 110 const { did, collection, rkey } = atUriParts(uri); 111 const recordType = collection; 112 113 const indexSpec = config.index_fields?.[recordType]; 114 //if (!indexSpec) return; 115 116 const indexedFields = extractIndexedFields(record, indexSpec); 117 118 const metadata = { 119 "$metadata.uri": uri, 120 "$metadata.cid": cid, 121 "$metadata.indexedAt": indexedAt, 122 "$metadata.did": did, 123 "$metadata.collection": collection, 124 "$metadata.rkey": rkey, 125 }; 126 127 const body = { 128 ...metadata, 129 ...indexedFields, 130 "$raw": record, 131 }; 132 133 const res = await fetch( 134 `${config.es_url}/${config.index_name}/_doc/${encodeURIComponent(uri)}`, 135 { 136 method: "PUT", 137 headers: { "Content-Type": "application/json" }, 138 body: JSON.stringify(body), 139 } 140 ); 141 await onEvent({ 142 type: 'index', 143 uri, 144 data: body 145 }); 146 147 if (!res.ok) { 148 console.error("Indexing failed:", await res.text()); 149 } 150} 151 152export async function deleteDocument( 153 config: AppConfig, 154 onEvent: OnEventCallback, 155 uri: string 156) { 157 const res = await fetch( 158 `${config.es_url}/${config.index_name}/_doc/${encodeURIComponent(uri)}`, 159 { 160 method: "DELETE", 161 } 162 ); 163 await onEvent({ 164 type: 'delete', 165 uri 166 }); 167 168 if (!res.ok && res.status !== 404) { 169 console.error("Delete failed:", await res.text()); 170 } 171}