From 3a1b33bb5430eff63cca0a762f91f8846f936fea Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 9 Jan 2021 14:21:33 +0900 Subject: [PATCH 01/11] select method now accepts home and count option --- src/PostgrestClient.ts | 1 - src/lib/PostgrestTransformBuilder.ts | 22 ++++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/PostgrestClient.ts b/src/PostgrestClient.ts index 5cd9be7d..38652acb 100644 --- a/src/PostgrestClient.ts +++ b/src/PostgrestClient.ts @@ -1,6 +1,5 @@ import PostgrestQueryBuilder from './lib/PostgrestQueryBuilder' import PostgrestTransformBuilder from './lib/PostgrestTransformBuilder' -import { PostgrestBuilder } from './lib/types' export default class PostgrestClient { url: string diff --git a/src/lib/PostgrestTransformBuilder.ts b/src/lib/PostgrestTransformBuilder.ts index 80d874ba..9c86268f 100644 --- a/src/lib/PostgrestTransformBuilder.ts +++ b/src/lib/PostgrestTransformBuilder.ts @@ -1,5 +1,7 @@ import { PostgrestBuilder, PostgrestSingleResponse } from './types' +type CountMethod = 'exact' | 'planned' | 'estimated' + /** * Post-filters (transforms) */ @@ -9,8 +11,10 @@ export default class PostgrestTransformBuilder extends PostgrestBuilder { * Performs vertical filtering with SELECT. * * @param columns The columns to retrieve, separated by commas. + * @param head When set to true, select will void data. + * @param count Count algorithm to use to count rows in a table. */ - select(columns = '*'): this { + select(columns = '*', { head, count }: { head?: boolean; count?: CountMethod } = {}): this { // Remove whitespaces except when quoted let quoted = false const cleanedColumns = columns @@ -26,6 +30,11 @@ export default class PostgrestTransformBuilder extends PostgrestBuilder { }) .join('') this.url.searchParams.set('select', cleanedColumns) + + if (count) { + // TODO append prefer header + // this.headers['Prefer'] += `count=${count}` + } return this } @@ -59,10 +68,7 @@ export default class PostgrestTransformBuilder extends PostgrestBuilder { * @param count The maximum no. of rows to limit to. * @param foreignTable The foreign table to use (for foreign columns). */ - limit( - count: number, - { foreignTable }: { foreignTable?: string } = {} - ): this { + limit(count: number, { foreignTable }: { foreignTable?: string } = {}): this { const key = typeof foreignTable === 'undefined' ? 'limit' : `"${foreignTable}".limit` this.url.searchParams.set(key, `${count}`) return this @@ -75,11 +81,7 @@ export default class PostgrestTransformBuilder extends PostgrestBuilder { * @param to The last index to which to limit the result, inclusive. * @param foreignTable The foreign table to use (for foreign columns). */ - range( - from: number, - to: number, - { foreignTable }: { foreignTable?: string } = {} - ): this { + range(from: number, to: number, { foreignTable }: { foreignTable?: string } = {}): this { const keyOffset = typeof foreignTable === 'undefined' ? 'offset' : `"${foreignTable}".offset` const keyLimit = typeof foreignTable === 'undefined' ? 'limit' : `"${foreignTable}".limit` this.url.searchParams.set(keyOffset, `${from}`) From 8f459a56ec95c8005c036cf6f7e9b49d24c2e591 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 9 Jan 2021 15:07:52 +0900 Subject: [PATCH 02/11] removed head and count option from PostgrestTransformBuilder and added home and count option to select in PostgrestQueryBuilder --- src/lib/PostgrestQueryBuilder.ts | 10 +++++++++- src/lib/PostgrestTransformBuilder.ts | 11 +---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/lib/PostgrestQueryBuilder.ts b/src/lib/PostgrestQueryBuilder.ts index 47b76b42..02195e89 100644 --- a/src/lib/PostgrestQueryBuilder.ts +++ b/src/lib/PostgrestQueryBuilder.ts @@ -17,8 +17,13 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { * Performs vertical filtering with SELECT. * * @param columns The columns to retrieve, separated by commas. + * @param head When set to true, select will void data. + * @param count Count algorithm to use to count rows in a table. */ - select(columns = '*'): PostgrestFilterBuilder { + select( + columns = '*', + { head, count }: { head?: boolean; count?: 'exact' | 'planned' | 'estimated' } = {} + ): PostgrestFilterBuilder { this.method = 'GET' // Remove whitespaces except when quoted let quoted = false @@ -35,6 +40,9 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { }) .join('') this.url.searchParams.set('select', cleanedColumns) + if (count) { + this.headers['Prefer'] = `count=${count}` + } return new PostgrestFilterBuilder(this) } diff --git a/src/lib/PostgrestTransformBuilder.ts b/src/lib/PostgrestTransformBuilder.ts index 9c86268f..a94c93c8 100644 --- a/src/lib/PostgrestTransformBuilder.ts +++ b/src/lib/PostgrestTransformBuilder.ts @@ -1,7 +1,5 @@ import { PostgrestBuilder, PostgrestSingleResponse } from './types' -type CountMethod = 'exact' | 'planned' | 'estimated' - /** * Post-filters (transforms) */ @@ -11,10 +9,8 @@ export default class PostgrestTransformBuilder extends PostgrestBuilder { * Performs vertical filtering with SELECT. * * @param columns The columns to retrieve, separated by commas. - * @param head When set to true, select will void data. - * @param count Count algorithm to use to count rows in a table. */ - select(columns = '*', { head, count }: { head?: boolean; count?: CountMethod } = {}): this { + select(columns = '*'): this { // Remove whitespaces except when quoted let quoted = false const cleanedColumns = columns @@ -30,11 +26,6 @@ export default class PostgrestTransformBuilder extends PostgrestBuilder { }) .join('') this.url.searchParams.set('select', cleanedColumns) - - if (count) { - // TODO append prefer header - // this.headers['Prefer'] += `count=${count}` - } return this } From e270b373caba1b7c408c2d943e837255fb332955 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 9 Jan 2021 20:48:54 +0900 Subject: [PATCH 03/11] select now returns count value when count option is selected --- src/lib/types.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/lib/types.ts b/src/lib/types.ts index 9d6c363f..b30ee8b1 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -26,12 +26,14 @@ interface PostgrestResponseSuccess extends PostgrestResponseBase { error: null data: T[] body: T[] + count: number | null } interface PostgrestResponseFailure extends PostgrestResponseBase { error: PostgrestError data: null // For backward compatibility: body === data body: null + count: null } export type PostgrestResponse = PostgrestResponseSuccess | PostgrestResponseFailure @@ -80,18 +82,32 @@ export abstract class PostgrestBuilder implements PromiseLike { - let error, data + let error, data, count if (res.ok) { error = null const isReturnMinimal = this.headers['Prefer']?.split(',').includes('return=minimal') data = isReturnMinimal ? null : await res.json() + + const countHeader = this.headers['Prefer']?.match(/count=(exact|planned|estimated)/) + if (countHeader) { + const contentRange = res.headers.get('content-range')?.split('/') + if (contentRange && contentRange.length > 1) { + count = parseInt(contentRange[1]) + } else { + count = null + } + } else { + count = null + } } else { error = await res.json() data = null + count = null } const postgrestResponse: PostgrestResponse = { error, data, + count, status: res.status, statusText: res.statusText, body: data, From 01d4c2c716cc3d3034d493a5e5844a5c88bc29c5 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sun, 10 Jan 2021 11:02:06 +0900 Subject: [PATCH 04/11] Enabled head for select --- src/lib/PostgrestQueryBuilder.ts | 3 +++ src/lib/types.ts | 8 ++++++-- test/transforms.ts | 4 +++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/lib/PostgrestQueryBuilder.ts b/src/lib/PostgrestQueryBuilder.ts index 02195e89..a98ebdf7 100644 --- a/src/lib/PostgrestQueryBuilder.ts +++ b/src/lib/PostgrestQueryBuilder.ts @@ -43,6 +43,9 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { if (count) { this.headers['Prefer'] = `count=${count}` } + if (head) { + this.method = 'HEAD' + } return new PostgrestFilterBuilder(this) } diff --git a/src/lib/types.ts b/src/lib/types.ts index b30ee8b1..53f6782f 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -85,8 +85,12 @@ export abstract class PostgrestBuilder implements PromiseLike { }) test('select on stored procedure', async () => { - const res = await postgrest.rpc('get_username_and_status', { name_param: 'supabot' }).select('status') + const res = await postgrest + .rpc('get_username_and_status', { name_param: 'supabot' }) + .select('status') expect(res).toMatchSnapshot() }) From 106ddac504eb757e0aa7343d92787893c281039c Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sun, 10 Jan 2021 12:45:01 +0900 Subject: [PATCH 05/11] Updated the test snapshots to include count field --- test/__snapshots__/index.test.ts.snap | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/__snapshots__/index.test.ts.snap b/test/__snapshots__/index.test.ts.snap index e00e329e..22d6365b 100644 --- a/test/__snapshots__/index.test.ts.snap +++ b/test/__snapshots__/index.test.ts.snap @@ -27,6 +27,7 @@ Object { "username": "awailas", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -114,6 +115,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 2, @@ -161,6 +163,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 1, @@ -194,6 +197,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 1, @@ -234,6 +238,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 1, @@ -288,6 +293,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 2, @@ -356,6 +362,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 1, @@ -417,6 +424,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 1, @@ -478,6 +486,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 1, @@ -532,6 +541,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 2, @@ -572,6 +582,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 1, @@ -633,6 +644,7 @@ Object { "username": "dragarcia", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -680,6 +692,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -706,6 +719,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -747,6 +761,7 @@ Object { "messages": Array [], }, ], + "count": null, "data": Array [ Object { "messages": Array [ @@ -806,6 +821,7 @@ Object { "messages": Array [], }, ], + "count": null, "data": Array [ Object { "messages": Array [ @@ -865,6 +881,7 @@ Object { "messages": Array [], }, ], + "count": null, "data": Array [ Object { "messages": Array [ @@ -924,6 +941,7 @@ Object { "messages": Array [], }, ], + "count": null, "data": Array [ Object { "messages": Array [ @@ -983,6 +1001,7 @@ Object { "messages": Array [], }, ], + "count": null, "data": Array [ Object { "messages": Array [ @@ -1022,6 +1041,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1048,6 +1068,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1074,6 +1095,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1100,6 +1122,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 2, @@ -1133,6 +1156,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 1, @@ -1166,6 +1190,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1213,6 +1238,7 @@ Object { "username": "dragarcia", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1281,6 +1307,7 @@ Object { "username": "dragarcia", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1328,6 +1355,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1354,6 +1382,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1380,6 +1409,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 1, @@ -1413,6 +1443,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "channel_id": 1, @@ -1446,6 +1477,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1464,6 +1496,7 @@ Object { exports[`missing table 1`] = ` Object { "body": null, + "count": null, "data": null, "error": Object { "code": "42P01", @@ -1487,6 +1520,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1527,6 +1561,7 @@ Object { "username": "dragarcia", }, ], + "count": null, "data": Array [ Object { "age_range": "[25,35)", @@ -1581,6 +1616,7 @@ Object { "username": "dragarcia", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1635,6 +1671,7 @@ Object { "username": "dragarcia", }, ], + "count": null, "data": Array [ Object { "age_range": "[25,35)", @@ -1675,6 +1712,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1701,6 +1739,7 @@ Object { "username": "dragarcia", }, ], + "count": null, "data": Array [ Object { "age_range": "[20,30)", @@ -1734,6 +1773,7 @@ Object { "username": "kiwicopple", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1788,6 +1828,7 @@ Object { "username": "awailas", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1835,6 +1876,7 @@ Object { "username": "dragarcia", }, ], + "count": null, "data": Array [ Object { "age_range": "[20,30)", @@ -1868,6 +1910,7 @@ Object { "username": "kiwicopple", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1901,6 +1944,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -1941,6 +1985,7 @@ Object { "username": "dragarcia", }, ], + "count": null, "data": Array [ Object { "age_range": "[25,35)", @@ -1977,6 +2022,7 @@ Object { "status": "ONLINE", }, ], + "count": null, "data": Array [ Object { "status": "ONLINE", @@ -1995,6 +2041,7 @@ Object { "status": "ONLINE", }, ], + "count": null, "data": Array [ Object { "status": "ONLINE", @@ -2015,6 +2062,7 @@ Object { "status": "ONLINE", "username": "supabot", }, + "count": null, "data": Object { "age_range": "[1,2)", "catchphrase": "'cat' 'fat'", @@ -2037,6 +2085,7 @@ Object { "status": "ONLINE", "username": "foo", }, + "count": null, "data": Object { "age_range": null, "catchphrase": null, @@ -2061,6 +2110,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -2094,6 +2144,7 @@ Object { "username": "awailas", }, ], + "count": null, "data": Array [ Object { "age_range": "[25,35)", @@ -2119,6 +2170,7 @@ Object { exports[`stored procedure 1`] = ` Object { "body": "ONLINE", + "count": null, "data": "ONLINE", "error": null, "status": 200, @@ -2160,6 +2212,7 @@ Object { "username": "leroyjenkins", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", @@ -2209,6 +2262,7 @@ Object { "username": "supabot", }, ], + "count": null, "data": Array [ Object { "age_range": "[1,2)", From 49dd86fe04ce4fd2c65da32bfee5f34cf0692f3d Mon Sep 17 00:00:00 2001 From: Tyler <18113850+dshukertjr@users.noreply.github.com> Date: Mon, 11 Jan 2021 17:52:59 +0900 Subject: [PATCH 06/11] Set default values for head and count Co-authored-by: Bobbie Soedirgo --- src/lib/PostgrestQueryBuilder.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib/PostgrestQueryBuilder.ts b/src/lib/PostgrestQueryBuilder.ts index a98ebdf7..1fe01492 100644 --- a/src/lib/PostgrestQueryBuilder.ts +++ b/src/lib/PostgrestQueryBuilder.ts @@ -22,7 +22,13 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { */ select( columns = '*', - { head, count }: { head?: boolean; count?: 'exact' | 'planned' | 'estimated' } = {} + { + head = false, + count = null, + }: { + head?: boolean + count?: null | 'exact' | 'planned' | 'estimated' + } = {} ): PostgrestFilterBuilder { this.method = 'GET' // Remove whitespaces except when quoted From 32e6ce74da4bc7dc3de6ceb59f6051d4fc8a0de2 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Wed, 13 Jan 2021 22:20:05 +0900 Subject: [PATCH 07/11] Added basic test cases for select with head and count parameters --- test/__snapshots__/index.test.ts.snap | 113 ++++++++++++++++++++++++++ test/basic.ts | 25 ++++++ 2 files changed, 138 insertions(+) diff --git a/test/__snapshots__/index.test.ts.snap b/test/__snapshots__/index.test.ts.snap index 22d6365b..cc343556 100644 --- a/test/__snapshots__/index.test.ts.snap +++ b/test/__snapshots__/index.test.ts.snap @@ -2053,6 +2053,119 @@ Object { } `; +exports[`select with count:exact 1`] = ` +Object { + "body": 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", + }, + ], + "count": 4, + "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", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", +} +`; + +exports[`select with head:true 1`] = ` +Object { + "body": null, + "count": null, + "data": null, + "error": null, + "status": 200, + "statusText": "OK", +} +`; + +exports[`select with head:true, count:estimated 1`] = ` +Object { + "body": null, + "count": 510, + "data": null, + "error": null, + "status": 206, + "statusText": "Partial Content", +} +`; + +exports[`select with head:true, count:exact 1`] = ` +Object { + "body": null, + "count": 4, + "data": null, + "error": null, + "status": 200, + "statusText": "OK", +} +`; + +exports[`select with head:true, count:planned 1`] = ` +Object { + "body": null, + "count": 510, + "data": null, + "error": null, + "status": 206, + "statusText": "Partial Content", +} +`; + exports[`single 1`] = ` Object { "body": Object { diff --git a/test/basic.ts b/test/basic.ts index 6a81a982..3f7213a3 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -140,3 +140,28 @@ test('Prefer: return=minimal', async () => { await postgrest.from('users').delete().eq('username', 'bar') }) + +test('select with head:true', async () => { + const res = await postgrest.from('users').select('*', { head: true }) + expect(res).toMatchSnapshot() +}) + +test('select with head:true, count:exact', async () => { + const res = await postgrest.from('users').select('*', { head: true, count: 'exact' }) + expect(res).toMatchSnapshot() +}) + +test('select with head:true, count:planned', async () => { + const res = await postgrest.from('users').select('*', { head: true, count: 'planned' }) + expect(res).toMatchSnapshot() +}) + +test('select with head:true, count:estimated', async () => { + const res = await postgrest.from('users').select('*', { head: true, count: 'estimated' }) + expect(res).toMatchSnapshot() +}) + +test('select with count:exact', async () => { + const res = await postgrest.from('users').select('*', { count: 'exact' }) + expect(res).toMatchSnapshot() +}) From fe1f7c0793a0b08c6c68563edc5ecdaee89815ae Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Thu, 14 Jan 2021 08:00:07 +0900 Subject: [PATCH 08/11] Added count and head option to rpc, and added count option to insert, update, delete --- src/lib/PostgrestQueryBuilder.ts | 49 ++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/src/lib/PostgrestQueryBuilder.ts b/src/lib/PostgrestQueryBuilder.ts index 1fe01492..45ad6ed6 100644 --- a/src/lib/PostgrestQueryBuilder.ts +++ b/src/lib/PostgrestQueryBuilder.ts @@ -73,6 +73,11 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { upsert?: boolean onConflict?: string returning?: 'minimal' | 'representation' + } = {}, + { + count = null, + }: { + count?: null | 'exact' | 'planned' | 'estimated' } = {} ): PostgrestFilterBuilder { this.method = 'POST' @@ -84,6 +89,9 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { if (upsert && onConflict !== undefined) this.url.searchParams.set('on_conflict', onConflict) this.body = values + if (count) { + this.headers['Prefer'] = `count=${count}` + } return new PostgrestFilterBuilder(this) } @@ -95,11 +103,19 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { */ update( values: Partial, - { returning = 'representation' }: { returning?: 'minimal' | 'representation' } = {} + { returning = 'representation' }: { returning?: 'minimal' | 'representation' } = {}, + { + count = null, + }: { + count?: null | 'exact' | 'planned' | 'estimated' + } = {} ): PostgrestFilterBuilder { this.method = 'PATCH' this.headers['Prefer'] = `return=${returning}` this.body = values + if (count) { + this.headers['Prefer'] = `count=${count}` + } return new PostgrestFilterBuilder(this) } @@ -108,18 +124,41 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { * * @param returning If `true`, return the deleted row(s) in the response. */ - delete({ - returning = 'representation', - }: { returning?: 'minimal' | 'representation' } = {}): PostgrestFilterBuilder { + delete( + { returning = 'representation' }: { returning?: 'minimal' | 'representation' } = {}, + { + count = null, + }: { + count?: null | 'exact' | 'planned' | 'estimated' + } = {} + ): PostgrestFilterBuilder { this.method = 'DELETE' this.headers['Prefer'] = `return=${returning}` + if (count) { + this.headers['Prefer'] = `count=${count}` + } return new PostgrestFilterBuilder(this) } /** @internal */ - rpc(params?: object): PostgrestTransformBuilder { + rpc( + params?: object, + { + head = false, + count = null, + }: { + head?: boolean + count?: null | 'exact' | 'planned' | 'estimated' + } = {} + ): PostgrestTransformBuilder { this.method = 'POST' this.body = params + if (count) { + this.headers['Prefer'] = `count=${count}` + } + if (head) { + this.method = 'HEAD' + } return new PostgrestTransformBuilder(this) } } From a2598b7cdc2b32c48e35e1c240b7a14996abbb03 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Thu, 14 Jan 2021 08:41:52 +0900 Subject: [PATCH 09/11] Added count tests for insert, update, delete rpc --- src/lib/PostgrestQueryBuilder.ts | 40 +- test/__snapshots__/index.test.ts.snap | 592 ++++++++++++++++++++++++++ test/basic.ts | 58 +++ 3 files changed, 672 insertions(+), 18 deletions(-) diff --git a/src/lib/PostgrestQueryBuilder.ts b/src/lib/PostgrestQueryBuilder.ts index 45ad6ed6..43a601d1 100644 --- a/src/lib/PostgrestQueryBuilder.ts +++ b/src/lib/PostgrestQueryBuilder.ts @@ -69,14 +69,11 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { upsert = false, onConflict, returning = 'representation', + count = null, }: { upsert?: boolean onConflict?: string returning?: 'minimal' | 'representation' - } = {}, - { - count = null, - }: { count?: null | 'exact' | 'planned' | 'estimated' } = {} ): PostgrestFilterBuilder { @@ -90,7 +87,8 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { if (upsert && onConflict !== undefined) this.url.searchParams.set('on_conflict', onConflict) this.body = values if (count) { - this.headers['Prefer'] = `count=${count}` + prefersHeaders.push(`count=${count}`) + this.headers['Prefer'] = prefersHeaders.join(',') } return new PostgrestFilterBuilder(this) } @@ -103,18 +101,22 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { */ update( values: Partial, - { returning = 'representation' }: { returning?: 'minimal' | 'representation' } = {}, { + returning = 'representation', count = null, }: { + returning?: 'minimal' | 'representation' count?: null | 'exact' | 'planned' | 'estimated' } = {} ): PostgrestFilterBuilder { this.method = 'PATCH' - this.headers['Prefer'] = `return=${returning}` + let prefersHeaders = [] + prefersHeaders.push(`return=${returning}`) + this.headers['Prefer'] = prefersHeaders.join(',') this.body = values if (count) { - this.headers['Prefer'] = `count=${count}` + prefersHeaders.push(`count=${count}`) + this.headers['Prefer'] = prefersHeaders.join(',') } return new PostgrestFilterBuilder(this) } @@ -124,18 +126,20 @@ export default class PostgrestQueryBuilder extends PostgrestBuilder { * * @param returning If `true`, return the deleted row(s) in the response. */ - delete( - { returning = 'representation' }: { returning?: 'minimal' | 'representation' } = {}, - { - count = null, - }: { - count?: null | 'exact' | 'planned' | 'estimated' - } = {} - ): PostgrestFilterBuilder { + delete({ + returning = 'representation', + count = null, + }: { + returning?: 'minimal' | 'representation' + count?: null | 'exact' | 'planned' | 'estimated' + } = {}): PostgrestFilterBuilder { this.method = 'DELETE' - this.headers['Prefer'] = `return=${returning}` + let prefersHeaders = [] + prefersHeaders.push(`return=${returning}`) + this.headers['Prefer'] = prefersHeaders.join(',') if (count) { - this.headers['Prefer'] = `count=${count}` + prefersHeaders.push(`count=${count}`) + this.headers['Prefer'] = prefersHeaders.join(',') } return new PostgrestFilterBuilder(this) } diff --git a/test/__snapshots__/index.test.ts.snap b/test/__snapshots__/index.test.ts.snap index cc343556..e71a5557 100644 --- a/test/__snapshots__/index.test.ts.snap +++ b/test/__snapshots__/index.test.ts.snap @@ -1275,6 +1275,598 @@ Object { } `; +exports[`insert, update, delete with count: 'exact' basic delete 1`] = ` +Object { + "body": Array [ + Object { + "channel_id": 2, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 7, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 8, + "message": "foo", + "username": "supabot", + }, + ], + "count": null, + "data": Array [ + Object { + "channel_id": 2, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 7, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 8, + "message": "foo", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", +} +`; + +exports[`insert, update, delete with count: 'exact' basic delete 2`] = ` +Object { + "body": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + ], + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", +} +`; + +exports[`insert, update, delete with count: 'exact' bulk insert with count: 'exact' 1`] = ` +Object { + "body": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 7, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 8, + "message": "foo", + "username": "supabot", + }, + ], + "count": 2, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 7, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 8, + "message": "foo", + "username": "supabot", + }, + ], + "error": null, + "status": 201, + "statusText": "Created", +} +`; + +exports[`insert, update, delete with count: 'exact' bulk insert with count: 'exact' 2`] = ` +Object { + "body": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 7, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 8, + "message": "foo", + "username": "supabot", + }, + ], + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 7, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 8, + "message": "foo", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", +} +`; + +exports[`insert, update, delete with count: 'exact' insert with count: 'exact' 1`] = ` +Object { + "body": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + ], + "count": 1, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + ], + "error": null, + "status": 201, + "statusText": "Created", +} +`; + +exports[`insert, update, delete with count: 'exact' insert with count: 'exact' 2`] = ` +Object { + "body": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + ], + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", +} +`; + +exports[`insert, update, delete with count: 'exact' update with count: 'exact' 1`] = ` +Object { + "body": Array [ + Object { + "channel_id": 2, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 7, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 8, + "message": "foo", + "username": "supabot", + }, + ], + "count": 4, + "data": Array [ + Object { + "channel_id": 2, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 7, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 8, + "message": "foo", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", +} +`; + +exports[`insert, update, delete with count: 'exact' update with count: 'exact' 2`] = ` +Object { + "body": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 7, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 8, + "message": "foo", + "username": "supabot", + }, + ], + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 7, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 8, + "message": "foo", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", +} +`; + +exports[`insert, update, delete with count: 'exact' upsert with count: 'exact' 1`] = ` +Object { + "body": Array [ + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + ], + "count": 1, + "data": Array [ + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + ], + "error": null, + "status": 201, + "statusText": "Created", +} +`; + +exports[`insert, update, delete with count: 'exact' upsert with count: 'exact' 2`] = ` +Object { + "body": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + ], + "count": null, + "data": Array [ + Object { + "channel_id": 1, + "data": null, + "id": 1, + "message": "Hello World 👋", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 2, + "message": "Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.", + "username": "supabot", + }, + Object { + "channel_id": 1, + "data": null, + "id": 6, + "message": "foo", + "username": "supabot", + }, + Object { + "channel_id": 2, + "data": null, + "id": 3, + "message": "foo", + "username": "supabot", + }, + ], + "error": null, + "status": 200, + "statusText": "OK", +} +`; + exports[`is 1`] = ` Object { "body": Array [ diff --git a/test/basic.ts b/test/basic.ts index 3f7213a3..6b8131d5 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -165,3 +165,61 @@ test('select with count:exact', async () => { const res = await postgrest.from('users').select('*', { count: 'exact' }) expect(res).toMatchSnapshot() }) + +describe("insert, update, delete with count: 'exact'", () => { + test("insert with count: 'exact'", async () => { + let res = await postgrest + .from('messages') + .insert({ message: 'foo', username: 'supabot', channel_id: 1 }, { count: 'exact' }) + expect(res).toMatchSnapshot() + + res = await postgrest.from('messages').select() + expect(res).toMatchSnapshot() + }) + + test("upsert with count: 'exact'", async () => { + let res = await postgrest + .from('messages') + .insert( + { id: 3, message: 'foo', username: 'supabot', channel_id: 2 }, + { upsert: true, count: 'exact' } + ) + expect(res).toMatchSnapshot() + + res = await postgrest.from('messages').select() + expect(res).toMatchSnapshot() + }) + + test("bulk insert with count: 'exact'", async () => { + let res = await postgrest.from('messages').insert( + [ + { message: 'foo', username: 'supabot', channel_id: 1 }, + { message: 'foo', username: 'supabot', channel_id: 1 }, + ], + { count: 'exact' } + ) + expect(res).toMatchSnapshot() + + res = await postgrest.from('messages').select() + expect(res).toMatchSnapshot() + }) + + test("update with count: 'exact'", async () => { + let res = await postgrest + .from('messages') + .update({ channel_id: 2 }, { count: 'exact' }) + .eq('message', 'foo') + expect(res).toMatchSnapshot() + + res = await postgrest.from('messages').select() + expect(res).toMatchSnapshot() + }) + + test('basic delete', async () => { + let res = await postgrest.from('messages').delete().eq('message', 'foo') + expect(res).toMatchSnapshot() + + res = await postgrest.from('messages').select() + expect(res).toMatchSnapshot() + }) +}) From 6c8c50d9d76936281dec725d93669a6240e4f1c7 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Thu, 14 Jan 2021 19:30:53 +0900 Subject: [PATCH 10/11] Added head and count test for rpc --- test/__snapshots__/index.test.ts.snap | 38 ++++++++++++++++++++++++--- test/basic.ts | 18 +++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/test/__snapshots__/index.test.ts.snap b/test/__snapshots__/index.test.ts.snap index e71a5557..df01e3f2 100644 --- a/test/__snapshots__/index.test.ts.snap +++ b/test/__snapshots__/index.test.ts.snap @@ -1275,7 +1275,7 @@ Object { } `; -exports[`insert, update, delete with count: 'exact' basic delete 1`] = ` +exports[`insert, update, delete with count: 'exact' basic delete count: 'exact' 1`] = ` Object { "body": Array [ Object { @@ -1307,7 +1307,7 @@ Object { "username": "supabot", }, ], - "count": null, + "count": 4, "data": Array [ Object { "channel_id": 2, @@ -1344,7 +1344,7 @@ Object { } `; -exports[`insert, update, delete with count: 'exact' basic delete 2`] = ` +exports[`insert, update, delete with count: 'exact' basic delete count: 'exact' 2`] = ` Object { "body": Array [ Object { @@ -2883,6 +2883,38 @@ Object { } `; +exports[`stored procedure with count: 'exact' 1`] = ` +Object { + "body": null, + "count": null, + "data": null, + "error": Object { + "code": "42883", + "details": null, + "hint": "No function matches the given name and argument types. You might need to add explicit type casts.", + "message": "function public.get_status(count => text, name_param => text) does not exist", + }, + "status": 404, + "statusText": "Not Found", +} +`; + +exports[`stored procedure with count: 'exact', head: true 1`] = ` +Object { + "body": null, + "count": null, + "data": null, + "error": Object { + "code": "42883", + "details": null, + "hint": "No function matches the given name and argument types. You might need to add explicit type casts.", + "message": "function public.get_status(count => text, head => text, name_param => text) does not exist", + }, + "status": 404, + "statusText": "Not Found", +} +`; + exports[`switch schema 1`] = ` Object { "body": Array [ diff --git a/test/basic.ts b/test/basic.ts index 6b8131d5..eedaffc8 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -166,6 +166,20 @@ test('select with count:exact', async () => { expect(res).toMatchSnapshot() }) +test("stored procedure with count: 'exact'", async () => { + const res = await postgrest.rpc('get_status', { name_param: 'supabot', count: 'exact' }) + expect(res).toMatchSnapshot() +}) + +test("stored procedure with count: 'exact', head: true", async () => { + const res = await postgrest.rpc('get_status', { + name_param: 'supabot', + count: 'exact', + head: true, + }) + expect(res).toMatchSnapshot() +}) + describe("insert, update, delete with count: 'exact'", () => { test("insert with count: 'exact'", async () => { let res = await postgrest @@ -215,8 +229,8 @@ describe("insert, update, delete with count: 'exact'", () => { expect(res).toMatchSnapshot() }) - test('basic delete', async () => { - let res = await postgrest.from('messages').delete().eq('message', 'foo') + test("basic delete count: 'exact'", async () => { + let res = await postgrest.from('messages').delete({ count: 'exact' }).eq('message', 'foo') expect(res).toMatchSnapshot() res = await postgrest.from('messages').select() From 35d6768a25e66a76a4fc26128b1a1daca9cfa29e Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Fri, 15 Jan 2021 07:43:46 +0900 Subject: [PATCH 11/11] Updated test so that count value for 'planned' and 'estimated' would match any number --- test/__snapshots__/index.test.ts.snap | 4 ++-- test/basic.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/test/__snapshots__/index.test.ts.snap b/test/__snapshots__/index.test.ts.snap index df01e3f2..15c9c5b8 100644 --- a/test/__snapshots__/index.test.ts.snap +++ b/test/__snapshots__/index.test.ts.snap @@ -2728,7 +2728,7 @@ Object { exports[`select with head:true, count:estimated 1`] = ` Object { "body": null, - "count": 510, + "count": Any, "data": null, "error": null, "status": 206, @@ -2750,7 +2750,7 @@ Object { exports[`select with head:true, count:planned 1`] = ` Object { "body": null, - "count": 510, + "count": Any, "data": null, "error": null, "status": 206, diff --git a/test/basic.ts b/test/basic.ts index eedaffc8..c3aafdfd 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -153,12 +153,16 @@ test('select with head:true, count:exact', async () => { test('select with head:true, count:planned', async () => { const res = await postgrest.from('users').select('*', { head: true, count: 'planned' }) - expect(res).toMatchSnapshot() + expect(res).toMatchSnapshot({ + count: expect.any(Number), + }) }) test('select with head:true, count:estimated', async () => { const res = await postgrest.from('users').select('*', { head: true, count: 'estimated' }) - expect(res).toMatchSnapshot() + expect(res).toMatchSnapshot({ + count: expect.any(Number), + }) }) test('select with count:exact', async () => {