|
| 1 | +/** |
| 2 | + * Specialized class for building SQL queries related to blockchain events |
| 3 | + * |
| 4 | + * This class encapsulates the complex logic for constructing SQL queries |
| 5 | + * to retrieve events from the database with various filtering criteria. |
| 6 | + */ |
| 7 | +export default class EventQueryBuilder { |
| 8 | + private readonly HEIGHT_BATCH_SIZE = 200; |
| 9 | + |
| 10 | + /** |
| 11 | + * Calculates the height range for block filtering based on min/max height parameters |
| 12 | + * |
| 13 | + * @param minHeight - Minimum block height to include (optional) |
| 14 | + * @param maxHeight - Maximum block height to include (optional) |
| 15 | + * @returns Object containing fromHeight and toHeight values |
| 16 | + */ |
| 17 | + private calculateHeightRange(minHeight?: number | null, maxHeight?: number | null) { |
| 18 | + let fromHeight = 0; |
| 19 | + let toHeight = 0; |
| 20 | + |
| 21 | + if (minHeight && maxHeight) { |
| 22 | + fromHeight = minHeight; |
| 23 | + toHeight = maxHeight - minHeight > 100 ? minHeight + this.HEIGHT_BATCH_SIZE : maxHeight; |
| 24 | + } else if (minHeight) { |
| 25 | + fromHeight = minHeight; |
| 26 | + toHeight = minHeight + this.HEIGHT_BATCH_SIZE; |
| 27 | + } else if (maxHeight) { |
| 28 | + fromHeight = maxHeight - this.HEIGHT_BATCH_SIZE; |
| 29 | + toHeight = maxHeight; |
| 30 | + } |
| 31 | + |
| 32 | + const isHeightFiltered = Boolean(fromHeight || toHeight); |
| 33 | + return { fromHeight, toHeight, isHeightFiltered }; |
| 34 | + } |
| 35 | + |
| 36 | + /** |
| 37 | + * Builds the SQL query for fetching events with various filtering options |
| 38 | + * |
| 39 | + * @param params - Object containing parameters needed to build the query |
| 40 | + * @returns Object containing the query string and parameters array |
| 41 | + */ |
| 42 | + private buildEventQuery(params: { |
| 43 | + module: string; |
| 44 | + name: string; |
| 45 | + limit: number; |
| 46 | + order: string; |
| 47 | + after: string | null; |
| 48 | + before: string | null; |
| 49 | + blockHash?: string | null; |
| 50 | + chainId?: string | null; |
| 51 | + fromHeight: number; |
| 52 | + toHeight: number; |
| 53 | + requestKey?: string | null; |
| 54 | + isHeightChainOrBlockHash: boolean; |
| 55 | + }) { |
| 56 | + const { |
| 57 | + module, |
| 58 | + name, |
| 59 | + limit, |
| 60 | + order, |
| 61 | + after, |
| 62 | + before, |
| 63 | + blockHash, |
| 64 | + chainId, |
| 65 | + fromHeight, |
| 66 | + toHeight, |
| 67 | + requestKey, |
| 68 | + isHeightChainOrBlockHash, |
| 69 | + } = params; |
| 70 | + |
| 71 | + const queryParams: (string | number)[] = [limit, module, name]; |
| 72 | + const blockQueryParams: (string | number)[] = []; |
| 73 | + let conditions = ''; |
| 74 | + let eventConditions = ''; |
| 75 | + |
| 76 | + // Process pagination parameters - keep their indices consistent for all query types |
| 77 | + if (after) { |
| 78 | + queryParams.push(after); |
| 79 | + } |
| 80 | + |
| 81 | + if (before) { |
| 82 | + queryParams.push(before); |
| 83 | + } |
| 84 | + |
| 85 | + // Add pagination conditions (indices need to be right) |
| 86 | + let idx = 3; // Starting after [limit, module, name] |
| 87 | + |
| 88 | + if (after) { |
| 89 | + idx++; // Increment to account for the 'after' parameter |
| 90 | + eventConditions += `\nAND e.id < $${idx}`; |
| 91 | + } |
| 92 | + |
| 93 | + if (before) { |
| 94 | + idx++; // Increment to account for the 'before' parameter |
| 95 | + eventConditions += `\nAND e.id > $${idx}`; |
| 96 | + } |
| 97 | + |
| 98 | + // Initialize a flag to track if we've added any conditions |
| 99 | + let hasAddedBlockCondition = false; |
| 100 | + |
| 101 | + if (blockHash) { |
| 102 | + blockQueryParams.push(blockHash); |
| 103 | + conditions += `WHERE b.hash = $${blockQueryParams.length + queryParams.length}`; |
| 104 | + hasAddedBlockCondition = true; |
| 105 | + } |
| 106 | + |
| 107 | + if (chainId) { |
| 108 | + blockQueryParams.push(chainId); |
| 109 | + if (hasAddedBlockCondition) { |
| 110 | + conditions += `\nAND b."chainId" = $${blockQueryParams.length + queryParams.length}`; |
| 111 | + } else { |
| 112 | + conditions += `WHERE b."chainId" = $${blockQueryParams.length + queryParams.length}`; |
| 113 | + hasAddedBlockCondition = true; |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + if (fromHeight && toHeight) { |
| 118 | + blockQueryParams.push(fromHeight); |
| 119 | + if (hasAddedBlockCondition) { |
| 120 | + conditions += `\nAND b."height" >= $${blockQueryParams.length + queryParams.length}`; |
| 121 | + } else { |
| 122 | + conditions += `WHERE b."height" >= $${blockQueryParams.length + queryParams.length}`; |
| 123 | + hasAddedBlockCondition = true; |
| 124 | + } |
| 125 | + blockQueryParams.push(toHeight); |
| 126 | + conditions += `\nAND b."height" <= $${blockQueryParams.length + queryParams.length}`; |
| 127 | + } |
| 128 | + |
| 129 | + let query = ''; |
| 130 | + if (isHeightChainOrBlockHash) { |
| 131 | + query = ` |
| 132 | + WITH block_filtered AS ( |
| 133 | + select * |
| 134 | + from "Blocks" b |
| 135 | + ${conditions} |
| 136 | + ) |
| 137 | + SELECT |
| 138 | + e.id as id, |
| 139 | + e.requestkey as "requestKey", |
| 140 | + e."chainId" as "chainId", |
| 141 | + b.height as height, |
| 142 | + e."orderIndex" as "orderIndex", |
| 143 | + e.module as "moduleName", |
| 144 | + e.name as name, |
| 145 | + e.params as parameters, |
| 146 | + b.hash as "blockHash" |
| 147 | + FROM block_filtered b |
| 148 | + join "Transactions" t ON t."blockId" = b.id |
| 149 | + join "Events" e ON e."transactionId" = t.id |
| 150 | + WHERE e.module = $2 |
| 151 | + AND e.name = $3 |
| 152 | + ${eventConditions} |
| 153 | + ORDER BY b.height ${order} |
| 154 | + LIMIT $1 |
| 155 | + `; |
| 156 | + } else if (requestKey) { |
| 157 | + queryParams.push(requestKey); |
| 158 | + query = ` |
| 159 | + WITH event_transaction_filtered AS ( |
| 160 | + SELECT e.*, t."blockId" |
| 161 | + FROM "Transactions" t |
| 162 | + JOIN "Events" e ON t.id = e."transactionId" |
| 163 | + WHERE e.module = $2 |
| 164 | + AND e.name = $3 |
| 165 | + AND t.requestkey = $${blockQueryParams.length + queryParams.length} |
| 166 | + ${eventConditions} |
| 167 | + ORDER BY e.id ${order} |
| 168 | + ) |
| 169 | + SELECT |
| 170 | + et.id as id, |
| 171 | + et.requestkey as "requestKey", |
| 172 | + et."chainId" as "chainId", |
| 173 | + b.height as height, |
| 174 | + et."orderIndex" as "orderIndex", |
| 175 | + et.module as "moduleName", |
| 176 | + et.name as name, |
| 177 | + et.params as parameters, |
| 178 | + b.hash as "blockHash" |
| 179 | + FROM event_transaction_filtered et |
| 180 | + JOIN "Blocks" b ON b.id = et."blockId" |
| 181 | + ${conditions} |
| 182 | + LIMIT $1 |
| 183 | + `; |
| 184 | + } else { |
| 185 | + query = ` |
| 186 | + WITH event_filtered AS ( |
| 187 | + select * |
| 188 | + from "Events" e |
| 189 | + WHERE e.module = $2 |
| 190 | + AND e.name = $3 |
| 191 | + ${eventConditions} |
| 192 | + ORDER BY e.id ${order} |
| 193 | + ) |
| 194 | + SELECT |
| 195 | + e.id as id, |
| 196 | + e.requestkey as "requestKey", |
| 197 | + e."chainId" as "chainId", |
| 198 | + b.height as height, |
| 199 | + e."orderIndex" as "orderIndex", |
| 200 | + e.module as "moduleName", |
| 201 | + e.name as name, |
| 202 | + e.params as parameters, |
| 203 | + b.hash as "blockHash" |
| 204 | + FROM event_filtered e |
| 205 | + join "Transactions" t ON t.id = e."transactionId" |
| 206 | + join "Blocks" b ON b.id = t."blockId" |
| 207 | + ${conditions} |
| 208 | + LIMIT $1 |
| 209 | + `; |
| 210 | + } |
| 211 | + |
| 212 | + return { query, queryParams: [...queryParams, ...blockQueryParams] }; |
| 213 | + } |
| 214 | + |
| 215 | + /** |
| 216 | + * Builds a complete query for events with qualified name, handling all filtering parameters |
| 217 | + * |
| 218 | + * @param params - Object containing all query parameters and filtering options |
| 219 | + * @returns Object containing the query string and parameters array |
| 220 | + */ |
| 221 | + buildEventsWithQualifiedNameQuery(params: { |
| 222 | + qualifiedEventName: string; |
| 223 | + limit: number; |
| 224 | + order: string; |
| 225 | + after: string | null; |
| 226 | + before: string | null; |
| 227 | + blockHash?: string | null; |
| 228 | + chainId?: string | null; |
| 229 | + minHeight?: number | null; |
| 230 | + maxHeight?: number | null; |
| 231 | + requestKey?: string | null; |
| 232 | + }) { |
| 233 | + const { |
| 234 | + qualifiedEventName, |
| 235 | + limit, |
| 236 | + order, |
| 237 | + after, |
| 238 | + before, |
| 239 | + blockHash, |
| 240 | + chainId, |
| 241 | + minHeight, |
| 242 | + maxHeight, |
| 243 | + requestKey, |
| 244 | + } = params; |
| 245 | + |
| 246 | + const splitted = qualifiedEventName.split('.'); |
| 247 | + const name = splitted.pop() ?? ''; |
| 248 | + const module = splitted.join('.'); |
| 249 | + |
| 250 | + const { fromHeight, toHeight, isHeightFiltered } = this.calculateHeightRange( |
| 251 | + minHeight, |
| 252 | + maxHeight, |
| 253 | + ); |
| 254 | + const isHeightChainOrBlockHash = isHeightFiltered || Boolean(blockHash || chainId); |
| 255 | + |
| 256 | + return this.buildEventQuery({ |
| 257 | + module, |
| 258 | + name, |
| 259 | + limit, |
| 260 | + order, |
| 261 | + after, |
| 262 | + before, |
| 263 | + blockHash, |
| 264 | + chainId, |
| 265 | + fromHeight, |
| 266 | + toHeight, |
| 267 | + requestKey, |
| 268 | + isHeightChainOrBlockHash, |
| 269 | + }); |
| 270 | + } |
| 271 | +} |
0 commit comments