// Credits to @bnjmnt4n (https://www.npmjs.com/package/postgrest-query) 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[] /** * Parser errors. */ type ParserError = { error: true } & Message type GenericStringError = ParserError<'Received a generic string'> export type SelectQueryError = { error: true } & Message /** * Trims whitespace from the left of the input. */ type EatWhitespace = string extends Input ? GenericStringError : Input extends `${Whitespace}${infer Remainder}` ? EatWhitespace : Input type HasFKey = Relationships extends [infer R] ? R extends { foreignKeyName: FKeyName } ? true : false : Relationships extends [infer R, ...infer Rest] ? HasFKey extends true ? true : HasFKey : false 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 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 Definitions Record of definitions, possibly generated from PostgREST's OpenAPI spec. * @param Name Name of the table being queried. * @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 { 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 ? 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 ? Child | null : Child[] : Child[] : never } : Field extends { name: string; original: string } ? Field['original'] extends keyof Row ? { [K in Field['name']]: Row[Field['original']] } : SelectQueryError<`Referencing missing column \`${Field['original']}\``> : Field extends { name: string; type: infer T } ? { [K in Field['name']]: T } : 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 more than 1 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 more than 1 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. * For now, identifiers are just sequences of more than 1 letter. */ 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 node. * A node is one of the following: * - `*` * - `field` * - `field->json...` * - `field(nodes)` * - `field!hint(nodes)` * - `field!inner(nodes)` * - `field!hint!inner(nodes)` * - `renamed_field:field` * - `renamed_field:field->json...` * - `renamed_field:field(nodes)` * - `renamed_field:field!hint(nodes)` * - `renamed_field:field!inner(nodes)` * - `renamed_field:field!hint!inner(nodes)` * * TODO: casting operators `::text`, more support for JSON operators `->`, `->>`. */ type ParseNode = Input extends '' ? ParserError<'Empty string'> : // `*` Input extends `*${infer Remainder}` ? [{ star: true }, EatWhitespace] : 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 }, EatWhitespace] : ParseEmbeddedResource> extends ParserError ? ParseEmbeddedResource> : ParserError<'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 }, EatWhitespace] : ParseEmbeddedResource> extends ParserError ? ParseEmbeddedResource> : ParserError<'Expected embedded resource after `!inner`'> : ParseEmbeddedResource> extends [ infer Fields, `${infer Remainder}` ] ? // `field!hint(nodes)` [{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace] : ParseEmbeddedResource> extends ParserError ? ParseEmbeddedResource> : ParserError<'Expected embedded resource after `!hint`'> : ParserError<'Expected identifier after `!`'> : EatWhitespace extends `:${infer Remainder}` ? ParseIdentifier> extends [infer OriginalName, `${infer Remainder}`] ? EatWhitespace extends `!inner${infer Remainder}` ? ParseEmbeddedResource> extends [ infer Fields, `${infer Remainder}` ] ? // `renamed_field:field!inner(nodes)` [{ name: Name; original: OriginalName; children: Fields }, EatWhitespace] : ParseEmbeddedResource> extends ParserError ? ParseEmbeddedResource> : ParserError<'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}` ] ? // `renamed_field:field!hint!inner(nodes)` [ { name: Name; original: OriginalName; hint: Hint; children: Fields }, EatWhitespace ] : ParseEmbeddedResource> extends ParserError ? ParseEmbeddedResource> : ParserError<'Expected embedded resource after `!inner`'> : ParseEmbeddedResource> extends [ infer Fields, `${infer Remainder}` ] ? // `renamed_field:field!hint(nodes)` [ { name: Name original: OriginalName hint: Hint children: Fields }, EatWhitespace ] : ParseEmbeddedResource> extends ParserError ? ParseEmbeddedResource> : ParserError<'Expected embedded resource after `!hint`'> : ParserError<'Expected identifier after `!`'> : ParseEmbeddedResource> extends [ infer Fields, `${infer Remainder}` ] ? // `renamed_field:field(nodes)` [{ name: Name; original: OriginalName; children: Fields }, EatWhitespace] : ParseJsonAccessor> extends [ infer _PropertyName, infer PropertyType, `${infer Remainder}` ] ? // `renamed_field:field->json...` [{ name: Name; type: PropertyType }, EatWhitespace] : ParseEmbeddedResource> extends ParserError ? ParseEmbeddedResource> : // `renamed_field:field` [{ name: Name; original: OriginalName }, EatWhitespace] : ParseIdentifier> : ParseEmbeddedResource> extends [infer Fields, `${infer Remainder}`] ? // `field(nodes)` [{ name: Name; original: Name; children: Fields }, EatWhitespace] : ParseJsonAccessor> extends [ infer PropertyName, infer PropertyType, `${infer Remainder}` ] ? // `field->json...` [{ name: PropertyName; type: PropertyType }, EatWhitespace] : ParseEmbeddedResource> extends ParserError ? ParseEmbeddedResource> : // `field` [{ name: Name; original: Name }, EatWhitespace] : 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 * 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 extends [] ? ParserError<'Expected fields after `(`'> : [Fields, EatWhitespace] : ParserError<`Expected ")"`> : ParseNodes> : 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 Row Record. * @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