// 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 { GenericSchema, Prettify } from './types' 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' type Letter = Alphabet | Digit | '_' type Json = string | number | boolean | null | { [key: string]: Json } | Json[] type SingleValuePostgreSQLTypes = | 'bool' | 'int2' | 'int4' | 'int8' | 'float4' | 'float8' | 'numeric' | 'bytea' | 'bpchar' | 'varchar' | 'date' | 'text' | 'citext' | 'time' | 'timetz' | 'timestamp' | 'timestamptz' | 'uuid' | 'vector' | 'json' | 'jsonb' | 'void' | 'record' | string type ArrayPostgreSQLTypes = `_${SingleValuePostgreSQLTypes}` type PostgreSQLTypes = SingleValuePostgreSQLTypes | ArrayPostgreSQLTypes type TypeScriptSingleValueTypes = T extends 'bool' ? boolean : T extends 'int2' | 'int4' | 'int8' | 'float4' | 'float8' | 'numeric' ? number : T extends | 'bytea' | 'bpchar' | 'varchar' | 'date' | 'text' | 'citext' | 'time' | 'timetz' | 'timestamp' | 'timestamptz' | 'uuid' | 'vector' ? string : T extends 'json' | 'jsonb' ? Json : T extends 'void' ? undefined : T extends 'record' ? Record : unknown type AggregateFunctions = 'count' | 'sum' | 'avg' | 'min' | 'max' type StripUnderscore = T extends `_${infer U}` ? U : T type TypeScriptTypes = T extends ArrayPostgreSQLTypes ? TypeScriptSingleValueTypes>>[] : TypeScriptSingleValueTypes /** * Parser errors. */ type ParserError = { error: true } & Message type GenericStringError = ParserError<'Received a generic string'> export type SelectQueryError = { error: true } & Message /** * Creates a new {@link ParserError} if the given input is not already a parser error. */ type CreateParserErrorIfRequired = Input extends ParserError ? Input : ParserError /** * Trims whitespace from the left of the input. */ type EatWhitespace = string extends Input ? GenericStringError : Input extends `${Whitespace}${infer Remainder}` ? EatWhitespace : Input /** * Returns a boolean representing whether there is a foreign key with the given name. */ type HasFKey = Relationships extends [infer R] ? R extends { foreignKeyName: FKeyName } ? true : false : Relationships extends [infer R, ...infer Rest] ? HasFKey extends true ? true : HasFKey : false /** * Returns a boolean representing whether there the foreign key has a unique constraint. */ type HasUniqueFKey = Relationships extends [infer R] ? R extends { foreignKeyName: FKeyName; isOneToOne: true } ? true : false : Relationships extends [infer R, ...infer Rest] ? HasUniqueFKey extends true ? true : HasUniqueFKey : false /** * Returns a boolean representing whether there is a foreign key referencing * a given relation. */ type HasFKeyToFRel = Relationships extends [infer R] ? R extends { referencedRelation: FRelName } ? true : false : Relationships extends [infer R, ...infer Rest] ? HasFKeyToFRel extends true ? true : HasFKeyToFRel : false type HasUniqueFKeyToFRel = Relationships extends [infer R] ? R extends { referencedRelation: FRelName; isOneToOne: true } ? true : false : Relationships extends [infer R, ...infer Rest] ? HasUniqueFKeyToFRel extends true ? true : HasUniqueFKeyToFRel : false /** * Constructs a type definition for a single field of an object. * * @param Schema Database schema. * @param Row Type of a row in the given table. * @param Relationships Relationships between different tables in the database. * @param Field Single field parsed by `ParseQuery`. */ type ConstructFieldDefinition< Schema extends GenericSchema, Row extends Record, RelationName, Relationships, Field > = Field extends { star: true } ? Row : Field extends { spread: true; original: string; children: unknown[] } ? GetResultHelper< Schema, (Schema['Tables'] & Schema['Views'])[Field['original']]['Row'], Field['original'], (Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R } ? R : unknown, Field['children'], unknown > : Field extends { children: [] } ? {} : Field extends { name: string; original: string; hint: string; children: unknown[] } ? { [_ in Field['name']]: GetResultHelper< Schema, (Schema['Tables'] & Schema['Views'])[Field['original']]['Row'], Field['original'], (Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R } ? R : unknown, Field['children'], unknown > extends infer Child ? // One-to-one relationship - referencing column(s) has unique/pkey constraint. HasUniqueFKey< Field['hint'], (Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R } ? R : unknown > extends true ? Child | null : Relationships extends unknown[] ? HasFKey extends true ? Field extends { inner: true } ? Child : Child | null : Child[] : Child[] : never } : Field extends { name: string; original: string; children: unknown[] } ? { [_ in Field['name']]: GetResultHelper< Schema, (Schema['Tables'] & Schema['Views'])[Field['original']]['Row'], Field['original'], (Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R } ? R : unknown, Field['children'], unknown > extends infer Child ? // One-to-one relationship - referencing column(s) has unique/pkey constraint. HasUniqueFKeyToFRel< RelationName, (Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R } ? R : unknown > extends true ? Child | null : Relationships extends unknown[] ? HasFKeyToFRel extends true ? Field extends { inner: true } ? Child : Child | null : Child[] : Child[] : never } : Field extends { name: string; type: infer T } ? { [K in Field['name']]: T } : Field extends { name: string; original: string } ? Field['original'] extends keyof Row ? { [K in Field['name']]: Row[Field['original']] } : Field['original'] extends 'count' ? { count: number } : SelectQueryError<`Referencing missing column \`${Field['original']}\``> : Record /** * 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`. */ /** * Reads a consecutive sequence of 1 or more letter, where letters are `[0-9a-zA-Z_]`. */ type ReadLetters = string extends Input ? GenericStringError : ReadLettersHelper extends [`${infer Letters}`, `${infer Remainder}`] ? Letters extends '' ? ParserError<`Expected letter at \`${Input}\``> : [Letters, Remainder] : ReadLettersHelper type ReadLettersHelper = string extends Input ? GenericStringError : Input extends `${infer L}${infer Remainder}` ? L extends Letter ? ReadLettersHelper : [Acc, Input] : [Acc, ''] /** * Reads a consecutive sequence of 1 or more double-quoted letters, * where letters are `[^"]`. */ type ReadQuotedLetters = string extends Input ? GenericStringError : Input extends `"${infer Remainder}` ? ReadQuotedLettersHelper extends [`${infer Letters}`, `${infer Remainder}`] ? Letters extends '' ? ParserError<`Expected string at \`${Remainder}\``> : [Letters, Remainder] : ReadQuotedLettersHelper : ParserError<`Not a double-quoted string at \`${Input}\``> type ReadQuotedLettersHelper = string extends Input ? GenericStringError : Input extends `${infer L}${infer Remainder}` ? L extends '"' ? [Acc, Remainder] : ReadQuotedLettersHelper : ParserError<`Missing closing double-quote in \`"${Acc}${Input}\``> /** * Parses a (possibly double-quoted) identifier. * Identifiers are sequences of 1 or more letters. */ type ParseIdentifier = ReadLetters extends [ infer Name, `${infer Remainder}` ] ? [Name, `${Remainder}`] : ReadQuotedLetters extends [infer Name, `${infer Remainder}`] ? [Name, `${Remainder}`] : ParserError<`No (possibly double-quoted) identifier at \`${Input}\``> /** * Parses a field without preceding field renaming. * A field is one of the following: * - a field with an embedded resource * - `field(nodes)` * - `field!hint(nodes)` * - `field!inner(nodes)` * - `field!hint!inner(nodes)` * - a field without an embedded resource (see {@link ParseFieldWithoutEmbeddedResource}) */ type ParseField = Input extends '' ? ParserError<'Empty string'> : ParseIdentifier extends [infer Name, `${infer Remainder}`] ? EatWhitespace extends `!inner${infer Remainder}` ? ParseEmbeddedResource> extends [infer Fields, `${infer Remainder}`] ? // `field!inner(nodes)` [{ name: Name; original: Name; children: Fields; inner: true }, EatWhitespace] : CreateParserErrorIfRequired< ParseEmbeddedResource>, 'Expected embedded resource after `!inner`' > : EatWhitespace extends `!${infer Remainder}` ? ParseIdentifier> extends [infer Hint, `${infer Remainder}`] ? EatWhitespace extends `!inner${infer Remainder}` ? ParseEmbeddedResource> extends [ infer Fields, `${infer Remainder}` ] ? // `field!hint!inner(nodes)` [ { name: Name; original: Name; hint: Hint; children: Fields; inner: true }, EatWhitespace ] : CreateParserErrorIfRequired< ParseEmbeddedResource>, 'Expected embedded resource after `!inner`' > : ParseEmbeddedResource> extends [ infer Fields, `${infer Remainder}` ] ? // `field!hint(nodes)` [{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace] : CreateParserErrorIfRequired< ParseEmbeddedResource>, 'Expected embedded resource after `!hint`' > : ParserError<'Expected identifier after `!`'> : ParseEmbeddedResource> extends [infer Fields, `${infer Remainder}`] ? // `field(nodes)` [{ name: Name; original: Name; children: Fields }, EatWhitespace] : ParseEmbeddedResource> extends ParserError ? // Return error if start of embedded resource was detected but not found. ParseEmbeddedResource> : // Otherwise try to match a field without embedded resource. ParseFieldWithoutEmbeddedResource : ParserError<`Expected identifier 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 ParseFieldWithoutEmbeddedResource = ParseFieldWithoutEmbeddedResourceAndAggregation extends [infer Field, `${infer Remainder}`] ? ParseFieldAggregation> extends [ `${infer AggregateFunction}`, `${infer Remainder}` ] ? ParseFieldTypeCast> extends [infer Type, `${infer Remainder}`] ? // `field.aggregate()::type` [ Omit & { name: AggregateFunction original: AggregateFunction type: Type }, EatWhitespace ] : ParseFieldTypeCast> extends ParserError ? ParseFieldTypeCast> : // `field.aggregate()` [ Omit & { name: AggregateFunction original: AggregateFunction }, EatWhitespace ] : ParseFieldAggregation> extends ParserError ? ParseFieldAggregation> : // `field` [Field, EatWhitespace] : CreateParserErrorIfRequired< ParseFieldWithoutEmbeddedResourceAndAggregation, `Expected field at \`${Input}\`` > /** * Parses a field excluding embedded resources or aggregation, without preceding field renaming. * This is one of the following: * - `field` * - `field::type` * - `field->json...` * - `field->json...::type` */ type ParseFieldWithoutEmbeddedResourceAndAggregation = ParseFieldWithoutEmbeddedResourceAndTypeCast extends [infer Field, `${infer Remainder}`] ? ParseFieldTypeCast> extends [infer Type, `${infer Remainder}`] ? // `field::type` or `field->json...::type` [Omit & { type: Type }, EatWhitespace] : ParseFieldTypeCast> extends ParserError ? ParseFieldTypeCast> : // `field` or `field->json...` [Field, EatWhitespace] : CreateParserErrorIfRequired< ParseFieldWithoutEmbeddedResourceAndTypeCast, `Expected field at \`${Input}\`` > /** * Parses a field excluding embedded resources or typecasting, without preceding field renaming. * This is one of the following: * - `field` * - `field->json...` */ type ParseFieldWithoutEmbeddedResourceAndTypeCast = ParseIdentifier extends [infer Name, `${infer Remainder}`] ? ParseJsonAccessor> extends [ infer PropertyName, infer PropertyType, `${infer Remainder}` ] ? // `field->json...` [ { name: PropertyName; original: PropertyName; type: PropertyType }, EatWhitespace ] : // `field` [{ name: Name; original: Name }, EatWhitespace] : ParserError<`Expected field at \`${Input}\``> /** * Parses a field typecast (`::type`), returning a tuple of ["Type", "Remainder of text"] * or the original string input indicating that no typecast was found. */ type ParseFieldTypeCast = EatWhitespace extends `::${infer Remainder}` ? ParseIdentifier> extends [`${infer CastType}`, `${infer Remainder}`] ? // Ensure that `CastType` is a valid type. CastType extends PostgreSQLTypes ? [TypeScriptTypes, EatWhitespace] : ParserError<`Invalid type for \`::\` operator \`${CastType}\``> : ParserError<`Invalid type for \`::\` operator at \`${Remainder}\``> : Input /** * Parses a field aggregation (`.max()`), returning a tuple of ["Aggregate function", "Remainder of text"] * or the original string input indicating that no aggregation was found. */ type ParseFieldAggregation = EatWhitespace extends `.${infer Remainder}` ? ParseIdentifier> extends [ `${infer FunctionName}`, `${infer Remainder}` ] ? // Ensure that aggregation function is valid. FunctionName extends AggregateFunctions ? EatWhitespace extends `()${infer Remainder}` ? [FunctionName, EatWhitespace] : ParserError<`Expected \`()\` after \`.\` operator \`${FunctionName}\``> : ParserError<`Invalid type for \`.\` operator \`${FunctionName}\``> : ParserError<`Invalid type for \`.\` operator at \`${Remainder}\``> : Input /** * 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}` ? [{ star: true }, EatWhitespace] : // `...field` Input extends `...${infer Remainder}` ? ParseField> extends [infer Field, `${infer Remainder}`] ? Field extends { children: unknown[] } ? [Prettify<{ spread: true } & Field>, EatWhitespace] : ParserError<'Unable to parse spread resource'> : ParserError<'Unable to parse spread resource'> : ParseIdentifier extends [infer Name, `${infer Remainder}`] ? EatWhitespace extends `::${infer _Remainder}` ? // `field::` // Special case to detect type-casting before renaming. ParseField : EatWhitespace extends `:${infer Remainder}` ? // `renamed_field:` ParseField> extends [infer Field, `${infer Remainder}`] ? Field extends { name: string } ? [Prettify & { name: Name }>, EatWhitespace] : ParserError<`Unable to parse renamed field`> : ParserError<`Unable to parse renamed field`> : // Otherwise, just parse it as a field without renaming. ParseField : 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"] * or the original string input indicating that no opening `->` was found. */ type ParseJsonAccessor = Input extends `->${infer Remainder}` ? Remainder extends `>${infer Remainder}` ? ParseIdentifier extends [infer Name, `${infer Remainder}`] ? [Name, string, 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 `->`'> : 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}` ? ParseNodes> extends [infer Fields, `${infer Remainder}`] ? EatWhitespace extends `)${infer Remainder}` ? [Fields, EatWhitespace] : ParserError<`Expected ")"`> : // If no nodes were detected, check for `)` for empty embedded resources `()`. ParseNodes> extends ParserError ? EatWhitespace extends `)${infer Remainder}` ? [[], EatWhitespace] : ParseNodes> : ParserError<'Expected embedded resource fields or `)`'> : Input /** * 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 Field, `${infer Remainder}` ] ? EatWhitespace extends `,${infer Remainder}` ? ParseNodesHelper, [Field, ...Fields]> : [[Field, ...Fields], EatWhitespace] : ParseNode /** * 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. */ type ParseQuery = string extends Query ? GenericStringError : ParseNodes> extends [infer Fields, `${infer Remainder}`] ? EatWhitespace extends '' ? Fields : ParserError<`Unexpected input: ${Remainder}`> : ParseNodes> type GetResultHelper< Schema extends GenericSchema, Row extends Record, RelationName, Relationships, Fields extends unknown[], Acc > = Fields extends [infer R] ? ConstructFieldDefinition extends SelectQueryError< infer E > ? SelectQueryError : GetResultHelper< Schema, Row, RelationName, Relationships, [], ConstructFieldDefinition & Acc > : Fields extends [infer R, ...infer Rest] ? ConstructFieldDefinition extends SelectQueryError< infer E > ? SelectQueryError : GetResultHelper< Schema, Row, RelationName, Relationships, Rest, ConstructFieldDefinition & Acc > : Prettify /** * Constructs a type definition for an object based on a given PostgREST query. * * @param Schema Database schema. * @param Row Type of a row in the given table. * @param Relationships Relationships between different tables in the database. * @param Query Select query string literal to parse. */ export type GetResult< Schema extends GenericSchema, Row extends Record, RelationName, Relationships, Query extends string > = ParseQuery extends unknown[] ? GetResultHelper, unknown> : ParseQuery