diff --git a/src/select-query-parser.ts b/src/select-query-parser.ts index 07fa8ceb..114d59d5 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. */ @@ -118,6 +125,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 +138,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 +151,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 +178,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< @@ -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 /** @@ -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 = string extends Input ? GenericStringError @@ -266,7 +283,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 +306,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, @@ -301,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...` @@ -311,36 +327,20 @@ 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] - : 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}` @@ -350,89 +350,21 @@ 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}`] - ? 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] - : 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] @@ -442,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 ->. @@ -560,7 +527,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<