import { ClientServerOptions, GenericTable } from '../types' import { ContainsNull, GenericRelationship, PostgreSQLTypes } from './types' import { Ast, ParseQuery } from './parser' import { AggregateFunctions, ExtractFirstProperty, GenericSchema, IsNonEmptyArray, Prettify, TablesAndViews, TypeScriptTypes, } from './types' import { CheckDuplicateEmbededReference, GetComputedFields, GetFieldNodeResultName, IsAny, IsRelationNullable, IsStringUnion, JsonPathToType, ResolveRelationship, SelectQueryError, } from './utils' export type SpreadOnManyEnabled = PostgrestVersion extends `13${string}` ? true : false /** * 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, RelationName, Relationships, Query extends string, ClientOptions extends ClientServerOptions > = IsAny extends true ? ParseQuery extends infer ParsedQuery ? ParsedQuery extends Ast.Node[] ? RelationName extends string ? ProcessNodesWithoutSchema : 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 extends infer ParsedQuery ? ParsedQuery extends Ast.Node[] ? RPCCallNodes : ParsedQuery : Row : ParseQuery extends infer ParsedQuery ? ParsedQuery extends Ast.Node[] ? RelationName extends string ? Relationships extends GenericRelationship[] ? ProcessNodes : SelectQueryError<'Invalid Relationships cannot infer result type'> : SelectQueryError<'Invalid RelationName cannot infer result type'> : ParsedQuery : never type ProcessSimpleFieldWithoutSchema = 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['castType'] extends PostgreSQLTypes ? TypeScriptTypes : number } : { // Aliases override the property name in the result [K in GetFieldNodeResultName]: Field['castType'] extends PostgreSQLTypes // We apply the detected casted as the result type ? TypeScriptTypes : any } type ProcessFieldNodeWithoutSchema = IsNonEmptyArray< Node['children'] > extends true ? { [K in GetFieldNodeResultName]: Node['children'] extends Ast.Node[] ? ProcessNodesWithoutSchema[] : ProcessSimpleFieldWithoutSchema } : ProcessSimpleFieldWithoutSchema /** * Processes a single Node without schema and returns the resulting TypeScript type. */ type ProcessNodeWithoutSchema = 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['castType'] extends PostgreSQLTypes ? TypeScriptTypes : any } : any : Node extends Ast.FieldNode ? ProcessFieldNodeWithoutSchema : any /** * Processes nodes when Schema is any, providing basic type inference */ type ProcessNodesWithoutSchema< Nodes extends Ast.Node[], Acc extends Record = {} > = Nodes extends [infer FirstNode, ...infer RestNodes] ? FirstNode extends Ast.Node ? RestNodes extends Ast.Node[] ? ProcessNodeWithoutSchema extends infer FieldResult ? FieldResult extends Record ? ProcessNodesWithoutSchema : FieldResult : any : any : any : Prettify /** * 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, 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> : 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, Acc extends Record = {} // Acc is now an object > = Nodes extends [infer FirstNode, ...infer RestNodes] ? FirstNode extends Ast.Node ? RestNodes extends Ast.Node[] ? ProcessRPCNode extends infer FieldResult ? FieldResult extends Record ? RPCCallNodes : FieldResult extends SelectQueryError ? SelectQueryError : 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 /** * 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, RelationName extends string, Relationships extends GenericRelationship[], Nodes extends Ast.Node[], Acc extends Record = {} // Acc is now an object > = CheckDuplicateEmbededReference 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 ? ProcessNodes< ClientOptions, Schema, Row, RelationName, Relationships, RestNodes, // TODO: // This SHOULD be `Omit & 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 ? SelectQueryError : 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 : Prettify> /** * 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, 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 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> : NodeType['type'] extends Ast.SpreadNode['type'] // If the selection is a ...spread ? ProcessSpreadNode< ClientOptions, Schema, Row, RelationName, Relationships, Extract > : NodeType['type'] extends Ast.FieldNode['type'] ? ProcessFieldNode< ClientOptions, Schema, Row, RelationName, Relationships, Extract > : 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, RelationName extends string, Relationships extends GenericRelationship[], Field extends Ast.FieldNode > = Field['children'] extends [] ? {} : IsNonEmptyArray extends true // Has embedded resource? ? ProcessEmbeddedResource : ProcessSimpleField type ResolveJsonPathType< Value, Path extends string | undefined, CastType extends PostgreSQLTypes > = Path extends string ? JsonPathToType extends never ? // Always fallback if JsonPathToType returns never TypeScriptTypes : JsonPathToType 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 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 : TypeScriptTypes : // No json path, use regular type casting TypeScriptTypes /** * 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, 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['castType'] extends PostgreSQLTypes ? TypeScriptTypes : number } : { // Aliases override the property name in the result [K in GetFieldNodeResultName]: Field['castType'] extends PostgreSQLTypes ? ResolveJsonPathType : 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 & string > = ResolveRelationship extends infer Resolved ? Resolved extends { referencedTable: Pick relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' } direction: string } ? ProcessEmbeddedResourceResult : // Otherwise the Resolved is a SelectQueryError return it { [K in GetFieldNodeResultName]: Resolved } : { [K in GetFieldNodeResultName]: 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 relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' } direction: string }, Field extends Ast.FieldNode, CurrentTableOrView extends keyof TablesAndViews > = ProcessNodes< ClientOptions, Schema, Resolved['referencedTable']['Row'], Field['name'], Resolved['referencedTable']['Relationships'], Field['children'] extends undefined ? [] : Exclude extends Ast.Node[] ? Exclude : [] > extends infer ProcessedChildren ? { [K in GetFieldNodeResultName]: Resolved['direction'] extends 'forward' ? Field extends { innerJoin: true } ? Resolved['relation']['isOneToOne'] extends true ? ProcessedChildren : ProcessedChildren[] : Resolved['relation']['isOneToOne'] extends true ? 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[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[CurrentTableOrView], Resolved['relation'] > extends true ? Field extends { innerJoin: true } ? ProcessedChildren : ProcessedChildren | null : ProcessedChildren } : { [K in GetFieldNodeResultName]: 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, RelationName extends string, Relationships extends GenericRelationship[], Spread extends Ast.SpreadNode > = ProcessNode< ClientOptions, Schema, Row, RelationName, Relationships, Spread['target'] > extends infer Result ? Result extends SelectQueryError ? SelectQueryError : ExtractFirstProperty extends unknown[] ? SpreadOnManyEnabled extends true // Spread over an many-to-many relationship, turn all the result fields into correlated arrays ? ProcessManyToManySpreadNodeResult : { [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 : 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 extends Record< string, SelectQueryError | null > ? Result : ExtractFirstProperty extends infer SpreadedObject ? SpreadedObject extends Array> ? { [K in keyof SpreadedObject[number]]: Array } : 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 extends Record< string, SelectQueryError | null > ? Result : ExtractFirstProperty extends infer SpreadedObject ? ContainsNull 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'>