From 1de25c88342575345e43219d6cbaba84361b03f4 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sat, 4 Nov 2023 18:20:48 +0800 Subject: [PATCH 1/3] Improve comments --- src/select-query-parser.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/select-query-parser.ts b/src/select-query-parser.ts index 07fa8ceb..6043036e 100644 --- a/src/select-query-parser.ts +++ b/src/select-query-parser.ts @@ -118,6 +118,9 @@ type EatWhitespace = string extends Input ? 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 @@ -128,6 +131,9 @@ type HasFKey = Relationships extends [infer R] : 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 @@ -138,6 +144,10 @@ type HasUniqueFKey = Relationships extends [infer R] : 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 @@ -161,8 +171,9 @@ type HasUniqueFKeyToFRel = 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< @@ -246,8 +257,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 = string extends Input ? GenericStringError @@ -266,7 +276,7 @@ type ReadLettersHelper = 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 = string extends Input @@ -289,7 +299,7 @@ type ReadQuotedLettersHelper = 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 = ReadLetters extends [ infer Name, @@ -560,7 +570,9 @@ type GetResultHelper< /** * Constructs a type definition for an object based on a given PostgREST query. * - * @param Row Record. + * @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< From 8c1ab9793755ea351c652ef167a33979c2374d48 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sat, 4 Nov 2023 18:20:49 +0800 Subject: [PATCH 2/3] Add parser error helper --- src/select-query-parser.ts | 49 ++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/select-query-parser.ts b/src/select-query-parser.ts index 6043036e..bba0028e 100644 --- a/src/select-query-parser.ts +++ b/src/select-query-parser.ts @@ -109,6 +109,13 @@ 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. */ @@ -348,9 +355,10 @@ type ParseNode = Input extends '' ? 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`'> + : CreateParserErrorIfRequired< + ParseEmbeddedResource>, + 'Expected embedded resource after `!inner`' + > : EatWhitespace extends `!${infer Remainder}` ? ParseIdentifier> extends [infer Hint, `${infer Remainder}`] ? EatWhitespace extends `!inner${infer Remainder}` @@ -360,18 +368,20 @@ type ParseNode = Input extends '' ] ? // `field!hint!inner(nodes)` [{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace] - : ParseEmbeddedResource> extends ParserError - ? ParseEmbeddedResource> - : ParserError<'Expected embedded resource after `!inner`'> + : 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] - : ParseEmbeddedResource> extends ParserError - ? ParseEmbeddedResource> - : ParserError<'Expected embedded resource after `!hint`'> + : CreateParserErrorIfRequired< + ParseEmbeddedResource>, + 'Expected embedded resource after `!hint`' + > : ParserError<'Expected identifier after `!`'> : EatWhitespace extends `:${infer Remainder}` ? ParseIdentifier> extends [infer OriginalName, `${infer Remainder}`] @@ -389,9 +399,10 @@ type ParseNode = Input extends '' ] ? // `renamed_field:field!inner(nodes)` [{ name: Name; original: OriginalName; children: Fields }, EatWhitespace] - : ParseEmbeddedResource> extends ParserError - ? ParseEmbeddedResource> - : ParserError<'Expected embedded resource after `!inner`'> + : CreateParserErrorIfRequired< + ParseEmbeddedResource>, + 'Expected embedded resource after `!inner`' + > : EatWhitespace extends `!${infer Remainder}` ? ParseIdentifier> extends [infer Hint, `${infer Remainder}`] ? EatWhitespace extends `!inner${infer Remainder}` @@ -404,9 +415,10 @@ type ParseNode = Input extends '' { name: Name; original: OriginalName; hint: Hint; children: Fields }, EatWhitespace ] - : ParseEmbeddedResource> extends ParserError - ? ParseEmbeddedResource> - : ParserError<'Expected embedded resource after `!inner`'> + : CreateParserErrorIfRequired< + ParseEmbeddedResource>, + 'Expected embedded resource after `!inner`' + > : ParseEmbeddedResource> extends [ infer Fields, `${infer Remainder}` @@ -421,9 +433,10 @@ type ParseNode = Input extends '' }, EatWhitespace ] - : ParseEmbeddedResource> extends ParserError - ? ParseEmbeddedResource> - : ParserError<'Expected embedded resource after `!hint`'> + : CreateParserErrorIfRequired< + ParseEmbeddedResource>, + 'Expected embedded resource after `!hint`' + > : ParserError<'Expected identifier after `!`'> : ParseEmbeddedResource> extends [ infer Fields, From 22e0003d8f5325575d96c870a2b732724d79f416 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sat, 4 Nov 2023 18:20:50 +0800 Subject: [PATCH 3/3] Add `ParseField` helper This removes the duplication of parsing required for renamed fields and non-renamed fields. --- src/select-query-parser.ts | 142 +++++++++++-------------------------- 1 file changed, 43 insertions(+), 99 deletions(-) diff --git a/src/select-query-parser.ts b/src/select-query-parser.ts index bba0028e..114d59d5 100644 --- a/src/select-query-parser.ts +++ b/src/select-query-parser.ts @@ -249,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 /** @@ -318,9 +318,8 @@ type ParseIdentifier = ReadLetters 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...` @@ -328,30 +327,13 @@ type ParseIdentifier = ReadLetters extends [ * - `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 '' +type ParseField = Input extends '' ? ParserError<'Empty string'> - : // `*` - Input extends `*${infer Remainder}` - ? [{ star: true }, EatWhitespace] : ParseIdentifier extends [infer Name, `${infer Remainder}`] - ? EatWhitespace extends `::${infer Remainder}` - ? ParseIdentifier extends [infer CastType, `${infer Remainder}`] - ? // `field::type` - CastType extends PostgreSQLTypes - ? [{ name: Name; type: TypeScriptTypes }, EatWhitespace] - : never - : ParserError<`Unexpected type cast at \`${Input}\``> - : EatWhitespace extends `!inner${infer Remainder}` + ? EatWhitespace extends `!inner${infer Remainder}` ? ParseEmbeddedResource> extends [infer Fields, `${infer Remainder}`] ? // `field!inner(nodes)` [{ name: Name; original: Name; children: Fields }, EatWhitespace] @@ -383,79 +365,6 @@ type ParseNode = Input extends '' 'Expected embedded resource after `!hint`' > : ParserError<'Expected identifier after `!`'> - : EatWhitespace extends `:${infer Remainder}` - ? ParseIdentifier> extends [infer OriginalName, `${infer Remainder}`] - ? EatWhitespace extends `::${infer Remainder}` - ? ParseIdentifier extends [infer CastType, `${infer Remainder}`] - ? // `renamed_field:field::type` - CastType extends PostgreSQLTypes - ? [{ name: Name; type: TypeScriptTypes }, EatWhitespace] - : never - : ParserError<`Unexpected type cast at \`${Input}\``> - : EatWhitespace extends `!inner${infer Remainder}` - ? ParseEmbeddedResource> extends [ - infer Fields, - `${infer Remainder}` - ] - ? // `renamed_field:field!inner(nodes)` - [{ name: Name; original: OriginalName; children: Fields }, 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}` - ] - ? // `renamed_field:field!hint!inner(nodes)` - [ - { name: Name; original: OriginalName; hint: Hint; children: Fields }, - EatWhitespace - ] - : CreateParserErrorIfRequired< - ParseEmbeddedResource>, - '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 - ] - : CreateParserErrorIfRequired< - ParseEmbeddedResource>, - '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] @@ -465,13 +374,48 @@ type ParseNode = Input extends '' `${infer Remainder}` ] ? // `field->json...` - [{ name: PropertyName; type: PropertyType }, EatWhitespace] + [{ name: PropertyName; original: PropertyName; type: PropertyType }, EatWhitespace] : ParseEmbeddedResource> extends ParserError ? ParseEmbeddedResource> + : EatWhitespace extends `::${infer Remainder}` + ? ParseIdentifier extends [`${infer CastType}`, `${infer Remainder}`] + ? // `field::type` + CastType extends PostgreSQLTypes + ? [{ name: Name; type: TypeScriptTypes }, EatWhitespace] + : ParserError<`Invalid type for \`::\` operator \`${CastType}\``> + : ParserError<`Invalid type for \`::\` operator at \`${Remainder}\``> : // `field` [{ name: Name; original: Name }, EatWhitespace] : 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 '' + ? ParserError<'Empty string'> + : // `*` + Input extends `*${infer Remainder}` + ? [{ star: true }, EatWhitespace] + : 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 ->.