Last active
May 24, 2023 22:14
-
-
Save bradennapier/d7d53b087b214aef93a158840af7e15c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-unused-vars */ | |
/* eslint-disable @typescript-eslint/no-shadow */ | |
// Credits to @bnjmnt4n (https://www.npmjs.com/package/postgrest-query) | |
import type { GenericSchema } from '@supabase/supabase-js/dist/module/lib/types'; | |
// https://twitter.com/mattpocockuk/status/1622730173446557697 | |
type Prettify<T> = { [K in keyof T]: T[K] } & {}; | |
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; | |
type OptFlags = { null?: boolean; single?: boolean; many?: boolean }; | |
type ParseOptFlags<Output, Opts extends OptFlags> = [ | |
Opts['null'] extends false ? never : null, | |
Opts['many'] extends false ? never : Output[], | |
Opts['single'] extends false ? never : Output, | |
][number]; | |
/** | |
* 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, | |
Opts extends OptFlags = { maybe: false; single: true; many: false }, | |
> = 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'], | |
Opts, | |
unknown | |
> extends infer Child | |
? ParseOptFlags<Child, Opts> | |
: 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[], | |
Opts extends OptFlags, | |
Acc, | |
> = Fields extends [infer R] | |
? GetResultHelper< | |
Schema, | |
Row, | |
[], | |
Opts, | |
ConstructFieldDefinition<Schema, Row, R, Opts> & Acc | |
> | |
: Fields extends [infer R, ...infer Rest] | |
? GetResultHelper< | |
Schema, | |
Row, | |
Rest, | |
Opts, | |
ConstructFieldDefinition<Schema, Row, R, Opts> & 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, | |
Opts extends OptFlags = { null: true; single: true; many: true }, | |
> = ParseQuery<Query> extends unknown[] | |
? GetResultHelper<Schema, Row, ParseQuery<Query>, Opts, unknown> | |
: ParseQuery<Query>; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment