diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 469adac6..4412210f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,6 @@ jobs: - run: | npm clean-install npm run test + + - name: Coveralls + uses: coverallsapp/github-action@v2 diff --git a/package-lock.json b/package-lock.json index 5200b488..3fb7db94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,9 @@ "ts-expect": "^1.3.0", "ts-jest": "^28.0.3", "tsd": "^0.31.2", + "type-fest": "^4.32.0", "typedoc": "^0.22.16", - "typescript": "~4.7", + "typescript": "^4.5.5", "wait-for-localhost-cli": "^3.0.0" } }, @@ -1315,6 +1316,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -5904,12 +5918,13 @@ } }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.32.0.tgz", + "integrity": "sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5982,6 +5997,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7223,6 +7239,14 @@ "dev": true, "requires": { "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } } }, "ansi-regex": { @@ -10553,9 +10577,9 @@ "dev": true }, "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.32.0.tgz", + "integrity": "sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==", "dev": true }, "typedoc": { diff --git a/package.json b/package.json index dda574d6..d46e8b73 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "docs": "typedoc src/index.ts --out docs/v2", "docs:json": "typedoc --json docs/v2/spec.json --excludeExternals src/index.ts", "test": "run-s format:check test:types db:clean db:run test:run db:clean && node test/smoke.cjs && node test/smoke.mjs", - "test:run": "jest --runInBand", + "test:run": "jest --runInBand --coverage", "test:update": "run-s db:clean db:run && jest --runInBand --updateSnapshot && run-s db:clean", "test:types": "run-s build && tsd --files 'test/**/*.test-d.ts'", "db:clean": "cd test/db && docker compose down --volumes", @@ -59,8 +59,9 @@ "ts-expect": "^1.3.0", "ts-jest": "^28.0.3", "tsd": "^0.31.2", + "type-fest": "^4.32.0", "typedoc": "^0.22.16", - "typescript": "~4.7", + "typescript": "^4.5.5", "wait-for-localhost-cli": "^3.0.0" } } diff --git a/src/PostgrestBuilder.ts b/src/PostgrestBuilder.ts index 6327b2d1..dfd8a3c0 100644 --- a/src/PostgrestBuilder.ts +++ b/src/PostgrestBuilder.ts @@ -1,11 +1,14 @@ // @ts-ignore import nodeFetch from '@supabase/node-fetch' -import type { Fetch, FetchOptions, PostgrestSingleResponse } from './types' +import type { Fetch, FetchOptions, PostgrestSingleResponse, PostgrestResponseSuccess } from './types' import PostgrestError from './PostgrestError' -export default abstract class PostgrestBuilder - implements PromiseLike> +export default abstract class PostgrestBuilder + implements + PromiseLike< + ThrowOnError extends true ? PostgrestResponseSuccess : PostgrestSingleResponse + > { protected method: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE' protected url: URL @@ -42,9 +45,9 @@ export default abstract class PostgrestBuilder * * {@link https://github.com/supabase/supabase-js/issues/92} */ - throwOnError(): this { + throwOnError(): this & PostgrestBuilder { this.shouldThrowOnError = true - return this + return this as this & PostgrestBuilder } /** @@ -56,9 +59,18 @@ export default abstract class PostgrestBuilder return this } - then, TResult2 = never>( + then< + TResult1 = ThrowOnError extends true + ? PostgrestResponseSuccess + : PostgrestSingleResponse, + TResult2 = never + >( onfulfilled?: - | ((value: PostgrestSingleResponse) => TResult1 | PromiseLike) + | (( + value: ThrowOnError extends true + ? PostgrestResponseSuccess + : PostgrestSingleResponse + ) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null diff --git a/src/PostgrestFilterBuilder.ts b/src/PostgrestFilterBuilder.ts index 21cc4090..c0de7d33 100644 --- a/src/PostgrestFilterBuilder.ts +++ b/src/PostgrestFilterBuilder.ts @@ -1,4 +1,5 @@ import PostgrestTransformBuilder from './PostgrestTransformBuilder' +import { JsonPathToAccessor, JsonPathToType } from './select-query-parser/utils' import { GenericSchema } from './types' type FilterOperator = @@ -25,6 +26,49 @@ type FilterOperator = | 'phfts' | 'wfts' +export type IsStringOperator = Path extends `${string}->>${string}` + ? true + : false + +// Match relationship filters with `table.column` syntax and resolve underlying +// column value. If not matched, fallback to generic type. +// TODO: Validate the relationship itself ala select-query-parser. Currently we +// assume that all tables have valid relationships to each other, despite +// nonexistent foreign keys. +type ResolveFilterValue< + Schema extends GenericSchema, + Row extends Record, + ColumnName extends string +> = ColumnName extends `${infer RelationshipTable}.${infer Remainder}` + ? Remainder extends `${infer _}.${infer _}` + ? ResolveFilterValue + : ResolveFilterRelationshipValue + : ColumnName extends keyof Row + ? Row[ColumnName] + : // If the column selection is a jsonpath like `data->value` or `data->>value` we attempt to match + // the expected type with the parsed custom json type + IsStringOperator extends true + ? string + : JsonPathToType> extends infer JsonPathValue + ? JsonPathValue extends never + ? never + : JsonPathValue + : never + +type ResolveFilterRelationshipValue< + Schema extends GenericSchema, + RelationshipTable extends string, + RelationshipColumn extends string +> = Schema['Tables'] & Schema['Views'] extends infer TablesAndViews + ? RelationshipTable extends keyof TablesAndViews + ? 'Row' extends keyof TablesAndViews[RelationshipTable] + ? RelationshipColumn extends keyof TablesAndViews[RelationshipTable]['Row'] + ? TablesAndViews[RelationshipTable]['Row'][RelationshipColumn] + : unknown + : unknown + : unknown + : never + export default class PostgrestFilterBuilder< Schema extends GenericSchema, Row extends Record, @@ -32,11 +76,6 @@ export default class PostgrestFilterBuilder< RelationName = unknown, Relationships = unknown > extends PostgrestTransformBuilder { - eq( - column: ColumnName, - value: NonNullable - ): this - eq(column: string, value: NonNullable): this /** * Match only rows where `column` is equal to `value`. * @@ -45,20 +84,35 @@ export default class PostgrestFilterBuilder< * @param column - The column to filter on * @param value - The value to filter with */ - eq(column: string, value: unknown): this { + eq( + column: ColumnName, + value: ResolveFilterValue extends never + ? NonNullable + : // We want to infer the type before wrapping it into a `NonNullable` to avoid too deep + // type resolution error + ResolveFilterValue extends infer ResolvedFilterValue + ? NonNullable + : // We should never enter this case as all the branches are covered above + never + ): this { this.url.searchParams.append(column, `eq.${value}`) return this } - neq(column: ColumnName, value: Row[ColumnName]): this - neq(column: string, value: unknown): this /** * Match only rows where `column` is not equal to `value`. * * @param column - The column to filter on * @param value - The value to filter with */ - neq(column: string, value: unknown): this { + neq( + column: ColumnName, + value: ResolveFilterValue extends never + ? unknown + : ResolveFilterValue extends infer ResolvedFilterValue + ? ResolvedFilterValue + : never + ): this { this.url.searchParams.append(column, `neq.${value}`) return this } @@ -227,18 +281,25 @@ export default class PostgrestFilterBuilder< return this } - in( - column: ColumnName, - values: ReadonlyArray - ): this - in(column: string, values: readonly unknown[]): this /** * Match only rows where `column` is included in the `values` array. * * @param column - The column to filter on * @param values - The values array to filter with */ - in(column: string, values: readonly unknown[]): this { + in( + column: ColumnName, + values: ReadonlyArray< + ResolveFilterValue extends never + ? unknown + : // We want to infer the type before wrapping it into a `NonNullable` to avoid too deep + // type resolution error + ResolveFilterValue extends infer ResolvedFilterValue + ? ResolvedFilterValue + : // We should never enter this case as all the branches are covered above + never + > + ): this { const cleanedValues = Array.from(new Set(values)) .map((s) => { // handle postgrest reserved characters diff --git a/src/PostgrestTransformBuilder.ts b/src/PostgrestTransformBuilder.ts index e98656f6..74772990 100644 --- a/src/PostgrestTransformBuilder.ts +++ b/src/PostgrestTransformBuilder.ts @@ -226,7 +226,7 @@ export default class PostgrestTransformBuilder< ResultOne = Result extends (infer ResultOne)[] ? ResultOne : never >(): PostgrestBuilder { this.headers['Accept'] = 'application/vnd.pgrst.object+json' - return this as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** @@ -246,7 +246,7 @@ export default class PostgrestTransformBuilder< this.headers['Accept'] = 'application/vnd.pgrst.object+json' } this.isMaybeSingle = true - return this as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** @@ -254,7 +254,7 @@ export default class PostgrestTransformBuilder< */ csv(): PostgrestBuilder { this.headers['Accept'] = 'text/csv' - return this as PostgrestBuilder + return this as unknown as PostgrestBuilder } /** @@ -262,7 +262,7 @@ export default class PostgrestTransformBuilder< */ geojson(): PostgrestBuilder> { this.headers['Accept'] = 'application/geo+json' - return this as PostgrestBuilder> + return this as unknown as PostgrestBuilder> } /** @@ -319,8 +319,8 @@ export default class PostgrestTransformBuilder< this.headers[ 'Accept' ] = `application/vnd.pgrst.plan+${format}; for="${forMediatype}"; options=${options};` - if (format === 'json') return this as PostgrestBuilder[]> - else return this as PostgrestBuilder + if (format === 'json') return this as unknown as PostgrestBuilder[]> + else return this as unknown as PostgrestBuilder } /** diff --git a/src/index.ts b/src/index.ts index 06ac6622..466a71bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,3 +29,6 @@ export type { PostgrestSingleResponse, PostgrestMaybeSingleResponse, } from './types' +// https://github.com/supabase/postgrest-js/issues/551 +// To be replaced with a helper type that only uses public types +export type { GetResult as UnstableGetResult } from './select-query-parser/result' diff --git a/src/select-query-parser/parser.ts b/src/select-query-parser/parser.ts index 77a57e30..c4336fe4 100644 --- a/src/select-query-parser/parser.ts +++ b/src/select-query-parser/parser.ts @@ -2,6 +2,7 @@ // See https://github.com/PostgREST/postgrest/blob/2f91853cb1de18944a4556df09e52450b881cfb3/src/PostgREST/ApiRequest/QueryParams.hs#L282-L284 import { SimplifyDeep } from '../types' +import { JsonPathToAccessor } from './utils' /** * Parses a query. @@ -12,10 +13,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 +37,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 +61,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 +72,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 +100,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 +124,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 +190,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}\``> @@ -216,13 +221,24 @@ type ParseNonEmbeddedResourceField = ParseIdentifier${infer _}` + Remainder extends `->${infer PathAndRest}` ? ParseJsonAccessor extends [ infer PropertyName, infer PropertyType, `${infer Remainder}` ] - ? [{ type: 'field'; name: Name; alias: PropertyName; castType: PropertyType }, Remainder] + ? [ + { + type: 'field' + name: Name + alias: PropertyName + castType: PropertyType + jsonPath: JsonPathToAccessor< + PathAndRest extends `${infer Path},${string}` ? Path : PathAndRest + > + }, + Remainder + ] : ParseJsonAccessor : [{ type: 'field'; name: Name }, Remainder] ) extends infer Parsed @@ -397,6 +413,7 @@ export namespace Ast { hint?: string innerJoin?: true castType?: string + jsonPath?: string aggregateFunction?: Token.AggregateFunction children?: Node[] } diff --git a/src/select-query-parser/result.ts b/src/select-query-parser/result.ts index 0384389c..c1cd42ef 100644 --- a/src/select-query-parser/result.ts +++ b/src/select-query-parser/result.ts @@ -15,6 +15,8 @@ import { GetFieldNodeResultName, IsAny, IsRelationNullable, + IsStringUnion, + JsonPathToType, ResolveRelationship, SelectQueryError, } from './utils' @@ -35,14 +37,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[] @@ -74,15 +80,9 @@ type ProcessFieldNodeWithoutSchema = IsNonEmptyArray Node['children'] > extends true ? { - [K in Node['name']]: Node['children'] extends Ast.StarNode[] - ? any[] - : Node['children'] extends Ast.FieldNode[] - ? { - [P in Node['children'][number] as GetFieldNodeResultName

]: P['castType'] extends PostgreSQLTypes - ? TypeScriptTypes - : any - }[] - : any[] + [K in GetFieldNodeResultName]: Node['children'] extends Ast.Node[] + ? ProcessNodesWithoutSchema[] + : ProcessSimpleFieldWithoutSchema } : ProcessSimpleFieldWithoutSchema @@ -111,11 +111,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 @@ -130,11 +134,12 @@ export type ProcessRPCNode< Row extends Record, RelationName extends string, NodeType extends Ast.Node -> = NodeType extends Ast.StarNode // If the selection is * +> = NodeType['type'] extends Ast.StarNode['type'] // If the selection is * ? Row - : NodeType extends Ast.FieldNode - ? ProcessSimpleField - : SelectQueryError<'Unsupported node type.'> + : NodeType['type'] extends Ast.FieldNode['type'] + ? ProcessSimpleField> + : SelectQueryError<'RPC Unsupported node type.'> + /** * Process select call that can be chained after an rpc call */ @@ -143,14 +148,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 /** @@ -171,14 +180,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> @@ -197,13 +210,15 @@ export type ProcessNode< RelationName extends string, Relationships extends GenericRelationship[], NodeType extends Ast.Node -> = NodeType extends Ast.StarNode // If the selection is * - ? Row - : NodeType extends Ast.SpreadNode // If the selection is a ...spread - ? ProcessSpreadNode - : NodeType extends Ast.FieldNode - ? ProcessFieldNode - : SelectQueryError<'Unsupported node type.'> +> = + // 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 * + ? Row + : NodeType['type'] extends Ast.SpreadNode['type'] // If the selection is a ...spread + ? ProcessSpreadNode> + : NodeType['type'] extends Ast.FieldNode['type'] + ? ProcessFieldNode> + : SelectQueryError<'Unsupported node type.'> /** * Processes a FieldNode and returns the resulting TypeScript type. @@ -226,6 +241,30 @@ type ProcessFieldNode< ? 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). * @@ -248,8 +287,8 @@ type ProcessSimpleField< } : { // 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 + [K in GetFieldNodeResultName]: Field['castType'] extends PostgreSQLTypes + ? ResolveJsonPathType : Row[Field['name']] } : SelectQueryError<`column '${Field['name']}' does not exist on '${RelationName}'.`> @@ -369,7 +408,12 @@ type ProcessSpreadNode< /** * Helper type to process the result of a spread node. */ -type ProcessSpreadNodeResult = ExtractFirstProperty extends infer SpreadedObject +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> diff --git a/src/select-query-parser/utils.ts b/src/select-query-parser/utils.ts index 90ca4d60..bcbfef04 100644 --- a/src/select-query-parser/utils.ts +++ b/src/select-query-parser/utils.ts @@ -14,6 +14,23 @@ export type IsAny = 0 extends 1 & T ? true : false export type SelectQueryError = { 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 [ + infer First, + ...infer Rest +] + ? First extends Rest[number] + ? DeduplicateRelationships + : [First, ...DeduplicateRelationships] + : T + export type GetFieldNodeResultName = Field['alias'] extends string ? Field['alias'] : Field['aggregateFunction'] extends AggregateFunctions @@ -38,27 +55,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 +83,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 @@ -88,30 +104,38 @@ type CheckDuplicates = Arr extends [infer Head, ...i /** * Iterates over the elements of the array to find duplicates */ -type FindDuplicates = Arr extends [infer Head, ...infer Tail] - ? CheckDuplicates | FindDuplicates +type FindDuplicatesWithinDeduplicated = Arr extends [infer Head, ...infer Tail] + ? CheckDuplicates | FindDuplicatesWithinDeduplicated : never +type FindDuplicates = FindDuplicatesWithinDeduplicated< + DeduplicateRelationships +> + export type CheckDuplicateEmbededReference< Schema extends GenericSchema, 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 @@ -134,17 +158,22 @@ type HasFKeyToFRel = Relationships extends [infer R] /** * Checks if there is more than one relation to a given foreign relation name in the Relationships. */ -type HasMultipleFKeysToFRel = Relationships extends [ +type HasMultipleFKeysToFRelDeduplicated = Relationships extends [ infer R, ...infer Rest ] ? R extends { referencedRelation: FRelName } ? HasFKeyToFRel extends true ? true - : HasMultipleFKeysToFRel - : HasMultipleFKeysToFRel + : HasMultipleFKeysToFRelDeduplicated + : HasMultipleFKeysToFRelDeduplicated : false +type HasMultipleFKeysToFRel< + FRelName, + Relationships extends unknown[] +> = HasMultipleFKeysToFRelDeduplicated> + type CheckRelationshipError< Schema extends GenericSchema, Relationships extends GenericRelationship[], @@ -155,33 +184,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 +251,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 +280,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 +300,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 +321,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 +380,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 +407,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 +415,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,14 +475,18 @@ type ResolveForwardRelationship< * referencedColumns: ["id"] * } */ -export type FindJoinTableRelationship< +type ResolveJoinTableRelationship< Schema extends GenericSchema, CurrentTableOrView extends keyof TablesAndViews & string, FieldName extends string > = { - [TableName in keyof TablesAndViews]: TablesAndViews[TableName]['Relationships'] extends readonly (infer Rel)[] + [TableName in keyof TablesAndViews]: DeduplicateRelationships< + TablesAndViews[TableName]['Relationships'] + > extends readonly (infer Rel)[] ? Rel extends { referencedRelation: CurrentTableOrView } - ? TablesAndViews[TableName]['Relationships'] extends readonly (infer OtherRel)[] + ? DeduplicateRelationships< + TablesAndViews[TableName]['Relationships'] + > extends readonly (infer OtherRel)[] ? OtherRel extends { referencedRelation: FieldName } ? OtherRel : never @@ -447,6 +495,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,44 +511,70 @@ 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'] } : SelectQueryError<'Failed to find matching relation via name'> + +export type JsonPathToAccessor = 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 + : Path extends `${infer P1}::${infer _}` // Handle type casting + ? JsonPathToAccessor + : Path extends `${infer P1}${')' | ','}${infer _}` // Handle closing parenthesis and comma + ? P1 + : Path + +export type JsonPathToType = Path extends '' + ? T + : ContainsNull extends true + ? JsonPathToType, Path> + : Path extends `${infer Key}.${infer Rest}` + ? Key extends keyof T + ? JsonPathToType + : never + : Path extends keyof T + ? T[Path] + : never + +export type IsStringUnion = string extends T + ? false + : T extends string + ? [T] extends [never] + ? false + : true + : false diff --git a/test/basic.ts b/test/basic.ts index 44daf354..37b8879e 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -1,5 +1,5 @@ import { PostgrestClient } from '../src/index' -import { Database } from './types' +import { CustomUserDataType, Database } from './types' const REST_URL = 'http://localhost:3000' const postgrest = new PostgrestClient(REST_URL) @@ -60,6 +60,62 @@ test('basic select table', async () => { `) }) +test('basic select returns types override', async () => { + const res = await postgrest.from('users').select().returns<{ status: 'ONLINE' | 'OFFLINE' }>() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "age_range": "[1,2)", + "catchphrase": "'cat' 'fat'", + "data": null, + "status": "ONLINE", + "username": "supabot", + }, + Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'cat'", + "data": null, + "status": "OFFLINE", + "username": "kiwicopple", + }, + Object { + "age_range": "[25,35)", + "catchphrase": "'bat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "awailas", + }, + Object { + "age_range": "[20,30)", + "catchphrase": "'fat' 'rat'", + "data": null, + "status": "ONLINE", + "username": "dragarcia", + }, + Object { + "age_range": "[20,30)", + "catchphrase": "'json' 'test'", + "data": Object { + "foo": Object { + "bar": Object { + "nested": "value", + }, + "baz": "string value", + }, + }, + "status": "ONLINE", + "username": "jsonuser", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('basic select view', async () => { const res = await postgrest.from('updatable_view').select() expect(res).toMatchInlineSnapshot(` @@ -546,6 +602,28 @@ describe('basic insert, update, delete', () => { `) }) + test('insert quoted column', async () => { + let res = await postgrest + .from('cornercase') + .insert([{ 'column whitespace': 'foo', id: 1 }]) + .select('"column whitespace", id ') + + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": null, + "error": Object { + "code": "23505", + "details": "Key (id)=(1) already exists.", + "hint": null, + "message": "duplicate key value violates unique constraint \\"cornercase_pkey\\"", + }, + "status": 409, + "statusText": "Conflict", + } + `) + }) + test('basic update', async () => { let res = await postgrest .from('messages') @@ -1615,7 +1693,10 @@ test('select with no match', async () => { }) test('update with no match - return=minimal', async () => { - const res = await postgrest.from('users').update({ data: '' }).eq('username', 'missing') + const res = await postgrest + .from('users') + .update({ data: '' as unknown as CustomUserDataType }) + .eq('username', 'missing') expect(res).toMatchInlineSnapshot(` Object { "count": null, @@ -1628,7 +1709,11 @@ test('update with no match - return=minimal', async () => { }) test('update with no match - return=representation', async () => { - const res = await postgrest.from('users').update({ data: '' }).eq('username', 'missing').select() + const res = await postgrest + .from('users') + .update({ data: '' as unknown as CustomUserDataType }) + .eq('username', 'missing') + .select() expect(res).toMatchInlineSnapshot(` Object { "count": null, diff --git a/test/db/00-schema.sql b/test/db/00-schema.sql index 07170703..ee3f6e2e 100644 --- a/test/db/00-schema.sql +++ b/test/db/00-schema.sql @@ -156,3 +156,10 @@ $$ language sql immutable; create function public.function_with_array_param(param uuid[]) returns void as '' language sql immutable; + + +create table public.cornercase ( + id int primary key, + "column whitespace" text, + array_column text[] +); diff --git a/test/db/01-dummy-data.sql b/test/db/01-dummy-data.sql index 8a2343eb..1ad59ca8 100644 --- a/test/db/01-dummy-data.sql +++ b/test/db/01-dummy-data.sql @@ -81,3 +81,9 @@ VALUES (2, 1), -- Smartphone is in Electronics (3, 1), -- Headphones are in Electronics (3, 3); -- Headphones are also in Audio + +INSERT INTO public.cornercase (id, array_column) +VALUES + (1, ARRAY['test', 'one']), + (2, ARRAY['another']), + (3, ARRAY['test2']); \ No newline at end of file diff --git a/test/filters.ts b/test/filters.ts index 8348f0a4..3338ec37 100644 --- a/test/filters.ts +++ b/test/filters.ts @@ -370,6 +370,56 @@ test('contains', async () => { `) }) +test('contains with json', async () => { + const res = await postgrest + .from('users') + .select('data') + .contains('data', { foo: { baz: 'string value' } }) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "data": Object { + "foo": Object { + "bar": Object { + "nested": "value", + }, + "baz": "string value", + }, + }, + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('contains with array', async () => { + const res = await postgrest + .from('cornercase') + .select('array_column') + .contains('array_column', ['test']) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "array_column": Array [ + "test", + "one", + ], + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('containedBy', async () => { const res = await postgrest.from('users').select('age_range').containedBy('age_range', '[1,2)') expect(res).toMatchInlineSnapshot(` @@ -387,6 +437,38 @@ test('containedBy', async () => { `) }) +test('containedBy with json', async () => { + const res = await postgrest + .from('users') + .select('data') + .containedBy('data', { foo: { baz: 'string value' } }) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + +test('containedBy with array', async () => { + const res = await postgrest + .from('cornercase') + .select('array_column') + .containedBy('array_column', ['test']) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('rangeLt', async () => { const res = await postgrest.from('users').select('age_range').rangeLt('age_range', '[2,25)') expect(res).toMatchInlineSnapshot(` @@ -510,6 +592,29 @@ test('overlaps', async () => { `) }) +test('overlaps with array', async () => { + const res = await postgrest + .from('cornercase') + .select('array_column') + .overlaps('array_column', ['test']) + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Array [ + Object { + "array_column": Array [ + "test", + "one", + ], + }, + ], + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('textSearch', async () => { const res = await postgrest .from('users') diff --git a/test/index.test-d.ts b/test/index.test-d.ts index e64dabcc..5db92c5d 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -1,5 +1,6 @@ +import { TypeEqual } from 'ts-expect' import { expectError, expectType } from 'tsd' -import { PostgrestClient } from '../src/index' +import { PostgrestClient, PostgrestError } from '../src/index' import { Prettify } from '../src/types' import { Database, Json } from './types' @@ -21,27 +22,124 @@ const postgrest = new PostgrestClient(REST_URL) expectError(postgrest.from('users').select().eq('username', nullableVar)) } +// `.eq()`, '.neq()' and `.in()` validate provided filter value when column is an enum. +// Behaves the same for simple columns, as well as relationship filters. +{ + expectError(postgrest.from('users').select().eq('status', 'invalid')) + expectError(postgrest.from('users').select().neq('status', 'invalid')) + expectError(postgrest.from('users').select().in('status', ['invalid'])) + + expectError( + postgrest.from('best_friends').select('users!first_user(status)').eq('users.status', 'invalid') + ) + expectError( + postgrest.from('best_friends').select('users!first_user(status)').neq('users.status', 'invalid') + ) + expectError( + postgrest + .from('best_friends') + .select('users!first_user(status)') + .in('users.status', ['invalid']) + ) + // Validate deeply nested embedded tables + expectError( + postgrest.from('users').select('messages(channels(*))').eq('messages.channels.id', 'invalid') + ) + expectError( + postgrest.from('users').select('messages(channels(*))').neq('messages.channels.id', 'invalid') + ) + expectError( + postgrest.from('users').select('messages(channels(*))').in('messages.channels.id', ['invalid']) + ) + + { + const result = await postgrest.from('users').select('status').eq('status', 'ONLINE') + if (result.error) { + throw new Error(result.error.message) + } + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(result.data) + } + + { + const result = await postgrest.from('users').select('status').neq('status', 'ONLINE') + if (result.error) { + throw new Error(result.error.message) + } + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(result.data) + } + + { + const result = await postgrest + .from('users') + .select('status') + .in('status', ['ONLINE', 'OFFLINE']) + if (result.error) { + throw new Error(result.error.message) + } + expectType<{ status: Database['public']['Enums']['user_status'] | null }[]>(result.data) + } + + { + const result = await postgrest + .from('best_friends') + .select('users!first_user(status)') + .eq('users.status', 'ONLINE') + if (result.error) { + throw new Error(result.error.message) + } + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>( + result.data + ) + } + + { + const result = await postgrest + .from('best_friends') + .select('users!first_user(status)') + .neq('users.status', 'ONLINE') + if (result.error) { + throw new Error(result.error.message) + } + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>( + result.data + ) + } + + { + const result = await postgrest + .from('best_friends') + .select('users!first_user(status)') + .in('users.status', ['ONLINE', 'OFFLINE']) + if (result.error) { + throw new Error(result.error.message) + } + expectType<{ users: { status: Database['public']['Enums']['user_status'] | null } }[]>( + result.data + ) + } +} + // can override result type { - const { data, error } = await postgrest + const result = await postgrest .from('users') .select('*, messages(*)') .returns<{ messages: { foo: 'bar' }[] }[]>() - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ foo: 'bar' }[]>(data[0].messages) + expectType<{ foo: 'bar' }[]>(result.data[0].messages) } { - const { data, error } = await postgrest + const result = await postgrest .from('users') .insert({ username: 'foo' }) .select('*, messages(*)') .returns<{ messages: { foo: 'bar' }[] }[]>() - if (error) { - throw new Error(error.message) + if (result.error) { + throw new Error(result.error.message) } - expectType<{ foo: 'bar' }[]>(data[0].messages) + expectType<{ foo: 'bar' }[]>(result.data[0].messages) } // cannot update non-updatable views @@ -56,60 +154,54 @@ const postgrest = new PostgrestClient(REST_URL) // spread resource with single column in select query { - const { data, error } = await postgrest - .from('messages') - .select('message, ...users(status)') - .single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('messages').select('message, ...users(status)').single() + if (result.error) { + throw new Error(result.error.message) } expectType<{ message: string | null; status: Database['public']['Enums']['user_status'] | null }>( - data + result.data ) } // spread resource with all columns in select query { - const { data, error } = await postgrest.from('messages').select('message, ...users(*)').single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('messages').select('message, ...users(*)').single() + if (result.error) { + throw new Error(result.error.message) } expectType>( - data + result.data ) } // `count` in embedded resource { - const { data, error } = await postgrest.from('messages').select('message, users(count)').single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('messages').select('message, users(count)').single() + if (result.error) { + throw new Error(result.error.message) } - expectType<{ message: string | null; users: { count: number } }>(data) + expectType<{ message: string | null; users: { count: number } }>(result.data) } // json accessor in select query { - const { data, error } = await postgrest - .from('users') - .select('data->foo->bar, data->foo->>baz') - .single() - if (error) { - throw new Error(error.message) + const result = await postgrest.from('users').select('data->foo->bar, data->foo->>baz').single() + if (result.error) { + throw new Error(result.error.message) } // getting this w/o the cast, not sure why: // Parameter type Json is declared too wide for argument type Json - expectType(data.bar) - expectType(data.baz) + expectType(result.data.bar) + expectType(result.data.baz) } // rpc return type { - const { data, error } = await postgrest.rpc('get_status') - if (error) { - throw new Error(error.message) + const result = await postgrest.rpc('get_status') + if (result.error) { + throw new Error(result.error.message) } - expectType<'ONLINE' | 'OFFLINE'>(data) + expectType<'ONLINE' | 'OFFLINE'>(result.data) } // PostgrestBuilder's children retains class when using inherited methods @@ -117,6 +209,107 @@ const postgrest = new PostgrestClient(REST_URL) const x = postgrest.from('channels').select() const y = x.throwOnError() const z = x.setHeader('', '') - expectType(y) - expectType(z) + expectType(true) + expectType(true) +} + +// Should have nullable data and error field +{ + const result = await postgrest.from('users').select('username, messages(id, message)').limit(1) + let expected: + | { + username: string + messages: { + id: number + message: string | null + }[] + }[] + | null + const { data } = result + const { error } = result + expectType>(true) + let err: PostgrestError | null + expectType>(true) +} + +// Should have non nullable data and no error fields if throwOnError is added +{ + const result = await postgrest + .from('users') + .select('username, messages(id, message)') + .limit(1) + .throwOnError() + const { data } = result + const { error } = result + let expected: + | { + username: string + messages: { + id: number + message: string | null + }[] + }[] + expectType>(true) + expectType>(true) + error +} + +// Should work with throwOnError middle of the chaining +{ + const result = await postgrest + .from('users') + .select('username, messages(id, message)') + .throwOnError() + .eq('username', 'test') + .limit(1) + const { data } = result + const { error } = result + let expected: + | { + username: string + messages: { + id: number + message: string | null + }[] + }[] + expectType>(true) + expectType>(true) + error +} + +// Json Accessor with custom types overrides +{ + const result = await postgrest + .schema('personal') + .from('users') + .select('data->bar->baz, data->en, data->bar') + if (result.error) { + throw new Error(result.error.message) + } + expectType< + { + baz: number + en: 'ONE' | 'TWO' | 'THREE' + bar: { + baz: number + } + }[] + >(result.data) +} +// Json string Accessor with custom types overrides +{ + const result = await postgrest + .schema('personal') + .from('users') + .select('data->bar->>baz, data->>en, data->>bar') + if (result.error) { + throw new Error(result.error.message) + } + expectType< + { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + }[] + >(result.data) } diff --git a/test/issue-1354-d.ts b/test/issue-1354-d.ts new file mode 100644 index 00000000..247efa43 --- /dev/null +++ b/test/issue-1354-d.ts @@ -0,0 +1,296 @@ +import { expectType } from 'tsd' +import { PostgrestClient } from '../src/index' +import type { MergeDeep } from 'type-fest' + +export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] + +export type Database = { + public: { + Tables: { + foo: { + Row: { + created_at: string | null + bar: Json + id: string + baz: Json + game_id: string + updated_at: string | null + user_id: string | null + } + Insert: { + created_at?: string | null + bar: Json + id?: string + baz: Json + game_id: string + updated_at?: string | null + user_id?: string | null + } + Update: { + created_at?: string | null + bar?: Json + id?: string + baz?: Json + game_id?: string + updated_at?: string | null + user_id?: string | null + } + Relationships: [] + } + } + Views: {} + Functions: {} + Enums: {} + CompositeTypes: { + [_ in never]: never + } + } +} + +type PublicSchema = Database[Extract] + +export type Tables< + PublicTableNameOrOptions extends + | keyof (PublicSchema['Tables'] & PublicSchema['Views']) + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] & + Database[PublicTableNameOrOptions['schema']]['Views']) + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? (Database[PublicTableNameOrOptions['schema']]['Tables'] & + Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends { + Row: infer R + } + ? R + : never + : PublicTableNameOrOptions extends keyof (PublicSchema['Tables'] & PublicSchema['Views']) + ? (PublicSchema['Tables'] & PublicSchema['Views'])[PublicTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { + Insert: infer I + } + ? I + : never + : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] + ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { + Update: infer U + } + ? U + : never + : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] + ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] | { schema: keyof Database }, + EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicEnumNameOrOptions['schema']]['Enums'] + : never = never +> = PublicEnumNameOrOptions extends { schema: keyof Database } + ? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName] + : PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] + ? PublicSchema['Enums'][PublicEnumNameOrOptions] + : never + +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof PublicSchema['CompositeTypes'] + | { schema: keyof Database }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'] + : never = never +> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } + ? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof PublicSchema['CompositeTypes'] + ? PublicSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions] + : never + +type Custom = { + version: number + events: Array<{ + type: string + [x: string]: any + }> +} + +export type DatabaseOverride = MergeDeep< + Database, + { + public: { + Tables: { + foo: { + Row: { + bar: Custom + baz: Custom + } + Insert: { + bar: Custom + baz: Custom + } + Update: { + bar?: Custom + baz?: Custom + } + } + } + } + } +> + +const postgrest = new PostgrestClient('http://localhost:3000') + +const postgrestOverrideTypes = new PostgrestClient('http://localhost:3000') + +// Basic types +{ + const res = await postgrest.from('foo').select('id').eq('id', '...').single() + + const bar = {} as Custom + const baz = {} as Custom + if (res.error) { + throw new Error(res.error.message) + } + const result = await postgrest + .from('foo') + .update({ + bar, + baz, + }) + .eq('id', res.data.id) + expectType(result.data) +} + +// basic types with postgres jsonpath selector +{ + const res = await postgrest.from('foo').select('id, bar, baz').eq('bar->version', 31).single() + + const bar = {} as Json + const baz = {} as Json + if (res.error) { + throw new Error(res.error.message) + } + const result = await postgrest + .from('foo') + .update({ + bar, + baz, + }) + .eq('bar->version', 31) + expectType(result.data) + const resIn = await postgrest + .from('foo') + .select('id, bar, baz') + .in('bar->version', [1, 2]) + .single() + + if (resIn.error) { + throw new Error(resIn.error.message) + } + expectType<{ id: string; bar: Json; baz: Json }>(resIn.data) +} + +// extended types +{ + const res = await postgrestOverrideTypes + .from('foo') + .select('id, bar, baz') + .eq('id', '...') + .single() + + const bar = {} as Custom + const baz = {} as Custom + if (res.error) { + throw new Error(res.error.message) + } + const result = await postgrestOverrideTypes + .from('foo') + .update({ + bar, + baz, + }) + .eq('id', res.data.id) + expectType(result.data) + const resIn = await postgrestOverrideTypes + .from('foo') + .select('id, bar, baz') + .in('bar', [ + { version: 1, events: [] }, + { version: 2, events: [] }, + ]) + .single() + + if (resIn.error) { + throw new Error(resIn.error.message) + } + expectType<{ id: string; bar: Custom; baz: Custom }>(resIn.data) +} + +// extended types with postgres jsonpath selector +{ + const res = await postgrestOverrideTypes + .from('foo') + .select('id, bar, baz') + .eq('bar->version', 31) + .single() + + const bar = {} as Custom + const baz = {} as Custom + if (res.error) { + throw new Error(res.error.message) + } + const result = await postgrestOverrideTypes + .from('foo') + .update({ + bar, + baz, + }) + .eq('bar->version', res.data.bar.version) + expectType(result.data) + const resIn = await postgrestOverrideTypes + .from('foo') + .select('id, bar, baz') + .in('bar->version', [31]) + .single() + await postgrestOverrideTypes + .from('foo') + .select('id, bar, baz') + // the type become a string when using the string json accessor operator + .in('bar->>version', ['something']) + .single() + + if (resIn.error) { + throw new Error(resIn.error.message) + } + expectType<{ id: string; bar: Custom; baz: Custom }>(resIn.data) +} diff --git a/test/relationships.ts b/test/relationships.ts index 6b3cf7a4..bf40a1f1 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 () => { @@ -1822,7 +1830,7 @@ test('self reference relation via column', async () => { }) test('aggregate on missing column with alias', async () => { - const res = await selectQueries.aggregateOnMissingColumnWithAlias.eq('id', 1).limit(1).single() + const res = await selectQueries.aggregateOnMissingColumnWithAlias.eq('id', 2).limit(1).single() expect(res).toMatchInlineSnapshot(` Object { "count": null, @@ -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/default-inference-d.ts b/test/select-query-parser/default-inference.test-d.ts similarity index 87% rename from test/select-query-parser/default-inference-d.ts rename to test/select-query-parser/default-inference.test-d.ts index 98b2ba95..3ca463b5 100644 --- a/test/select-query-parser/default-inference-d.ts +++ b/test/select-query-parser/default-inference.test-d.ts @@ -35,6 +35,22 @@ const REST_URL = 'http://localhost:3000' } expectType>(true) } +// embeding renaming +{ + const postgrest = new PostgrestClient(REST_URL) + const { data } = await postgrest + .from('projects') + .select('status,service:services(service_api_keys(*))') + .single() + let result: Exclude + let expected: { + status: any + service: { + service_api_keys: any[] + }[] + } + expectType>(true) +} // spread operator with stars should return any { const postgrest = new PostgrestClient(REST_URL) diff --git a/test/select-query-parser/parser.test-d.ts b/test/select-query-parser/parser.test-d.ts index 8c7291ac..b241e13d 100644 --- a/test/select-query-parser/parser.test-d.ts +++ b/test/select-query-parser/parser.test-d.ts @@ -81,17 +81,53 @@ import { selectParams } from '../relationships' // Select with JSON accessor { expectTypepreferences->theme'>>([ - { type: 'field', name: 'data', alias: 'theme', castType: 'json' }, + { + type: 'field', + name: 'data', + alias: 'theme', + castType: 'json', + jsonPath: 'preferences.theme', + }, ]) } // Select with JSON accessor and text conversion { expectTypepreferences->>theme'>>([ - { type: 'field', name: 'data', alias: 'theme', castType: 'text' }, + { + type: 'field', + name: 'data', + alias: 'theme', + castType: 'text', + jsonPath: 'preferences.theme', + }, + ]) +} +{ + expectTypepreferences->>theme, data->>some, data->foo->bar->>biz'>>([ + { + type: 'field', + name: 'data', + alias: 'theme', + castType: 'text', + jsonPath: 'preferences.theme', + }, + { + type: 'field', + name: 'data', + alias: 'some', + castType: 'text', + jsonPath: 'some', + }, + { + type: 'field', + name: 'data', + alias: 'biz', + castType: 'text', + jsonPath: 'foo.bar.biz', + }, ]) } - // Select with spread { expectType>([ @@ -196,7 +232,13 @@ import { selectParams } from '../relationships' }, ], }, - { type: 'field', name: 'profile', alias: 'theme', castType: 'text' }, + { + type: 'field', + name: 'profile', + alias: 'theme', + castType: 'text', + jsonPath: 'settings.theme', + }, ]) } { @@ -327,7 +369,13 @@ import { selectParams } from '../relationships' // Select with nested JSON accessors { expectTypepreferences->theme->color'>>([ - { type: 'field', name: 'data', alias: 'color', castType: 'json' }, + { + type: 'field', + name: 'data', + alias: 'color', + castType: 'json', + jsonPath: 'preferences.theme.color', + }, ]) } @@ -464,7 +512,7 @@ import { selectParams } from '../relationships' expectTypeage::int'>>([ { type: 'field', name: 'id', castType: 'text' }, { type: 'field', name: 'created_at', castType: 'date' }, - { type: 'field', name: 'data', alias: 'age', castType: 'int' }, + { type: 'field', name: 'data', alias: 'age', castType: 'int', jsonPath: 'age' }, ]) } @@ -480,8 +528,8 @@ import { selectParams } from '../relationships' // select JSON accessor { expect>([ - { type: 'field', name: 'data', alias: 'bar', castType: 'json' }, - { type: 'field', name: 'data', alias: 'baz', castType: 'text' }, + { type: 'field', name: 'data', alias: 'bar', castType: 'json', jsonPath: 'foo.bar' }, + { type: 'field', name: 'data', alias: 'baz', castType: 'text', jsonPath: 'foo.baz' }, ]) } @@ -614,3 +662,36 @@ import { selectParams } from '../relationships' 0 as any as ParserError<'Unexpected input: ->->theme'> ) } + +// JSON accessor within embedded tables +{ + expectTypebar->>baz, data->>en, data->bar)'>>([ + { + type: 'field', + name: 'users', + children: [ + { + type: 'field', + name: 'data', + alias: 'baz', + castType: 'text', + jsonPath: 'bar.baz', + }, + { + type: 'field', + name: 'data', + alias: 'en', + castType: 'text', + jsonPath: 'en', + }, + { + type: 'field', + name: 'data', + alias: 'bar', + castType: 'json', + jsonPath: 'bar', + }, + ], + }, + ]) +} diff --git a/test/select-query-parser/result.test-d.ts b/test/select-query-parser/result.test-d.ts index 38dcccae..508424f0 100644 --- a/test/select-query-parser/result.test-d.ts +++ b/test/select-query-parser/result.test-d.ts @@ -3,6 +3,7 @@ import { selectParams } from '../relationships' import { GetResult } from '../../src/select-query-parser/result' import { expectType } from 'tsd' import { TypeEqual } from 'ts-expect' +import { SelectQueryError } from '../../src/select-query-parser/utils' type SelectQueryFromTableResult< TableName extends keyof Database['public']['Tables'], @@ -70,3 +71,126 @@ type SelectQueryFromTableResult< } expectType>(true) } + +// nested query with selective fields and inner join should error on non existing relation +{ + let result: SelectQueryFromTableResult< + 'users', + `msgs:messages(id, ...message_details(created_at, channel!inner(id, slug, owner:users(*))))` + > + let expected: { + msgs: { + id: number + message_details: SelectQueryError<'could not find the relation between messages and message_details'> + }[] + } + expectType>(true) +} + +{ + let result: SelectQueryFromTableResult< + 'users', + `msgs:messages(id, ...channels(id, ...channel_details(id, missing_col)))` + > + let expected: { + msgs: { + id: number + channel_details: SelectQueryError<"column 'missing_col' does not exist on 'channel_details'."> | null + }[] + } + expectType>(true) +} + +{ + let result1: SelectQueryFromTableResult< + 'users', + `msgs:messages(...channels(slug, channel_details!inner(id, details, channel:channels(*))))` + > + let result2: SelectQueryFromTableResult< + 'users', + `msgs:messages(...channels(slug, channel_details!inner(channel:channels(*), id, details)))` + > + let result3: SelectQueryFromTableResult< + 'users', + `msgs:messages(...channels!inner(slug, channel_details!inner(id, details, channel:channels(*))))` + > + // All variations should not change the result + expectType(result2!) + expectType(result3!) +} + +{ + type SelectQueryFromPersonalTableResult< + TableName extends keyof Database['personal']['Tables'], + Q extends string + > = GetResult< + Database['personal'], + Database['personal']['Tables'][TableName]['Row'], + TableName, + Database['personal']['Tables'][TableName]['Relationships'], + Q + > + // Should work with Json object accessor + { + let result: SelectQueryFromPersonalTableResult<'users', `data->bar->baz, data->en, data->bar`> + let expected: { + baz: number + en: 'ONE' | 'TWO' | 'THREE' + bar: { + baz: number + } + } + expectType>(true) + } + // Should work with Json string accessor + { + let result: SelectQueryFromPersonalTableResult< + 'users', + `data->bar->>baz, data->>en, data->>bar` + > + let expected: { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + } + expectType>(true) + } + // Should fallback to defaults if unknown properties are mentionned + { + let result: SelectQueryFromPersonalTableResult<'users', `data->bar->>nope, data->neither`> + let expected: { + nope: string + neither: Json + } + expectType>(true) + } + // Should work with embeded Json object accessor + { + let result: SelectQueryFromTableResult<'messages', `users(data->bar->baz, data->en, data->bar)`> + let expected: { + users: { + baz: number + en: 'ONE' | 'TWO' | 'THREE' + bar: { + baz: number + } + } + } + expectType>(true) + } + // Should work with embeded Json string accessor + { + let result: SelectQueryFromTableResult< + 'messages', + `users(data->bar->>baz, data->>en, data->>bar)` + > + let expected: { + users: { + baz: string + en: 'ONE' | 'TWO' | 'THREE' + bar: string + } + } + 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..adc54416 100644 --- a/test/select-query-parser/select.test-d.ts +++ b/test/select-query-parser/select.test-d.ts @@ -3,7 +3,7 @@ import { TypeEqual } from 'ts-expect' import { Json } from '../../src/select-query-parser/types' import { SelectQueryError } from '../../src/select-query-parser/utils' import { Prettify } from '../../src/types' -import { Database } from '../types' +import { CustomUserDataType, Database } from '../types' import { selectQueries } from '../relationships' // This test file is here to ensure that for a query against a specfic datatabase @@ -617,7 +617,7 @@ type Schema = Database['public'] users: { age_range: unknown | null catchphrase: unknown | null - data: Json | null + data: CustomUserDataType | null status: Database['public']['Enums']['user_status'] | null username: string } @@ -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 diff --git a/test/select-query-parser/types.test-d.ts b/test/select-query-parser/types.test-d.ts index 049630dd..d52432df 100644 --- a/test/select-query-parser/types.test-d.ts +++ b/test/select-query-parser/types.test-d.ts @@ -1,199 +1,55 @@ -import { Database } from '../types' -import { selectParams } from '../relationships' -import { - ProcessEmbeddedResource, - ProcessNode, - ProcessNodes, -} from '../../src/select-query-parser/result' import { expectType } from 'tsd' import { TypeEqual } from 'ts-expect' -import { - FindMatchingTableRelationships, - IsRelationNullable, - FindJoinTableRelationship, -} from '../../src/select-query-parser/utils' -import { Json } from '../../src/select-query-parser/types' -import { ParseQuery } from '../../src/select-query-parser/parser' +import { DeduplicateRelationships } from '../../src/select-query-parser/utils' +// Deduplicate exact sames relationships +{ + type rels = [ + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'project_subscriptions' + referencedColumns: ['project_id'] + }, + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'projects' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'projects' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'sls_physical_backups_monitoring' + referencedColumns: ['project_id'] + } + ] + type expected = [ + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'project_subscriptions' + referencedColumns: ['project_id'] + }, + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'projects' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'test_fkey' + columns: ['project_id'] + referencedRelation: 'sls_physical_backups_monitoring' + referencedColumns: ['project_id'] + } + ] -// This test file is here to ensure some of our helpers behave as expected for ease of development -// and debugging purposes - -// Searching for a relationship by direct foreignkey name -{ - let result: FindMatchingTableRelationships< - Database['public'], - Database['public']['Tables']['best_friends']['Relationships'], - 'best_friends_first_user_fkey' - > - let expected: { - foreignKeyName: 'best_friends_first_user_fkey' - columns: ['first_user'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - } & { match: 'fkname' } - expectType>(true) -} -// Searching for a relationship by column hoding the value reference -{ - let result: FindMatchingTableRelationships< - Database['public'], - Database['public']['Tables']['best_friends']['Relationships'], - 'first_user' - > - let expected: { - foreignKeyName: 'best_friends_first_user_fkey' - columns: ['first_user'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - } & { match: 'col' } - expectType>(true) -} -// should return the relation matching the "Tables" references -{ - let result: FindMatchingTableRelationships< - Database['public'], - Database['public']['Tables']['user_profiles']['Relationships'], - 'username' - > - let expected: { - foreignKeyName: 'user_profiles_username_fkey' - columns: ['username'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - } & { match: 'col' } - expectType>(true) -} -// Searching for a relationship by referenced table name -{ - let result: FindMatchingTableRelationships< - Database['public'], - Database['public']['Tables']['messages']['Relationships'], - 'users' - > - let expected: { - foreignKeyName: 'messages_username_fkey' - columns: ['username'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['username'] - } & { match: 'refrel' } - expectType>(true) -} -{ - let result: FindMatchingTableRelationships< - Database['public'], - Database['public']['Tables']['messages']['Relationships'], - 'channels' - > - let expected: { - foreignKeyName: 'messages_channel_id_fkey' - columns: ['channel_id'] - isOneToOne: false - referencedRelation: 'channels' - referencedColumns: ['id'] - } & { match: 'refrel' } - expectType>(true) -} - -// IsRelationNullable -{ - type BestFriendsTable = Database['public']['Tables']['best_friends'] - type NonNullableRelation = FindMatchingTableRelationships< - Database['public'], - BestFriendsTable['Relationships'], - 'best_friends_first_user_fkey' - > - type NullableRelation = FindMatchingTableRelationships< - Database['public'], - BestFriendsTable['Relationships'], - 'best_friends_third_wheel_fkey' - > - let nonNullableResult: IsRelationNullable - let nullableResult: IsRelationNullable - expectType(false) - expectType(true) -} - -// Test nodes relations crawling utils -{ - const { from, select } = selectParams.nestedQueryWithSelectiveFields - type Schema = Database['public'] - type RelationName = typeof from - type Row = Schema['Tables'][RelationName]['Row'] - type Relationships = Schema['Tables'][RelationName]['Relationships'] - type ParsedQuery = ParseQuery - type r1 = ProcessNode - expectType>(true) - type r2 = ProcessNodes - // fail because result for messages is ({id: string} | {message: string | null })[] - expectType< - TypeEqual - >(true) - type f3 = ParsedQuery[1] - type r3 = ProcessEmbeddedResource - expectType>(true) -} -// Select from the column holding the relation (0-1 relation) -{ - const { from, select } = selectParams.joinSelectViaColumn - type Schema = Database['public'] - type RelationName = typeof from - type Row = Schema['Tables'][RelationName]['Row'] - type Relationships = Schema['Tables'][RelationName]['Relationships'] - type ParsedQuery = ParseQuery - type r1 = ProcessNode - let expected: { - username: { - age_range: unknown | null - catchphrase: unknown | null - data: Json | null - status: Database['public']['Enums']['user_status'] | null - username: string - } | null - } - expectType(expected!) - type r2 = ProcessNodes - expectType(expected!) -} - -{ - type Schema = Database['public'] - type CurrentTableOrView = 'products' - type FieldName = 'categories' - type R = FindJoinTableRelationship - let expected: { - foreignKeyName: 'product_categories_category_id_fkey' - columns: ['category_id'] - isOneToOne: false - referencedRelation: 'categories' - referencedColumns: ['id'] - } - expectType(expected!) -} - -{ - type Schema = Database['public'] - type CurrentTableOrView = 'categories' - type FieldName = 'products' - type R = FindJoinTableRelationship - let expected: { - foreignKeyName: 'product_categories_product_id_fkey' - columns: ['product_id'] - isOneToOne: false - referencedRelation: 'products' - referencedColumns: ['id'] - } - expectType(expected!) -} - -{ - type Schema = Database['public'] - type CurrentTableOrView = 'categories' - type FieldName = 'missing' - type R = FindJoinTableRelationship - let expected: never - expectType(expected!) + type result = DeduplicateRelationships + expectType>(true) } diff --git a/test/transforms.ts b/test/transforms.ts index 26344050..46904309 100644 --- a/test/transforms.ts +++ b/test/transforms.ts @@ -296,6 +296,37 @@ test('csv', async () => { `) }) +test('geojson', async () => { + const res = await postgrest.from('shops').select().geojson() + expect(res).toMatchInlineSnapshot(` + Object { + "count": null, + "data": Object { + "features": Array [ + Object { + "geometry": Object { + "coordinates": Array [ + -71.10044, + 42.373695, + ], + "type": "Point", + }, + "properties": Object { + "address": "1369 Cambridge St", + "id": 1, + }, + "type": "Feature", + }, + ], + "type": "FeatureCollection", + }, + "error": null, + "status": 200, + "statusText": "OK", + } + `) +}) + test('abort signal', async () => { const ac = new AbortController() as globalThis.AbortController ac.abort() diff --git a/test/types.ts b/test/types.ts index bd26dd1e..47d9bde3 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,24 +1,32 @@ export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] +export type CustomUserDataType = { + foo: string + bar: { + baz: number + } + en: 'ONE' | 'TWO' | 'THREE' +} + export type Database = { personal: { Tables: { users: { Row: { age_range: unknown | null - data: Json | null + data: CustomUserDataType | null status: Database['public']['Enums']['user_status'] | null username: string } Insert: { age_range?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username: string } Update: { age_range?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username?: string } @@ -86,6 +94,13 @@ export type Database = { referencedRelation: 'users' referencedColumns: ['username'] }, + { + foreignKeyName: 'best_friends_first_user_fkey' + columns: ['first_user'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_second_user_fkey' columns: ['second_user'] @@ -107,6 +122,13 @@ export type Database = { referencedRelation: 'users' referencedColumns: ['username'] }, + { + foreignKeyName: 'best_friends_second_user_fkey' + columns: ['second_user'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_third_wheel_fkey' columns: ['third_wheel'] @@ -121,6 +143,13 @@ export type Database = { referencedRelation: 'updatable_view' referencedColumns: ['username'] }, + { + foreignKeyName: 'best_friends_third_wheel_fkey' + columns: ['third_wheel'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + }, { foreignKeyName: 'best_friends_third_wheel_fkey' columns: ['third_wheel'] @@ -144,6 +173,13 @@ export type Database = { id?: number } Relationships: [ + { + foreignKeyName: 'channel_details_id_fkey' + columns: ['id'] + isOneToOne: true + referencedRelation: 'channels' + referencedColumns: ['id'] + }, { foreignKeyName: 'channel_details_id_fkey' columns: ['id'] @@ -188,6 +224,13 @@ export type Database = { parent_id?: number | null } Relationships: [ + { + foreignKeyName: 'collections_parent_id_fkey' + columns: ['parent_id'] + isOneToOne: false + referencedRelation: 'collections' + referencedColumns: ['id'] + }, { foreignKeyName: 'collections_parent_id_fkey' columns: ['parent_id'] @@ -220,6 +263,13 @@ export type Database = { username?: string } Relationships: [ + { + foreignKeyName: 'messages_channel_id_fkey' + columns: ['channel_id'] + isOneToOne: false + referencedRelation: 'channels' + referencedColumns: ['id'] + }, { foreignKeyName: 'messages_channel_id_fkey' columns: ['channel_id'] @@ -241,6 +291,13 @@ export type Database = { referencedRelation: 'updatable_view' referencedColumns: ['username'] }, + { + foreignKeyName: 'messages_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + }, { foreignKeyName: 'messages_username_fkey' columns: ['username'] @@ -264,6 +321,20 @@ export type Database = { product_id?: number } Relationships: [ + { + foreignKeyName: 'product_categories_category_id_fkey' + columns: ['category_id'] + isOneToOne: false + referencedRelation: 'categories' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'product_categories_product_id_fkey' + columns: ['product_id'] + isOneToOne: false + referencedRelation: 'products' + referencedColumns: ['id'] + }, { foreignKeyName: 'product_categories_category_id_fkey' columns: ['category_id'] @@ -337,25 +408,43 @@ export type Database = { } Relationships: [] } + cornercase: { + Row: { + 'column whitespace': string | null + array_column: unknown | null + id: number + } + Insert: { + 'column whitespace'?: string | null + array_column?: unknown | null + id: number + } + Update: { + 'column whitespace'?: string | null + array_column?: unknown | null + id?: number + } + Relationships: [] + } users: { Row: { age_range: unknown | null catchphrase: unknown | null - data: Json | null + data: CustomUserDataType | null status: Database['public']['Enums']['user_status'] | null username: string } Insert: { age_range?: unknown | null catchphrase?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username: string } Update: { age_range?: unknown | null catchphrase?: unknown | null - data?: Json | null + data?: CustomUserDataType | null status?: Database['public']['Enums']['user_status'] | null username?: string } @@ -389,6 +478,13 @@ export type Database = { referencedRelation: 'updatable_view' referencedColumns: ['username'] }, + { + foreignKeyName: 'user_profiles_username_fkey' + columns: ['username'] + isOneToOne: false + referencedRelation: 'users' + referencedColumns: ['username'] + }, { foreignKeyName: 'user_profiles_username_fkey' columns: ['username'] diff --git a/wrapper.mjs b/wrapper.mjs index 55629686..67b173b5 100644 --- a/wrapper.mjs +++ b/wrapper.mjs @@ -5,6 +5,7 @@ const { PostgrestFilterBuilder, PostgrestTransformBuilder, PostgrestBuilder, + PostgrestError, } = index export { @@ -13,6 +14,7 @@ export { PostgrestFilterBuilder, PostgrestQueryBuilder, PostgrestTransformBuilder, + PostgrestError, } // compatibility with CJS output @@ -22,4 +24,5 @@ export default { PostgrestFilterBuilder, PostgrestTransformBuilder, PostgrestBuilder, + PostgrestError, }