Skip to content

Commit e06f278

Browse files
committed
fix(postgrest): reject excess properties in insert, update, and upsert
The generic signatures `<Row extends Base>(values: Row)` bypass TypeScript's excess property checking, allowing extra keys that cause runtime Postgres errors. This adds a RejectExcessProperties utility type that maps unknown keys to never, catching them at compile time. Fixes #1636
1 parent 15254fa commit e06f278

File tree

3 files changed

+59
-7
lines changed

3 files changed

+59
-7
lines changed

packages/core/postgrest-js/src/PostgrestQueryBuilder.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
GenericTable,
88
GenericView,
99
} from './types/common/common'
10+
import { RejectExcessProperties } from './types/types'
1011

1112
export default class PostgrestQueryBuilder<
1213
ClientOptions extends ClientServerOptions,
@@ -919,7 +920,10 @@ export default class PostgrestQueryBuilder<
919920

920921
// TODO(v3): Make `defaultToNull` consistent for both single & bulk inserts.
921922
insert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
922-
values: Row,
923+
values: RejectExcessProperties<
924+
Relation extends { Insert: unknown } ? Relation['Insert'] : never,
925+
Row
926+
>,
923927
options?: {
924928
count?: 'exact' | 'planned' | 'estimated'
925929
}
@@ -933,7 +937,10 @@ export default class PostgrestQueryBuilder<
933937
'POST'
934938
>
935939
insert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
936-
values: Row[],
940+
values: RejectExcessProperties<
941+
Relation extends { Insert: unknown } ? Relation['Insert'] : never,
942+
Row
943+
>[],
937944
options?: {
938945
count?: 'exact' | 'planned' | 'estimated'
939946
defaultToNull?: boolean
@@ -1059,7 +1066,15 @@ export default class PostgrestQueryBuilder<
10591066
* ```
10601067
*/
10611068
insert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
1062-
values: Row | Row[],
1069+
values:
1070+
| RejectExcessProperties<
1071+
Relation extends { Insert: unknown } ? Relation['Insert'] : never,
1072+
Row
1073+
>
1074+
| RejectExcessProperties<
1075+
Relation extends { Insert: unknown } ? Relation['Insert'] : never,
1076+
Row
1077+
>[],
10631078
{
10641079
count,
10651080
defaultToNull = true,
@@ -1107,7 +1122,10 @@ export default class PostgrestQueryBuilder<
11071122

11081123
// TODO(v3): Make `defaultToNull` consistent for both single & bulk upserts.
11091124
upsert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
1110-
values: Row,
1125+
values: RejectExcessProperties<
1126+
Relation extends { Insert: unknown } ? Relation['Insert'] : never,
1127+
Row
1128+
>,
11111129
options?: {
11121130
onConflict?: string
11131131
ignoreDuplicates?: boolean
@@ -1123,7 +1141,10 @@ export default class PostgrestQueryBuilder<
11231141
'POST'
11241142
>
11251143
upsert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
1126-
values: Row[],
1144+
values: RejectExcessProperties<
1145+
Relation extends { Insert: unknown } ? Relation['Insert'] : never,
1146+
Row
1147+
>[],
11271148
options?: {
11281149
onConflict?: string
11291150
ignoreDuplicates?: boolean
@@ -1349,7 +1370,15 @@ export default class PostgrestQueryBuilder<
13491370
*/
13501371

13511372
upsert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
1352-
values: Row | Row[],
1373+
values:
1374+
| RejectExcessProperties<
1375+
Relation extends { Insert: unknown } ? Relation['Insert'] : never,
1376+
Row
1377+
>
1378+
| RejectExcessProperties<
1379+
Relation extends { Insert: unknown } ? Relation['Insert'] : never,
1380+
Row
1381+
>[],
13531382
{
13541383
onConflict,
13551384
ignoreDuplicates = false,
@@ -1542,7 +1571,10 @@ export default class PostgrestQueryBuilder<
15421571
* ```
15431572
*/
15441573
update<Row extends Relation extends { Update: unknown } ? Relation['Update'] : never>(
1545-
values: Row,
1574+
values: RejectExcessProperties<
1575+
Relation extends { Update: unknown } ? Relation['Update'] : never,
1576+
Row
1577+
>,
15461578
{
15471579
count,
15481580
}: {

packages/core/postgrest-js/src/types/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export type DatabaseWithOptions<Database, Options extends ClientServerOptions> =
3838
// https://twitter.com/mattpocockuk/status/1622730173446557697
3939
export type Prettify<T> = { [K in keyof T]: T[K] } & {}
4040

41+
// Rejects excess properties that aren't in Base.
42+
// Works around TypeScript not checking excess properties on generic parameters.
43+
export type RejectExcessProperties<Base, Row> = Row & {
44+
[K in Exclude<keyof Row, keyof Base>]: never
45+
}
46+
4147
// https://github.com/sindresorhus/type-fest
4248
export type SimplifyDeep<Type, ExcludeType = never> = ConditionalSimplifyDeep<
4349
Type,

packages/core/postgrest-js/test/index.test-d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,20 @@ const postgrestWithOptions = new PostgrestClient<DatabaseWithOptions>(REST_URL)
173173
postgrest.from('updatable_view').update({ non_updatable_column: 0 })
174174
}
175175

176+
// reject excess properties on insert, update, and upsert (#1636)
177+
{
178+
// @ts-expect-error No overload matches this call.
179+
postgrest.from('users').insert({ username: 'foo', nonexistent: 'bad' })
180+
}
181+
{
182+
// @ts-expect-error Type 'string' is not assignable to type 'never'.
183+
postgrest.from('users').update({ username: 'foo', nonexistent: 'bad' })
184+
}
185+
{
186+
// @ts-expect-error No overload matches this call.
187+
postgrest.from('users').upsert({ username: 'foo', nonexistent: 'bad' })
188+
}
189+
176190
// spread resource with single column in select query
177191
{
178192
const result = await postgrest.from('messages').select('message, ...users(status)').single()

0 commit comments

Comments
 (0)