sql-util.ts365 lines · main
| 1 | import { A_Const, A_Expr, ColumnRef, CreatePolicyStmt, Node, parseQuery } from 'libpg-query/wasm' |
| 2 | |
| 3 | export type PolicyInfo = { |
| 4 | name: string |
| 5 | relation: string |
| 6 | command?: string |
| 7 | roles: string[] |
| 8 | usingNode?: Node |
| 9 | withCheckNode?: Node |
| 10 | } |
| 11 | |
| 12 | /** |
| 13 | * Extracts keys from a union type. |
| 14 | */ |
| 15 | type ExtractKeys<T> = T extends T ? keyof T : never |
| 16 | |
| 17 | /** |
| 18 | * Unwraps a Node to get its underlying value. |
| 19 | */ |
| 20 | type NodeValue<T extends Node, U extends ExtractKeys<Node>> = |
| 21 | T extends Record<U, infer V> ? V : never |
| 22 | |
| 23 | export class AssertionError extends Error { |
| 24 | constructor(message: string) { |
| 25 | super(message) |
| 26 | |
| 27 | // Pop the top line from the stack trace so that |
| 28 | // debug tools will reference the code calling the |
| 29 | // assertion function and not this error within it |
| 30 | if (this.stack) { |
| 31 | // Capture the current stack trace and split it into lines |
| 32 | const stackLines = this.stack.split('\n') |
| 33 | |
| 34 | // Remove the second line which is the current constructor |
| 35 | stackLines.splice(1, 1) |
| 36 | |
| 37 | // Reassign the modified stack trace back to the error object |
| 38 | this.stack = stackLines.join('\n') |
| 39 | } |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * Asserts that a value is defined. |
| 45 | * |
| 46 | * Useful for type narrowing. |
| 47 | */ |
| 48 | export function assertDefined<T>(value: T | undefined, errorMessage: string): asserts value is T { |
| 49 | if (value === undefined) { |
| 50 | throw new AssertionError(errorMessage) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * Asserts that a `Node` is a specific type. |
| 56 | * |
| 57 | * Useful for type narrowing. |
| 58 | */ |
| 59 | export function assertNodeType<T extends Node>( |
| 60 | node: Node, |
| 61 | type: ExtractKeys<T>, |
| 62 | errorMessage: string |
| 63 | ): asserts node is T { |
| 64 | if (!(type in node)) { |
| 65 | throw new AssertionError(errorMessage) |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | /** |
| 70 | * Asserts that a `Node` is a specific type and |
| 71 | * unwraps its underlying value. |
| 72 | * |
| 73 | * @returns The unwrapped `Node` value. |
| 74 | * @throws If `node` is not of type `type`. |
| 75 | */ |
| 76 | export function assertAndUnwrapNode<T extends Node, U extends ExtractKeys<Node>>( |
| 77 | node: Node, |
| 78 | type: U, |
| 79 | errorMessage: string |
| 80 | ): NodeValue<T, U> { |
| 81 | if (!(type in node)) { |
| 82 | throw new AssertionError(errorMessage) |
| 83 | } |
| 84 | return (node as any)[type] |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Unwraps a `Node`'s underlying value. |
| 89 | * |
| 90 | * @returns The unwrapped `Node` value or `undefined` if |
| 91 | * the node is not of type `type`. |
| 92 | */ |
| 93 | export function unwrapNode<T extends Node, U extends ExtractKeys<Node>>( |
| 94 | node: Node, |
| 95 | type: U |
| 96 | ): NodeValue<T, U> | undefined { |
| 97 | if (!(type in node)) { |
| 98 | return undefined |
| 99 | } |
| 100 | return (node as any)[type] |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * Asserts that either the left or right side of the |
| 105 | * expression is processed through `fn` without throwing |
| 106 | * any errors. |
| 107 | * |
| 108 | * If both sides throw errors, the assertion fails and the |
| 109 | * error from the left side will be thrown. |
| 110 | */ |
| 111 | export function assertEitherSideOfExpression<U>( |
| 112 | expression: A_Expr, |
| 113 | fn: (node: Node, side: 'left' | 'right') => U |
| 114 | ): U { |
| 115 | assertDefined(expression.lexpr, 'Expected left side of expression to exist') |
| 116 | assertDefined(expression.rexpr, 'Expected right side of expression to exist') |
| 117 | |
| 118 | try { |
| 119 | return fn(expression.lexpr, 'left') |
| 120 | } catch (leftError) { |
| 121 | try { |
| 122 | return fn(expression.rexpr, 'right') |
| 123 | } catch (rightError) { |
| 124 | throw leftError |
| 125 | } |
| 126 | } |
| 127 | } |
| 128 | /** |
| 129 | * Asserts that both sides of the expression are processed |
| 130 | * without throwing any errors. |
| 131 | * |
| 132 | * Order doesn't matter. As long as `firstFn` and `secondFn` |
| 133 | * pass separately on either side of the expression, the assertion |
| 134 | * will pass. Otherwise if `firstFn` and `secondFn` both fail |
| 135 | * after trying on both sides separately, the assertion will fail. |
| 136 | */ |
| 137 | export function assertEachSideOfExpression<U>( |
| 138 | expression: A_Expr, |
| 139 | firstFn: (node: Node) => void, |
| 140 | secondFn: (node: Node) => void |
| 141 | ): void { |
| 142 | assertDefined(expression.lexpr, 'Expected left side of expression to exist') |
| 143 | assertDefined(expression.rexpr, 'Expected right side of expression to exist') |
| 144 | |
| 145 | let firstSide: Node |
| 146 | let secondSide: Node |
| 147 | |
| 148 | try { |
| 149 | // Try `firstFn` on the left first |
| 150 | firstSide = expression.lexpr |
| 151 | secondSide = expression.rexpr |
| 152 | firstFn(firstSide) |
| 153 | } catch (firstError) { |
| 154 | // Otherwise try `firstFn` on the right |
| 155 | firstSide = expression.rexpr |
| 156 | secondSide = expression.lexpr |
| 157 | try { |
| 158 | firstFn(firstSide) |
| 159 | } catch (secondError) { |
| 160 | // `firstFn` failed on both sides, so we |
| 161 | // need to throw an error not matter what |
| 162 | |
| 163 | // Perform one more test using `secondFn` |
| 164 | // to help determine which error to show |
| 165 | // for `firstFn` |
| 166 | try { |
| 167 | secondFn(secondSide) |
| 168 | } catch (_) { |
| 169 | throw firstError |
| 170 | } |
| 171 | throw secondError |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | try { |
| 176 | // `firstFn` passed on one of the sides, so |
| 177 | // try `secondFn` on the opposite side |
| 178 | secondFn(secondSide) |
| 179 | } catch (err) { |
| 180 | // `secondFn` failed, so we need to throw an error |
| 181 | throw err |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * Extracts all the `CREATE POLICY` statements |
| 187 | * from a SQL string as parsed ASTs. |
| 188 | */ |
| 189 | export async function getPolicies(sql: string) { |
| 190 | const result = await parseQuery(sql) |
| 191 | |
| 192 | assertDefined(result.stmts, 'Expected parse result to contain statements') |
| 193 | |
| 194 | return result.stmts.reduce<CreatePolicyStmt[]>((filtered, stmt) => { |
| 195 | assertDefined(stmt.stmt, 'Expected statement to exist') |
| 196 | |
| 197 | const createPolicyStatement = unwrapNode(stmt.stmt, 'CreatePolicyStmt') |
| 198 | |
| 199 | if (createPolicyStatement) { |
| 200 | return [...filtered, createPolicyStatement] |
| 201 | } |
| 202 | |
| 203 | return filtered |
| 204 | }, []) |
| 205 | } |
| 206 | |
| 207 | /** |
| 208 | * Parses a Postgres SQL policy. |
| 209 | * |
| 210 | * @returns Information about the policy, including its name, table, command, and expressions. |
| 211 | */ |
| 212 | export async function getPolicyInfo(createPolicyStatement: CreatePolicyStmt) { |
| 213 | assertDefined(createPolicyStatement.policy_name, 'Expected policy to have a name') |
| 214 | assertDefined(createPolicyStatement.table?.relname, 'Expected policy to have a relation') |
| 215 | |
| 216 | const name = createPolicyStatement.policy_name |
| 217 | const relation = createPolicyStatement.table.relname |
| 218 | const command = createPolicyStatement.cmd_name |
| 219 | |
| 220 | const roles = |
| 221 | createPolicyStatement.roles?.map((node) => { |
| 222 | const roleSpec = assertAndUnwrapNode( |
| 223 | node, |
| 224 | 'RoleSpec', |
| 225 | 'Expected roles to contain a list of RoleSpec' |
| 226 | ) |
| 227 | assertDefined(roleSpec.rolename, 'Expected RoleSpec to have a rolename') |
| 228 | |
| 229 | return roleSpec.rolename |
| 230 | }) ?? [] |
| 231 | |
| 232 | const usingExpression = createPolicyStatement.qual |
| 233 | const checkExpression = createPolicyStatement.with_check |
| 234 | |
| 235 | const policyInfo: PolicyInfo = { |
| 236 | name, |
| 237 | relation, |
| 238 | command, |
| 239 | roles, |
| 240 | usingNode: usingExpression, |
| 241 | withCheckNode: checkExpression, |
| 242 | } |
| 243 | |
| 244 | return policyInfo |
| 245 | } |
| 246 | |
| 247 | export function renderTargets<T>(targets: Node[], renderTarget: (node: Node) => T): T[] { |
| 248 | return targets.map((node) => { |
| 249 | const target = assertAndUnwrapNode( |
| 250 | node, |
| 251 | 'ResTarget', |
| 252 | 'Expected target list to contain ResTargets' |
| 253 | ) |
| 254 | |
| 255 | assertDefined(target.val, 'Expected ResTarget to have a val') |
| 256 | return renderTarget(target.val) |
| 257 | }) |
| 258 | } |
| 259 | |
| 260 | export function renderColumn(column: ColumnRef) { |
| 261 | assertDefined(column.fields, 'Expected column to have fields') |
| 262 | return renderFields(column.fields) |
| 263 | } |
| 264 | |
| 265 | export function assertAndRenderColumn(node: Node, errorMessage: string) { |
| 266 | const column = assertAndUnwrapNode(node, 'ColumnRef', errorMessage) |
| 267 | return renderColumn(column) |
| 268 | } |
| 269 | |
| 270 | export function renderJsonExpression(expression: A_Expr): string { |
| 271 | assertDefined(expression.name, 'Expected expression to have an operator') |
| 272 | |
| 273 | if (expression.name.length > 1) { |
| 274 | throw new AssertionError('Only one JSON operator supported per expression') |
| 275 | } |
| 276 | |
| 277 | const [name] = expression.name |
| 278 | const operatorString = assertAndUnwrapNode( |
| 279 | name, |
| 280 | 'String', |
| 281 | 'Expected JSON operator to be a string' |
| 282 | ) |
| 283 | assertDefined(operatorString.sval, 'PG string expected to have an sval') |
| 284 | const operator = operatorString.sval |
| 285 | |
| 286 | if (!['->', '->>'].includes(operator)) { |
| 287 | throw new AssertionError(`Invalid JSON operator ${operator}`) |
| 288 | } |
| 289 | |
| 290 | assertDefined(expression.lexpr, 'Expected JSON expression to have a left-side component') |
| 291 | assertDefined(expression.rexpr, 'Expected JSON expression to have a right-side component') |
| 292 | |
| 293 | let left: string | number |
| 294 | let right: string | number |
| 295 | |
| 296 | const leftConstant = unwrapNode(expression.lexpr, 'A_Const') |
| 297 | const leftColumn = unwrapNode(expression.lexpr, 'ColumnRef') |
| 298 | const leftFuncCall = unwrapNode(expression.lexpr, 'FuncCall') |
| 299 | const leftExpression = unwrapNode(expression.lexpr, 'A_Expr') |
| 300 | |
| 301 | if (leftConstant) { |
| 302 | // JSON path cannot contain a float |
| 303 | if ('fval' in leftConstant) { |
| 304 | throw new AssertionError('Invalid JSON path: Expression cannot contain a float') |
| 305 | } |
| 306 | left = `'${parseConstant(leftConstant)}'` |
| 307 | } else if (leftColumn) { |
| 308 | left = renderColumn(leftColumn) |
| 309 | } else if (leftFuncCall) { |
| 310 | assertDefined(leftFuncCall.funcname, 'Expected function call to have a name') |
| 311 | const functionName = renderFields(leftFuncCall.funcname) |
| 312 | left = `${functionName}()` |
| 313 | } else if (leftExpression) { |
| 314 | left = renderJsonExpression(leftExpression) |
| 315 | } else { |
| 316 | throw new AssertionError('Invalid JSON path') |
| 317 | } |
| 318 | |
| 319 | const rightConstant = unwrapNode(expression.rexpr, 'A_Const') |
| 320 | |
| 321 | if (rightConstant) { |
| 322 | // JSON path cannot contain a float |
| 323 | if ('fval' in rightConstant) { |
| 324 | throw new AssertionError('Invalid JSON path: Expression cannot contain a float') |
| 325 | } |
| 326 | right = `'${parseConstant(rightConstant)}'` |
| 327 | } else { |
| 328 | throw new AssertionError('Invalid JSON path') |
| 329 | } |
| 330 | |
| 331 | return `${left}${operator}${right}` |
| 332 | } |
| 333 | |
| 334 | export function renderFields(fields: Node[]) { |
| 335 | const nameSegments = fields |
| 336 | .map((field) => { |
| 337 | const stringField = unwrapNode(field, 'String') |
| 338 | const starField = unwrapNode(field, 'A_Star') |
| 339 | |
| 340 | if (stringField !== undefined) { |
| 341 | return stringField.sval |
| 342 | } else if (starField !== undefined) { |
| 343 | return '*' |
| 344 | } else { |
| 345 | const [internalType] = Object.keys(field) |
| 346 | throw new Error(`Unsupported internal type '${internalType}' for fields`) |
| 347 | } |
| 348 | }) |
| 349 | .filter((name): name is string => name !== undefined) |
| 350 | |
| 351 | return nameSegments.join('.') |
| 352 | } |
| 353 | |
| 354 | export function parseConstant(constant: A_Const) { |
| 355 | if ('sval' in constant) { |
| 356 | return constant.sval?.sval ?? '' |
| 357 | } else if ('ival' in constant) { |
| 358 | // The PG parser turns 0 into undefined, so convert it back here |
| 359 | return constant.ival?.ival ?? 0 |
| 360 | } else if ('fval' in constant) { |
| 361 | return constant.fval?.fval ? parseFloat(constant.fval.fval) : 0 |
| 362 | } else { |
| 363 | throw new AssertionError(`Constant values must be a string, integer, or float`) |
| 364 | } |
| 365 | } |