Skip to content

Commit ad74608

Browse files
authored
Merge pull request #417 from steve-chavez/pgrst-11
Updates for postgREST 11
2 parents 1e2981f + 29b3808 commit ad74608

File tree

8 files changed

+260
-93
lines changed

8 files changed

+260
-93
lines changed

src/PostgrestFilterBuilder.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,32 @@ export default class PostgrestFilterBuilder<
123123
return this
124124
}
125125

126+
likeAllOf<ColumnName extends string & keyof Row>(column: ColumnName, patterns: string[]): this
127+
likeAllOf(column: string, patterns: string[]): this
128+
/**
129+
* Match only rows where `column` matches all of `patterns` case-sensitively.
130+
*
131+
* @param column - The column to filter on
132+
* @param patterns - The patterns to match with
133+
*/
134+
likeAllOf(column: string, patterns: string[]): this {
135+
this.url.searchParams.append(column, `like(all).{${patterns.join(',')}}`)
136+
return this
137+
}
138+
139+
likeAnyOf<ColumnName extends string & keyof Row>(column: ColumnName, patterns: string[]): this
140+
likeAnyOf(column: string, patterns: string[]): this
141+
/**
142+
* Match only rows where `column` matches any of `patterns` case-sensitively.
143+
*
144+
* @param column - The column to filter on
145+
* @param patterns - The patterns to match with
146+
*/
147+
likeAnyOf(column: string, patterns: string[]): this {
148+
this.url.searchParams.append(column, `like(any).{${patterns.join(',')}}`)
149+
return this
150+
}
151+
126152
ilike<ColumnName extends string & keyof Row>(column: ColumnName, pattern: string): this
127153
ilike(column: string, pattern: string): this
128154
/**
@@ -136,6 +162,32 @@ export default class PostgrestFilterBuilder<
136162
return this
137163
}
138164

165+
ilikeAllOf<ColumnName extends string & keyof Row>(column: ColumnName, patterns: string[]): this
166+
ilikeAllOf(column: string, patterns: string[]): this
167+
/**
168+
* Match only rows where `column` matches all of `patterns` case-insensitively.
169+
*
170+
* @param column - The column to filter on
171+
* @param patterns - The patterns to match with
172+
*/
173+
ilikeAllOf(column: string, patterns: string[]): this {
174+
this.url.searchParams.append(column, `ilike(all).{${patterns.join(',')}}`)
175+
return this
176+
}
177+
178+
ilikeAnyOf<ColumnName extends string & keyof Row>(column: ColumnName, patterns: string[]): this
179+
ilikeAnyOf(column: string, patterns: string[]): this
180+
/**
181+
* Match only rows where `column` matches any of `patterns` case-insensitively.
182+
*
183+
* @param column - The column to filter on
184+
* @param patterns - The patterns to match with
185+
*/
186+
ilikeAnyOf(column: string, patterns: string[]): this {
187+
this.url.searchParams.append(column, `ilike(any).{${patterns.join(',')}}`)
188+
return this
189+
}
190+
139191
is<ColumnName extends string & keyof Row>(
140192
column: ColumnName,
141193
value: Row[ColumnName] & (boolean | null)

src/PostgrestQueryBuilder.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -113,24 +113,31 @@ export default class PostgrestQueryBuilder<
113113
*
114114
* `"estimated"`: Uses exact count for low numbers and planned count for high
115115
* numbers.
116+
*
117+
* @param options.defaultToNull - Make missing fields default to `null`.
118+
* Otherwise, use the default value for the column.
116119
*/
117120
insert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
118121
values: Row | Row[],
119122
{
120123
count,
124+
defaultToNull = true,
121125
}: {
122126
count?: 'exact' | 'planned' | 'estimated'
127+
defaultToNull?: boolean
123128
} = {}
124129
): PostgrestFilterBuilder<Schema, Relation['Row'], null> {
125130
const method = 'POST'
126131

127132
const prefersHeaders = []
128-
const body = values
133+
if (this.headers['Prefer']) {
134+
prefersHeaders.push(this.headers['Prefer'])
135+
}
129136
if (count) {
130137
prefersHeaders.push(`count=${count}`)
131138
}
132-
if (this.headers['Prefer']) {
133-
prefersHeaders.unshift(this.headers['Prefer'])
139+
if (!defaultToNull) {
140+
prefersHeaders.push('missing=default')
134141
}
135142
this.headers['Prefer'] = prefersHeaders.join(',')
136143

@@ -147,7 +154,7 @@ export default class PostgrestQueryBuilder<
147154
url: this.url,
148155
headers: this.headers,
149156
schema: this.schema,
150-
body,
157+
body: values,
151158
fetch: this.fetch,
152159
allowEmpty: false,
153160
} as unknown as PostgrestBuilder<null>)
@@ -185,39 +192,56 @@ export default class PostgrestQueryBuilder<
185192
*
186193
* `"estimated"`: Uses exact count for low numbers and planned count for high
187194
* numbers.
195+
*
196+
* @param options.defaultToNull - Make missing fields default to `null`.
197+
* Otherwise, use the default value for the column. This only applies when
198+
* inserting new rows, not when merging with existing rows under
199+
* `ignoreDuplicates: false`.
188200
*/
189201
upsert<Row extends Relation extends { Insert: unknown } ? Relation['Insert'] : never>(
190202
values: Row | Row[],
191203
{
192204
onConflict,
193205
ignoreDuplicates = false,
194206
count,
207+
defaultToNull = true,
195208
}: {
196209
onConflict?: string
197210
ignoreDuplicates?: boolean
198211
count?: 'exact' | 'planned' | 'estimated'
212+
defaultToNull?: boolean
199213
} = {}
200214
): PostgrestFilterBuilder<Schema, Relation['Row'], null> {
201215
const method = 'POST'
202216

203217
const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`]
204218

205219
if (onConflict !== undefined) this.url.searchParams.set('on_conflict', onConflict)
206-
const body = values
220+
if (this.headers['Prefer']) {
221+
prefersHeaders.push(this.headers['Prefer'])
222+
}
207223
if (count) {
208224
prefersHeaders.push(`count=${count}`)
209225
}
210-
if (this.headers['Prefer']) {
211-
prefersHeaders.unshift(this.headers['Prefer'])
226+
if (!defaultToNull) {
227+
prefersHeaders.push('missing=default')
212228
}
213229
this.headers['Prefer'] = prefersHeaders.join(',')
214230

231+
if (Array.isArray(values)) {
232+
const columns = values.reduce((acc, x) => acc.concat(Object.keys(x)), [] as string[])
233+
if (columns.length > 0) {
234+
const uniqueColumns = [...new Set(columns)].map((column) => `"${column}"`)
235+
this.url.searchParams.set('columns', uniqueColumns.join(','))
236+
}
237+
}
238+
215239
return new PostgrestFilterBuilder({
216240
method,
217241
url: this.url,
218242
headers: this.headers,
219243
schema: this.schema,
220-
body,
244+
body: values,
221245
fetch: this.fetch,
222246
allowEmpty: false,
223247
} as unknown as PostgrestBuilder<null>)
@@ -254,21 +278,20 @@ export default class PostgrestQueryBuilder<
254278
): PostgrestFilterBuilder<Schema, Relation['Row'], null> {
255279
const method = 'PATCH'
256280
const prefersHeaders = []
257-
const body = values
281+
if (this.headers['Prefer']) {
282+
prefersHeaders.push(this.headers['Prefer'])
283+
}
258284
if (count) {
259285
prefersHeaders.push(`count=${count}`)
260286
}
261-
if (this.headers['Prefer']) {
262-
prefersHeaders.unshift(this.headers['Prefer'])
263-
}
264287
this.headers['Prefer'] = prefersHeaders.join(',')
265288

266289
return new PostgrestFilterBuilder({
267290
method,
268291
url: this.url,
269292
headers: this.headers,
270293
schema: this.schema,
271-
body,
294+
body: values,
272295
fetch: this.fetch,
273296
allowEmpty: false,
274297
} as unknown as PostgrestBuilder<null>)

src/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ export interface PostgrestResponseFailure extends PostgrestResponseBase {
3535
// TODO: in v3:
3636
// - remove PostgrestResponse and PostgrestMaybeSingleResponse
3737
// - rename PostgrestSingleResponse to PostgrestResponse
38-
export type PostgrestSingleResponse<T> =
39-
| PostgrestResponseSuccess<T>
40-
| PostgrestResponseFailure
38+
export type PostgrestSingleResponse<T> = PostgrestResponseSuccess<T> | PostgrestResponseFailure
4139
export type PostgrestMaybeSingleResponse<T> = PostgrestSingleResponse<T | null>
4240
export type PostgrestResponse<T> = PostgrestSingleResponse<T[]>
4341

test/basic.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,62 @@ describe("insert, update, delete with count: 'exact'", () => {
10781078
`)
10791079
})
10801080

