Skip to content
This repository was archived by the owner on Oct 9, 2025. It is now read-only.

Commit 4081771

Browse files
committed
feat: allow overriding result types
1 parent 15bdb1a commit 4081771

8 files changed

+1143
-110
lines changed

package-lock.json

Lines changed: 1055 additions & 65 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@
2626
"build:module": "tsc -p tsconfig.module.json",
2727
"docs": "typedoc src/index.ts --out docs/v2",
2828
"docs:json": "typedoc --json docs/v2/spec.json --excludeExternals src/index.ts",
29-
"test": "run-s test:db && jest --runInBand",
30-
"test:clean": "cd test/db && docker-compose down",
31-
"test:db": "cd test/db && docker-compose down && docker-compose up -d && wait-for-localhost 3000"
29+
"test": "run-s test:types db:clean db:run test:run db:clean",
30+
"test:run": "jest --runInBand",
31+
"test:update": "run-s db:clean db:run && jest --runInBand --updateSnapshot && run-s db:clean",
32+
"test:types": "run-s build:module && tsd --files test/*.test-d.ts",
33+
"db:clean": "cd test/db && docker-compose down --volumes",
34+
"db:run": "cd test/db && docker-compose up --detach && wait-for-localhost 3000"
3235
},
3336
"dependencies": {
3437
"cross-fetch": "^3.1.5"
@@ -42,8 +45,9 @@
4245
"rimraf": "^3.0.2",
4346
"semantic-release-plugin-update-version-in-files": "^1.1.0",
4447
"ts-jest": "^28.0.3",
48+
"tsd": "^0.24.1",
4549
"typedoc": "^0.22.16",
46-
"typescript": "~4.7",
50+
"typescript": "~4.8",
4751
"wait-for-localhost-cli": "^3.0.0"
4852
}
4953
}

