Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: query parser cleanup #496

Merged
merged 3 commits into from
Feb 8, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 81 additions & 112 deletions src/select-query-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ type ParserError<Message extends string> = { error: true } & Message
type GenericStringError = ParserError<'Received a generic string'>
export type SelectQueryError<Message extends string> = { error: true } & Message

/**
* Creates a new {@link ParserError} if the given input is not already a parser error.
*/
type CreateParserErrorIfRequired<Input, Message extends string> = Input extends ParserError<string>
? Input
: ParserError<Message>

/**
* Trims whitespace from the left of the input.
*/
Expand All @@ -118,6 +125,9 @@ type EatWhitespace<Input extends string> = string extends Input
? EatWhitespace<Remainder>
: Input

/**
* Returns a boolean representing whether there is a foreign key with the given name.
*/
type HasFKey<FKeyName, Relationships> = Relationships extends [infer R]
? R extends { foreignKeyName: FKeyName }
? true
Expand All @@ -128,6 +138,9 @@ type HasFKey<FKeyName, Relationships> = Relationships extends [infer R]
: HasFKey<FKeyName, Rest>
: false

/**
* Returns a boolean representing whether there the foreign key has a unique constraint.
*/
type HasUniqueFKey<FKeyName, Relationships> = Relationships extends [infer R]
? R extends { foreignKeyName: FKeyName; isOneToOne: true }
? true
Expand All @@ -138,6 +151,10 @@ type HasUniqueFKey<FKeyName, Relationships> = Relationships extends [infer R]
: HasUniqueFKey<FKeyName, Rest>
: false

