wip:milestone 0 fixes
Some checks failed
CI/CD Pipeline / unit-tests (push) Failing after 1m16s
CI/CD Pipeline / integration-tests (push) Failing after 2m32s
CI/CD Pipeline / lint (push) Successful in 5m22s
CI/CD Pipeline / e2e-tests (push) Has been skipped
CI/CD Pipeline / build (push) Has been skipped

This commit is contained in:
2026-03-15 12:35:42 +02:00
parent 6708cf28a7
commit cffdf8af86
61266 changed files with 4511646 additions and 1938 deletions

View File

@@ -0,0 +1,476 @@
// Credits to @bnjmnt4n (https://www.npmjs.com/package/postgrest-query)
// See https://github.com/PostgREST/postgrest/blob/2f91853cb1de18944a4556df09e52450b881cfb3/src/PostgREST/ApiRequest/QueryParams.hs#L282-L284
import { SimplifyDeep } from '../types/types'
import { JsonPathToAccessor } from './utils'
/**
* 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.
*/
export type ParseQuery<Query extends string> = string extends Query
? GenericStringError
: ParseNodes<EatWhitespace<Query>> extends [infer Nodes, `${infer Remainder}`]
? Nodes extends Ast.Node[]
? EatWhitespace<Remainder> extends ''
? SimplifyDeep<Nodes>
: ParserError<`Unexpected input: ${Remainder}`>
: ParserError<'Invalid nodes array structure'>
: ParseNodes<EatWhitespace<Query>>
/**
* 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`.
*/
/**
* 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, Nodes extends Ast.Node[]> =
ParseNode<Input> extends [infer Node, `${infer Remainder}`]
? Node extends Ast.Node
? EatWhitespace<Remainder> extends `,${infer Remainder}`
? ParseNodesHelper<EatWhitespace<Remainder>, [...Nodes, Node]>
: [[...Nodes, Node], EatWhitespace<Remainder>]
: ParserError<'Invalid node type in nodes helper'>
: ParseNode<Input>
/**
* Parses a node.
* A node is one of the following:
* - `*`
* - a field, as defined above
* - a renamed field, `renamed_field:field`
* - a spread field, `...field`
*/
type ParseNode<Input extends string> = Input extends ''
? ParserError<'Empty string'>
: // `*`
Input extends `*${infer Remainder}`
? [Ast.StarNode, EatWhitespace<Remainder>]
: // `...field`
Input extends `...${infer Remainder}`
? ParseField<EatWhitespace<Remainder>> extends [infer TargetField, `${infer Remainder}`]
? TargetField extends Ast.FieldNode
? [{ type: 'spread'; target: TargetField }, EatWhitespace<Remainder>]
: ParserError<'Invalid target field type in spread'>
: ParserError<`Unable to parse spread resource at \`${Input}\``>
: ParseIdentifier<Input> extends [infer NameOrAlias, `${infer Remainder}`]
? EatWhitespace<Remainder> extends `::${infer _}`
? // It's a type cast and not an alias, so treat it as part of the field.
ParseField<Input>
: EatWhitespace<Remainder> extends `:${infer Remainder}`
? // `alias:`
ParseField<EatWhitespace<Remainder>> extends [infer Field, `${infer Remainder}`]
? Field extends Ast.FieldNode
? [Omit<Field, 'alias'> & { alias: NameOrAlias }, EatWhitespace<Remainder>]
: ParserError<'Invalid field type in alias parsing'>
: ParserError<`Unable to parse renamed field at \`${Input}\``>
: // Otherwise, just parse it as a field without alias.
ParseField<Input>
: ParserError<`Expected identifier at \`${Input}\``>
/**
* Parses a field without preceding alias.
* A field is one of the following:
* - a top-level `count` field: https://docs.postgrest.org/en/v12/references/api/aggregate_functions.html#the-case-of-count
* - a field with an embedded resource
* - `field(nodes)`
* - `field!hint(nodes)`
* - `field!inner(nodes)`
* - `field!left(nodes)`
* - `field!hint!inner(nodes)`
* - `field!hint!left(nodes)`
* - a field without an embedded resource (see {@link ParseNonEmbeddedResourceField})
*/
type ParseField<Input extends string> = Input extends ''
? ParserError<'Empty string'>
: ParseIdentifier<Input> extends [infer Name, `${infer Remainder}`]
? Name extends 'count'
? ParseCountField<Input>
: Remainder extends `!inner${infer Remainder}`
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Children,
`${infer Remainder}`,
]
? Children extends Ast.Node[]
? // `field!inner(nodes)`
[{ type: 'field'; name: Name; innerJoin: true; children: Children }, Remainder]
: ParserError<'Invalid children array in inner join'>
: CreateParserErrorIfRequired<
ParseEmbeddedResource<EatWhitespace<Remainder>>,
`Expected embedded resource after "!inner" at \`${Remainder}\``
>
: EatWhitespace<Remainder> extends `!left${infer Remainder}`
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Children,
`${infer Remainder}`,
]
? Children extends Ast.Node[]
? // `field!left(nodes)`
// !left is a noise word - treat it the same way as a non-`!inner`.
[{ type: 'field'; name: Name; children: Children }, EatWhitespace<Remainder>]
: ParserError<'Invalid children array in left join'>
: CreateParserErrorIfRequired<
ParseEmbeddedResource<EatWhitespace<Remainder>>,
`Expected embedded resource after "!left" at \`${EatWhitespace<Remainder>}\``
>
: EatWhitespace<Remainder> extends `!${infer Remainder}`
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer Hint, `${infer Remainder}`]
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Children,
`${infer Remainder}`,
]
? Children extends Ast.Node[]
? // `field!hint!inner(nodes)`
[
{
type: 'field'
name: Name
hint: Hint
innerJoin: true
children: Children
},
EatWhitespace<Remainder>,
]
: ParserError<'Invalid children array in hint inner join'>
: ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Children,
`${infer Remainder}`,
]
? Children extends Ast.Node[]
? // `field!hint(nodes)`
[
{ type: 'field'; name: Name; hint: Hint; children: Children },
EatWhitespace<Remainder>,
]
: ParserError<'Invalid children array in hint'>
: ParseEmbeddedResource<EatWhitespace<Remainder>>
: ParserError<`Expected identifier after "!" at \`${EatWhitespace<Remainder>}\``>
: EatWhitespace<Remainder> extends `(${infer _}`
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
infer Children,
`${infer Remainder}`,
]
? Children extends Ast.Node[]
? // `field(nodes)`
[{ type: 'field'; name: Name; children: Children }, EatWhitespace<Remainder>]
: ParserError<'Invalid children array in field'>
: // Return error if start of embedded resource was detected but not found.
ParseEmbeddedResource<EatWhitespace<Remainder>>
: // Otherwise it's a non-embedded resource field.
ParseNonEmbeddedResourceField<Input>
: ParserError<`Expected identifier at \`${Input}\``>
type ParseCountField<Input extends string> =
ParseIdentifier<Input> extends ['count', `${infer Remainder}`]
? (
EatWhitespace<Remainder> extends `()${infer Remainder_}`
? EatWhitespace<Remainder_>
: EatWhitespace<Remainder>
) extends `${infer Remainder}`
? Remainder extends `::${infer _}`
? ParseFieldTypeCast<Remainder> extends [infer CastType, `${infer Remainder}`]
? [
{ type: 'field'; name: 'count'; aggregateFunction: 'count'; castType: CastType },
Remainder,
]
: ParseFieldTypeCast<Remainder>
: [{ type: 'field'; name: 'count'; aggregateFunction: 'count' }, Remainder]
: never
: ParserError<`Expected "count" at \`${Input}\``>
/**
* Parses an embedded resource, which is an opening `(`, followed by a sequence of
* 0 or more 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}`
? EatWhitespace<Remainder> extends `)${infer Remainder}`
? [[], EatWhitespace<Remainder>]
: ParseNodes<EatWhitespace<Remainder>> extends [infer Nodes, `${infer Remainder}`]
? Nodes extends Ast.Node[]
? EatWhitespace<Remainder> extends `)${infer Remainder}`
? [Nodes, EatWhitespace<Remainder>]
: ParserError<`Expected ")" at \`${EatWhitespace<Remainder>}\``>
: ParserError<'Invalid nodes array in embedded resource'>
: ParseNodes<EatWhitespace<Remainder>>
: ParserError<`Expected "(" at \`${Input}\``>
/**
* Parses a field excluding embedded resources, without preceding field renaming.
* This is one of the following:
* - `field`
* - `field.aggregate()`
* - `field.aggregate()::type`
* - `field::type`
* - `field::type.aggregate()`
* - `field::type.aggregate()::type`
* - `field->json...`
* - `field->json.aggregate()`
* - `field->json.aggregate()::type`
* - `field->json::type`
* - `field->json::type.aggregate()`
* - `field->json::type.aggregate()::type`
*/
type ParseNonEmbeddedResourceField<Input extends string> =
ParseIdentifier<Input> extends [infer Name, `${infer Remainder}`]
? // Parse optional JSON path.
(
Remainder extends `->${infer PathAndRest}`
? ParseJsonAccessor<Remainder> extends [
infer PropertyName,
infer PropertyType,
`${infer Remainder}`,
]
? [
{
type: 'field'
name: Name
alias: PropertyName
castType: PropertyType
jsonPath: JsonPathToAccessor<
PathAndRest extends `${infer Path},${string}` ? Path : PathAndRest
>
},
Remainder,
]
: ParseJsonAccessor<Remainder>
: [{ type: 'field'; name: Name }, Remainder]
) extends infer Parsed
? Parsed extends [infer Field, `${infer Remainder}`]
? // Parse optional typecast or aggregate function input typecast.
(
Remainder extends `::${infer _}`
? ParseFieldTypeCast<Remainder> extends [infer CastType, `${infer Remainder}`]
? [Omit<Field, 'castType'> & { castType: CastType }, Remainder]
: ParseFieldTypeCast<Remainder>
: [Field, Remainder]
) extends infer Parsed
? Parsed extends [infer Field, `${infer Remainder}`]
? // Parse optional aggregate function.
Remainder extends `.${infer _}`
? ParseFieldAggregation<Remainder> extends [
infer AggregateFunction,
`${infer Remainder}`,
]
? // Parse optional aggregate function output typecast.
Remainder extends `::${infer _}`
? ParseFieldTypeCast<Remainder> extends [infer CastType, `${infer Remainder}`]
? [
Omit<Field, 'castType'> & {
aggregateFunction: AggregateFunction
castType: CastType
},
Remainder,
]
: ParseFieldTypeCast<Remainder>
: [Field & { aggregateFunction: AggregateFunction }, Remainder]
: ParseFieldAggregation<Remainder>
: [Field, Remainder]
: Parsed
: never
: Parsed
: never
: 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"]
*/
type ParseJsonAccessor<Input extends string> = Input extends `->${infer Remainder}`
? Remainder extends `>${infer Remainder}`
? ParseIdentifier<Remainder> extends [infer Name, `${infer Remainder}`]
? [Name, 'text', 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 `->`'>
: ParserError<'Expected ->'>
/**
* Parses a field typecast (`::type`), returning a tuple of ["Type", "Remainder of text"].
*/
type ParseFieldTypeCast<Input extends string> =
EatWhitespace<Input> extends `::${infer Remainder}`
? ParseIdentifier<EatWhitespace<Remainder>> extends [`${infer CastType}`, `${infer Remainder}`]
? [CastType, EatWhitespace<Remainder>]
: ParserError<`Invalid type for \`::\` operator at \`${Remainder}\``>
: ParserError<'Expected ::'>
/**
* Parses a field aggregation (`.max()`), returning a tuple of ["Aggregate function", "Remainder of text"]
*/
type ParseFieldAggregation<Input extends string> =
EatWhitespace<Input> extends `.${infer Remainder}`
? ParseIdentifier<EatWhitespace<Remainder>> extends [
`${infer FunctionName}`,
`${infer Remainder}`,
]
? // Ensure that aggregation function is valid.
FunctionName extends Token.AggregateFunction
? EatWhitespace<Remainder> extends `()${infer Remainder}`
? [FunctionName, EatWhitespace<Remainder>]
: ParserError<`Expected \`()\` after \`.\` operator \`${FunctionName}\``>
: ParserError<`Invalid type for \`.\` operator \`${FunctionName}\``>
: ParserError<`Invalid type for \`.\` operator at \`${Remainder}\``>
: ParserError<'Expected .'>
/**
* Parses a (possibly double-quoted) identifier.
* Identifiers are sequences of 1 or more letters.
*/
type ParseIdentifier<Input extends string> =
ParseLetters<Input> extends [infer Name, `${infer Remainder}`]
? [Name, EatWhitespace<Remainder>]
: ParseQuotedLetters<Input> extends [infer Name, `${infer Remainder}`]
? [Name, EatWhitespace<Remainder>]
: ParserError<`No (possibly double-quoted) identifier at \`${Input}\``>
/**
* Parse a consecutive sequence of 1 or more letter, where letters are `[0-9a-zA-Z_]`.
*/
type ParseLetters<Input extends string> = string extends Input
? GenericStringError
: ParseLettersHelper<Input, ''> extends [`${infer Letters}`, `${infer Remainder}`]
? Letters extends ''
? ParserError<`Expected letter at \`${Input}\``>
: [Letters, Remainder]
: ParseLettersHelper<Input, ''>
type ParseLettersHelper<Input extends string, Acc extends string> = string extends Input
? GenericStringError
: Input extends `${infer L}${infer Remainder}`
? L extends Token.Letter
? ParseLettersHelper<Remainder, `${Acc}${L}`>
: [Acc, Input]
: [Acc, '']
/**
* Parse a consecutive sequence of 1 or more double-quoted letters,
* where letters are `[^"]`.
*/
type ParseQuotedLetters<Input extends string> = string extends Input
? GenericStringError
: Input extends `"${infer Remainder}`
? ParseQuotedLettersHelper<Remainder, ''> extends [`${infer Letters}`, `${infer Remainder}`]
? Letters extends ''
? ParserError<`Expected string at \`${Remainder}\``>
: [Letters, Remainder]
: ParseQuotedLettersHelper<Remainder, ''>
: ParserError<`Not a double-quoted string at \`${Input}\``>
type ParseQuotedLettersHelper<Input extends string, Acc extends string> = string extends Input
? GenericStringError
: Input extends `${infer L}${infer Remainder}`
? L extends '"'
? [Acc, Remainder]
: ParseQuotedLettersHelper<Remainder, `${Acc}${L}`>
: ParserError<`Missing closing double-quote in \`"${Acc}${Input}\``>
/**
* Trims whitespace from the left of the input.
*/
type EatWhitespace<Input extends string> = string extends Input
? GenericStringError
: Input extends `${Token.Whitespace}${infer Remainder}`
? EatWhitespace<Remainder>
: Input
/**
* 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>
/**
* Parser errors.
*/
export type ParserError<Message extends string> = { error: true } & Message
type GenericStringError = ParserError<'Received a generic string'>
export namespace Ast {
export type Node = FieldNode | StarNode | SpreadNode
export type FieldNode = {
type: 'field'
name: string
alias?: string
hint?: string
innerJoin?: true
castType?: string
jsonPath?: string
aggregateFunction?: Token.AggregateFunction
children?: Node[]
}
export type StarNode = {
type: 'star'
}
export type SpreadNode = {
type: 'spread'
target: FieldNode & { children: Node[] }
}
}
namespace Token {
export 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'
export type Letter = Alphabet | Digit | '_'
export type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'
}

View File

@@ -0,0 +1,524 @@
import { Ast, ParseQuery } from './parser'
import {
AggregateFunctions,
ExtractFirstProperty,
GenericSchema,
IsNonEmptyArray,
Prettify,
TablesAndViews,
TypeScriptTypes,
ContainsNull,
GenericRelationship,
PostgreSQLTypes,
GenericTable,
ClientServerOptions,
} from './types'
import {
CheckDuplicateEmbededReference,
GetComputedFields,
GetFieldNodeResultName,
IsAny,
IsRelationNullable,
IsStringUnion,
JsonPathToType,
ResolveRelationship,
SelectQueryError,
} from './utils'
import type { SpreadOnManyEnabled } from '../types/feature-flags'
/**
* Main entry point for constructing the result type of a PostgREST query.
*
* @param Schema - Database schema.
* @param Row - The type of a row in the current table.
* @param RelationName - The name of the current table or view.
* @param Relationships - Relationships of the current table.
* @param Query - The select query string literal to parse.
*/
export type GetResult<
Schema extends GenericSchema,
Row extends Record<string, unknown>,
RelationName,
Relationships,
Query extends string,
ClientOptions extends ClientServerOptions,
> =
IsAny<Schema> extends true
? ParseQuery<Query> extends infer ParsedQuery
? ParsedQuery extends Ast.Node[]
? RelationName extends string
? ProcessNodesWithoutSchema<ParsedQuery>
: any
: ParsedQuery
: any
: Relationships extends null // For .rpc calls the passed relationships will be null in that case, the result will always be the function return type
? ParseQuery<Query> extends infer ParsedQuery
? ParsedQuery extends Ast.Node[]
? RPCCallNodes<ParsedQuery, RelationName extends string ? RelationName : 'rpc_call', Row>
: ParsedQuery
: Row
: ParseQuery<Query> extends infer ParsedQuery
? ParsedQuery extends Ast.Node[]
? RelationName extends string
? Relationships extends GenericRelationship[]
? ProcessNodes<ClientOptions, Schema, Row, RelationName, Relationships, ParsedQuery>
: SelectQueryError<'Invalid Relationships cannot infer result type'>
: SelectQueryError<'Invalid RelationName cannot infer result type'>
: ParsedQuery
: never
type ProcessSimpleFieldWithoutSchema<Field extends Ast.FieldNode> =
Field['aggregateFunction'] extends AggregateFunctions
? {
// An aggregate function will always override the column name id.sum() will become sum
// except if it has been aliased
[K in GetFieldNodeResultName<Field>]: Field['castType'] extends PostgreSQLTypes
? TypeScriptTypes<Field['castType']>
: number
}
: {
// Aliases override the property name in the result
[K in GetFieldNodeResultName<Field>]: Field['castType'] extends PostgreSQLTypes // We apply the detected casted as the result type
? TypeScriptTypes<Field['castType']>
: any
}
type ProcessFieldNodeWithoutSchema<Node extends Ast.FieldNode> =
IsNonEmptyArray<Node['children']> extends true
? {
[K in GetFieldNodeResultName<Node>]: Node['children'] extends Ast.Node[]
? ProcessNodesWithoutSchema<Node['children']>[]
: ProcessSimpleFieldWithoutSchema<Node>
}
: ProcessSimpleFieldWithoutSchema<Node>
/**
* Processes a single Node without schema and returns the resulting TypeScript type.
*/
type ProcessNodeWithoutSchema<Node extends Ast.Node> = Node extends Ast.StarNode
? any
: Node extends Ast.SpreadNode
? Node['target']['children'] extends Ast.StarNode[]
? any
: Node['target']['children'] extends Ast.FieldNode[]
? {
[P in Node['target']['children'][number] as GetFieldNodeResultName<P>]: P['castType'] extends PostgreSQLTypes
? TypeScriptTypes<P['castType']>
: any
}
: any
: Node extends Ast.FieldNode
? ProcessFieldNodeWithoutSchema<Node>
: any
/**
* Processes nodes when Schema is any, providing basic type inference
*/
type ProcessNodesWithoutSchema<
Nodes extends Ast.Node[],
Acc extends Record<string, unknown> = {},
> = Nodes extends [infer FirstNode, ...infer RestNodes]
? FirstNode extends Ast.Node
? RestNodes extends Ast.Node[]
? ProcessNodeWithoutSchema<FirstNode> extends infer FieldResult
? FieldResult extends Record<string, unknown>
? ProcessNodesWithoutSchema<RestNodes, Acc & FieldResult>
: FieldResult
: any
: any
: any
: Prettify<Acc>
/**
* Processes a single Node from a select chained after a rpc call
*
* @param Row - The type of a row in the current table.
* @param RelationName - The name of the current rpc function
* @param NodeType - The Node to process.
*/
export type ProcessRPCNode<
Row extends Record<string, unknown>,
RelationName extends string,
NodeType extends Ast.Node,
> = NodeType['type'] extends Ast.StarNode['type'] // If the selection is *
? Row
: NodeType['type'] extends Ast.FieldNode['type']
? ProcessSimpleField<Row, RelationName, Extract<NodeType, Ast.FieldNode>>
: SelectQueryError<'RPC Unsupported node type.'>
/**
* Process select call that can be chained after an rpc call
*/
export type RPCCallNodes<
Nodes extends Ast.Node[],
RelationName extends string,
Row extends Record<string, unknown>,
Acc extends Record<string, unknown> = {}, // Acc is now an object
> = Nodes extends [infer FirstNode, ...infer RestNodes]
? FirstNode extends Ast.Node
? RestNodes extends Ast.Node[]
? ProcessRPCNode<Row, RelationName, FirstNode> extends infer FieldResult
? FieldResult extends Record<string, unknown>
? RPCCallNodes<RestNodes, RelationName, Row, Acc & FieldResult>
: FieldResult extends SelectQueryError<infer E>
? SelectQueryError<E>
: SelectQueryError<'Could not retrieve a valid record or error value'>
: SelectQueryError<'Processing node failed.'>
: SelectQueryError<'Invalid rest nodes array in RPC call'>
: SelectQueryError<'Invalid first node in RPC call'>
: Prettify<Acc>
/**
* Recursively processes an array of Nodes and accumulates the resulting TypeScript type.
*
* @param Schema - Database schema.
* @param Row - The type of a row in the current table.
* @param RelationName - The name of the current table or view.
* @param Relationships - Relationships of the current table.
* @param Nodes - An array of AST nodes to process.
* @param Acc - Accumulator for the constructed type.
*/
export type ProcessNodes<
ClientOptions extends ClientServerOptions,
Schema extends GenericSchema,
Row extends Record<string, unknown>,
RelationName extends string,
Relationships extends GenericRelationship[],
Nodes extends Ast.Node[],
Acc extends Record<string, unknown> = {}, // Acc is now an object
> =
CheckDuplicateEmbededReference<Schema, RelationName, Relationships, Nodes> extends false
? Nodes extends [infer FirstNode, ...infer RestNodes]
? FirstNode extends Ast.Node
? RestNodes extends Ast.Node[]
? ProcessNode<
ClientOptions,
Schema,
Row,
RelationName,
Relationships,
FirstNode
> extends infer FieldResult
? FieldResult extends Record<string, unknown>
? ProcessNodes<
ClientOptions,
Schema,
Row,
RelationName,
Relationships,
RestNodes,
// TODO:
// This SHOULD be `Omit<Acc, keyof FieldResult> & FieldResult` since in the case where the key
// is present in the Acc already, the intersection will create bad intersection types
// (eg: `{ a: number } & { a: { property } }` will become `{ a: number & { property } }`)
// but using Omit here explode the inference complexity resulting in "infinite recursion error" from typescript
// very early (see: 'Check that selecting many fields doesn't yield an possibly infinite recursion error') test
// in this case we can't get above ~10 fields before reaching the recursion error
// If someone find a better way to do this, please do it !
// It'll also allow to fix those two tests:
// - `'join over a 1-M relation with both nullables and non-nullables fields using column name hinting on nested relation'`
// - `'self reference relation via column''`
Acc & FieldResult
>
: FieldResult extends SelectQueryError<infer E>
? SelectQueryError<E>
: SelectQueryError<'Could not retrieve a valid record or error value'>
: SelectQueryError<'Processing node failed.'>
: SelectQueryError<'Invalid rest nodes array type in ProcessNodes'>
: SelectQueryError<'Invalid first node type in ProcessNodes'>
: Prettify<Acc>
: Prettify<CheckDuplicateEmbededReference<Schema, RelationName, Relationships, Nodes>>
/**
* Processes a single Node and returns the resulting TypeScript type.
*
* @param Schema - Database schema.
* @param Row - The type of a row in the current table.
* @param RelationName - The name of the current table or view.
* @param Relationships - Relationships of the current table.
* @param NodeType - The Node to process.
*/
export type ProcessNode<
ClientOptions extends ClientServerOptions,
Schema extends GenericSchema,
Row extends Record<string, unknown>,
RelationName extends string,
Relationships extends GenericRelationship[],
NodeType extends Ast.Node,
> =
// TODO: figure out why comparing the `type` property is necessary vs. `NodeType extends Ast.StarNode`
NodeType['type'] extends Ast.StarNode['type'] // If the selection is *
? // If the row has computed field, postgrest will omit them from star selection per default
GetComputedFields<Schema, RelationName> extends never
? // If no computed fields are detected on the row, we can return it as is
Row
: // otherwise we omit all the computed field from the star result return
Omit<Row, GetComputedFields<Schema, RelationName>>
: NodeType['type'] extends Ast.SpreadNode['type'] // If the selection is a ...spread
? ProcessSpreadNode<
ClientOptions,
Schema,
Row,
RelationName,
Relationships,
Extract<NodeType, Ast.SpreadNode>
>
: NodeType['type'] extends Ast.FieldNode['type']
? ProcessFieldNode<
ClientOptions,
Schema,
Row,
RelationName,
Relationships,
Extract<NodeType, Ast.FieldNode>
>
: SelectQueryError<'Unsupported node type.'>
/**
* Processes a FieldNode and returns the resulting TypeScript type.
*
* @param Schema - Database schema.
* @param Row - The type of a row in the current table.
* @param RelationName - The name of the current table or view.
* @param Relationships - Relationships of the current table.
* @param Field - The FieldNode to process.
*/
type ProcessFieldNode<
ClientOptions extends ClientServerOptions,
Schema extends GenericSchema,
Row extends Record<string, unknown>,
RelationName extends string,
Relationships extends GenericRelationship[],
Field extends Ast.FieldNode,
> = Field['children'] extends []
? {}
: IsNonEmptyArray<Field['children']> extends true // Has embedded resource?
? ProcessEmbeddedResource<ClientOptions, Schema, Relationships, Field, RelationName>
: ProcessSimpleField<Row, RelationName, Field>
type ResolveJsonPathType<
Value,
Path extends string | undefined,
CastType extends PostgreSQLTypes,
> = Path extends string
? JsonPathToType<Value, Path> extends never
? // Always fallback if JsonPathToType returns never
TypeScriptTypes<CastType>
: JsonPathToType<Value, Path> extends infer PathResult
? PathResult extends string
? // Use the result if it's a string as we know that even with the string accessor ->> it's a valid type
PathResult
: IsStringUnion<PathResult> extends true
? // Use the result if it's a union of strings
PathResult
: CastType extends 'json'
? // If the type is not a string, ensure it was accessed with json accessor ->
PathResult
: // Otherwise it means non-string value accessed with string accessor ->> use the TypeScriptTypes result
TypeScriptTypes<CastType>
: TypeScriptTypes<CastType>
: // No json path, use regular type casting
TypeScriptTypes<CastType>
/**
* Processes a simple field (without embedded resources).
*
* @param Row - The type of a row in the current table.
* @param RelationName - The name of the current table or view.
* @param Field - The FieldNode to process.
*/
type ProcessSimpleField<
Row extends Record<string, unknown>,
RelationName extends string,
Field extends Ast.FieldNode,
> = Field['name'] extends keyof Row | 'count'
? Field['aggregateFunction'] extends AggregateFunctions
? {
// An aggregate function will always override the column name id.sum() will become sum
// except if it has been aliased
[K in GetFieldNodeResultName<Field>]: Field['castType'] extends PostgreSQLTypes
? TypeScriptTypes<Field['castType']>
: number
}
: {
// Aliases override the property name in the result
[K in GetFieldNodeResultName<Field>]: Field['castType'] extends PostgreSQLTypes
? ResolveJsonPathType<Row[Field['name']], Field['jsonPath'], Field['castType']>
: Row[Field['name']]
}
: SelectQueryError<`column '${Field['name']}' does not exist on '${RelationName}'.`>
/**
* Processes an embedded resource (relation).
*
* @param Schema - Database schema.
* @param Row - The type of a row in the current table.
* @param RelationName - The name of the current table or view.
* @param Relationships - Relationships of the current table.
* @param Field - The FieldNode to process.
*/
export type ProcessEmbeddedResource<
ClientOptions extends ClientServerOptions,
Schema extends GenericSchema,
Relationships extends GenericRelationship[],
Field extends Ast.FieldNode,
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
> =
ResolveRelationship<Schema, Relationships, Field, CurrentTableOrView> extends infer Resolved
? Resolved extends {
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' }
direction: string
}
? ProcessEmbeddedResourceResult<ClientOptions, Schema, Resolved, Field, CurrentTableOrView>
: // Otherwise the Resolved is a SelectQueryError return it
{ [K in GetFieldNodeResultName<Field>]: Resolved }
: {
[K in GetFieldNodeResultName<Field>]: SelectQueryError<'Failed to resolve relationship.'> &
string
}
/**
* Helper type to process the result of an embedded resource.
*/
type ProcessEmbeddedResourceResult<
ClientOptions extends ClientServerOptions,
Schema extends GenericSchema,
Resolved extends {
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
relation: GenericRelationship & {
match: 'refrel' | 'col' | 'fkname' | 'func'
isNotNullable?: boolean
referencedRelation: string
isSetofReturn?: boolean
}
direction: string
},
Field extends Ast.FieldNode,
CurrentTableOrView extends keyof TablesAndViews<Schema>,
> =
ProcessNodes<
ClientOptions,
Schema,
Resolved['referencedTable']['Row'],
// For embeded function selection, the source of truth is the 'referencedRelation'
// coming from the SetofOptions.to parameter
Resolved['relation']['match'] extends 'func'
? Resolved['relation']['referencedRelation']
: Field['name'],
Resolved['referencedTable']['Relationships'],
Field['children'] extends undefined
? []
: Exclude<Field['children'], undefined> extends Ast.Node[]
? Exclude<Field['children'], undefined>
: []
> extends infer ProcessedChildren
? {
[K in GetFieldNodeResultName<Field>]: Resolved['direction'] extends 'forward'
? Field extends { innerJoin: true }
? Resolved['relation']['isOneToOne'] extends true
? ProcessedChildren
: ProcessedChildren[]
: Resolved['relation']['isOneToOne'] extends true
? Resolved['relation']['match'] extends 'func'
? Resolved['relation']['isNotNullable'] extends true
? Resolved['relation']['isSetofReturn'] extends true
? ProcessedChildren
: // TODO: This shouldn't be necessary but is due in an inconsitency in PostgREST v12/13 where if a function
// is declared with RETURNS <table-name> instead of RETURNS SETOF <table-name> ROWS 1
// In case where there is no object matching the relations, the object will be returned with all the properties within it
// set to null, we mimic this buggy behavior for type safety an issue is opened on postgREST here:
// https://github.com/PostgREST/postgrest/issues/4234
{ [P in keyof ProcessedChildren]: ProcessedChildren[P] | null }
: ProcessedChildren | null
: ProcessedChildren | null
: ProcessedChildren[]
: // If the relation is a self-reference it'll always be considered as reverse relationship
Resolved['relation']['referencedRelation'] extends CurrentTableOrView
? // It can either be a reverse reference via a column inclusion (eg: parent_id(*))
// in such case the result will be a single object
Resolved['relation']['match'] extends 'col'
? IsRelationNullable<
TablesAndViews<Schema>[CurrentTableOrView],
Resolved['relation']
> extends true
? ProcessedChildren | null
: ProcessedChildren
: // Or it can be a reference via the reference relation (eg: collections(*))
// in such case, the result will be an array of all the values (all collection with parent_id being the current id)
ProcessedChildren[]
: // Otherwise if it's a non self-reference reverse relationship it's a single object
IsRelationNullable<
TablesAndViews<Schema>[CurrentTableOrView],
Resolved['relation']
> extends true
? Field extends { innerJoin: true }
? ProcessedChildren
: ProcessedChildren | null
: ProcessedChildren
}
: {
[K in GetFieldNodeResultName<Field>]: SelectQueryError<'Failed to process embedded resource nodes.'> &
string
}
/**
* Processes a SpreadNode by processing its target node.
*
* @param Schema - Database schema.
* @param Row - The type of a row in the current table.
* @param RelationName - The name of the current table or view.
* @param Relationships - Relationships of the current table.
* @param Spread - The SpreadNode to process.
*/
type ProcessSpreadNode<
ClientOptions extends ClientServerOptions,
Schema extends GenericSchema,
Row extends Record<string, unknown>,
RelationName extends string,
Relationships extends GenericRelationship[],
Spread extends Ast.SpreadNode,
> =
ProcessNode<
ClientOptions,
Schema,
Row,
RelationName,
Relationships,
Spread['target']
> extends infer Result
? Result extends SelectQueryError<infer E>
? SelectQueryError<E>
: ExtractFirstProperty<Result> extends unknown[]
? SpreadOnManyEnabled<ClientOptions['PostgrestVersion']> extends true // Spread over an many-to-many relationship, turn all the result fields into correlated arrays
? ProcessManyToManySpreadNodeResult<Result>
: {
[K in Spread['target']['name']]: SelectQueryError<`"${RelationName}" and "${Spread['target']['name']}" do not form a many-to-one or one-to-one relationship spread not possible`>
}
: ProcessSpreadNodeResult<Result>
: never
/**
* Helper type to process the result of a many-to-many spread node.
* Converts all fields in the spread object into arrays.
*/
type ProcessManyToManySpreadNodeResult<Result> =
Result extends Record<string, SelectQueryError<string> | null>
? Result
: ExtractFirstProperty<Result> extends infer SpreadedObject
? SpreadedObject extends Array<Record<string, unknown>>
? { [K in keyof SpreadedObject[number]]: Array<SpreadedObject[number][K]> }
: SelectQueryError<'An error occurred spreading the many-to-many object'>
: SelectQueryError<'An error occurred spreading the many-to-many object'>
/**
* Helper type to process the result of a spread node.
*/
type ProcessSpreadNodeResult<Result> =
Result extends Record<string, SelectQueryError<string> | null>
? Result
: ExtractFirstProperty<Result> extends infer SpreadedObject
? ContainsNull<SpreadedObject> extends true
? Exclude<{ [K in keyof SpreadedObject]: SpreadedObject[K] | null }, null>
: Exclude<{ [K in keyof SpreadedObject]: SpreadedObject[K] }, null>
: SelectQueryError<'An error occurred spreading the object'>

View File

@@ -0,0 +1,129 @@
import type {
GenericRelationship,
GenericSchema,
GenericTable,
ClientServerOptions,
GenericSetofOption,
GenericFunction,
} from '../types/common/common'
import type { Prettify } from '../types/types'
export type {
GenericRelationship,
GenericSchema,
GenericTable,
ClientServerOptions,
GenericSetofOption,
Prettify,
GenericFunction,
}
export type AggregateWithoutColumnFunctions = 'count'
export type AggregateWithColumnFunctions =
| 'sum'
| 'avg'
| 'min'
| 'max'
| AggregateWithoutColumnFunctions
export type AggregateFunctions = AggregateWithColumnFunctions
export type Json =
| string
| number
| boolean
| null
| {
[key: string]: Json | undefined
}
| Json[]
type PostgresSQLNumberTypes = 'int2' | 'int4' | 'int8' | 'float4' | 'float8' | 'numeric'
type PostgresSQLStringTypes =
| 'bytea'
| 'bpchar'
| 'varchar'
| 'date'
| 'text'
| 'citext'
| 'time'
| 'timetz'
| 'timestamp'
| 'timestamptz'
| 'uuid'
| 'vector'
type SingleValuePostgreSQLTypes =
| PostgresSQLNumberTypes
| PostgresSQLStringTypes
| 'bool'
| 'json'
| 'jsonb'
| 'void'
| 'record'
| string
type ArrayPostgreSQLTypes = `_${SingleValuePostgreSQLTypes}`
type TypeScriptSingleValueTypes<T extends SingleValuePostgreSQLTypes> = T extends 'bool'
? boolean
: T extends PostgresSQLNumberTypes
? number
: T extends PostgresSQLStringTypes
? string
: T extends 'json' | 'jsonb'
? Json
: T extends 'void'
? undefined
: T extends 'record'
? Record<string, unknown>
: unknown
type StripUnderscore<T extends string> = T extends `_${infer U}` ? U : T
// Represents all possible PostgreSQL types, including array types, allow for custom types with 'string' in union
export type PostgreSQLTypes = SingleValuePostgreSQLTypes | ArrayPostgreSQLTypes
// Helper type to convert PostgreSQL types to their TypeScript equivalents
export type TypeScriptTypes<T extends PostgreSQLTypes> = T extends ArrayPostgreSQLTypes
? TypeScriptSingleValueTypes<StripUnderscore<Extract<T, SingleValuePostgreSQLTypes>>>[]
: TypeScriptSingleValueTypes<T>
// Utility types for working with unions
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never
export type LastOf<T> =
UnionToIntersection<T extends any ? () => T : never> extends () => infer R ? R : never
export type Push<T extends any[], V> = [...T, V]
// Converts a union type to a tuple type
export type UnionToTuple<T, L = LastOf<T>, N = [T] extends [never] ? true : false> = N extends true
? []
: Push<UnionToTuple<Exclude<T, L>>, L>
export type UnionToArray<T> = UnionToTuple<T>
// Extracts the type of the first property in an object type
export type ExtractFirstProperty<T> = T extends { [K in keyof T]: infer U } ? U : never
// Type predicates
export type ContainsNull<T> = null extends T ? true : false
export type IsNonEmptyArray<T> =
Exclude<T, undefined> extends readonly [unknown, ...unknown[]] ? true : false
// Types for working with database schemas
export type TablesAndViews<Schema extends GenericSchema> = Schema['Tables'] &
Exclude<Schema['Views'], ''>
export type GetTableRelationships<
Schema extends GenericSchema,
Tname extends string,
> = TablesAndViews<Schema>[Tname] extends { Relationships: infer R } ? R : false

View File

@@ -0,0 +1,674 @@
import { Ast } from './parser'
import {
AggregateFunctions,
ContainsNull,
GenericRelationship,
GenericSchema,
GenericTable,
IsNonEmptyArray,
TablesAndViews,
UnionToArray,
GenericFunction,
GenericSetofOption,
} from './types'
export type IsAny<T> = 0 extends 1 & T ? true : false
export type SelectQueryError<Message extends string> = { error: true } & Message
/*
** Because of pg-meta types generation there is some cases where a same relationship can be duplicated
** if the relation is across schemas and views this ensure that we dedup those relations and treat them
** as postgrest would.
** This is no longer the case and has been patched here: https://github.com/supabase/postgres-meta/pull/809
** But we still need this for retro-compatibilty with older generated types
** TODO: Remove this in next major version
*/
export type DeduplicateRelationships<T extends readonly unknown[]> = T extends readonly [
infer First,
...infer Rest,
]
? First extends Rest[number]
? DeduplicateRelationships<Rest extends readonly unknown[] ? Rest : []>
: [First, ...DeduplicateRelationships<Rest extends readonly unknown[] ? Rest : []>]
: T
export type GetFieldNodeResultName<Field extends Ast.FieldNode> = Field['alias'] extends string
? Field['alias']
: Field['aggregateFunction'] extends AggregateFunctions
? Field['aggregateFunction']
: Field['name']
type FilterRelationNodes<Nodes extends Ast.Node[]> = UnionToArray<
{
[K in keyof Nodes]: Nodes[K] extends Ast.SpreadNode
? Nodes[K]['target']
: Nodes[K] extends Ast.FieldNode
? IsNonEmptyArray<Nodes[K]['children']> extends true
? Nodes[K]
: never
: never
}[number]
>
type ResolveRelationships<
Schema extends GenericSchema,
RelationName extends string,
Relationships extends GenericRelationship[],
Nodes extends Ast.FieldNode[],
> = UnionToArray<{
[K in keyof Nodes]: Nodes[K] extends Ast.FieldNode
? ResolveRelationship<Schema, Relationships, Nodes[K], RelationName> extends infer Relation
? Relation extends {
relation: {
referencedRelation: string
foreignKeyName: string
match: string
}
from: string
}
? {
referencedTable: Relation['relation']['referencedRelation']
fkName: Relation['relation']['foreignKeyName']
from: Relation['from']
match: Relation['relation']['match']
fieldName: GetFieldNodeResultName<Nodes[K]>
}
: Relation
: never
: never
}>[0]
/**
* Checks if a relation is implicitly referenced twice, requiring disambiguation
*/
type IsDoubleReference<T, U> = T extends {
referencedTable: infer RT
fieldName: infer FN
match: infer M
}
? M extends 'col' | 'refrel'
? U extends { referencedTable: RT; fieldName: FN; match: M }
? true
: false
: false
: false
/**
* Compares one element with all other elements in the array to find duplicates
*/
type CheckDuplicates<Arr extends any[], Current> = Arr extends [infer Head, ...infer Tail]
? IsDoubleReference<Current, Head> extends true
? Head | CheckDuplicates<Tail, Current> // Return the Head if duplicate
: CheckDuplicates<Tail, Current> // Otherwise, continue checking
: never
/**
* Iterates over the elements of the array to find duplicates
*/
type FindDuplicatesWithinDeduplicated<Arr extends any[]> = Arr extends [infer Head, ...infer Tail]
? CheckDuplicates<Tail, Head> | FindDuplicatesWithinDeduplicated<Tail>
: never
type FindDuplicates<Arr extends any[]> = FindDuplicatesWithinDeduplicated<
DeduplicateRelationships<Arr>
>
export type CheckDuplicateEmbededReference<
Schema extends GenericSchema,
RelationName extends string,
Relationships extends GenericRelationship[],
Nodes extends Ast.Node[],
> =
FilterRelationNodes<Nodes> extends infer RelationsNodes
? RelationsNodes extends Ast.FieldNode[]
? ResolveRelationships<
Schema,
RelationName,
Relationships,
RelationsNodes
> extends infer ResolvedRels
? ResolvedRels extends unknown[]
? FindDuplicates<ResolvedRels> extends infer Duplicates
? Duplicates extends never
? false
: Duplicates extends { fieldName: infer FieldName }
? FieldName extends string
? {
[K in FieldName]: SelectQueryError<`table "${RelationName}" specified more than once use hinting for desambiguation`>
}
: false
: false
: false
: false
: false
: false
: 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
: false
: Relationships extends [infer R, ...infer Rest]
? HasFKeyToFRel<FRelName, [R]> extends true
? true
: HasFKeyToFRel<FRelName, Rest>
: false
/**
* Checks if there is more than one relation to a given foreign relation name in the Relationships.
*/
type HasMultipleFKeysToFRelDeduplicated<FRelName, Relationships> = Relationships extends [
infer R,
...infer Rest,
]
? R extends { referencedRelation: FRelName }
? HasFKeyToFRel<FRelName, Rest> extends true
? true
: HasMultipleFKeysToFRelDeduplicated<FRelName, Rest>
: HasMultipleFKeysToFRelDeduplicated<FRelName, Rest>
: false
type HasMultipleFKeysToFRel<
FRelName,
Relationships extends unknown[],
> = HasMultipleFKeysToFRelDeduplicated<FRelName, DeduplicateRelationships<Relationships>>
type CheckRelationshipError<
Schema extends GenericSchema,
Relationships extends GenericRelationship[],
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
FoundRelation,
> =
FoundRelation extends SelectQueryError<string>
? FoundRelation
: // If the relation is a reverse relation with no hint (matching by name)
FoundRelation extends {
relation: {
referencedRelation: infer RelatedRelationName
name: string
}
direction: 'reverse'
}
? RelatedRelationName extends string
? // We check if there is possible confusion with other relations with this table
HasMultipleFKeysToFRel<RelatedRelationName, Relationships> extends true
? // If there is, postgrest will fail at runtime, and require desambiguation via hinting
SelectQueryError<`Could not embed because more than one relationship was found for '${RelatedRelationName}' and '${CurrentTableOrView}' you need to hint the column with ${RelatedRelationName}!<columnName> ?`>
: FoundRelation
: never
: // Same check for forward relationships, but we must gather the relationships from the found relation
FoundRelation extends {
relation: {
referencedRelation: infer RelatedRelationName
name: string
}
direction: 'forward'
from: infer From
}
? RelatedRelationName extends string
? From extends keyof TablesAndViews<Schema> & string
? HasMultipleFKeysToFRel<
RelatedRelationName,
TablesAndViews<Schema>[From]['Relationships']
> extends true
? SelectQueryError<`Could not embed because more than one relationship was found for '${From}' and '${RelatedRelationName}' you need to hint the column with ${From}!<columnName> ?`>
: FoundRelation
: never
: never
: FoundRelation
/**
* Resolves relationships for embedded resources and retrieves the referenced Table
*/
export type ResolveRelationship<
Schema extends GenericSchema,
Relationships extends GenericRelationship[],
Field extends Ast.FieldNode,
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
> =
ResolveReverseRelationship<
Schema,
Relationships,
Field,
CurrentTableOrView
> extends infer ReverseRelationship
? ReverseRelationship extends false
? CheckRelationshipError<
Schema,
Relationships,
CurrentTableOrView,
ResolveForwardRelationship<Schema, Field, CurrentTableOrView>
>
: CheckRelationshipError<Schema, Relationships, CurrentTableOrView, ReverseRelationship>
: never
/**
* Resolves reverse relationships (from children to parent)
*/
type ResolveReverseRelationship<
Schema extends GenericSchema,
Relationships extends GenericRelationship[],
Field extends Ast.FieldNode,
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
> =
FindFieldMatchingRelationships<Schema, Relationships, Field> extends infer FoundRelation
? FoundRelation extends never
? false
: FoundRelation extends { referencedRelation: infer RelatedRelationName }
? RelatedRelationName extends string
? RelatedRelationName extends keyof TablesAndViews<Schema>
? // If the relation was found via hinting we just return it without any more checks
FoundRelation extends { hint: string }
? {
referencedTable: TablesAndViews<Schema>[RelatedRelationName]
relation: FoundRelation
direction: 'reverse'
from: CurrentTableOrView
}
: // If the relation was found via implicit relation naming, we must ensure there is no conflicting matches
HasMultipleFKeysToFRel<RelatedRelationName, Relationships> extends true
? SelectQueryError<`Could not embed because more than one relationship was found for '${RelatedRelationName}' and '${CurrentTableOrView}' you need to hint the column with ${RelatedRelationName}!<columnName> ?`>
: {
referencedTable: TablesAndViews<Schema>[RelatedRelationName]
relation: FoundRelation
direction: 'reverse'
from: CurrentTableOrView
}
: SelectQueryError<`Relation '${RelatedRelationName}' not found in schema.`>
: false
: false
: false
export type FindMatchingTableRelationships<
Schema extends GenericSchema,
Relationships extends GenericRelationship[],
value extends string,
> = Relationships extends [infer R, ...infer Rest]
? Rest extends GenericRelationship[]
? R extends { referencedRelation: infer ReferencedRelation }
? ReferencedRelation extends keyof Schema['Tables']
? R extends { foreignKeyName: value }
? R & { match: 'fkname' }
: R extends { referencedRelation: value }
? R & { match: 'refrel' }
: R extends { columns: [value] }
? R & { match: 'col' }
: FindMatchingTableRelationships<Schema, Rest, value>
: FindMatchingTableRelationships<Schema, Rest, value>
: false
: false
: false
export type FindMatchingViewRelationships<
Schema extends GenericSchema,
Relationships extends GenericRelationship[],
value extends string,
> = Relationships extends [infer R, ...infer Rest]
? Rest extends GenericRelationship[]
? R extends { referencedRelation: infer ReferencedRelation }
? ReferencedRelation extends keyof Schema['Views']
? R extends { foreignKeyName: value }
? R & { match: 'fkname' }
: R extends { referencedRelation: value }
? R & { match: 'refrel' }
: R extends { columns: [value] }
? R & { match: 'col' }
: FindMatchingViewRelationships<Schema, Rest, value>
: FindMatchingViewRelationships<Schema, Rest, value>
: false
: false
: false
export type FindMatchingHintTableRelationships<
Schema extends GenericSchema,
Relationships extends GenericRelationship[],
hint extends string,
name extends string,
> = Relationships extends [infer R, ...infer Rest]
? Rest extends GenericRelationship[]
? R extends { referencedRelation: infer ReferencedRelation }
? ReferencedRelation extends name
? R extends { foreignKeyName: hint }
? R & { match: 'fkname' }
: R extends { referencedRelation: hint }
? R & { match: 'refrel' }
: R extends { columns: [hint] }
? R & { match: 'col' }
: FindMatchingHintTableRelationships<Schema, Rest, hint, name>
: FindMatchingHintTableRelationships<Schema, Rest, hint, name>
: false
: false
: false
export type FindMatchingHintViewRelationships<
Schema extends GenericSchema,
Relationships extends GenericRelationship[],
hint extends string,
name extends string,
> = Relationships extends [infer R, ...infer Rest]
? Rest extends GenericRelationship[]
? R extends { referencedRelation: infer ReferencedRelation }
? ReferencedRelation extends name
? R extends { foreignKeyName: hint }
? R & { match: 'fkname' }
: R extends { referencedRelation: hint }
? R & { match: 'refrel' }
: R extends { columns: [hint] }
? R & { match: 'col' }
: FindMatchingHintViewRelationships<Schema, Rest, hint, name>
: FindMatchingHintViewRelationships<Schema, Rest, hint, name>
: false
: false
: false
type IsColumnsNullable<
Table extends Pick<GenericTable, 'Row'>,
Columns extends (keyof Table['Row'])[],
> = Columns extends [infer Column, ...infer Rest]
? Column extends keyof Table['Row']
? ContainsNull<Table['Row'][Column]> extends true
? true
: IsColumnsNullable<Table, Rest extends (keyof Table['Row'])[] ? Rest : []>
: false
: false
// Check weither or not a 1-1 relation is nullable by checking against the type of the columns
export type IsRelationNullable<
Table extends GenericTable,
Relation extends GenericRelationship,
> = IsColumnsNullable<Table, Relation['columns']>
type TableForwardRelationships<
Schema extends GenericSchema,
TName,
> = TName extends keyof TablesAndViews<Schema>
? UnionToArray<
RecursivelyFindRelationships<Schema, TName, keyof TablesAndViews<Schema>>
> extends infer R
? R extends (GenericRelationship & { from: keyof TablesAndViews<Schema> })[]
? R
: []
: []
: []
type RecursivelyFindRelationships<
Schema extends GenericSchema,
TName,
Keys extends keyof TablesAndViews<Schema>,
> = Keys extends infer K
? K extends keyof TablesAndViews<Schema>
? FilterRelationships<TablesAndViews<Schema>[K]['Relationships'], TName, K> extends never
? RecursivelyFindRelationships<Schema, TName, Exclude<Keys, K>>
:
| FilterRelationships<TablesAndViews<Schema>[K]['Relationships'], TName, K>
| RecursivelyFindRelationships<Schema, TName, Exclude<Keys, K>>
: false
: false
type FilterRelationships<R, TName, From> = R extends readonly (infer Rel)[]
? Rel extends { referencedRelation: TName }
? Rel & { from: From }
: never
: never
export type ResolveForwardRelationship<
Schema extends GenericSchema,
Field extends Ast.FieldNode,
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
> =
FindFieldMatchingRelationships<
Schema,
TablesAndViews<Schema>[Field['name']]['Relationships'],
Ast.FieldNode & { name: CurrentTableOrView; hint: Field['hint'] }
> extends infer FoundByName
? FoundByName extends GenericRelationship
? {
referencedTable: TablesAndViews<Schema>[Field['name']]
relation: FoundByName
direction: 'forward'
from: Field['name']
type: 'found-by-name'
}
: FindFieldMatchingRelationships<
Schema,
TableForwardRelationships<Schema, CurrentTableOrView>,
Field
> extends infer FoundByMatch
? FoundByMatch extends GenericRelationship & {
from: keyof TablesAndViews<Schema>
}
? {
referencedTable: TablesAndViews<Schema>[FoundByMatch['from']]
relation: FoundByMatch
direction: 'forward'
from: CurrentTableOrView
type: 'found-by-match'
}
: FindJoinTableRelationship<
Schema,
CurrentTableOrView,
Field['name']
> extends infer FoundByJoinTable
? FoundByJoinTable extends GenericRelationship
? {
referencedTable: TablesAndViews<Schema>[FoundByJoinTable['referencedRelation']]
relation: FoundByJoinTable & { match: 'refrel' }
direction: 'forward'
from: CurrentTableOrView
type: 'found-by-join-table'
}
: ResolveEmbededFunctionJoinTableRelationship<
Schema,
CurrentTableOrView,
Field['name']
> extends infer FoundEmbededFunctionJoinTableRelation
? FoundEmbededFunctionJoinTableRelation extends GenericSetofOption
? {
referencedTable: TablesAndViews<Schema>[FoundEmbededFunctionJoinTableRelation['to']]
relation: {
foreignKeyName: `${Field['name']}_${CurrentTableOrView}_${FoundEmbededFunctionJoinTableRelation['to']}_forward`
columns: []
isOneToOne: FoundEmbededFunctionJoinTableRelation['isOneToOne'] extends true
? true
: false
referencedColumns: []
referencedRelation: FoundEmbededFunctionJoinTableRelation['to']
} & {
match: 'func'
isNotNullable: FoundEmbededFunctionJoinTableRelation['isNotNullable'] extends true
? true
: FoundEmbededFunctionJoinTableRelation['isSetofReturn'] extends true
? false
: true
isSetofReturn: FoundEmbededFunctionJoinTableRelation['isSetofReturn']
}
direction: 'forward'
from: CurrentTableOrView
type: 'found-by-embeded-function'
}
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
/**
* Given a CurrentTableOrView, finds all join tables to this relation.
* For example, if products and categories are linked via product_categories table:
*
* @example
* Given:
* - CurrentTableView = 'products'
* - FieldName = "categories"
*
* It should return this relationship from product_categories:
* {
* foreignKeyName: "product_categories_category_id_fkey",
* columns: ["category_id"],
* isOneToOne: false,
* referencedRelation: "categories",
* referencedColumns: ["id"]
* }
*/
type ResolveJoinTableRelationship<
Schema extends GenericSchema,
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
FieldName extends string,
> = {
[TableName in keyof TablesAndViews<Schema>]: DeduplicateRelationships<
TablesAndViews<Schema>[TableName]['Relationships']
> extends readonly (infer Rel)[]
? Rel extends { referencedRelation: CurrentTableOrView }
? DeduplicateRelationships<
TablesAndViews<Schema>[TableName]['Relationships']
> extends readonly (infer OtherRel)[]
? OtherRel extends { referencedRelation: FieldName }
? OtherRel
: never
: never
: never
: never
}[keyof TablesAndViews<Schema>]
type ResolveEmbededFunctionJoinTableRelationship<
Schema extends GenericSchema,
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
FieldName extends string,
> =
FindMatchingFunctionBySetofFrom<
Schema['Functions'][FieldName],
CurrentTableOrView
> extends infer Fn
? Fn extends GenericFunction
? Fn['SetofOptions']
: false
: false
export type FindJoinTableRelationship<
Schema extends GenericSchema,
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
FieldName extends string,
> =
ResolveJoinTableRelationship<Schema, CurrentTableOrView, FieldName> extends infer Result
? [Result] extends [never]
? false
: Result
: never
/**
* Finds a matching relationship based on the FieldNode's name and optional hint.
*/
export type FindFieldMatchingRelationships<
Schema extends GenericSchema,
Relationships extends GenericRelationship[],
Field extends Ast.FieldNode,
> = Field extends { hint: string }
? FindMatchingHintTableRelationships<
Schema,
Relationships,
Field['hint'],
Field['name']
> extends GenericRelationship
? FindMatchingHintTableRelationships<Schema, Relationships, Field['hint'], Field['name']> & {
branch: 'found-in-table-via-hint'
hint: Field['hint']
}
: FindMatchingHintViewRelationships<
Schema,
Relationships,
Field['hint'],
Field['name']
> extends GenericRelationship
? FindMatchingHintViewRelationships<Schema, Relationships, Field['hint'], Field['name']> & {
branch: 'found-in-view-via-hint'
hint: Field['hint']
}
: SelectQueryError<'Failed to find matching relation via hint'>
: FindMatchingTableRelationships<Schema, Relationships, Field['name']> extends GenericRelationship
? FindMatchingTableRelationships<Schema, Relationships, Field['name']> & {
branch: 'found-in-table-via-name'
name: Field['name']
}
: FindMatchingViewRelationships<
Schema,
Relationships,
Field['name']
> extends GenericRelationship
? FindMatchingViewRelationships<Schema, Relationships, Field['name']> & {
branch: 'found-in-view-via-name'
name: Field['name']
}
: SelectQueryError<'Failed to find matching relation via name'>
export type JsonPathToAccessor<Path extends string> = Path extends `${infer P1}->${infer P2}`
? P2 extends `>${infer Rest}` // Handle ->> operator
? JsonPathToAccessor<`${P1}.${Rest}`>
: P2 extends string // Handle -> operator
? JsonPathToAccessor<`${P1}.${P2}`>
: Path
: Path extends `>${infer Rest}` // Clean up any remaining > characters
? JsonPathToAccessor<Rest>
: Path extends `${infer P1}::${infer _}` // Handle type casting
? JsonPathToAccessor<P1>
: Path extends `${infer P1}${')' | ','}${infer _}` // Handle closing parenthesis and comma
? P1
: Path
export type JsonPathToType<T, Path extends string> = Path extends ''
? T
: ContainsNull<T> extends true
? JsonPathToType<Exclude<T, null>, Path>
: Path extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? JsonPathToType<T[Key], Rest>
: never
: Path extends keyof T
? T[Path]
: never
export type IsStringUnion<T> = string extends T
? false
: T extends string
? [T] extends [never]
? false
: true
: false
type MatchingFunctionBySetofFrom<
Fn extends GenericFunction,
TableName extends string,
> = Fn['SetofOptions'] extends GenericSetofOption
? TableName extends Fn['SetofOptions']['from']
? Fn
: never
: false
type FindMatchingFunctionBySetofFrom<
FnUnion,
TableName extends string,
> = FnUnion extends infer Fn extends GenericFunction
? MatchingFunctionBySetofFrom<Fn, TableName>
: false
type ComputedField<
Schema extends GenericSchema,
RelationName extends keyof TablesAndViews<Schema>,
FieldName extends keyof TablesAndViews<Schema>[RelationName]['Row'],
> = FieldName extends keyof Schema['Functions']
? Schema['Functions'][FieldName] extends {
Args: { '': TablesAndViews<Schema>[RelationName]['Row'] }
Returns: any
}
? FieldName
: never
: never
// Given a relation name (Table or View) extract all the "computed fields" based on the Row
// object, and the schema functions definitions
export type GetComputedFields<
Schema extends GenericSchema,
RelationName extends keyof TablesAndViews<Schema>,
> = {
[K in keyof TablesAndViews<Schema>[RelationName]['Row']]: ComputedField<Schema, RelationName, K>
}[keyof TablesAndViews<Schema>[RelationName]['Row']]