src/PostgrestBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export default abstract class PostgrestBuilder<Result>
8282
if (res.ok) {
8383
if (this.method !== 'HEAD') {
8484
const body = await res.text()
85-
if (body === "") {
85+
if (body === '') {
8686
// Prefer: return=minimal
8787
} else if (this.headers['Accept'] === 'text/csv') {
8888
data = body

src/PostgrestClient.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,14 @@ export default class PostgrestClient<
6464
from<
6565
TableName extends string & keyof Schema['Tables'],
6666
Table extends Schema['Tables'][TableName]
67-
>(relation: TableName): PostgrestQueryBuilder<Table>
67+
>(relation: TableName): PostgrestQueryBuilder<Schema, Table>
6868
from<ViewName extends string & keyof Schema['Views'], View extends Schema['Views'][ViewName]>(
6969
relation: ViewName
70-
): PostgrestQueryBuilder<View>
71-
from(relation: string): PostgrestQueryBuilder<any>
72-
from(relation: string): PostgrestQueryBuilder<any> {
70+
): PostgrestQueryBuilder<Schema, View>
71+
from(relation: string): PostgrestQueryBuilder<Schema, any>
72+
from(relation: string): PostgrestQueryBuilder<Schema, any> {
7373
const url = new URL(`${this.url}/${relation}`)
74-
return new PostgrestQueryBuilder<any>(url, {
74+
return new PostgrestQueryBuilder<Schema, any>(url, {
7575
headers: { ...this.headers },
7676
schema: this.schema,
7777
fetch: this.fetch,
@@ -113,6 +113,7 @@ export default class PostgrestClient<
113113
count?: 'exact' | 'planned' | 'estimated'
114114
} = {}
115115
): PostgrestFilterBuilder<
116+
Schema,
116117
Function_['Returns'] extends any[]
117118
? Function_['Returns'][number] extends Record<string, unknown>
118119
? Function_['Returns'][number]

src/PostgrestFilterBuilder.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import PostgrestTransformBuilder from './PostgrestTransformBuilder'
2+
import { GenericSchema } from './types'
23

34
type FilterOperator =
45
| 'eq'
@@ -25,9 +26,10 @@ type FilterOperator =
2526
| 'wfts'
2627

2728
export default class PostgrestFilterBuilder<
29+
Schema extends GenericSchema,
2830
Row extends Record<string, unknown>,
2931
Result
30-
> extends PostgrestTransformBuilder<Row, Result> {
32+
> extends PostgrestTransformBuilder<Schema, Row, Result> {
3133
/**
3234
* Match only rows where `column` is equal to `value`.
3335
*

src/PostgrestQueryBuilder.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import PostgrestBuilder from './PostgrestBuilder'
22
import PostgrestFilterBuilder from './PostgrestFilterBuilder'
33
import { GetResult } from './select-query-parser'
4-
import { Fetch, GenericTable, GenericView } from './types'
4+
import { Fetch, GenericSchema, GenericTable, GenericView } from './types'
55

6-
export default class PostgrestQueryBuilder<Relation extends GenericTable | GenericView> {
6+
export default class PostgrestQueryBuilder<
7+
Schema extends GenericSchema,
8+
Relation extends GenericTable | GenericView
9+
> {
710
url: URL
811
headers: Record<string, string>
912
schema?: string
@@ -51,7 +54,7 @@ export default class PostgrestQueryBuilder<Relation extends GenericTable | Gener
5154
*/
5255
select<
5356
Query extends string = '*',
54-
Result = GetResult<Relation['Row'], Query extends '*' ? '*' : Query>
57+
Result = GetResult<Schema, Relation['Row'], Query>
5558
>(
5659
columns?: Query,
5760
{
@@ -61,7 +64,7 @@ export default class PostgrestQueryBuilder<Relation extends GenericTable | Gener
6164
head?: boolean
6265
count?: 'exact' | 'planned' | 'estimated'
6366
} = {}
64-
): PostgrestFilterBuilder<Relation['Row'], Result> {
67+
): PostgrestFilterBuilder<Schema, Relation['Row'], Result> {
6568
const method = head ? 'HEAD' : 'GET'
6669
// Remove whitespaces except when quoted
6770
let quoted = false
@@ -121,7 +124,7 @@ export default class PostgrestQueryBuilder<Relation extends GenericTable | Gener
121124
}: {
122125
count?: 'exact' | 'planned' | 'estimated'
123126
} = {}
124-
): PostgrestFilterBuilder<Relation['Row'], undefined> {
127+
): PostgrestFilterBuilder<Schema, Relation['Row'], undefined> {
125128
const method = 'POST'
126129

127130
const prefersHeaders = []
@@ -197,7 +200,7 @@ export default class PostgrestQueryBuilder<Relation extends GenericTable | Gener
197200
ignoreDuplicates?: boolean
198201
count?: 'exact' | 'planned' | 'estimated'
199202
} = {}
200-
): PostgrestFilterBuilder<Relation['Row'], undefined> {
203+
): PostgrestFilterBuilder<Schema, Relation['Row'], undefined> {
201204
const method = 'POST'
202205

203206
const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`]
@@ -251,7 +254,7 @@ export default class PostgrestQueryBuilder<Relation extends GenericTable | Gener
251254
}: {
252255
count?: 'exact' | 'planned' | 'estimated'
253256
} = {}
254-
): PostgrestFilterBuilder<Relation['Row'], undefined> {
257+
): PostgrestFilterBuilder<Schema, Relation['Row'], undefined> {
255258
const method = 'PATCH'
256259
const prefersHeaders = []
257260
const body = values
@@ -297,7 +300,7 @@ export default class PostgrestQueryBuilder<Relation extends GenericTable | Gener
297300
count,
298301
}: {
299302
count?: 'exact' | 'planned' | 'estimated'
300-
} = {}): PostgrestFilterBuilder<Relation['Row'], undefined> {
303+
} = {}): PostgrestFilterBuilder<Schema, Relation['Row'], undefined> {
301304
const method = 'DELETE'
302305
const prefersHeaders = []
303306
if (count) {

src/PostgrestTransformBuilder.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import PostgrestBuilder from './PostgrestBuilder'
22
import { GetResult } from './select-query-parser'
3-
import { PostgrestMaybeSingleResponse, PostgrestResponse, PostgrestSingleResponse } from './types'
3+
import {
4+
GenericSchema,
5+
PostgrestMaybeSingleResponse,
6+
PostgrestResponse,
7+
PostgrestSingleResponse,
8+
} from './types'
49

510
export default class PostgrestTransformBuilder<
11+
Schema extends GenericSchema,
612
Row extends Record<string, unknown>,
713
Result
814
> extends PostgrestBuilder<Result> {
@@ -15,9 +21,9 @@ export default class PostgrestTransformBuilder<
1521
*
1622
* @param columns - The columns to retrieve, separated by commas
1723
*/
18-
select<Query extends string = '*', NewResult = GetResult<Row, Query extends '*' ? '*' : Query>>(
24+
select<Query extends string = '*', NewResult = GetResult<Schema, Row, Query>>(
1925
columns?: Query
20-
): PostgrestTransformBuilder<Row, NewResult> {
26+
): PostgrestTransformBuilder<Schema, Row, NewResult> {
2127
// Remove whitespaces except when quoted
2228
let quoted = false
2329
const cleanedColumns = (columns ?? '*')
@@ -37,7 +43,7 @@ export default class PostgrestTransformBuilder<
3743
this.headers['Prefer'] += ','
3844
}
3945
this.headers['Prefer'] += 'return=representation'
40-
return this as unknown as PostgrestTransformBuilder<Row, NewResult>
46+
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResult>
4147
}
4248

4349
/**
@@ -234,4 +240,13 @@ export default class PostgrestTransformBuilder<
234240
}
235241
return this
236242
}
243+
244+
/**
245+
* Override the type of the returned `data`.
246+
*
247+
* @typeParam NewResult - The new result type to override with
248+
*/
249+
returns<NewResult>(): PostgrestTransformBuilder<Schema, Row, NewResult> {
250+
return this as unknown as PostgrestTransformBuilder<Schema, Row, NewResult>
251+
}
237252
}

src/select-query-parser.ts

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Credits to @bnjmnt4n (https://www.npmjs.com/package/postgrest-query)
22

3+
import { GenericSchema } from './types'
4+
35
type Whitespace = ' ' | '\n' | '\t'
46

57
type LowerAlphabet =
@@ -67,12 +69,25 @@ type EatWhitespace<Input extends string> = string extends Input
6769
* @param Name Name of the table being queried.
6870
* @param Field Single field parsed by `ParseQuery`.
6971
*/
70-
type ConstructFieldDefinition<Row extends Record<string, unknown>, Field> = Field extends {
72+
type ConstructFieldDefinition<
73+
Schema extends GenericSchema,
74+
Row extends Record<string, unknown>,
75+
Field
76+
> = Field extends {
7177
star: true
7278
}
7379
? Row
74-
: Field extends { name: string; foreignTable: true }
75-
? { [K in Field['name']]: unknown }
80+
: Field extends { name: string; original: string; children: unknown[] }
81+
? {
82+
[_ in Field['name']]: GetResultHelper<
83+
Schema,
84+
(Schema['Tables'] & Schema['Views'])[Field['original']]['Row'],
85+
Field['children'],
86+
unknown
87+
> extends infer Child
88+
? Child | Child[] | null
89+
: never
90+
}
7691
: Field extends { name: string; original: string }
7792
? { [K in Field['name']]: Row[Field['original']] }
7893
: Record<string, unknown>
@@ -135,30 +150,30 @@ type ParseNode<Input extends string> = Input extends ''
135150
? [{ star: true }, EatWhitespace<Remainder>]
136151
: ParseIdentifier<Input> extends [infer Name, `${infer Remainder}`]
137152
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
138-
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [infer _Fields, `${infer Remainder}`]
153+
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [infer Fields, `${infer Remainder}`]
139154
? // `field!inner(nodes)`
140-
[{ name: Name; foreignTable: true }, EatWhitespace<Remainder>]
155+
[{ name: Name; original: Name; children: Fields }, EatWhitespace<Remainder>]
141156
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
142157
? ParseEmbeddedResource<EatWhitespace<Remainder>>
143158
: ParserError<'Expected embedded resource after `!inner`'>
144159
: EatWhitespace<Remainder> extends `!${infer Remainder}`
145160
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer _Hint, `${infer Remainder}`]
146161
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
147162
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
148-
infer _Fields,
163+
infer Fields,
149164
`${infer Remainder}`
150165
]
151166
? // `field!hint!inner(nodes)`
152-
[{ name: Name; foreignTable: true }, EatWhitespace<Remainder>]
167+
[{ name: Name; original: Name; children: Fields }, EatWhitespace<Remainder>]
153168
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
154169
? ParseEmbeddedResource<EatWhitespace<Remainder>>
155170
: ParserError<'Expected embedded resource after `!inner`'>
156171
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
157-
infer _Fields,
172+
infer Fields,
158173
`${infer Remainder}`
159174
]
160175
? // `field!hint(nodes)`
161-
[{ name: Name; foreignTable: true }, EatWhitespace<Remainder>]
176+
[{ name: Name; original: Name; children: Fields }, EatWhitespace<Remainder>]
162177
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
163178
? ParseEmbeddedResource<EatWhitespace<Remainder>>
164179
: ParserError<'Expected embedded resource after `!hint`'>
@@ -167,35 +182,36 @@ type ParseNode<Input extends string> = Input extends ''
167182
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer OriginalName, `${infer Remainder}`]
168183
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
169184
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
170-
infer _Fields,
185+
infer Fields,
171186
`${infer Remainder}`
172187
]
173188
? // `renamed_field:field!inner(nodes)`
174-
[{ name: Name; foreignTable: true }, EatWhitespace<Remainder>]
189+
[{ name: Name; original: OriginalName; children: Fields }, EatWhitespace<Remainder>]
175190
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
176191
? ParseEmbeddedResource<EatWhitespace<Remainder>>
177192
: ParserError<'Expected embedded resource after `!inner`'>
178193
: EatWhitespace<Remainder> extends `!${infer Remainder}`
179194
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer _Hint, `${infer Remainder}`]
180195
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
181196
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
182-
infer _Fields,
197+
infer Fields,
183198
`${infer Remainder}`
184199
]
185200
? // `renamed_field:field!hint!inner(nodes)`
186-
[{ name: Name; foreignTable: true }, EatWhitespace<Remainder>]
201+
[{ name: Name; original: OriginalName; children: Fields }, EatWhitespace<Remainder>]
187202
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
188203
? ParseEmbeddedResource<EatWhitespace<Remainder>>
189204
: ParserError<'Expected embedded resource after `!inner`'>
190205
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
191-
infer _Fields,
206+
infer Fields,
192207
`${infer Remainder}`
193208
]
194209
? // `renamed_field:field!hint(nodes)`
195210
[
196211
{
197212
name: Name
198-
foreignTable: true
213+
original: OriginalName
214+
children: Fields
199215
},
200216
EatWhitespace<Remainder>
201217
]
@@ -204,19 +220,19 @@ type ParseNode<Input extends string> = Input extends ''
204220
: ParserError<'Expected embedded resource after `!hint`'>
205221
: ParserError<'Expected identifier after `!`'>
206222
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
207-
infer _Fields,
223+
infer Fields,
208224
`${infer Remainder}`
209225
]
210226
? // `renamed_field:field(nodes)`
211-
[{ name: Name; foreignTable: true }, EatWhitespace<Remainder>]
227+
[{ name: Name; original: OriginalName; children: Fields }, EatWhitespace<Remainder>]
212228
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
213229
? ParseEmbeddedResource<EatWhitespace<Remainder>>
214230
: // `renamed_field:field`
215231
[{ name: Name; original: OriginalName }, EatWhitespace<Remainder>]
216232
: ParseIdentifier<EatWhitespace<Remainder>>
217-
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [infer _Fields, `${infer Remainder}`]
233+
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [infer Fields, `${infer Remainder}`]
218234
? // `field(nodes)`
219-
[{ name: Name; foreignTable: true }, EatWhitespace<Remainder>]
235+
[{ name: Name; original: Name; children: Fields }, EatWhitespace<Remainder>]
220236
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
221237
? ParseEmbeddedResource<EatWhitespace<Remainder>>
222238
: // `field`
@@ -274,13 +290,14 @@ type ParseQuery<Query extends string> = string extends Query
274290
: ParseNodes<EatWhitespace<Query>>
275291

276292
type GetResultHelper<
293+
Schema extends GenericSchema,
277294
Row extends Record<string, unknown>,
278295
Fields extends unknown[],
279296
Acc
280297
> = Fields extends [infer R]
281-
? GetResultHelper<Row, [], ConstructFieldDefinition<Row, R> & Acc>
298+
? GetResultHelper<Schema, Row, [], ConstructFieldDefinition<Schema, Row, R> & Acc>
282299
: Fields extends [infer R, ...infer Rest]
283-
? GetResultHelper<Row, Rest, ConstructFieldDefinition<Row, R> & Acc>
300+
? GetResultHelper<Schema, Row, Rest, ConstructFieldDefinition<Schema, Row, R> & Acc>
284301
: Acc
285302

286303
/**
@@ -290,8 +307,9 @@ type GetResultHelper<
290307
* @param Query Select query string literal to parse.
291308
*/
292309
export type GetResult<
310+
Schema extends GenericSchema,
293311
Row extends Record<string, unknown>,
294312
Query extends string
295313
> = ParseQuery<Query> extends unknown[]
296-
? GetResultHelper<Row, ParseQuery<Query>, unknown>
314+
? GetResultHelper<Schema, Row, ParseQuery<Query>, unknown>
297315
: ParseQuery<Query>

0 commit comments

Comments
 (0)