/**
* Returns a boolean representing whether there is a foreign key referencing
* a given relation.
*/
type HasFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
? R extends { referencedRelation: FRelName }
? true
Expand All @@ -161,8 +178,9 @@ type HasUniqueFKeyToFRel<FRelName, Relationships> = Relationships extends [infer
/**
* 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 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<
Expand Down Expand Up @@ -231,12 +249,12 @@ type ConstructFieldDefinition<
: 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']] }
: SelectQueryError<`Referencing missing column \`${Field['original']}\``>
: Field extends { name: string; type: infer T }
? { [K in Field['name']]: T }
: Record<string, unknown>

/**
Expand All @@ -246,8 +264,7 @@ type ConstructFieldDefinition<
*/

/**
* Reads a consecutive sequence of more than 1 letter,
* where letters are `[0-9a-zA-Z_]`.
* Reads a consecutive sequence of 1 or more letter, where letters are `[0-9a-zA-Z_]`.
*/
type ReadLetters<Input extends string> = string extends Input
? GenericStringError
Expand All @@ -266,7 +283,7 @@ type ReadLettersHelper<Input extends string, Acc extends string> = string extend
: [Acc, '']

/**
* Reads a consecutive sequence of more than 1 double-quoted letters,
* Reads a consecutive sequence of 1 or more double-quoted letters,
* where letters are `[^"]`.
*/
type ReadQuotedLetters<Input extends string> = string extends Input
Expand All @@ -289,7 +306,7 @@ type ReadQuotedLettersHelper<Input extends string, Acc extends string> = string

/**
* Parses a (possibly double-quoted) identifier.
* For now, identifiers are just sequences of more than 1 letter.
* Identifiers are sequences of 1 or more letters.
*/
type ParseIdentifier<Input extends string> = ReadLetters<Input> extends [
infer Name,
Expand All @@ -301,46 +318,29 @@ type ParseIdentifier<Input extends string> = ReadLetters<Input> extends [
: ParserError<`No (possibly double-quoted) identifier at \`${Input}\``>

/**
* Parses a node.
* A node is one of the following:
* - `*`
* Parses a field without preceding field renaming.
* A field is one of the following:
* - `field`
* - `field::type`
* - `field->json...`
* - `field(nodes)`
* - `field!hint(nodes)`
* - `field!inner(nodes)`
* - `field!hint!inner(nodes)`
* - `renamed_field:field`
* - `renamed_field:field::type`
* - `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: more support for JSON operators `->`, `->>`.
* TODO: support type casting of JSON operators `a->b::type`, `a->>b::type`.
*/
type ParseNode<Input extends string> = Input extends ''
type ParseField<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 `::${infer Remainder}`
? ParseIdentifier<Remainder> extends [infer CastType, `${infer Remainder}`]
? // `field::type`
CastType extends PostgreSQLTypes
? [{ name: Name; type: TypeScriptTypes<CastType> }, EatWhitespace<Remainder>]
: never
: ParserError<`Unexpected type cast at \`${Input}\``>
: EatWhitespace<Remainder> extends `!inner${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`'>
: CreateParserErrorIfRequired<
ParseEmbeddedResource<EatWhitespace<Remainder>>,
'Expected embedded resource after `!inner`'
>
: EatWhitespace<Remainder> extends `!${infer Remainder}`
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer Hint, `${infer Remainder}`]
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
Expand All @@ -350,89 +350,21 @@ type ParseNode<Input extends string> = Input extends ''
]
? // `field!hint!inner(nodes)`
[{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace<Remainder>]
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<'Expected embedded resource after `!inner`'>
: CreateParserErrorIfRequired<
ParseEmbeddedResource<EatWhitespace<Remainder>>,
'Expected embedded resource after `!inner`'
>
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Fields,
`${infer Remainder}`
]
? // `field!hint(nodes)`
[{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace<Remainder>]
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<'Expected embedded resource after `!hint`'>
: CreateParserErrorIfRequired<
ParseEmbeddedResource<EatWhitespace<Remainder>>,
'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 `::${infer Remainder}`
? ParseIdentifier<Remainder> extends [infer CastType, `${infer Remainder}`]
? // `renamed_field:field::type`
CastType extends PostgreSQLTypes
? [{ name: Name; type: TypeScriptTypes<CastType> }, EatWhitespace<Remainder>]
: never
: ParserError<`Unexpected type cast at \`${Input}\``>
: 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; hint: Hint; 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
hint: Hint
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>]
Expand All @@ -442,13 +374,48 @@ type ParseNode<Input extends string> = Input extends ''
`${infer Remainder}`
]
? // `field->json...`
[{ name: PropertyName; type: PropertyType }, EatWhitespace<Remainder>]
[{ name: PropertyName; original: PropertyName; type: PropertyType }, EatWhitespace<Remainder>]
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
? ParseEmbeddedResource<EatWhitespace<Remainder>>
: EatWhitespace<Remainder> extends `::${infer Remainder}`
? ParseIdentifier<Remainder> extends [`${infer CastType}`, `${infer Remainder}`]
? // `field::type`
CastType extends PostgreSQLTypes
? [{ name: Name; type: TypeScriptTypes<CastType> }, EatWhitespace<Remainder>]
: ParserError<`Invalid type for \`::\` operator \`${CastType}\``>
: ParserError<`Invalid type for \`::\` operator at \`${Remainder}\``>
: // `field`
[{ name: Name; original: Name }, EatWhitespace<Remainder>]
: ParserError<`Expected identifier at \`${Input}\``>

/**
* Parses a node.
* A node is one of the following:
* - `*`
* - a field, as defined above
* - a renamed field, `renamed_field:field`
*/
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 `::${infer _Remainder}`
? // `field::`
// Special case to detect type-casting before renaming.
ParseField<Input>
: EatWhitespace<Remainder> extends `:${infer Remainder}`
? // `renamed_field:`
ParseField<EatWhitespace<Remainder>> extends [infer Field, `${infer Remainder}`]
? Field extends { name: string }
? [Prettify<Omit<Field, 'name'> & { name: Name }>, EatWhitespace<Remainder>]
: ParserError<`Unable to parse renamed field`>
: ParserError<`Unable to parse renamed field`>
: // Otherwise, just parse it as a field without renaming.
ParseField<Input>
: 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 ->.
Expand Down Expand Up @@ -560,7 +527,9 @@ type GetResultHelper<
/**
* Constructs a type definition for an object based on a given PostgREST query.
*
* @param Row Record<string, unknown>.
* @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<
Expand Down
Loading