this repo has no description
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 224 lines 7.6 kB view raw
1type QueryClause = { 2 match?: Record<string, unknown>; 3 term?: Record<string, unknown>; 4 bool?: { 5 must?: QueryClause[]; 6 must_not?: QueryClause[]; 7 should?: QueryClause[]; 8 filter?: QueryClause[]; 9 }; 10 range?: Record<string, Record<string, unknown>>; 11 nested?: { 12 path: string; 13 query: QueryClause; 14 }; 15 exists?: { 16 field: string; 17 }; 18}; 19 20// function getField(obj: Record<string, any>, path: string): any { 21// return path.split('.').reduce((acc, part) => acc && acc[part], obj); 22// } 23 24export function getSafeField(obj: Record<string, any>, path: string): any { 25 if (obj && typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, path)) { 26 return obj[path]; 27 } 28 return path.split('.').reduce((acc, part) => acc?.[part], obj); 29} 30 31export function matchDocAgainstQuery(doc: Record<string, unknown>, query: QueryClause): boolean { 32 console.log(JSON.stringify(doc, null, 2)); 33 if (!query || Object.keys(query).length === 0) { 34 console.log("✅ Accepted: No query provided, accepting all docs."); 35 return true; 36 } 37 38 for (const key in query) { 39 const clauseValue = (query as any)[key]; 40 41 switch (key) { 42 case 'term': { 43 const [field, value] = Object.entries(clauseValue as object)[0]; 44 const docValue = getSafeField(doc, field); 45 if (docValue !== value) { 46 console.log(`❌ Rejected: term mismatch on field '${field}', expected '${value}', got '${docValue}'`); 47 return false; 48 } 49 console.log(`✅ Passed: term match on field '${field}' with value '${value}'`); 50 break; 51 } 52 53 case 'terms': { 54 const [field, queryValues] = Object.entries(clauseValue as object)[0]; 55 if (!Array.isArray(queryValues)) return false; 56 57 const docValue = getSafeField(doc, field); 58 if (docValue === undefined) return false; 59 60 if (Array.isArray(docValue)) { 61 return docValue.some(v => queryValues.includes(v)); 62 } else { 63 return queryValues.includes(docValue); 64 } 65 } 66 67 case 'match': { 68 const [field, value] = Object.entries(clauseValue as object)[0]; 69 const fieldValue = getSafeField(doc, field); 70 if (typeof fieldValue !== 'string') { 71 console.log(`❌ Rejected: match expected string field '${field}', got non-string:`, fieldValue); 72 return false; 73 } 74 if (!fieldValue.includes(String(value))) { 75 console.log(`❌ Rejected: match failed on field '${field}', expected to include '${value}', got '${fieldValue}'`); 76 return false; 77 } 78 console.log(`✅ Passed: match succeeded on field '${field}' includes '${value}'`); 79 break; 80 } 81 82 case 'bool': { 83 const boolQuery = clauseValue as QueryClause['bool']; 84 85 if (boolQuery?.must?.some(q => !matchDocAgainstQuery(doc, q))) { 86 console.log("❌ Rejected: bool.must clause failed."); 87 return false; 88 } 89 if (boolQuery?.filter?.some(q => !matchDocAgainstQuery(doc, q))) { 90 console.log("❌ Rejected: bool.filter clause failed."); 91 return false; 92 } 93 if (boolQuery?.must_not?.some(q => matchDocAgainstQuery(doc, q))) { 94 console.log("❌ Rejected: bool.must_not clause matched (should not have)."); 95 return false; 96 } 97 if (boolQuery?.should?.length && !boolQuery.should.some(q => matchDocAgainstQuery(doc, q))) { 98 console.log("❌ Rejected: bool.should clause did not match any."); 99 return false; 100 } 101 console.log("✅ Passed: bool clause matched."); 102 break; 103 } 104 105 case 'range': { 106 const [field, range] = Object.entries(clauseValue as object)[0]; 107 const value = getSafeField(doc, field); 108 if (value === undefined) { 109 console.log(`❌ Rejected: range field '${field}' is undefined in doc.`); 110 return false; 111 } 112 113 for (const [op, opValue] of Object.entries(range as object)) { 114 const numValue = Number(value); 115 const numOpValue = Number(opValue); 116 const isNumeric = !isNaN(numValue) && !isNaN(numOpValue); 117 let passed = true; 118 119 switch (op) { 120 case 'gt': 121 passed = isNumeric ? numValue > numOpValue : String(value) > String(opValue); 122 break; 123 case 'gte': 124 passed = isNumeric ? numValue >= numOpValue : String(value) >= String(opValue); 125 break; 126 case 'lt': 127 passed = isNumeric ? numValue < numOpValue : String(value) < String(opValue); 128 break; 129 case 'lte': 130 passed = isNumeric ? numValue <= numOpValue : String(value) <= String(opValue); 131 break; 132 } 133 134 if (!passed) { 135 console.log(`❌ Rejected: range.${op} failed on field '${field}', value '${value}' vs '${opValue}'`); 136 return false; 137 } 138 } 139 140 console.log(`✅ Passed: range match on field '${field}'`); 141 break; 142 } 143 144 case 'exists': { 145 const field = (clauseValue as { field: string }).field; 146 if (getSafeField(doc, field) === undefined) { 147 console.log(`❌ Rejected: exists failed, field '${field}' not found`); 148 return false; 149 } 150 console.log(`✅ Passed: exists matched, field '${field}' exists`); 151 break; 152 } 153 154 default: 155 console.log(`ℹ️ Ignored unknown query clause: '${key}'`); 156 break; 157 } 158 } 159 160 console.log("✅ Accepted: Document matched all clauses."); 161 return true; 162} 163 164const ALLOWED_QUERY_CLAUSES = new Set([ 165 'term', 166 'terms', 167 'bool', 168 'range', 169 'exists', 170]); 171 172const FORBIDDEN_QUERY_CLAUSES = new Set([ 173 'match', 174 'multi_match', 175 'match_phrase', 176 'query_string', 177 'simple_query_string', 178 'fuzzy', 179 'script', 180 'function_score', 181 'more_like_this', 182 'percolate', 183]); 184 185function validateClause(clause: Record<string, unknown>) { 186 for (const key in clause) { 187 if (FORBIDDEN_QUERY_CLAUSES.has(key)) { 188 throw new Error(`Query clause '${key}' is not allowed for live sync due to non-deterministic behavior. Please use deterministic clauses like 'term' or 'range'.`); 189 } 190 191 if (!ALLOWED_QUERY_CLAUSES.has(key)) { 192 throw new Error(`Query clause '${key}' is not supported for live sync.`); 193 } 194 195 const value = clause[key]; 196 if (key === 'bool' && typeof value === 'object' && value !== null) { 197 const boolClauses = value as Record<string, unknown[]>; 198 for (const boolType of ['must', 'filter', 'should', 'must_not']) { 199 if (Array.isArray(boolClauses[boolType])) { 200 for (const subClause of boolClauses[boolType]) { 201 if (typeof subClause === 'object' && subClause !== null) { 202 validateClause(subClause as Record<string, unknown>); 203 } 204 } 205 } 206 } 207 } 208 } 209} 210 211export function validateLiveQuery(esQuery: Record<string, unknown>): void { 212 // a query must have an explicit 'sort' clause 213 // which prevents non-deterministic sorting (bad) 214 const sortClause = esQuery.sort as any[]; 215 if (!Array.isArray(sortClause) || sortClause.length === 0) { 216 throw new Error("Live queries must include an explicit 'sort' clause for deterministic ordering (e.g., sort: [{ '$metadata.indexedAt': 'desc' }])."); 217 } 218 219 // and also of the supported deterministic clauses (no text searches) 220 const queryPart = esQuery.query as Record<string, unknown>; 221 if (queryPart) { 222 validateClause(queryPart); 223 } 224}