Skip to content

Commit eef85a4

Browse files
authored
Merge pull request #297 from steve-chavez/rollback
feat: add rollback parameter to mutations and rpc
2 parents 6ea14e3 + 53aaaee commit eef85a4

File tree

6 files changed

+160
-5
lines changed

6 files changed

+160
-5
lines changed

src/PostgrestClient.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export default class PostgrestClient<
7171
* @param options Named parameters.
7272
* @param options.head When set to true, no data will be returned.
7373
* @param options.count Count algorithm to use to count rows in a table.
74+
* @param rollback Rollback the operation
7475
*/
7576
rpc<
7677
FunctionName extends string & keyof Schema['Functions'],
@@ -81,9 +82,11 @@ export default class PostgrestClient<
8182
{
8283
head = false,
8384
count,
85+
rollback = false,
8486
}: {
8587
head?: boolean
8688
count?: 'exact' | 'planned' | 'estimated'
89+
rollback?: boolean
8790
} = {}
8891
): PostgrestFilterBuilder<
8992
Function_['Returns'] extends any[]
@@ -107,9 +110,7 @@ export default class PostgrestClient<
107110
}
108111

109112
const headers = { ...this.headers }
110-
if (count) {
111-
headers['Prefer'] = `count=${count}`
112-
}
113+
headers['Prefer'] = [count ? `count=${count}` : null, rollback ? 'tx=rollback' : null].join(',')
113114

