From cc9344a8221d099fff0f0386de0cd0e819068751 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Wed, 6 Nov 2024 08:35:27 +0100 Subject: [PATCH] fix(types): make types retrocompatible to typescript 4.5.5 (#571) * fix(types): spread and embeding combination issues * chore: remove debug test case * chore: reproduce invalid node type error * chore: using type fix the test * fix(types): types result with schema as any * wip: make parser logic retrocompatible * wip: almost there * chore(types): make GetResult compatible with typescript 4.5.5 * chore: fix merge * chore: finish merge * chore: revert non essential changes * chore: add TODO --------- Co-authored-by: Bobbie Soedirgo Co-authored-by: Bobbie Soedirgo <31685197+soedirgo@users.noreply.github.com> --- package-lock.json | 15 +- package.json | 2 +- src/select-query-parser/parser.ts | 112 +++---- src/select-query-parser/result.ts | 70 ++-- src/select-query-parser/utils.ts | 375 ++++++++++++---------- test/relationships.ts | 26 ++ test/select-query-parser/result.test-d.ts | 4 +- test/select-query-parser/rpc.test-d.ts | 4 +- test/select-query-parser/select.test-d.ts | 2 +- 9 files changed, 337 insertions(+), 273 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5200b488..55395c0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "ts-jest": "^28.0.3", "tsd": "^0.31.2", "typedoc": "^0.22.16", - "typescript": "~4.7", + "typescript": "4.5.5", "wait-for-localhost-cli": "^3.0.0" } }, @@ -5978,10 +5978,11 @@ } }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10605,9 +10606,9 @@ } }, "typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true }, "v8-to-istanbul": { diff --git a/package.json b/package.json index dda574d6..e440b5ab 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "ts-jest": "^28.0.3", "tsd": "^0.31.2", "typedoc": "^0.22.16", - "typescript": "~4.7", + "typescript": "4.5.5", "wait-for-localhost-cli": "^3.0.0" } } diff --git a/src/select-query-parser/parser.ts b/src/select-query-parser/parser.ts index 77a57e30..a3944d73 100644 --- a/src/select-query-parser/parser.ts +++ b/src/select-query-parser/parser.ts @@ -12,10 +12,12 @@ import { SimplifyDeep } from '../types' */ export type ParseQuery = string extends Query ? GenericStringError - : ParseNodes> extends [infer Nodes extends Ast.Node[], `${infer Remainder}`] - ? EatWhitespace extends '' - ? SimplifyDeep - : ParserError<`Unexpected input: ${Remainder}`> + : ParseNodes> extends [infer Nodes, `${infer Remainder}`] + ? Nodes extends Ast.Node[] + ? EatWhitespace extends '' + ? SimplifyDeep + : ParserError<`Unexpected input: ${Remainder}`> + : ParserError<'Invalid nodes array structure'> : ParseNodes> /** @@ -34,14 +36,15 @@ type ParseNodes = string extends Input : ParseNodesHelper type ParseNodesHelper = ParseNode extends [ - infer Node extends Ast.Node, + infer Node, `${infer Remainder}` ] - ? EatWhitespace extends `,${infer Remainder}` - ? ParseNodesHelper, [...Nodes, Node]> - : [[...Nodes, Node], EatWhitespace] + ? Node extends Ast.Node + ? EatWhitespace extends `,${infer Remainder}` + ? ParseNodesHelper, [...Nodes, Node]> + : [[...Nodes, Node], EatWhitespace] + : ParserError<'Invalid node type in nodes helper'> : ParseNode - /** * Parses a node. * A node is one of the following: @@ -57,11 +60,10 @@ type ParseNode = Input extends '' ? [Ast.StarNode, EatWhitespace] : // `...field` Input extends `...${infer Remainder}` - ? ParseField> extends [ - infer TargetField extends Ast.FieldNode, - `${infer Remainder}` - ] - ? [{ type: 'spread'; target: TargetField }, EatWhitespace] + ? ParseField> extends [infer TargetField, `${infer Remainder}`] + ? TargetField extends Ast.FieldNode + ? [{ type: 'spread'; target: TargetField }, EatWhitespace] + : ParserError<'Invalid target field type in spread'> : ParserError<`Unable to parse spread resource at \`${Input}\``> : ParseIdentifier extends [infer NameOrAlias, `${infer Remainder}`] ? EatWhitespace extends `::${infer _}` @@ -69,11 +71,10 @@ type ParseNode = Input extends '' ParseField : EatWhitespace extends `:${infer Remainder}` ? // `alias:` - ParseField> extends [ - infer Field extends Ast.FieldNode, - `${infer Remainder}` - ] - ? [Omit & { alias: NameOrAlias }, EatWhitespace] + ParseField> extends [infer Field, `${infer Remainder}`] + ? Field extends Ast.FieldNode + ? [Omit & { alias: NameOrAlias }, EatWhitespace] + : 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 @@ -98,24 +99,22 @@ type ParseField = Input extends '' ? Name extends 'count' ? ParseCountField : Remainder extends `!inner${infer Remainder}` - ? ParseEmbeddedResource> extends [ - infer Children extends Ast.Node[], - `${infer Remainder}` - ] - ? // `field!inner(nodes)` - [{ type: 'field'; name: Name; innerJoin: true; children: Children }, Remainder] + ? ParseEmbeddedResource> 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>, `Expected embedded resource after "!inner" at \`${Remainder}\`` > : EatWhitespace extends `!left${infer Remainder}` - ? ParseEmbeddedResource> extends [ - infer Children extends Ast.Node[], - `${infer Remainder}` - ] - ? // `field!left(nodes)` - // !left is a noise word - treat it the same way as a non-`!inner`. - [{ type: 'field'; name: Name; children: Children }, EatWhitespace] + ? ParseEmbeddedResource> 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] + : ParserError<'Invalid children array in left join'> : CreateParserErrorIfRequired< ParseEmbeddedResource>, `Expected embedded resource after "!left" at \`${EatWhitespace}\`` @@ -124,30 +123,36 @@ type ParseField = Input extends '' ? ParseIdentifier> extends [infer Hint, `${infer Remainder}`] ? EatWhitespace extends `!inner${infer Remainder}` ? ParseEmbeddedResource> extends [ - infer Children extends Ast.Node[], + infer Children, `${infer Remainder}` ] - ? // `field!hint!inner(nodes)` - [ - { type: 'field'; name: Name; hint: Hint; innerJoin: true; children: Children }, - EatWhitespace - ] + ? Children extends Ast.Node[] + ? // `field!hint!inner(nodes)` + [ + { type: 'field'; name: Name; hint: Hint; innerJoin: true; children: Children }, + EatWhitespace + ] + : ParserError<'Invalid children array in hint inner join'> : ParseEmbeddedResource> : ParseEmbeddedResource> extends [ - infer Children extends Ast.Node[], + infer Children, `${infer Remainder}` ] - ? // `field!hint(nodes)` - [{ type: 'field'; name: Name; hint: Hint; children: Children }, EatWhitespace] + ? Children extends Ast.Node[] + ? // `field!hint(nodes)` + [ + { type: 'field'; name: Name; hint: Hint; children: Children }, + EatWhitespace + ] + : ParserError<'Invalid children array in hint'> : ParseEmbeddedResource> : ParserError<`Expected identifier after "!" at \`${EatWhitespace}\``> : EatWhitespace extends `(${infer _}` - ? ParseEmbeddedResource> extends [ - infer Children extends Ast.Node[], - `${infer Remainder}` - ] - ? // `field(nodes)` - [{ type: 'field'; name: Name; children: Children }, EatWhitespace] + ? ParseEmbeddedResource> extends [infer Children, `${infer Remainder}`] + ? Children extends Ast.Node[] + ? // `field(nodes)` + [{ type: 'field'; name: Name; children: Children }, EatWhitespace] + : ParserError<'Invalid children array in field'> : // Return error if start of embedded resource was detected but not found. ParseEmbeddedResource> : // Otherwise it's a non-embedded resource field. @@ -184,13 +189,12 @@ type ParseCountField = ParseIdentifier extends [ type ParseEmbeddedResource = Input extends `(${infer Remainder}` ? EatWhitespace extends `)${infer Remainder}` ? [[], EatWhitespace] - : ParseNodes> extends [ - infer Nodes extends Ast.Node[], - `${infer Remainder}` - ] - ? EatWhitespace extends `)${infer Remainder}` - ? [Nodes, EatWhitespace] - : ParserError<`Expected ")" at \`${EatWhitespace}\``> + : ParseNodes> extends [infer Nodes, `${infer Remainder}`] + ? Nodes extends Ast.Node[] + ? EatWhitespace extends `)${infer Remainder}` + ? [Nodes, EatWhitespace] + : ParserError<`Expected ")" at \`${EatWhitespace}\``> + : ParserError<'Invalid nodes array in embedded resource'> : ParseNodes> : ParserError<`Expected "(" at \`${Input}\``> diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index 88c0493e..41928ced 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -35,14 +35,18 @@ export type GetResult< Relationships, Query extends string > = IsAny extends true - ? ParseQuery extends infer ParsedQuery extends Ast.Node[] - ? RelationName extends string - ? ProcessNodesWithoutSchema - : any + ? 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 extends Ast.Node[] - ? RPCCallNodes + ? ParseQuery extends infer ParsedQuery + ? ParsedQuery extends Ast.Node[] + ? RPCCallNodes + : ParsedQuery : Row : ParseQuery extends infer ParsedQuery ? ParsedQuery extends Ast.Node[] @@ -111,11 +115,15 @@ type ProcessNodeWithoutSchema = Node extends Ast.StarNode type ProcessNodesWithoutSchema< Nodes extends Ast.Node[], Acc extends Record = {} -> = Nodes extends [infer FirstNode extends Ast.Node, ...infer RestNodes extends Ast.Node[]] - ? ProcessNodeWithoutSchema extends infer FieldResult - ? FieldResult extends Record - ? ProcessNodesWithoutSchema - : FieldResult +> = 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 @@ -144,14 +152,18 @@ export type RPCCallNodes< RelationName extends string, Row extends Record, Acc extends Record = {} // Acc is now an object -> = Nodes extends [infer FirstNode extends Ast.Node, ...infer 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.'> +> = 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 /** @@ -172,14 +184,18 @@ export type ProcessNodes< Nodes extends Ast.Node[], Acc extends Record = {} // Acc is now an object > = CheckDuplicateEmbededReference extends false - ? Nodes extends [infer FirstNode extends Ast.Node, ...infer RestNodes extends Ast.Node[]] - ? ProcessNode extends infer FieldResult - ? FieldResult extends Record - ? ProcessNodes - : FieldResult extends SelectQueryError - ? SelectQueryError - : SelectQueryError<'Could not retrieve a valid record or error value'> - : SelectQueryError<'Processing node failed.'> + ? Nodes extends [infer FirstNode, ...infer RestNodes] + ? FirstNode extends Ast.Node + ? RestNodes extends Ast.Node[] + ? ProcessNode extends infer FieldResult + ? FieldResult extends Record + ? ProcessNodes + : 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> diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index 90ca4d60..1b1a3bea 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -38,27 +38,24 @@ type ResolveRelationships< Relationships extends GenericRelationship[], Nodes extends Ast.FieldNode[] > = UnionToArray<{ - [K in keyof Nodes]: ResolveRelationship< - Schema, - Relationships, - Nodes[K], - RelationName - > extends infer Relation - ? Relation extends { - relation: { - referencedRelation: any - foreignKeyName: any - match: any - } - from: any - } - ? { - referencedTable: Relation['relation']['referencedRelation'] - fkName: Relation['relation']['foreignKeyName'] - from: Relation['from'] - match: Relation['relation']['match'] - fieldName: GetFieldNodeResultName + [K in keyof Nodes]: Nodes[K] extends Ast.FieldNode + ? ResolveRelationship 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 + } + : Relation : never : never }>[0] @@ -69,10 +66,12 @@ type ResolveRelationships< type IsDoubleReference = T extends { referencedTable: infer RT fieldName: infer FN - match: infer M extends 'col' | 'refrel' + match: infer M } - ? U extends { referencedTable: RT; fieldName: FN; match: M } - ? true + ? M extends 'col' | 'refrel' + ? U extends { referencedTable: RT; fieldName: FN; match: M } + ? true + : false : false : false @@ -97,21 +96,25 @@ export type CheckDuplicateEmbededReference< RelationName extends string, Relationships extends GenericRelationship[], Nodes extends Ast.Node[] -> = FilterRelationNodes extends infer RelationsNodes extends Ast.FieldNode[] - ? ResolveRelationships< - Schema, - RelationName, - Relationships, - RelationsNodes - > extends infer ResolvedRels - ? ResolvedRels extends unknown[] - ? FindDuplicates extends infer Duplicates - ? Duplicates extends never - ? false - : Duplicates extends { fieldName: infer FieldName extends string } - ? { - [K in FieldName]: SelectQueryError<`table "${RelationName}" specified more than once use hinting for desambiguation`> - } +> = FilterRelationNodes extends infer RelationsNodes + ? RelationsNodes extends Ast.FieldNode[] + ? ResolveRelationships< + Schema, + RelationName, + Relationships, + RelationsNodes + > extends infer ResolvedRels + ? ResolvedRels extends unknown[] + ? FindDuplicates 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 @@ -155,33 +158,38 @@ type CheckRelationshipError< : // If the relation is a reverse relation with no hint (matching by name) FoundRelation extends { relation: { - referencedRelation: infer RelatedRelationName extends string + referencedRelation: infer RelatedRelationName name: string } direction: 'reverse' } - ? // We check if there is possible confusion with other relations with this table - HasMultipleFKeysToFRel 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}! ?`> - : FoundRelation + ? RelatedRelationName extends string + ? // We check if there is possible confusion with other relations with this table + HasMultipleFKeysToFRel 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}! ?`> + : FoundRelation + : never : // Same check for forward relationships, but we must gather the relationships from the found relation FoundRelation extends { relation: { - referencedRelation: infer RelatedRelationName extends string + referencedRelation: infer RelatedRelationName name: string } direction: 'forward' - from: infer From extends keyof TablesAndViews & string + from: infer From } - ? HasMultipleFKeysToFRel< - RelatedRelationName, - TablesAndViews[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}! ?`> - : FoundRelation + ? RelatedRelationName extends string + ? From extends keyof TablesAndViews & string + ? HasMultipleFKeysToFRel< + RelatedRelationName, + TablesAndViews[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}! ?`> + : FoundRelation + : never + : never : FoundRelation - /** * Resolves relationships for embedded resources and retrieves the referenced Table */ @@ -217,26 +225,28 @@ type ResolveReverseRelationship< > = FindFieldMatchingRelationships extends infer FoundRelation ? FoundRelation extends never ? false - : FoundRelation extends { referencedRelation: infer RelatedRelationName extends string } - ? RelatedRelationName extends keyof TablesAndViews - ? // If the relation was found via hinting we just return it without any more checks - FoundRelation extends { hint: string } - ? { - referencedTable: TablesAndViews[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 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}! ?`> - : { - referencedTable: TablesAndViews[RelatedRelationName] - relation: FoundRelation - direction: 'reverse' - from: CurrentTableOrView - } - : SelectQueryError<`Relation '${RelatedRelationName}' not found in schema.`> + : FoundRelation extends { referencedRelation: infer RelatedRelationName } + ? RelatedRelationName extends string + ? RelatedRelationName extends keyof TablesAndViews + ? // If the relation was found via hinting we just return it without any more checks + FoundRelation extends { hint: string } + ? { + referencedTable: TablesAndViews[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 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}! ?`> + : { + referencedTable: TablesAndViews[RelatedRelationName] + relation: FoundRelation + direction: 'reverse' + from: CurrentTableOrView + } + : SelectQueryError<`Relation '${RelatedRelationName}' not found in schema.`> + : false : false : false @@ -244,17 +254,19 @@ export type FindMatchingTableRelationships< Schema extends GenericSchema, Relationships extends GenericRelationship[], value extends string -> = Relationships extends [infer R, ...infer 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' } +> = 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 : FindMatchingTableRelationships - : FindMatchingTableRelationships + : false : false : false @@ -262,17 +274,19 @@ export type FindMatchingViewRelationships< Schema extends GenericSchema, Relationships extends GenericRelationship[], value extends string -> = Relationships extends [infer R, ...infer 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' } +> = 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 : FindMatchingViewRelationships - : FindMatchingViewRelationships + : false : false : false @@ -281,36 +295,39 @@ export type FindMatchingHintTableRelationships< Relationships extends GenericRelationship[], hint extends string, name extends string -> = Relationships extends [infer R, ...infer 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' } +> = 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 : FindMatchingHintTableRelationships - : FindMatchingHintTableRelationships + : false : false : false - export type FindMatchingHintViewRelationships< Schema extends GenericSchema, Relationships extends GenericRelationship[], hint extends string, name extends string -> = Relationships extends [infer R, ...infer 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' } +> = 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 : FindMatchingHintViewRelationships - : FindMatchingHintViewRelationships + : false : false : false @@ -337,8 +354,10 @@ type TableForwardRelationships< > = TName extends keyof TablesAndViews ? UnionToArray< RecursivelyFindRelationships> - > extends infer R extends (GenericRelationship & { from: keyof TablesAndViews })[] - ? R + > extends infer R + ? R extends (GenericRelationship & { from: keyof TablesAndViews })[] + ? R + : [] : [] : [] @@ -362,8 +381,7 @@ type FilterRelationships = R extends readonly (infer Rel)[] : never : never -// Find a relationship from the parent to the childrens -type ResolveForwardRelationship< +export type ResolveForwardRelationship< Schema extends GenericSchema, Field extends Ast.FieldNode, CurrentTableOrView extends keyof TablesAndViews & string @@ -371,46 +389,46 @@ type ResolveForwardRelationship< Schema, TablesAndViews[Field['name']]['Relationships'], Ast.FieldNode & { name: CurrentTableOrView; hint: Field['hint'] } -> extends infer FoundByName extends GenericRelationship - ? { - referencedTable: TablesAndViews[Field['name']] - relation: FoundByName - direction: 'forward' - from: Field['name'] - type: 'found-by-name' - } - : // The Field['name'] can sometimes be a reference to the related foreign key - // In that case, we can't use the Field['name'] to get back the relations, instead, we will find all relations pointing - // to our current table or view, and search if we can find a match in it - FindFieldMatchingRelationships< - Schema, - TableForwardRelationships, - Field - > extends infer FoundByMatch extends GenericRelationship & { - from: keyof TablesAndViews - } - ? { - referencedTable: TablesAndViews[FoundByMatch['from']] - relation: FoundByMatch - direction: 'forward' - from: CurrentTableOrView - type: 'found-by-match' - } - : // Forward relations can also alias other tables via tables joins relationships - // in such cases we crawl all the tables looking for a join table between our current table - // and the Field['name'] desired desitnation - FindJoinTableRelationship< - Schema, - CurrentTableOrView, - Field['name'] - > extends infer FoundByJoinTable extends GenericRelationship - ? { - referencedTable: TablesAndViews[FoundByJoinTable['referencedRelation']] - relation: FoundByJoinTable & { match: 'refrel' } - direction: 'forward' - from: CurrentTableOrView - type: 'found-by-join-table' - } +> extends infer FoundByName + ? FoundByName extends GenericRelationship + ? { + referencedTable: TablesAndViews[Field['name']] + relation: FoundByName + direction: 'forward' + from: Field['name'] + type: 'found-by-name' + } + : FindFieldMatchingRelationships< + Schema, + TableForwardRelationships, + Field + > extends infer FoundByMatch + ? FoundByMatch extends GenericRelationship & { + from: keyof TablesAndViews + } + ? { + referencedTable: TablesAndViews[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[FoundByJoinTable['referencedRelation']] + relation: FoundByJoinTable & { match: 'refrel' } + direction: 'forward' + from: CurrentTableOrView + type: 'found-by-join-table' + } + : 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']}`> /** @@ -431,7 +449,7 @@ type ResolveForwardRelationship< * referencedColumns: ["id"] * } */ -export type FindJoinTableRelationship< +type ResolveJoinTableRelationship< Schema extends GenericSchema, CurrentTableOrView extends keyof TablesAndViews & string, FieldName extends string @@ -447,6 +465,15 @@ export type FindJoinTableRelationship< : never }[keyof TablesAndViews] +export type FindJoinTableRelationship< + Schema extends GenericSchema, + CurrentTableOrView extends keyof TablesAndViews & string, + FieldName extends string +> = ResolveJoinTableRelationship extends infer Result + ? [Result] extends [never] + ? false + : Result + : never /** * Finds a matching relationship based on the FieldNode's name and optional hint. */ @@ -454,43 +481,35 @@ export type FindFieldMatchingRelationships< Schema extends GenericSchema, Relationships extends GenericRelationship[], Field extends Ast.FieldNode -> = Field extends { hint: infer Hint extends string } +> = Field extends { hint: string } ? FindMatchingHintTableRelationships< Schema, Relationships, - Hint, + Field['hint'], Field['name'] - > extends infer TableRelationViaHint extends GenericRelationship - ? TableRelationViaHint & { + > extends GenericRelationship + ? FindMatchingHintTableRelationships & { branch: 'found-in-table-via-hint' hint: Field['hint'] } : FindMatchingHintViewRelationships< Schema, Relationships, - Hint, + Field['hint'], Field['name'] - > extends infer TableViewViaHint extends GenericRelationship - ? TableViewViaHint & { + > extends GenericRelationship + ? FindMatchingHintViewRelationships & { branch: 'found-in-view-via-hint' hint: Field['hint'] } : SelectQueryError<'Failed to find matching relation via hint'> - : FindMatchingTableRelationships< - Schema, - Relationships, - Field['name'] - > extends infer TableRelationViaName extends GenericRelationship - ? TableRelationViaName & { + : FindMatchingTableRelationships extends GenericRelationship + ? FindMatchingTableRelationships & { branch: 'found-in-table-via-name' name: Field['name'] } - : FindMatchingViewRelationships< - Schema, - Relationships, - Field['name'] - > extends infer ViewRelationViaName extends GenericRelationship - ? ViewRelationViaName & { + : FindMatchingViewRelationships extends GenericRelationship + ? FindMatchingViewRelationships & { branch: 'found-in-view-via-name' name: Field['name'] } diff --git a/test/relationships.ts b/test/relationships.ts index 6b3cf7a4..faca706a 100644 --- a/test/relationships.ts +++ b/test/relationships.ts @@ -185,6 +185,11 @@ export const selectParams = { from: 'products', select: '*, categories(*)', }, + nestedQueryWithSelectiveFieldsAndInnerJoin: { + from: 'users', + select: + 'msgs:messages(id, ...message_details(created_at, channel!inner(id, slug, owner:users(*))))', + }, } as const export const selectQueries = { @@ -363,6 +368,9 @@ export const selectQueries = { manyToManyWithJoinTable: postgrest .from(selectParams.manyToManyWithJoinTable.from) .select(selectParams.manyToManyWithJoinTable.select), + nestedQueryWithSelectiveFieldsAndInnerJoin: postgrest + .from(selectParams.nestedQueryWithSelectiveFieldsAndInnerJoin.from) + .select(selectParams.nestedQueryWithSelectiveFieldsAndInnerJoin.select), } as const test('nested query with selective fields', async () => { @@ -1868,3 +1876,21 @@ test('many-to-many with join table', async () => { } `) }) + +test('nested query with selective fields and inner join should error on non existing relation', async () => { + const res = await selectQueries.nestedQueryWithSelectiveFieldsAndInnerJoin.limit(1).single() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "PGRST200", + "details": "Searched for a foreign key relationship between 'messages' and 'message_details' in the schema 'public', but no matches were found.", + "hint": null, + "message": "Could not find a relationship between 'messages' and 'message_details' in the schema cache", + }, + "status": 400, + "statusText": "Bad Request", + } + `) +}) diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index ff841096..74a40836 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -72,7 +72,7 @@ type SelectQueryFromTableResult< expectType>(true) } -// nested query with selective fields +// nested query with selective fields and inner join should error on non existing relation { let result: SelectQueryFromTableResult< 'users', @@ -81,7 +81,7 @@ type SelectQueryFromTableResult< let expected: { msgs: { id: number - message_details: SelectQueryError<`Could not embed because more than one relationship was found for 'messages' and '${string}' you need to hint the column with messages! ?`> + message_details: SelectQueryError<'could not find the relation between messages and message_details'> }[] } expectType>(true) diff --git a/test/select-query-parser/rpc.test-d.ts b/test/select-query-parser/rpc.test-d.ts index 858434bf..f22b915b 100644 --- a/test/select-query-parser/rpc.test-d.ts +++ b/test/select-query-parser/rpc.test-d.ts @@ -5,9 +5,7 @@ import { TypeEqual } from 'ts-expect' // RPC call with no params { - const { data } = await postgrest - .rpc(RPC_NAME, { name_param: 'supabot' }) - .select(selectParams.noParams) + const { data } = await postgrest.rpc(RPC_NAME, { name_param: 'supabot' }).select() let result: Exclude let expected: Database['public']['Functions'][typeof RPC_NAME]['Returns'] expectType>(true) diff --git a/test/select-query-parser/select.test-d.ts b/test/select-query-parser/select.test-d.ts index af2fd540..2343fa6f 100644 --- a/test/select-query-parser/select.test-d.ts +++ b/test/select-query-parser/select.test-d.ts @@ -741,7 +741,7 @@ type Schema = Database['public'] { const { data, error } = await selectQueries.aggregateOnMissingColumnWithAlias.limit(1).single() if (error) throw error - expectType>(data) + expectType>(data!) } // many-to-many with join table