UNPKG

@supabase/sql-to-rest

Version:

[![Tests](https://github.com/supabase-community/sql-to-rest/actions/workflows/tests.yml/badge.svg)](https://github.com/supabase-community/sql-to-rest/actions?query=branch%3Amain) [![Package](https://img.shields.io/npm/v/@supabase/sql-to-rest)](https://www

1 lines 118 kB
{"version":3,"sources":["../src/errors.ts","../src/processor/index.ts","../src/processor/util.ts","../src/processor/aggregate.ts","../src/processor/filter.ts","../src/processor/limit.ts","../src/processor/sort.ts","../src/processor/select.ts","../node_modules/common-tags/src/TemplateTag/TemplateTag.js","../node_modules/common-tags/src/trimResultTransformer/trimResultTransformer.js","../node_modules/common-tags/src/stripIndentTransformer/stripIndentTransformer.js","../node_modules/common-tags/src/replaceResultTransformer/replaceResultTransformer.js","../node_modules/common-tags/src/replaceSubstitutionTransformer/replaceSubstitutionTransformer.js","../node_modules/common-tags/src/inlineArrayTransformer/inlineArrayTransformer.js","../node_modules/common-tags/src/splitStringTransformer/splitStringTransformer.js","../node_modules/common-tags/src/removeNonPrintingValuesTransformer/removeNonPrintingValuesTransformer.js","../node_modules/common-tags/src/commaLists/commaLists.js","../node_modules/common-tags/src/commaListsAnd/commaListsAnd.js","../node_modules/common-tags/src/commaListsOr/commaListsOr.js","../node_modules/common-tags/src/html/html.js","../node_modules/common-tags/src/safeHtml/safeHtml.js","../node_modules/common-tags/src/oneLine/oneLine.js","../node_modules/common-tags/src/oneLineTrim/oneLineTrim.js","../node_modules/common-tags/src/oneLineCommaLists/oneLineCommaLists.js","../node_modules/common-tags/src/oneLineCommaListsOr/oneLineCommaListsOr.js","../node_modules/common-tags/src/oneLineCommaListsAnd/oneLineCommaListsAnd.js","../node_modules/common-tags/src/inlineLists/inlineLists.js","../node_modules/common-tags/src/oneLineInlineLists/oneLineInlineLists.js","../node_modules/common-tags/src/stripIndent/stripIndent.js","../node_modules/common-tags/src/stripIndents/stripIndents.js","../src/renderers/util.ts","../src/renderers/http.ts","../src/renderers/supabase-js.ts"],"sourcesContent":["export class ParsingError extends Error {\n name = 'ParsingError'\n\n constructor(\n message: string,\n public hint?: string\n ) {\n super(sentenceCase(message))\n }\n}\n\nexport class UnimplementedError extends Error {\n name = 'UnimplementedError'\n}\n\nexport class UnsupportedError extends Error {\n name = 'UnsupportedError'\n\n constructor(\n message: string,\n public hint?: string\n ) {\n super(message)\n }\n}\n\nexport class RenderError extends Error {\n name = 'RenderError'\n\n constructor(\n message: string,\n public renderer: 'http' | 'supabase-js'\n ) {\n super(message)\n }\n}\n\nexport function sentenceCase(value: string) {\n return value[0].toUpperCase() + value.slice(1)\n}\n\n/**\n * Returns hints for common parsing errors.\n */\nexport function getParsingErrorHint(message: string) {\n switch (message) {\n case 'syntax error at or near \"from\"':\n return 'Did you leave a trailing comma in the select target list?'\n case 'syntax error at or near \"where\"':\n return 'Do you have an incomplete join in the FROM clause?'\n default:\n undefined\n }\n}\n","import { parseQuery } from 'libpg-query'\nimport { ParsingError, UnimplementedError, UnsupportedError, getParsingErrorHint } from '../errors'\nimport { ParsedQuery, Stmt } from '../types/libpg-query'\nimport { processSelectStatement } from './select'\nimport { Statement } from './types'\n\nexport { supportedAggregateFunctions } from './select'\nexport * from './types'\nexport { everyTarget, flattenTargets, someFilter, someTarget } from './util'\n\n/**\n * Coverts SQL into a PostgREST-compatible `Statement`.\n *\n * Expects SQL to contain only one statement.\n *\n * @returns An intermediate `Statement` object that\n * can be rendered to various targets (HTTP, supabase-js, etc).\n */\nexport async function processSql(sql: string): Promise<Statement> {\n try {\n const result: ParsedQuery = await parseQuery(sql)\n\n if (result.stmts.length === 0) {\n throw new UnsupportedError('Expected a statement, but received none')\n }\n\n if (result.stmts.length > 1) {\n throw new UnsupportedError('Expected a single statement, but received multiple')\n }\n\n const [statement] = result.stmts.map((stmt) => processStatement(stmt))\n\n return statement\n } catch (err) {\n if (err instanceof Error && 'cursorPosition' in err) {\n const hint = getParsingErrorHint(err.message)\n const parsingError = new ParsingError(err.message, hint)\n\n Object.assign(parsingError, err)\n throw parsingError\n } else {\n throw err\n }\n }\n}\n\n/**\n * Converts a pg-query `Stmt` into a PostgREST-compatible `Statement`.\n */\nfunction processStatement({ stmt }: Stmt): Statement {\n if ('SelectStmt' in stmt) {\n return processSelectStatement(stmt)\n } else if ('InsertStmt' in stmt) {\n throw new UnimplementedError(`Insert statements are not yet implemented by the translator`)\n } else if ('UpdateStmt' in stmt) {\n throw new UnimplementedError(`Update statements are not yet implemented by the translator`)\n } else if ('DeleteStmt' in stmt) {\n throw new UnimplementedError(`Delete statements are not yet implemented by the translator`)\n } else if ('ExplainStmt' in stmt) {\n throw new UnimplementedError(`Explain statements are not yet implemented by the translator`)\n } else {\n const [stmtType] = Object.keys(stmt)\n const statementType = stmtType.replace(/Stmt$/, '')\n throw new UnsupportedError(`${statementType} statements are not supported`)\n }\n}\n","import { UnsupportedError } from '../errors'\nimport { A_Const, A_Expr, Field, PgString } from '../types/libpg-query'\nimport {\n AggregateTarget,\n ColumnFilter,\n ColumnTarget,\n EmbeddedTarget,\n Filter,\n Relations,\n Target,\n} from './types'\n\nexport function processJsonTarget(expression: A_Expr, relations: Relations): ColumnTarget {\n if (expression.A_Expr.name.length > 1) {\n throw new UnsupportedError('Only one operator name supported per expression')\n }\n\n const [name] = expression.A_Expr.name\n const operator = name.String.sval\n\n if (!['->', '->>'].includes(operator)) {\n throw new UnsupportedError(`Invalid JSON operator`)\n }\n\n let cast: string | undefined = undefined\n let left: string | number\n let right: string | number\n\n if ('A_Const' in expression.A_Expr.lexpr) {\n // JSON path cannot contain a float\n if ('fval' in expression.A_Expr.lexpr.A_Const) {\n throw new UnsupportedError('Invalid JSON path')\n }\n left = parseConstant(expression.A_Expr.lexpr)\n } else if ('A_Expr' in expression.A_Expr.lexpr) {\n const { column } = processJsonTarget(expression.A_Expr.lexpr, relations)\n left = column\n } else if ('ColumnRef' in expression.A_Expr.lexpr) {\n left = renderFields(expression.A_Expr.lexpr.ColumnRef.fields, relations)\n } else {\n throw new UnsupportedError('Invalid JSON path')\n }\n\n if ('A_Const' in expression.A_Expr.rexpr) {\n // JSON path cannot contain a float\n if ('fval' in expression.A_Expr.rexpr.A_Const) {\n throw new UnsupportedError('Invalid JSON path')\n }\n right = parseConstant(expression.A_Expr.rexpr)\n } else if ('TypeCast' in expression.A_Expr.rexpr) {\n cast = renderDataType(expression.A_Expr.rexpr.TypeCast.typeName.names)\n\n if ('A_Const' in expression.A_Expr.rexpr.TypeCast.arg) {\n if ('sval' in expression.A_Expr.rexpr.TypeCast.arg.A_Const) {\n right = expression.A_Expr.rexpr.TypeCast.arg.A_Const.sval.sval\n } else {\n throw new UnsupportedError('Invalid JSON path')\n }\n } else {\n throw new UnsupportedError('Invalid JSON path')\n }\n } else {\n throw new UnsupportedError('Invalid JSON path')\n }\n\n return {\n type: 'column-target',\n column: `${left}${operator}${right}`,\n cast,\n }\n}\n\nexport function renderFields(\n fields: Field[],\n relations: Relations,\n syntax: 'dot' | 'parenthesis' = 'dot'\n) {\n // Get qualified column name segments, eg. `author.name` -> ['author', 'name']\n const nameSegments = fields.map((field) => {\n if ('String' in field) {\n return field.String.sval\n } else if ('A_Star' in field) {\n return '*'\n } else {\n const [internalType] = Object.keys(field)\n throw new UnsupportedError(`Unsupported internal type '${internalType}' for data type names`)\n }\n })\n\n // Relation and column names are last two parts of the qualified name\n const [relationOrAliasName] = nameSegments.slice(-2, -1)\n const [columnName] = nameSegments.slice(-1)\n\n const joinedRelation = relations.joined.find(\n (t) => (t.alias ?? t.relation) === relationOrAliasName\n )\n\n // If the column is prefixed with the primary relation, strip the prefix\n if (!relationOrAliasName || relationOrAliasName === relations.primary.reference) {\n return columnName\n }\n // If it's prefixed with a joined relation in the FROM clause, keep the relation prefix\n else if (joinedRelation) {\n // Joined relations that are spread don't support aliases, so we will\n // convert the alias back to the original relation name in this case\n const joinedRelationName = joinedRelation.flatten\n ? joinedRelation.relation\n : relationOrAliasName\n\n if (syntax === 'dot') {\n return [joinedRelationName, columnName].join('.')\n } else if (syntax === 'parenthesis') {\n return `${joinedRelationName}(${columnName})`\n } else {\n throw new Error(`Unknown render syntax '${syntax}'`)\n }\n }\n // If it's prefixed with an unknown relation, throw an error\n else {\n const qualifiedName = [relationOrAliasName, columnName].join('.')\n\n throw new UnsupportedError(\n `Found foreign column '${qualifiedName}' without a join to that relation`,\n 'Did you forget to join that relation or alias it to something else?'\n )\n }\n}\n\nexport function renderDataType(names: PgString[]) {\n const [first, ...rest] = names\n\n if (first.String.sval === 'pg_catalog' && rest.length === 1) {\n const [name] = rest\n\n // The PG parser converts some data types, eg. int -> pg_catalog.int4\n // so we'll map those back\n switch (name.String.sval) {\n case 'int2':\n return 'smallint'\n case 'int4':\n return 'int'\n case 'int8':\n return 'bigint'\n case 'float8':\n return 'float'\n default:\n return name.String.sval\n }\n } else if (rest.length > 0) {\n throw new UnsupportedError(\n `Casts can only reference data types by their unqualified name (not schema-qualified)`\n )\n } else {\n return first.String.sval\n }\n}\n\nexport function parseConstant(constant: A_Const) {\n if ('sval' in constant.A_Const) {\n return constant.A_Const.sval.sval\n } else if ('ival' in constant.A_Const) {\n // The PG parser turns 0 into undefined, so convert it back here\n return constant.A_Const.ival.ival ?? 0\n } else if ('fval' in constant.A_Const) {\n return parseFloat(constant.A_Const.fval.fval)\n } else {\n throw new UnsupportedError(`Constant values must be a string, integer, or float`)\n }\n}\n\n/**\n * Recursively flattens PostgREST embedded targets.\n */\nexport function flattenTargets(targets: Target[]): Target[] {\n return targets.flatMap((target) => {\n const { type } = target\n if (type === 'column-target' || type === 'aggregate-target') {\n return target\n } else if (type === 'embedded-target') {\n return [target, ...flattenTargets(target.targets)]\n } else {\n throw new UnsupportedError(`Unknown target type '${type}'`)\n }\n })\n}\n\n/**\n * Recursively iterates through PostgREST filters and checks if the predicate\n * matches any of them (ie. `some()`).\n */\nexport function someFilter(filter: Filter, predicate: (filter: ColumnFilter) => boolean): boolean {\n const { type } = filter\n\n if (type === 'column') {\n return predicate(filter)\n } else if (type === 'logical') {\n return filter.values.some((f) => someFilter(f, predicate))\n } else {\n throw new UnsupportedError(`Unknown filter type '${type}'`)\n }\n}\n\n/**\n * Recursively iterates through a PostgREST target list and checks if the predicate\n * matches every one of them (ie. `some()`).\n */\nexport function everyTarget(\n targets: Target[],\n predicate: (target: ColumnTarget | AggregateTarget, parent?: EmbeddedTarget) => boolean,\n parent?: EmbeddedTarget\n): boolean {\n return targets.every((target) => {\n const { type } = target\n\n if (type === 'column-target' || type === 'aggregate-target') {\n return predicate(target, parent)\n } else if (type === 'embedded-target') {\n return everyTarget(target.targets, predicate, target)\n } else {\n throw new UnsupportedError(`Unknown target type '${type}'`)\n }\n })\n}\n\n/**\n * Recursively iterates through a PostgREST target list and checks if the predicate\n * matches any of them (ie. `some()`).\n */\nexport function someTarget(\n targets: Target[],\n predicate: (target: ColumnTarget | AggregateTarget, parent?: EmbeddedTarget) => boolean,\n parent?: EmbeddedTarget\n): boolean {\n return targets.some((target) => {\n const { type } = target\n\n if (type === 'column-target' || type === 'aggregate-target') {\n return predicate(target, parent)\n } else if (type === 'embedded-target') {\n return someTarget(target.targets, predicate, target)\n } else {\n throw new UnsupportedError(`Unknown target type '${type}'`)\n }\n })\n}\n","import { UnsupportedError } from '../errors'\nimport { ColumnRef } from '../types/libpg-query'\nimport { Relations, Target } from './types'\nimport { everyTarget, renderFields, someTarget } from './util'\n\nexport function validateGroupClause(\n groupClause: ColumnRef[],\n targets: Target[],\n relations: Relations\n) {\n const groupByColumns =\n groupClause.map((columnRef) => renderFields(columnRef.ColumnRef.fields, relations)) ?? []\n\n if (\n !groupByColumns.every((column) =>\n someTarget(targets, (target, parent) => {\n // The `count()` special case aggregate has no column attached\n if (!('column' in target)) {\n return false\n }\n\n const path = parent\n ? // joined columns have to be prefixed with their relation\n [parent.alias && !parent.flatten ? parent.alias : parent.relation, target.column]\n : // top-level columns will have no prefix\n [target.column]\n\n const qualifiedName = path.join('.')\n return qualifiedName === column\n })\n )\n ) {\n throw new UnsupportedError(`Every group by column must also exist as a select target`)\n }\n\n if (\n someTarget(targets, (target) => target.type === 'aggregate-target') &&\n !everyTarget(targets, (target, parent) => {\n if (target.type === 'aggregate-target') {\n return true\n }\n\n const path = parent\n ? // joined columns have to be prefixed with their relation\n [parent.alias && !parent.flatten ? parent.alias : parent.relation, target.column]\n : // top-level columns will have no prefix\n [target.column]\n\n const qualifiedName = path.join('.')\n\n return groupByColumns.some((column) => qualifiedName === column)\n })\n ) {\n throw new UnsupportedError(\n `Every non-aggregate select target must also exist in a group by clause`\n )\n }\n\n if (\n groupByColumns.length > 0 &&\n !someTarget(targets, (target) => target.type === 'aggregate-target')\n ) {\n throw new UnsupportedError(\n `There must be at least one aggregate function in the select target list when using group by`\n )\n }\n}\n","import { UnsupportedError } from '../errors'\nimport { A_Expr, WhereExpression } from '../types/libpg-query'\nimport { ColumnFilter, Filter, Relations } from './types'\nimport { parseConstant, processJsonTarget, renderFields } from './util'\n\nexport function processWhereClause(expression: WhereExpression, relations: Relations): Filter {\n if ('A_Expr' in expression) {\n let column: string\n\n if (expression.A_Expr.name.length > 1) {\n throw new UnsupportedError('Only one operator name supported per expression')\n }\n\n const kind = expression.A_Expr.kind\n const [name] = expression.A_Expr.name\n const operatorSymbol = name.String.sval.toLowerCase()\n const operator = mapOperatorSymbol(kind, operatorSymbol)\n\n if ('A_Expr' in expression.A_Expr.lexpr) {\n try {\n const target = processJsonTarget(expression.A_Expr.lexpr, relations)\n column = target.column\n } catch (err) {\n throw new UnsupportedError(`Left side of WHERE clause must be a column`)\n }\n } else if ('ColumnRef' in expression.A_Expr.lexpr) {\n const { fields } = expression.A_Expr.lexpr.ColumnRef\n column = renderFields(fields, relations)\n } else if ('TypeCast' in expression.A_Expr.lexpr) {\n throw new UnsupportedError('Casting is not supported in the WHERE clause')\n } else if ('FuncCall' in expression.A_Expr.lexpr) {\n const functionName = renderFields(expression.A_Expr.lexpr.FuncCall.funcname, relations)\n\n // Only 'to_tsvector' function is supported on left side of WHERE clause (when using FTS `@@` operator))\n if (operator === 'fts') {\n if (functionName === 'to_tsvector') {\n if (\n !expression.A_Expr.lexpr.FuncCall.args ||\n expression.A_Expr.lexpr.FuncCall.args.length !== 1\n ) {\n throw new UnsupportedError(`${functionName} requires 1 column argument`)\n }\n\n // We grab the column passed to `to_tsvector` and discard the `to_tsvector` function\n // We can do this because Postgres will implicitly wrap text columns in `to_tsvector` at query time\n const [arg] = expression.A_Expr.lexpr.FuncCall.args\n\n if ('A_Expr' in arg) {\n try {\n const target = processJsonTarget(arg, relations)\n column = target.column\n } catch (err) {\n throw new UnsupportedError(`${functionName} requires a column argument`)\n }\n } else if ('ColumnRef' in arg) {\n const { fields } = arg.ColumnRef\n column = renderFields(fields, relations)\n } else if ('TypeCast' in arg) {\n throw new UnsupportedError('Casting is not supported in the WHERE clause')\n } else {\n throw new UnsupportedError(`${functionName} requires a column argument`)\n }\n } else {\n throw new UnsupportedError(\n `Only 'to_tsvector' function allowed on left side of text search operator`\n )\n }\n } else {\n throw new UnsupportedError(`Left side of WHERE clause must be a column`)\n }\n } else {\n throw new UnsupportedError(`Left side of WHERE clause must be a column`)\n }\n\n if (\n operator === 'eq' ||\n operator === 'neq' ||\n operator === 'gt' ||\n operator === 'gte' ||\n operator === 'lt' ||\n operator === 'lte'\n ) {\n if (!('A_Const' in expression.A_Expr.rexpr)) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be a constant`,\n `Did you forget to wrap your value in single quotes?`\n )\n }\n\n const value = parseConstant(expression.A_Expr.rexpr)\n return {\n type: 'column',\n column,\n operator,\n negate: false,\n value,\n }\n }\n // Between is not supported by PostgREST, but we can generate the equivalent using '>=' and '<='\n else if (\n operator === 'between' ||\n operator === 'between symmetric' ||\n operator === 'not between' ||\n operator === 'not between symmetric'\n ) {\n if (!('List' in expression.A_Expr.rexpr) || expression.A_Expr.rexpr.List.items.length !== 2) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must contain two constants`\n )\n }\n\n let [leftValue, rightValue] = expression.A_Expr.rexpr.List.items.map((item) =>\n parseConstant(item)\n )\n\n // 'between symmetric' doesn't care which argument comes first order-wise,\n // ie. it auto swaps the arguments if the left value is greater than the right value\n if (operator.includes('symmetric')) {\n // We can only implement the symmetric logic if the values are numbers\n // If they're strings, they could be dates, text columns, etc which we can't sort here\n if (typeof leftValue !== 'number' || typeof rightValue !== 'number') {\n throw new UnsupportedError(`BETWEEN SYMMETRIC is only supported with number values`)\n }\n\n // If the left value is greater than the right, swap them\n if (leftValue > rightValue) {\n const temp = rightValue\n rightValue = leftValue\n leftValue = temp\n }\n }\n\n const leftFilter: ColumnFilter = {\n type: 'column',\n column,\n operator: 'gte',\n negate: false,\n value: leftValue,\n }\n\n const rightFilter: ColumnFilter = {\n type: 'column',\n column,\n operator: 'lte',\n negate: false,\n value: rightValue,\n }\n\n return {\n type: 'logical',\n operator: 'and',\n negate: operator.includes('not'),\n values: [leftFilter, rightFilter],\n }\n } else if (\n operator === 'like' ||\n operator === 'ilike' ||\n operator === 'match' ||\n operator === 'imatch'\n ) {\n if (!('A_Const' in expression.A_Expr.rexpr) || !('sval' in expression.A_Expr.rexpr.A_Const)) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operator}' expression must be a string constant`\n )\n }\n\n const value = expression.A_Expr.rexpr.A_Const.sval.sval\n\n return {\n type: 'column',\n column,\n operator,\n negate: false,\n value,\n }\n } else if (operator === 'in') {\n if (\n !('List' in expression.A_Expr.rexpr) ||\n !expression.A_Expr.rexpr.List.items.every((item) => 'A_Const' in item)\n ) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operator}' expression must be a list of constants`\n )\n }\n\n const value = expression.A_Expr.rexpr.List.items.map((item) => parseConstant(item))\n\n return {\n type: 'column',\n column,\n operator,\n negate: false,\n value,\n }\n } else if (operator === 'fts') {\n const supportedTextSearchFunctions = [\n 'to_tsquery',\n 'plainto_tsquery',\n 'phraseto_tsquery',\n 'websearch_to_tsquery',\n ]\n\n if (!('FuncCall' in expression.A_Expr.rexpr)) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be one of these functions: ${supportedTextSearchFunctions.join(', ')}`\n )\n }\n\n const functionName = renderFields(expression.A_Expr.rexpr.FuncCall.funcname, relations)\n\n if (!supportedTextSearchFunctions.includes(functionName)) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be one of these functions: ${supportedTextSearchFunctions.join(', ')}`\n )\n }\n\n if (\n !expression.A_Expr.rexpr.FuncCall.args ||\n expression.A_Expr.rexpr.FuncCall.args.length === 0 ||\n expression.A_Expr.rexpr.FuncCall.args.length > 2\n ) {\n throw new UnsupportedError(`${functionName} requires 1 or 2 arguments`)\n }\n\n const args = expression.A_Expr.rexpr.FuncCall.args.map((arg) => {\n if (!('A_Const' in arg) || !('sval' in arg.A_Const)) {\n throw new UnsupportedError(`${functionName} only accepts text arguments`)\n }\n\n return arg.A_Const.sval.sval\n })\n\n // config (eg. 'english') is the first argument if passed\n const [config] = args.slice(-2, -1)\n\n // query is always the last argument\n const [query] = args.slice(-1)\n\n // Adjust operator based on FTS function\n const operator = mapTextSearchFunction(functionName)\n\n return {\n type: 'column',\n column,\n operator,\n config,\n value: query,\n negate: false,\n }\n } else {\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n } else if ('NullTest' in expression) {\n const { fields } = expression.NullTest.arg.ColumnRef\n\n const column = renderFields(fields, relations)\n const negate = expression.NullTest.nulltesttype === 'IS_NOT_NULL'\n const operator = 'is'\n const value = null\n\n return {\n type: 'column',\n column,\n operator,\n negate,\n value,\n }\n } else if ('BoolExpr' in expression) {\n let operator: 'and' | 'or' | 'not'\n\n if (expression.BoolExpr.boolop === 'AND_EXPR') {\n operator = 'and'\n } else if (expression.BoolExpr.boolop === 'OR_EXPR') {\n operator = 'or'\n } else if (expression.BoolExpr.boolop === 'NOT_EXPR') {\n operator = 'not'\n } else {\n throw new UnsupportedError(`Unknown boolop '${expression.BoolExpr.boolop}'`)\n }\n\n const values = expression.BoolExpr.args.map((arg) => processWhereClause(arg, relations))\n\n // The 'not' operator is special - instead of wrapping its child,\n // we just return the child directly and set negate=true on it.\n if (operator === 'not') {\n if (values.length > 1) {\n throw new UnsupportedError(\n `NOT expressions must have only 1 child, but received ${values.length} children`\n )\n }\n const [filter] = values\n filter.negate = true\n return filter\n }\n\n return {\n type: 'logical',\n operator,\n negate: false,\n values,\n }\n } else {\n throw new UnsupportedError(`The WHERE clause must contain an expression`)\n }\n}\n\nfunction mapOperatorSymbol(kind: A_Expr['A_Expr']['kind'], operatorSymbol: string) {\n switch (kind) {\n case 'AEXPR_OP': {\n switch (operatorSymbol) {\n case '=':\n return 'eq'\n case '<>':\n return 'neq'\n case '>':\n return 'gt'\n case '>=':\n return 'gte'\n case '<':\n return 'lt'\n case '<=':\n return 'lte'\n case '~':\n return 'match'\n case '~*':\n return 'imatch'\n case '@@':\n // 'fts' isn't necessarily the final operator (there is also plfts, phfts, wfts)\n // we adjust this downstream based on the tsquery function used\n return 'fts'\n default:\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n }\n case 'AEXPR_BETWEEN':\n case 'AEXPR_BETWEEN_SYM':\n case 'AEXPR_NOT_BETWEEN':\n case 'AEXPR_NOT_BETWEEN_SYM': {\n switch (operatorSymbol) {\n case 'between':\n return 'between'\n case 'between symmetric':\n return 'between symmetric'\n case 'not between':\n return 'not between'\n case 'not between symmetric':\n return 'not between symmetric'\n default:\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n }\n case 'AEXPR_LIKE': {\n switch (operatorSymbol) {\n case '~~':\n return 'like'\n default:\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n }\n case 'AEXPR_ILIKE': {\n switch (operatorSymbol) {\n case '~~*':\n return 'ilike'\n default:\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n }\n case 'AEXPR_IN': {\n switch (operatorSymbol) {\n case '=':\n return 'in'\n default:\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n }\n }\n}\n\n/**\n * Maps text search query functions to the respective PostgREST operator.\n */\nfunction mapTextSearchFunction(functionName: string) {\n switch (functionName) {\n case 'to_tsquery':\n return 'fts'\n case 'plainto_tsquery':\n return 'plfts'\n case 'phraseto_tsquery':\n return 'phfts'\n case 'websearch_to_tsquery':\n return 'wfts'\n default:\n throw new UnsupportedError(`Function '${functionName}' not supported for full-text search`)\n }\n}\n","import { UnsupportedError } from '../errors'\nimport { SelectStmt } from '../types/libpg-query'\nimport { Limit } from './types'\n\nexport function processLimit(selectStmt: SelectStmt): Limit | undefined {\n let count: number | undefined = undefined\n let offset: number | undefined = undefined\n\n if (selectStmt.SelectStmt.limitCount) {\n if (!('ival' in selectStmt.SelectStmt.limitCount.A_Const)) {\n throw new UnsupportedError(`Limit count must be an integer`)\n }\n\n count = selectStmt.SelectStmt.limitCount.A_Const.ival.ival\n }\n\n if (selectStmt.SelectStmt.limitOffset) {\n if (!('ival' in selectStmt.SelectStmt.limitOffset.A_Const)) {\n throw new UnsupportedError(`Limit offset must be an integer`)\n }\n\n offset = selectStmt.SelectStmt.limitOffset.A_Const.ival.ival\n }\n\n if (count === undefined && offset === undefined) {\n return undefined\n }\n\n return {\n count,\n offset,\n }\n}\n","import { UnsupportedError } from '../errors'\nimport { SortBy } from '../types/libpg-query'\nimport { Relations, Sort } from './types'\nimport { processJsonTarget, renderFields } from './util'\n\nexport function processSortClause(sorts: SortBy[], relations: Relations): Sort[] {\n return sorts.map((sortBy) => {\n let column: string\n\n if ('A_Expr' in sortBy.SortBy.node) {\n try {\n const target = processJsonTarget(sortBy.SortBy.node, relations)\n column = target.column\n } catch (err) {\n throw new UnsupportedError(`ORDER BY clause must reference a column`)\n }\n } else if ('ColumnRef' in sortBy.SortBy.node) {\n const { fields } = sortBy.SortBy.node.ColumnRef\n column = renderFields(fields, relations, 'parenthesis')\n } else if ('TypeCast' in sortBy.SortBy.node) {\n throw new UnsupportedError('Casting is not supported in the ORDER BY clause')\n } else {\n throw new UnsupportedError(`ORDER BY clause must reference a column`)\n }\n\n const direction = mapSortByDirection(sortBy.SortBy.sortby_dir)\n const nulls = mapSortByNulls(sortBy.SortBy.sortby_nulls)\n\n return {\n column,\n direction,\n nulls,\n }\n })\n}\n\nfunction mapSortByDirection(direction: string) {\n switch (direction) {\n case 'SORTBY_ASC':\n return 'asc'\n case 'SORTBY_DESC':\n return 'desc'\n case 'SORTBY_DEFAULT':\n return undefined\n default:\n throw new UnsupportedError(`Unknown sort by direction '${direction}'`)\n }\n}\n\nfunction mapSortByNulls(nulls: string) {\n switch (nulls) {\n case 'SORTBY_NULLS_FIRST':\n return 'first'\n case 'SORTBY_NULLS_LAST':\n return 'last'\n case 'SORTBY_NULLS_DEFAULT':\n return undefined\n default:\n throw new UnsupportedError(`Unknown sort by nulls '${nulls}'`)\n }\n}\n","import { UnsupportedError } from '../errors'\nimport {\n A_Const,\n A_Expr,\n ColumnRef,\n FromExpression,\n FuncCall,\n PgString,\n SelectResTarget,\n SelectStmt,\n TypeCast,\n} from '../types/libpg-query'\nimport { validateGroupClause } from './aggregate'\nimport { processWhereClause } from './filter'\nimport { processLimit } from './limit'\nimport { processSortClause } from './sort'\nimport {\n AggregateTarget,\n ColumnTarget,\n EmbeddedTarget,\n JoinedColumn,\n Relations,\n Select,\n Target,\n} from './types'\nimport { processJsonTarget, renderDataType, renderFields } from './util'\n\nexport const supportedAggregateFunctions = ['avg', 'count', 'max', 'min', 'sum']\n\nexport function processSelectStatement(stmt: SelectStmt): Select {\n if (!stmt.SelectStmt.fromClause) {\n throw new UnsupportedError('The query must have a from clause')\n }\n\n if (stmt.SelectStmt.fromClause.length > 1) {\n throw new UnsupportedError('Only one FROM source is supported')\n }\n\n if (stmt.SelectStmt.withClause) {\n throw new UnsupportedError('CTEs are not supported')\n }\n\n if (stmt.SelectStmt.distinctClause) {\n throw new UnsupportedError('SELECT DISTINCT is not supported')\n }\n\n if (stmt.SelectStmt.havingClause) {\n throw new UnsupportedError('The HAVING clause is not supported')\n }\n\n const [fromClause] = stmt.SelectStmt.fromClause\n\n const relations = processFromClause(fromClause)\n\n const from = relations.primary.name\n\n const targets = processTargetList(stmt.SelectStmt.targetList, relations)\n\n validateGroupClause(stmt.SelectStmt.groupClause ?? [], targets, relations)\n\n const filter = stmt.SelectStmt.whereClause\n ? processWhereClause(stmt.SelectStmt.whereClause, relations)\n : undefined\n\n const sorts = processSortClause(stmt.SelectStmt.sortClause ?? [], relations)\n\n const limit = processLimit(stmt)\n\n return {\n type: 'select',\n from,\n targets,\n filter,\n sorts,\n limit,\n }\n}\n\nfunction processFromClause(fromClause: FromExpression): Relations {\n if ('RangeVar' in fromClause) {\n return {\n primary: {\n name: fromClause.RangeVar.relname,\n alias: fromClause.RangeVar.alias?.aliasname,\n get reference() {\n return this.alias ?? this.name\n },\n },\n joined: [],\n }\n } else if ('JoinExpr' in fromClause) {\n const joinType = mapJoinType(fromClause.JoinExpr.jointype)\n const { primary, joined } = processFromClause(fromClause.JoinExpr.larg)\n\n const joinedRelationAlias = fromClause.JoinExpr.rarg.RangeVar.alias?.aliasname\n const joinedRelation = joinedRelationAlias ?? fromClause.JoinExpr.rarg.RangeVar.relname\n\n const existingRelations = [\n primary.reference,\n ...joined.map((t) => t.alias ?? t.relation),\n joinedRelation,\n ]\n\n if (!('A_Expr' in fromClause.JoinExpr.quals)) {\n throw new UnsupportedError(`Join qualifier must be an expression comparing columns`)\n }\n\n let leftQualifierRelation\n let rightQualifierRelation\n\n const joinQualifierExpression = fromClause.JoinExpr.quals.A_Expr\n\n if (!('ColumnRef' in joinQualifierExpression.lexpr)) {\n throw new UnsupportedError(`Left side of join qualifier must be a column`)\n }\n\n if (\n !joinQualifierExpression.lexpr.ColumnRef.fields.every(\n (field): field is PgString => 'String' in field\n )\n ) {\n throw new UnsupportedError(`Left side column of join qualifier must contain String fields`)\n }\n\n const leftColumnFields = joinQualifierExpression.lexpr.ColumnRef.fields.map(\n (field) => field.String.sval\n )\n\n // Relation and column names are last two parts of the qualified name\n const [leftRelationName] = leftColumnFields.slice(-2, -1)\n const [leftColumnName] = leftColumnFields.slice(-1)\n\n if (!leftRelationName) {\n leftQualifierRelation = primary.reference\n } else if (existingRelations.includes(leftRelationName)) {\n leftQualifierRelation = leftRelationName\n } else if (leftRelationName === joinedRelation) {\n leftQualifierRelation = joinedRelation\n } else {\n throw new UnsupportedError(\n `Left side of join qualifier references a different relation (${leftRelationName}) than the join (${existingRelations.join(', ')})`\n )\n }\n\n if (!('ColumnRef' in joinQualifierExpression.rexpr)) {\n throw new UnsupportedError(`Right side of join qualifier must be a column`)\n }\n\n if (\n !joinQualifierExpression.rexpr.ColumnRef.fields.every(\n (field): field is PgString => 'String' in field\n )\n ) {\n throw new UnsupportedError(`Right side column of join qualifier must contain String fields`)\n }\n\n const rightColumnFields = joinQualifierExpression.rexpr.ColumnRef.fields.map(\n (field) => field.String.sval\n )\n\n // Relation and column names are last two parts of the qualified name\n const [rightRelationName] = rightColumnFields.slice(-2, -1)\n const [rightColumnName] = rightColumnFields.slice(-1)\n\n if (!rightRelationName) {\n rightQualifierRelation = primary.reference\n } else if (existingRelations.includes(rightRelationName)) {\n rightQualifierRelation = rightRelationName\n } else if (rightRelationName === joinedRelation) {\n rightQualifierRelation = joinedRelation\n } else {\n throw new UnsupportedError(\n `Right side of join qualifier references a different relation (${rightRelationName}) than the join (${existingRelations.join(', ')})`\n )\n }\n\n if (rightQualifierRelation === leftQualifierRelation) {\n // TODO: support for recursive relationships\n throw new UnsupportedError(`Join qualifier cannot compare columns from same relation`)\n }\n\n if (rightQualifierRelation !== joinedRelation && leftQualifierRelation !== joinedRelation) {\n throw new UnsupportedError(`Join qualifier must reference a column from the joined table`)\n }\n\n const [qualifierOperatorString] = joinQualifierExpression.name\n\n if (qualifierOperatorString.String.sval !== '=') {\n throw new UnsupportedError(`Join qualifier operator must be '='`)\n }\n\n let left: JoinedColumn\n let right: JoinedColumn\n\n // If left qualifier referenced the joined relation, swap left and right\n if (rightQualifierRelation === joinedRelation) {\n left = {\n relation: leftQualifierRelation,\n column: leftColumnName,\n }\n right = {\n relation: rightQualifierRelation,\n column: rightColumnName,\n }\n } else {\n right = {\n relation: leftQualifierRelation,\n column: leftColumnName,\n }\n left = {\n relation: rightQualifierRelation,\n column: rightColumnName,\n }\n }\n\n const embeddedTarget: EmbeddedTarget = {\n type: 'embedded-target',\n relation: fromClause.JoinExpr.rarg.RangeVar.relname,\n alias: fromClause.JoinExpr.rarg.RangeVar.alias?.aliasname,\n joinType,\n targets: [], // these will be filled in later when processing the select target list\n flatten: true,\n joinedColumns: {\n left,\n right,\n },\n }\n\n return {\n primary,\n joined: [...joined, embeddedTarget],\n }\n } else {\n const [fieldType] = Object.keys(fromClause)\n throw new UnsupportedError(`Unsupported FROM clause type '${fieldType}'`)\n }\n}\n\nfunction processTargetList(targetList: SelectResTarget[], relations: Relations): Target[] {\n // First pass: map each SQL target column to a PostgREST target 1-to-1\n const flattenedColumnTargets: (ColumnTarget | AggregateTarget)[] = targetList.map((resTarget) => {\n const target = processTarget(resTarget.ResTarget.val, relations)\n target.alias = resTarget.ResTarget.name\n\n return target\n })\n\n // Second pass: transfer joined columns to `embeddedTargets`\n const columnTargets = flattenedColumnTargets.filter((target) => {\n // Account for the special case when the aggregate doesn't have a column attached\n // ie. `count()`: should always be applied to the top level relation\n if (target.type === 'aggregate-target' && !('column' in target)) {\n return true\n }\n\n const qualifiedName = target.column.split('.')\n\n // Relation and column names are last two parts of the qualified name\n const [relationName] = qualifiedName.slice(-2, -1)\n const [columnName] = qualifiedName.slice(-1)\n\n // If there is no prefix, this column belongs to the primary relation at the top level\n if (!relationName) {\n return true\n }\n\n // If this column is part of a joined relation\n if (relationName) {\n const embeddedTarget = relations.joined.find(\n (t) => (t.alias && !t.flatten ? t.alias : t.relation) === relationName\n )\n\n if (!embeddedTarget) {\n throw new UnsupportedError(\n `Found foreign column '${target.column}' in target list without a join to that relation`,\n 'Did you forget to join that relation or alias it to something else?'\n )\n }\n\n // Strip relation from column name\n target.column = columnName\n\n // Nest the column in the embedded target\n embeddedTarget.targets.push(target)\n\n // Remove this column from the top level\n return false\n }\n\n return true\n })\n\n // Third pass: nest embedded targets within each other based on the relations in their join qualifiers\n const nestedEmbeddedTargets = relations.joined.reduce<EmbeddedTarget[]>(\n (output, embeddedTarget) => {\n // If the embedded target was joined with the primary relation, return it\n if (embeddedTarget.joinedColumns.left.relation === relations.primary.reference) {\n return [...output, embeddedTarget]\n }\n\n // Otherwise identify the correct parent and nest it within its targets\n const parent = relations.joined.find(\n (t) => (t.alias ?? t.relation) === embeddedTarget.joinedColumns.left.relation\n )\n\n if (!parent) {\n throw new UnsupportedError(\n `Something went wrong, could not find parent embedded target for nested embedded target '${embeddedTarget.relation}'`\n )\n }\n\n parent.targets.push(embeddedTarget)\n return output\n },\n []\n )\n\n return [...columnTargets, ...nestedEmbeddedTargets]\n}\n\nfunction processTarget(\n target: TypeCast | ColumnRef | FuncCall | A_Expr | A_Const,\n relations: Relations\n): ColumnTarget | AggregateTarget {\n if ('TypeCast' in target) {\n return processCast(target, relations)\n } else if ('ColumnRef' in target) {\n return processColumn(target, relations)\n } else if ('A_Expr' in target) {\n return processExpression(target, relations)\n } else if ('FuncCall' in target) {\n return processFunctionCall(target, relations)\n } else {\n throw new UnsupportedError(\n 'Only columns, JSON fields, and aggregates are supported as query targets'\n )\n }\n}\n\nfunction mapJoinType(joinType: string) {\n switch (joinType) {\n case 'JOIN_INNER':\n return 'inner'\n case 'JOIN_LEFT':\n return 'left'\n default:\n throw new UnsupportedError(`Unsupported join type '${joinType}'`)\n }\n}\n\nfunction processCast(target: TypeCast, relations: Relations) {\n const cast = renderDataType(target.TypeCast.typeName.names)\n\n if ('A_Const' in target.TypeCast.arg) {\n throw new UnsupportedError(\n 'Only columns, JSON fields, and aggregates are supported as query targets'\n )\n }\n\n const nestedTarget = processTarget(target.TypeCast.arg, relations)\n\n const { type } = nestedTarget\n\n if (type === 'aggregate-target') {\n return {\n ...nestedTarget,\n outputCast: cast,\n }\n } else if (type === 'column-target') {\n return {\n ...nestedTarget,\n cast,\n }\n } else {\n throw new UnsupportedError(`Cannot process target with type '${type}'`)\n }\n}\n\nfunction processColumn(target: ColumnRef, relations: Relations): ColumnTarget {\n return {\n type: 'column-target',\n column: renderFields(target.ColumnRef.fields, relations),\n }\n}\n\nfunction processExpression(target: A_Expr, relations: Relations): ColumnTarget {\n try {\n return processJsonTarget(target, relations)\n } catch (err) {\n const maybeJsonHint =\n err instanceof Error && err.message === 'Invalid JSON path'\n ? 'Did you forget to quote a JSON path?'\n : undefined\n throw new UnsupportedError(`Expressions not supported as targets`, maybeJsonHint)\n }\n}\n\nfunction processFunctionCall(target: FuncCall, relations: Relations): AggregateTarget {\n const functionName = renderFields(target.FuncCall.funcname, relations)\n\n if (!supportedAggregateFunctions.includes(functionName)) {\n throw new UnsupportedError(\n `Only the following aggregate functions are supported: ${JSON.stringify(supportedAggregateFunctions)}`\n )\n }\n\n // The `count(*)` special case that has no columns attached\n if (functionName === 'count' && !target.FuncCall.args && target.FuncCall.agg_star) {\n return {\n type: 'aggregate-target',\n functionName,\n }\n }\n\n if (!target.FuncCall.args) {\n throw new UnsupportedError(`Aggregate function '${functionName}' requires a column argument`)\n }\n\n if (target.FuncCall.args && target.FuncCall.args.length > 1) {\n throw new UnsupportedError(`Aggregate functions only accept one argument`)\n }\n\n const [arg] = target.FuncCall.args\n\n const nestedTarget = processTarget(arg, relations)\n\n if (nestedTarget.type === 'aggregate-target') {\n throw new UnsupportedError(`Aggregate functions cannot contain another function`)\n }\n\n const { cast, ...columnTarget } = nestedTarget\n\n return {\n ...columnTarget,\n type: 'aggregate-target',\n functionName,\n inputCast: cast,\n }\n}\n","/**\n * @class TemplateTag\n * @classdesc Consumes a pipeline of composable transformer plugins and produces a template tag.\n */\nexport default class TemplateTag {\n /**\n * constructs a template tag\n * @constructs TemplateTag\n * @param {...Object} [...transformers] - an array or arguments list of transformers\n * @return {Function} - a template tag\n */\n constructor(...transformers) {\n // if first argument is an array, extrude it as a list of transformers\n if (transformers.length > 0 && Array.isArray(transformers[0])) {\n transformers = transformers[0];\n }\n\n // if any transformers are functions, this means they are not initiated - automatically initiate them\n this.transformers = transformers.map(transformer => {\n return typeof transformer === 'function' ? transformer() : transformer;\n });\n\n // return an ES2015 template tag\n return this.tag;\n }\n\n /**\n * Applies all transformers to a template literal tagged with this method.\n * If a function is passed as the first argument, assumes the function is a template tag\n * and applies it to the template, returning a template tag.\n * @param {(Function|String|Array<String>)} strings - Either a template tag or an array containing template strings separated by identifier\n * @param {...*} ...expressions - Optional list of substitution values.\n * @return {(String|Function)} - Either an intermediary tag function or the results of processing the template.\n */\n tag = (strings, ...expressions) => {\n if (typeof strings === 'function') {\n // if the first argument passed is a function, assume it is a template tag and return\n // an intermediary tag that processes the template using the aforementioned tag, passing the\n // result to our tag\n return this.interimTag.bind(this, strings);\n }\n\n if (typeof strings === 'string') {\n // if the first argument passed is a string, just transform it\n return this.transformEndResult(strings);\n }\n\n // else, return a transformed end result of processing the template with our tag\n strings = strings.map(this.transformString.bind(this));\n return this.transformEndResult(\n strings.reduce(this.processSubstitutions.bind(this, expressions)),\n );\n };\n\n /**\n * An intermediary template tag that receives a template tag and passes the result of calling the template with the received\n * template tag to our own template tag.\n * @param {Function} nextTag - the received template tag\n * @param {Array<String>} template - the template to process\n * @param {...*} ...substitutions - `substitutions` is an array of all substitutions in the template\n * @return {*} - the final processed value\n */\n interimTag(previousTag, template, ...substitutions) {\n return this.tag`${previousTag(template, ...substitutions)}`;\n }\n\n /**\n * Performs bulk processing on the tagged template, transforming each substitution and then\n * concatenating the resulting values into a string.\n * @param {Array<*>} substitutions - an array of all remaining substitutions present in this template\n * @param {String} resultSoFar - this iteration's result string so far\n * @param {String} remainingPart - the template chunk after the current substitution\n * @return {String} - the result of joining this iteration's processed substitution with the result\n */\n processSubstitutions(substitutions, resultSoFar, remainingPart) {\n const substitution = this.transformSubstitution(\n substitutions.shift(),\n resultSoFar,\n );\n return ''.concat(resultSoFar, substitution, remainingPart);\n }\n\n /**\n * Iterate through each transformer, applying the transformer's `onString` method to the template\n * strings before all substitutions are processed.\n * @param {String} str - The input string\n * @return {String} - The final results of processing each transformer\n */\n transformString(str) {\n const cb = (res, transform) =>\n transform.onString ? transform.onString(res) : res;\n return