114115
return new PostgrestFilterBuilder({
115116
method,

src/PostgrestQueryBuilder.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,18 @@ export default class PostgrestQueryBuilder<Table extends GenericTable> {
8181
/**
8282
* Performs an INSERT into the table.
8383
*
84-
* @param values The values to insert.
85-
* @param count Count algorithm to use to count rows in a table.
84+
* @param values The values to insert.
85+
* @param count Count algorithm to use to count rows in a table.
86+
* @param rollback Rollback the operation
8687
*/
8788
insert<Row extends Table['Insert']>(
8889
values: Row | Row[],
8990
{
9091
count,
92+
rollback = false,
9193
}: {
9294
count?: 'exact' | 'planned' | 'estimated'
95+
rollback?: boolean
9396
} = {}
9497
): PostgrestFilterBuilder<Table['Row'], undefined> {
9598
const method = 'POST'
@@ -99,6 +102,9 @@ export default class PostgrestQueryBuilder<Table extends GenericTable> {
99102
if (count) {
100103
prefersHeaders.push(`count=${count}`)
101104
}
105+
if (rollback) {
106+
prefersHeaders.push(`tx=rollback`)
107+
}
102108
if (this.headers['Prefer']) {
103109
prefersHeaders.unshift(this.headers['Prefer'])
104110
}
@@ -131,17 +137,20 @@ export default class PostgrestQueryBuilder<Table extends GenericTable> {
131137
* @param options Named parameters.
132138
* @param options.onConflict By specifying the `on_conflict` query parameter, you can make UPSERT work on a column(s) that has a UNIQUE constraint.
133139
* @param options.ignoreDuplicates Specifies if duplicate rows should be ignored and not inserted.
140+
* @param rollback Rollback the operation
134141
*/
135142
upsert<Row extends Table['Insert']>(
136143
values: Row | Row[],
137144
{
138145
onConflict,
139146
count,
140147
ignoreDuplicates = false,
148+
rollback = false,
141149
}: {
142150
onConflict?: string
143151
count?: 'exact' | 'planned' | 'estimated'
144152
ignoreDuplicates?: boolean
153+
rollback?: boolean
145154
} = {}
146155
): PostgrestFilterBuilder<Table['Row'], undefined> {
147156
const method = 'POST'
@@ -153,6 +162,9 @@ export default class PostgrestQueryBuilder<Table extends GenericTable> {
153162
if (count) {
154163
prefersHeaders.push(`count=${count}`)
155164
}
165+
if (rollback) {
166+
prefersHeaders.push(`tx=rollback`)
167+
}
156168
if (this.headers['Prefer']) {
157169
prefersHeaders.unshift(this.headers['Prefer'])
158170
}
@@ -174,13 +186,16 @@ export default class PostgrestQueryBuilder<Table extends GenericTable> {
174186
*
175187
* @param values The values to update.
176188
* @param count Count algorithm to use to count rows in a table.
189+
* @param rollback Rollback the operation
177190
*/
178191
update<Row extends Table['Update']>(
179192
values: Row,
180193
{
181194
count,
195+
rollback = false,
182196
}: {
183197
count?: 'exact' | 'planned' | 'estimated'
198+
rollback?: boolean
184199
} = {}
185200
): PostgrestFilterBuilder<Table['Row'], undefined> {
186201
const method = 'PATCH'
@@ -189,6 +204,9 @@ export default class PostgrestQueryBuilder<Table extends GenericTable> {
189204
if (count) {
190205
prefersHeaders.push(`count=${count}`)
191206
}
207+
if (rollback) {
208+
prefersHeaders.push(`tx=rollback`)
209+
}
192210
if (this.headers['Prefer']) {
193211
prefersHeaders.unshift(this.headers['Prefer'])
194212
}
@@ -209,17 +227,23 @@ export default class PostgrestQueryBuilder<Table extends GenericTable> {
209227
* Performs a DELETE on the table.
210228
*
211229
* @param count Count algorithm to use to count rows in a table.
230+
* @param rollback Rollback the operation
212231
*/
213232
delete({
214233
count,
234+
rollback = false,
215235
}: {
216236
count?: 'exact' | 'planned' | 'estimated'
237+
rollback?: boolean
217238
} = {}): PostgrestFilterBuilder<Table['Row'], undefined> {
218239
const method = 'DELETE'
219240
const prefersHeaders = []
220241
if (count) {
221242
prefersHeaders.push(`count=${count}`)
222243
}
244+
if (rollback) {
245+
prefersHeaders.push(`tx=rollback`)
246+
}
223247
if (this.headers['Prefer']) {
224248
prefersHeaders.unshift(this.headers['Prefer'])
225249
}

test/basic.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,122 @@ test('select with no match', async () => {
410410
}
411411
`)
412412
})
413+
414+
test('rollback insert/upsert', async () => {
415+
//No row at the start
416+
const res1 = await postgrest.from('users').select().eq('username', 'soedirgo')
417+
expect(res1.data).toMatchInlineSnapshot(`Array []`)
418+
419+
//Insert the row and rollback
420+
const res2 = await postgrest
421+
.from('users')
422+
.insert(
423+
{
424+
age_range: '[20,25)',
425+
catchphrase: "'cat' 'fat'",
426+
data: null,
427+
status: 'ONLINE',
428+
username: 'soedirgo',
429+
},
430+
{ rollback: true }
431+
)
432+
.select()
433+
.single()
434+
expect(res2.data).toMatchInlineSnapshot(`
435+
Object {
436+
"age_range": "[20,25)",
437+
"catchphrase": "'cat' 'fat'",
438+
"data": null,
439+
"status": "ONLINE",
440+
"username": "soedirgo",
441+
}
442+
`)
443+
444+
//Upsert the row and rollback
445+
const res3 = await postgrest
446+
.from('users')
447+
.upsert(
448+
{
449+
age_range: '[20,25)',
450+
catchphrase: "'cat' 'fat'",
451+
data: null,
452+
status: 'ONLINE',
453+
username: 'soedirgo',
454+
},
455+
{ rollback: true }
456+
)
457+
.select()
458+
.single()
459+
expect(res3.data).toMatchInlineSnapshot(`
460+
Object {
461+
"age_range": "[20,25)",
462+
"catchphrase": "'cat' 'fat'",
463+
"data": null,
464+
"status": "ONLINE",
465+
"username": "soedirgo",
466+
}
467+
`)
468+
469+
//No row at the end
470+
const res4 = await postgrest.from('users').select().eq('username', 'soedirgo')
471+
expect(res4.data).toMatchInlineSnapshot(`Array []`)
472+
})
473+
474+
test('rollback update/rpc', async () => {
475+
const res1 = await postgrest.from('users').select('status').eq('username', 'dragarcia').single()
476+
expect(res1.data).toMatchInlineSnapshot(`
477+
Object {
478+
"status": "ONLINE",
479+
}
480+
`)
481+
482+
const res2 = await postgrest
483+
.from('users')
484+
.update({ status: 'OFFLINE' }, { rollback: true })
485+
.eq('username', 'dragarcia')
486+
.select('status')
487+
.single()
488+
expect(res2.data).toMatchInlineSnapshot(`
489+
Object {
490+
"status": "OFFLINE",
491+
}
492+
`)
493+
494+
const res3 = await postgrest.rpc('offline_user', { name_param: 'dragarcia' }, { rollback: true })
495+
expect(res3.data).toMatchInlineSnapshot(`"OFFLINE"`)
496+
497+
const res4 = await postgrest.from('users').select('status').eq('username', 'dragarcia').single()
498+
expect(res4.data).toMatchInlineSnapshot(`
499+
Object {
500+
"status": "ONLINE",
501+
}
502+
`)
503+
})
504+
505+
test('rollback delete', async () => {
506+
const res1 = await postgrest.from('users').select('username').eq('username', 'dragarcia').single()
507+
expect(res1.data).toMatchInlineSnapshot(`
508+
Object {
509+
"username": "dragarcia",
510+
}
511+
`)
512+
513+
const res2 = await postgrest
514+
.from('users')
515+
.delete({ rollback: true })
516+
.eq('username', 'dragarcia')
517+
.select('username')
518+
.single()
519+
expect(res2.data).toMatchInlineSnapshot(`
520+
Object {
521+
"username": "dragarcia",
522+
}
523+
`)
524+
525+
const res3 = await postgrest.from('users').select('username').eq('username', 'dragarcia').single()
526+
expect(res3.data).toMatchInlineSnapshot(`
527+
Object {
528+
"username": "dragarcia",
529+
}
530+
`)
531+
})

test/db/00-schema.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ RETURNS TABLE(username text, status user_status) AS $$
4747
SELECT username, status from users WHERE username=name_param;
4848
$$ LANGUAGE SQL IMMUTABLE;
4949

50+
CREATE FUNCTION public.offline_user(name_param text)
51+
RETURNS user_status AS $$
52+
UPDATE users SET status = 'OFFLINE' WHERE username=name_param
53+
RETURNING status;
54+
$$ LANGUAGE SQL VOLATILE;
55+
5056
CREATE FUNCTION public.void_func()
5157
RETURNS void AS $$
5258
$$ LANGUAGE SQL;

test/db/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ services:
1212
PGRST_DB_EXTRA_SEARCH_PATH: extensions
1313
PGRST_DB_ANON_ROLE: postgres
1414
PGRST_DB_PLAN_ENABLED: 1
15+
PGRST_DB_TX_END: commit-allow-override
1516
depends_on:
1617
- db
1718
db:

test/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ export interface Database {
110110
Args: Record<PropertyKey, never>
111111
Returns: undefined
112112
}
113+
offline_user: {
114+
Args: { name_param: string }
115+
Returns: 'ONLINE' | 'OFFLINE'
116+
}
113117
}
114118
}
115119
}

0 commit comments

Comments
 (0)