1081+
test('bulk insert with column defaults', async () => {
1082+
let res = await postgrest
1083+
.from('channels')
1084+
.insert([{ id: 100 }, { slug: 'test-slug' }], { defaultToNull: false })
1085+
.select()
1086+
.rollback()
1087+
expect(res).toMatchInlineSnapshot(`
1088+
Object {
1089+
"count": null,
1090+
"data": Array [
1091+
Object {
1092+
"data": null,
1093+
"id": 100,
1094+
"slug": null,
1095+
},
1096+
Object {
1097+
"data": null,
1098+
"id": 4,
1099+
"slug": "test-slug",
1100+
},
1101+
],
1102+
"error": null,
1103+
"status": 201,
1104+
"statusText": "Created",
1105+
}
1106+
`)
1107+
})
1108+
1109+
test('bulk upsert with column defaults', async () => {
1110+
let res = await postgrest
1111+
.from('channels')
1112+
.upsert([{ id: 1 }, { slug: 'test-slug' }], { defaultToNull: false })
1113+
.select()
1114+
.rollback()
1115+
expect(res).toMatchInlineSnapshot(`
1116+
Object {
1117+
"count": null,
1118+
"data": Array [
1119+
Object {
1120+
"data": null,
1121+
"id": 1,
1122+
"slug": null,
1123+
},
1124+
Object {
1125+
"data": null,
1126+
"id": 6,
1127+
"slug": "test-slug",
1128+
},
1129+
],
1130+
"error": null,
1131+
"status": 201,
1132+
"statusText": "Created",
1133+
}
1134+
`)
1135+
})
1136+
10811137
test("update with count: 'exact'", async () => {
10821138
let res = await postgrest
10831139
.from('messages')

test/db/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
version: '3'
44
services:
55
rest:
6-
image: postgrest/postgrest:v10.1.2
6+
image: postgrest/postgrest:v11.0.0
77
ports:
88
- '3000:3000'
99
environment:

test/filters.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,49 @@ test('like', async () => {
182182
`)
183183
})
184184

185+
test('likeAllOf', async () => {
186+
const res = await postgrest
187+
.from('users')
188+
.select('username')
189+
.likeAllOf('username', ['%supa%', '%bot%'])
190+
expect(res).toMatchInlineSnapshot(`
191+
Object {
192+
"count": null,
193+
"data": Array [
194+
Object {
195+
"username": "supabot",
196+
},
197+
],
198+
"error": null,
199+
"status": 200,
200+
"statusText": "OK",
201+
}
202+
`)
203+
})
204+
205+
test('likeAnyOf', async () => {
206+
const res = await postgrest
207+
.from('users')
208+
.select('username')
209+
.likeAnyOf('username', ['%supa%', '%kiwi%'])
210+
expect(res).toMatchInlineSnapshot(`
211+
Object {
212+
"count": null,
213+
"data": Array [
214+
Object {
215+
"username": "supabot",
216+
},
217+
Object {
218+
"username": "kiwicopple",
219+
},
220+
],
221+
"error": null,
222+
"status": 200,
223+
"statusText": "OK",
224+
}
225+
`)
226+
})
227+
185228
test('ilike', async () => {
186229
const res = await postgrest.from('users').select('username').ilike('username', '%SUPA%')
187230
expect(res).toMatchInlineSnapshot(`
@@ -199,6 +242,49 @@ test('ilike', async () => {
199242
`)
200243
})
201244

245+
test('ilikeAllOf', async () => {
246+
const res = await postgrest
247+
.from('users')
248+
.select('username')
249+
.ilikeAllOf('username', ['%SUPA%', '%bot%'])
250+
expect(res).toMatchInlineSnapshot(`
251+
Object {
252+
"count": null,
253+
"data": Array [
254+
Object {
255+
"username": "supabot",
256+
},
257+
],
258+
"error": null,
259+
"status": 200,
260+
"statusText": "OK",
261+
}
262+
`)
263+
})
264+
265+
test('ilikeAnyOf', async () => {
266+
const res = await postgrest
267+
.from('users')
268+
.select('username')
269+
.ilikeAnyOf('username', ['%supa%', '%KIWI%'])
270+
expect(res).toMatchInlineSnapshot(`
271+
Object {
272+
"count": null,
273+
"data": Array [
274+
Object {
275+
"username": "supabot",
276+
},
277+
Object {
278+
"username": "kiwicopple",
279+
},
280+
],
281+
"error": null,
282+
"status": 200,
283+
"statusText": "OK",
284+
}
285+
`)
286+
})
287+
202288
test('is', async () => {
203289
const res = await postgrest.from('users').select('data').is('data', null)
204290
expect(res).toMatchInlineSnapshot(`

test/index.test-d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
5252
if (error) {
5353
throw new Error(error.message)
5454
}
55-
expectType<{ bar: Json, baz: string }>(data)
55+
expectType<{ bar: Json; baz: string }>(data)
5656
}
5757

5858
// rpc return type

0 commit comments

Comments
 (0)