// Credits to @bnjmnt4n (https://www.npmjs.com/package/postgrest-query) // See https://github.com/PostgREST/postgrest/blob/2f91853cb1de18944a4556df09e52450b881cfb3/src/PostgREST/ApiRequest/QueryParams.hs#L282-L284 import { SimplifyDeep } from '../types/types' import { JsonPathToAccessor } from './utils' /** * Parses a query. * A query is a sequence of nodes, separated by `,`, ensuring that there is * no remaining input after all nodes have been parsed. * * Returns an array of parsed nodes, or an error. */ export type ParseQuery = string extends Query ? GenericStringError : ParseNodes> extends [infer Nodes, `${infer Remainder}`] ? Nodes extends Ast.Node[] ? EatWhitespace extends '' ? SimplifyDeep : ParserError<`Unexpected input: ${Remainder}`> : ParserError<'Invalid nodes array structure'> : ParseNodes> /** * Notes: all `Parse*` types assume that their input strings have their whitespace * removed. They return tuples of ["Return Value", "Remainder of text"] or * a `ParserError`. */ /** * Parses a sequence of nodes, separated by `,`. * * Returns a tuple of ["Parsed fields", "Remainder of text"] or an error. */ type ParseNodes = string extends Input ? GenericStringError : ParseNodesHelper type ParseNodesHelper = ParseNode extends [infer Node, `${infer Remainder}`] ? Node extends Ast.Node ? EatWhitespace extends `,${infer Remainder}` ? ParseNodesHelper, [...Nodes, Node]> : [[...Nodes, Node], EatWhitespace] : ParserError<'Invalid node type in nodes helper'> : ParseNode /** * Parses a node. * A node is one of the following: * - `*` * - a field, as defined above * - a renamed field, `renamed_field:field` * - a spread field, `...field` */ type ParseNode = Input extends '' ? ParserError<'Empty string'> : // `*` Input extends `*${infer Remainder}` ? [Ast.StarNode, EatWhitespace] : // `...field` Input extends `...${infer Remainder}` ? ParseField> extends [infer TargetField, `${infer Remainder}`] ? TargetField extends Ast.FieldNode ? [{ type: 'spread'; target: TargetField }, EatWhitespace] : ParserError<'Invalid target field type in spread'> : ParserError<`Unable to parse spread resource at \`${Input}\``> : ParseIdentifier extends [infer NameOrAlias, `${infer Remainder}`] ? EatWhitespace extends `::${infer _}` ? // It's a type cast and not an alias, so treat it as part of the field. ParseField : EatWhitespace extends `:${infer Remainder}` ? // `alias:` ParseField> extends [infer Field, `${infer Remainder}`] ? Field extends Ast.FieldNode ? [Omit & { alias: NameOrAlias }, EatWhitespace] : ParserError<'Invalid field type in alias parsing'> : ParserError<`Unable to parse renamed field at \`${Input}\``> : // Otherwise, just parse it as a field without alias. ParseField : ParserError<`Expected identifier at \`${Input}\``> /** * Parses a field without preceding alias. * A field is one of the following: * - a top-level `count` field: https://docs.postgrest.org/en/v12/references/api/aggregate_functions.html#the-case-of-count * - a field with an embedded resource * - `field(nodes)` * - `field!hint(nodes)` * - `field!inner(nodes)` * - `field!left(nodes)` * - `field!hint!inner(nodes)` * - `field!hint!left(nodes)` * - a field without an embedded resource (see {@link ParseNonEmbeddedResourceField}) */ type ParseField = Input extends '' ? ParserError<'Empty string'> : ParseIdentifier extends [infer Name, `${infer Remainder}`] ? Name extends 'count' ? ParseCountField : Remainder extends `!inner${infer Remainder}` ? ParseEmbeddedResource> extends [ infer Children, `${infer Remainder}`, ] ? Children extends Ast.Node[] ? // `field!inner(nodes)` [{ type: 'field'; name: Name; innerJoin: true; children: Children }, Remainder] : ParserError<'Invalid children array in inner join'> : CreateParserErrorIfRequired< ParseEmbeddedResource>, `Expected embedded resource after "!inner" at \`${Remainder}\`` > : EatWhitespace extends `!left${infer Remainder}` ? ParseEmbeddedResource> extends [ infer Children, `${infer Remainder}`, ] ? Children extends Ast.Node[] ? // `field!left(nodes)` // !left is a noise word - treat it the same way as a non-`!inner`. [{ type: 'field'; name: Name; children: Children }, EatWhitespace] : ParserError<'Invalid children array in left join'> : CreateParserErrorIfRequired< ParseEmbeddedResource>, `Expected embedded resource after "!left" at \`${EatWhitespace}\`` > : EatWhitespace extends `!${infer Remainder}` ? ParseIdentifier> extends [infer Hint, `${infer Remainder}`] ? EatWhitespace extends `!inner${infer Remainder}` ? ParseEmbeddedResource> extends [ infer Children, `${infer Remainder}`, ] ? Children extends Ast.Node[] ? // `field!hint!inner(nodes)` [ { type: 'field' name: Name hint: Hint innerJoin: true children: Children }, EatWhitespace, ] : ParserError<'Invalid children array in hint inner join'> : ParseEmbeddedResource> : ParseEmbeddedResource> extends [ infer Children, `${infer Remainder}`, ] ? Children extends Ast.Node[] ? // `field!hint(nodes)` [ { type: 'field'; name: Name; hint: Hint; children: Children }, EatWhitespace, ] : ParserError<'Invalid children array in hint'> : ParseEmbeddedResource> : ParserError<`Expected identifier after "!" at \`${EatWhitespace}\``> : EatWhitespace extends `(${infer _}` ? ParseEmbeddedResource> extends [ infer Children, `${infer Remainder}`, ] ? Children extends Ast.Node[] ? // `field(nodes)` [{ type: 'field'; name: Name; children: Children }, EatWhitespace] : ParserError<'Invalid children array in field'> : // Return error if start of embedded resource was detected but not found. ParseEmbeddedResource> : // Otherwise it's a non-embedded resource field. ParseNonEmbeddedResourceField : ParserError<`Expected identifier at \`${Input}\``> type ParseCountField = ParseIdentifier extends ['count', `${infer Remainder}`] ? ( EatWhitespace extends `()${infer Remainder_}` ? EatWhitespace : EatWhitespace ) extends `${infer Remainder}` ? Remainder extends `::${infer _}` ? ParseFieldTypeCast extends [infer CastType, `${infer Remainder}`] ? [ { type: 'field'; name: 'count'; aggregateFunction: 'count'; castType: CastType }, Remainder, ] : ParseFieldTypeCast : [{ type: 'field'; name: 'count'; aggregateFunction: 'count' }, Remainder] : never : ParserError<`Expected "count" at \`${Input}\``> /** * Parses an embedded resource, which is an opening `(`, followed by a sequence of * 0 or more nodes separated by `,`, then a closing `)`. * * Returns a tuple of ["Parsed fields", "Remainder of text"], an error, * or the original string input indicating that no opening `(` was found. */ type ParseEmbeddedResource = Input extends `(${infer Remainder}` ? EatWhitespace extends `)${infer Remainder}` ? [[], EatWhitespace] : ParseNodes> extends [infer Nodes, `${infer Remainder}`] ? Nodes extends Ast.Node[] ? EatWhitespace extends `)${infer Remainder}` ? [Nodes, EatWhitespace] : ParserError<`Expected ")" at \`${EatWhitespace}\``> : ParserError<'Invalid nodes array in embedded resource'> : ParseNodes> : ParserError<`Expected "(" at \`${Input}\``> /** * Parses a field excluding embedded resources, without preceding field renaming. * This is one of the following: * - `field` * - `field.aggregate()` * - `field.aggregate()::type` * - `field::type` * - `field::type.aggregate()` * - `field::type.aggregate()::type` * - `field->json...` * - `field->json.aggregate()` * - `field->json.aggregate()::type` * - `field->json::type` * - `field->json::type.aggregate()` * - `field->json::type.aggregate()::type` */ type ParseNonEmbeddedResourceField = ParseIdentifier extends [infer Name, `${infer Remainder}`] ? // Parse optional JSON path. ( Remainder extends `->${infer PathAndRest}` ? ParseJsonAccessor extends [ infer PropertyName, infer PropertyType, `${infer Remainder}`, ] ? [ { type: 'field' name: Name alias: PropertyName castType: PropertyType jsonPath: JsonPathToAccessor< PathAndRest extends `${infer Path},${string}` ? Path : PathAndRest > }, Remainder, ] : ParseJsonAccessor : [{ type: 'field'; name: Name }, Remainder] ) extends infer Parsed ? Parsed extends [infer Field, `${infer Remainder}`] ? // Parse optional typecast or aggregate function input typecast. ( Remainder extends `::${infer _}` ? ParseFieldTypeCast extends [infer CastType, `${infer Remainder}`] ? [Omit & { castType: CastType }, Remainder] : ParseFieldTypeCast : [Field, Remainder] ) extends infer Parsed ? Parsed extends [infer Field, `${infer Remainder}`] ? // Parse optional aggregate function. Remainder extends `.${infer _}` ? ParseFieldAggregation extends [ infer AggregateFunction, `${infer Remainder}`, ] ? // Parse optional aggregate function output typecast. Remainder extends `::${infer _}` ? ParseFieldTypeCast extends [infer CastType, `${infer Remainder}`] ? [ Omit & { aggregateFunction: AggregateFunction castType: CastType }, Remainder, ] : ParseFieldTypeCast : [Field & { aggregateFunction: AggregateFunction }, Remainder] : ParseFieldAggregation : [Field, Remainder] : Parsed : never : Parsed : never : ParserError<`Expected identifier at \`${Input}\``> /** * Parses a JSON property accessor of the shape `->a->b->c`. The last accessor in * the series may convert to text by using the ->> operator instead of ->. * * Returns a tuple of ["Last property name", "Last property type", "Remainder of text"] */ type ParseJsonAccessor = Input extends `->${infer Remainder}` ? Remainder extends `>${infer Remainder}` ? ParseIdentifier extends [infer Name, `${infer Remainder}`] ? [Name, 'text', EatWhitespace] : ParserError<'Expected property name after `->>`'> : ParseIdentifier extends [infer Name, `${infer Remainder}`] ? ParseJsonAccessor extends [ infer PropertyName, infer PropertyType, `${infer Remainder}`, ] ? [PropertyName, PropertyType, EatWhitespace] : [Name, 'json', EatWhitespace] : ParserError<'Expected property name after `->`'> : ParserError<'Expected ->'> /** * Parses a field typecast (`::type`), returning a tuple of ["Type", "Remainder of text"]. */ type ParseFieldTypeCast = EatWhitespace extends `::${infer Remainder}` ? ParseIdentifier> extends [`${infer CastType}`, `${infer Remainder}`] ? [CastType, EatWhitespace] : ParserError<`Invalid type for \`::\` operator at \`${Remainder}\``> : ParserError<'Expected ::'> /** * Parses a field aggregation (`.max()`), returning a tuple of ["Aggregate function", "Remainder of text"] */ type ParseFieldAggregation = EatWhitespace extends `.${infer Remainder}` ? ParseIdentifier> extends [ `${infer FunctionName}`, `${infer Remainder}`, ] ? // Ensure that aggregation function is valid. FunctionName extends Token.AggregateFunction ? EatWhitespace extends `()${infer Remainder}` ? [FunctionName, EatWhitespace] : ParserError<`Expected \`()\` after \`.\` operator \`${FunctionName}\``> : ParserError<`Invalid type for \`.\` operator \`${FunctionName}\``> : ParserError<`Invalid type for \`.\` operator at \`${Remainder}\``> : ParserError<'Expected .'> /** * Parses a (possibly double-quoted) identifier. * Identifiers are sequences of 1 or more letters. */ type ParseIdentifier = ParseLetters extends [infer Name, `${infer Remainder}`] ? [Name, EatWhitespace] : ParseQuotedLetters extends [infer Name, `${infer Remainder}`] ? [Name, EatWhitespace] : ParserError<`No (possibly double-quoted) identifier at \`${Input}\``> /** * Parse a consecutive sequence of 1 or more letter, where letters are `[0-9a-zA-Z_]`. */ type ParseLetters = string extends Input ? GenericStringError : ParseLettersHelper extends [`${infer Letters}`, `${infer Remainder}`] ? Letters extends '' ? ParserError<`Expected letter at \`${Input}\``> : [Letters, Remainder] : ParseLettersHelper type ParseLettersHelper = string extends Input ? GenericStringError : Input extends `${infer L}${infer Remainder}` ? L extends Token.Letter ? ParseLettersHelper : [Acc, Input] : [Acc, ''] /** * Parse a consecutive sequence of 1 or more double-quoted letters, * where letters are `[^"]`. */ type ParseQuotedLetters = string extends Input ? GenericStringError : Input extends `"${infer Remainder}` ? ParseQuotedLettersHelper extends [`${infer Letters}`, `${infer Remainder}`] ? Letters extends '' ? ParserError<`Expected string at \`${Remainder}\``> : [Letters, Remainder] : ParseQuotedLettersHelper : ParserError<`Not a double-quoted string at \`${Input}\``> type ParseQuotedLettersHelper = string extends Input ? GenericStringError : Input extends `${infer L}${infer Remainder}` ? L extends '"' ? [Acc, Remainder] : ParseQuotedLettersHelper : ParserError<`Missing closing double-quote in \`"${Acc}${Input}\``> /** * Trims whitespace from the left of the input. */ type EatWhitespace = string extends Input ? GenericStringError : Input extends `${Token.Whitespace}${infer Remainder}` ? EatWhitespace : Input /** * Creates a new {@link ParserError} if the given input is not already a parser error. */ type CreateParserErrorIfRequired = Input extends ParserError ? Input : ParserError /** * Parser errors. */ export type ParserError = { error: true } & Message type GenericStringError = ParserError<'Received a generic string'> export namespace Ast { export type Node = FieldNode | StarNode | SpreadNode export type FieldNode = { type: 'field' name: string alias?: string hint?: string innerJoin?: true castType?: string jsonPath?: string aggregateFunction?: Token.AggregateFunction children?: Node[] } export type StarNode = { type: 'star' } export type SpreadNode = { type: 'spread' target: FieldNode & { children: Node[] } } } namespace Token { export type Whitespace = ' ' | '\n' | '\t' type LowerAlphabet = | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' type Alphabet = LowerAlphabet | Uppercase type Digit = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0' export type Letter = Alphabet | Digit | '_' export type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max' }