@miuiu/postgrest
Version:
Isomorphic PostgREST client
469 lines (443 loc) • 14.3 kB
text/typescript
// 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<LowerAlphabet>;
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[];
// /**
// * Parsed node types.
// * Currently only `*` and all other fields.
// */
// type ParsedNode =
// | { star: true }
// | { name: string; original: string }
// | { name: string; foreignTable: true }
// | { name: string; type: T };
/**
* Parser errors.
*/
type ParserError<Message extends string> = { error: true } & Message;
type GenericStringError = ParserError<"Received a generic string">;
/**
* Trims whitespace from the left of the input.
*/
type EatWhitespace<Input extends string> = string extends Input
? GenericStringError
: Input extends `${Whitespace}${infer Remainder}`
? EatWhitespace<Remainder>
: Input;
/**
* 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<string, unknown>,
Field,
> = Field extends {
star: true;
}
? Row
: Field extends { name: string; original: string; children: unknown[] }
? {
[_ in Field["name"]]: GetResultHelper<
Schema,
(Schema["Tables"] & Schema["Views"])[Field["original"]]["Row"],
Field["children"],
unknown
> extends infer Child
? Child | Child[] | null
: never;
}
: Field extends { name: string; original: string }
? { [K in Field["name"]]: Row[Field["original"]] }
: Field extends { name: string; type: infer T }
? { [K in Field["name"]]: T }
: Record<string, unknown>;
/**
* 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<Input extends string> = string extends Input
? GenericStringError
: ReadLettersHelper<Input, ""> extends [
`${infer Letters}`,
`${infer Remainder}`,
]
? Letters extends ""
? ParserError<`Expected letter at \`${Input}\``>
: [Letters, Remainder]
: ReadLettersHelper<Input, "">;
type ReadLettersHelper<
Input extends string,
Acc extends string,
> = string extends Input
? GenericStringError
: Input extends `${infer L}${infer Remainder}`
? L extends Letter
? ReadLettersHelper<Remainder, `${Acc}${L}`>
: [Acc, Input]
: [Acc, ""];
/**
* Reads a consecutive sequence of more than 1 double-quoted letters,
* where letters are `[^"]`.
*/
type ReadQuotedLetters<Input extends string> = string extends Input
? GenericStringError
: Input extends `"${infer Remainder}`
? ReadQuotedLettersHelper<Remainder, ""> extends [
`${infer Letters}`,
`${infer Remainder}`,
]
? Letters extends ""
? ParserError<`Expected string at \`${Remainder}\``>
: [Letters, Remainder]
: ReadQuotedLettersHelper<Remainder, "">
: ParserError<`Not a double-quoted string at \`${Input}\``>;
type ReadQuotedLettersHelper<
Input extends string,
Acc extends string,
> = string extends Input
? GenericStringError
: Input extends `${infer L}${infer Remainder}`
? L extends '"'
? [Acc, Remainder]
: ReadQuotedLettersHelper<Remainder, `${Acc}${L}`>
: 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<Input extends string> = ReadLetters<Input> extends [
infer Name,
`${infer Remainder}`,
]
? [Name, `${Remainder}`]
: ReadQuotedLetters<Input> 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 string> = Input extends ""
? ParserError<"Empty string">
: // `*`
Input extends `*${infer Remainder}`
? [{ star: true }, EatWhitespace<Remainder>]
: ParseIdentifier<Input> extends [infer Name, `${infer Remainder}`]
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`,
]
? // `field!inner(nodes)`
[
{ name: Name; original: Name; children: Fields },
EatWhitespace<Remainder>,
]
: ParseEmbeddedResource<
EatWhitespace<Remainder>
> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<"Expected embedded resource after `!inner`">
: EatWhitespace<Remainder> extends `!${infer Remainder}`
? ParseIdentifier<EatWhitespace<Remainder>> extends [
infer _Hint,
`${infer Remainder}`,
]
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`,
]
? // `field!hint!inner(nodes)`
[
{ name: Name; original: Name; children: Fields },
EatWhitespace<Remainder>,
]
: ParseEmbeddedResource<
EatWhitespace<Remainder>
> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<"Expected embedded resource after `!inner`">
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`,
]
? // `field!hint(nodes)`
[
{ name: Name; original: Name; children: Fields },
EatWhitespace<Remainder>,
]
: ParseEmbeddedResource<
EatWhitespace<Remainder>
> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<"Expected embedded resource after `!hint`">
: ParserError<"Expected identifier after `!`">
: EatWhitespace<Remainder> extends `:${infer Remainder}`
? ParseIdentifier<EatWhitespace<Remainder>> extends [
infer OriginalName,
`${infer Remainder}`,
]
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`,
]
? // `renamed_field:field!inner(nodes)`
[
{ name: Name; original: OriginalName; children: Fields },
EatWhitespace<Remainder>,
]
: ParseEmbeddedResource<
EatWhitespace<Remainder>
> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<"Expected embedded resource after `!inner`">
: EatWhitespace<Remainder> extends `!${infer Remainder}`
? ParseIdentifier<EatWhitespace<Remainder>> extends [
infer _Hint,
`${infer Remainder}`,
]
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`,
]
? // `renamed_field:field!hint!inner(nodes)`
[
{ name: Name; original: OriginalName; children: Fields },
EatWhitespace<Remainder>,
]
: ParseEmbeddedResource<
EatWhitespace<Remainder>
> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<"Expected embedded resource after `!inner`">
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`,
]
? // `renamed_field:field!hint(nodes)`
[
{
name: Name;
original: OriginalName;
children: Fields;
},
EatWhitespace<Remainder>,
]
: ParseEmbeddedResource<
EatWhitespace<Remainder>
> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<"Expected embedded resource after `!hint`">
: ParserError<"Expected identifier after `!`">
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`,
]
? // `renamed_field:field(nodes)`
[
{ name: Name; original: OriginalName; children: Fields },
EatWhitespace<Remainder>,
]
: ParseJsonAccessor<EatWhitespace<Remainder>> extends [
infer _PropertyName,
infer PropertyType,
`${infer Remainder}`,
]
? // `renamed_field:field->json...`
[{ name: Name; type: PropertyType }, EatWhitespace<Remainder>]
: ParseEmbeddedResource<
EatWhitespace<Remainder>
> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: // `renamed_field:field`
[{ name: Name; original: OriginalName }, EatWhitespace<Remainder>]
: ParseIdentifier<EatWhitespace<Remainder>>
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`,
]
? // `field(nodes)`
[
{ name: Name; original: Name; children: Fields },
EatWhitespace<Remainder>,
]
: ParseJsonAccessor<EatWhitespace<Remainder>> extends [
infer PropertyName,
infer PropertyType,
`${infer Remainder}`,
]
? // `field->json...`
[{ name: PropertyName; type: PropertyType }, EatWhitespace<Remainder>]
: ParseEmbeddedResource<
EatWhitespace<Remainder>
> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: // `field`
[{ name: Name; original: Name }, EatWhitespace<Remainder>]
: 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 string> =
Input extends `->${infer Remainder}`
? Remainder extends `>${infer Remainder}`
? ParseIdentifier<Remainder> extends [infer Name, `${infer Remainder}`]
? [Name, string, EatWhitespace<Remainder>]
: ParserError<"Expected property name after `->>`">
: ParseIdentifier<Remainder> extends [infer Name, `${infer Remainder}`]
? ParseJsonAccessor<Remainder> extends [
infer PropertyName,
infer PropertyType,
`${infer Remainder}`,
]
? [PropertyName, PropertyType, EatWhitespace<Remainder>]
: [Name, Json, EatWhitespace<Remainder>]
: 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 string> =
Input extends `(${infer Remainder}`
? ParseNodes<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`,
]
? EatWhitespace<Remainder> extends `)${infer Remainder}`
? Fields extends []
? ParserError<"Expected fields after `(`">
: [Fields, EatWhitespace<Remainder>]
: ParserError<`Expected ")"`>
: ParseNodes<EatWhitespace<Remainder>>
: Input;
/**
* Parses a sequence of nodes, separated by `,`.
*
* Returns a tuple of ["Parsed fields", "Remainder of text"] or an error.
*/
type ParseNodes<Input extends string> = string extends Input
? GenericStringError
: ParseNodesHelper<Input, []>;
type ParseNodesHelper<
Input extends string,
Fields extends unknown[],
> = ParseNode<Input> extends [infer Field, `${infer Remainder}`]
? EatWhitespace<Remainder> extends `,${infer Remainder}`
? ParseNodesHelper<EatWhitespace<Remainder>, [Field, ...Fields]>
: [[Field, ...Fields], EatWhitespace<Remainder>]
: ParseNode<Input>;
/**
* 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<Query extends string> = string extends Query
? GenericStringError
: ParseNodes<EatWhitespace<Query>> extends [
infer Fields,
`${infer Remainder}`,
]
? EatWhitespace<Remainder> extends ""
? Fields
: ParserError<`Unexpected input: ${Remainder}`>
: ParseNodes<EatWhitespace<Query>>;
type GetResultHelper<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Fields extends unknown[],
Acc,
> = Fields extends [infer R]
? GetResultHelper<
Schema,
Row,
[],
ConstructFieldDefinition<Schema, Row, R> & Acc
>
: Fields extends [infer R, ...infer Rest]
? GetResultHelper<
Schema,
Row,
Rest,
ConstructFieldDefinition<Schema, Row, R> & Acc
>
: Prettify<Acc>;
/**
* Constructs a type definition for an object based on a given PostgREST query.
*
* @param Row Record<string, unknown>.
* @param Query Select query string literal to parse.
*/
export type GetResult<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
Query extends string,
> = ParseQuery<Query> extends unknown[]
? GetResultHelper<Schema, Row, ParseQuery<Query>, unknown>
: ParseQuery<Query>;