@supabase/postgrest-js 2.101.1 → 2.102.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +212 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +53 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +212 -71
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/PostgrestBuilder.ts +239 -84
- package/src/PostgrestClient.ts +14 -0
- package/src/PostgrestQueryBuilder.ts +25 -1
- package/src/types/common/common.ts +27 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/PostgrestBuilder.ts
CHANGED
|
@@ -5,10 +5,66 @@ import type {
|
|
|
5
5
|
MergePartialResult,
|
|
6
6
|
IsValidResultOverride,
|
|
7
7
|
} from './types/types'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
ClientServerOptions,
|
|
10
|
+
Fetch,
|
|
11
|
+
DEFAULT_MAX_RETRIES,
|
|
12
|
+
getRetryDelay,
|
|
13
|
+
RETRYABLE_STATUS_CODES,
|
|
14
|
+
RETRYABLE_METHODS,
|
|
15
|
+
} from './types/common/common'
|
|
9
16
|
import PostgrestError from './PostgrestError'
|
|
10
17
|
import { ContainsNull } from './select-query-parser/types'
|
|
11
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Sleep for a given number of milliseconds.
|
|
21
|
+
* If an AbortSignal is provided, the sleep resolves early when the signal is aborted.
|
|
22
|
+
*/
|
|
23
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
if (signal?.aborted) {
|
|
26
|
+
resolve()
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const id = setTimeout(() => {
|
|
30
|
+
signal?.removeEventListener('abort', onAbort)
|
|
31
|
+
resolve()
|
|
32
|
+
}, ms)
|
|
33
|
+
function onAbort() {
|
|
34
|
+
clearTimeout(id)
|
|
35
|
+
resolve()
|
|
36
|
+
}
|
|
37
|
+
signal?.addEventListener('abort', onAbort)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if a request should be retried based on method and status code.
|
|
43
|
+
*/
|
|
44
|
+
function shouldRetry(
|
|
45
|
+
method: string,
|
|
46
|
+
status: number,
|
|
47
|
+
attemptCount: number,
|
|
48
|
+
retryEnabled: boolean
|
|
49
|
+
): boolean {
|
|
50
|
+
// Don't retry if retries are disabled or we've exhausted attempts
|
|
51
|
+
if (!retryEnabled || attemptCount >= DEFAULT_MAX_RETRIES) {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Only retry idempotent methods (GET, HEAD, OPTIONS)
|
|
56
|
+
if (!RETRYABLE_METHODS.includes(method as (typeof RETRYABLE_METHODS)[number])) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Only retry on specific status codes (520 - Cloudflare errors)
|
|
61
|
+
if (!RETRYABLE_STATUS_CODES.includes(status as (typeof RETRYABLE_STATUS_CODES)[number])) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
12
68
|
export default abstract class PostgrestBuilder<
|
|
13
69
|
ClientOptions extends ClientServerOptions,
|
|
14
70
|
Result,
|
|
@@ -29,6 +85,9 @@ export default abstract class PostgrestBuilder<
|
|
|
29
85
|
protected isMaybeSingle: boolean
|
|
30
86
|
protected urlLengthLimit: number
|
|
31
87
|
|
|
88
|
+
// Retry configuration - enabled by default
|
|
89
|
+
protected retryEnabled: boolean = true
|
|
90
|
+
|
|
32
91
|
/**
|
|
33
92
|
* Creates a builder configured for a specific PostgREST request.
|
|
34
93
|
*
|
|
@@ -65,6 +124,8 @@ export default abstract class PostgrestBuilder<
|
|
|
65
124
|
fetch?: Fetch
|
|
66
125
|
isMaybeSingle?: boolean
|
|
67
126
|
urlLengthLimit?: number
|
|
127
|
+
// Retry option
|
|
128
|
+
retry?: boolean
|
|
68
129
|
}) {
|
|
69
130
|
this.method = builder.method
|
|
70
131
|
this.url = builder.url
|
|
@@ -75,6 +136,7 @@ export default abstract class PostgrestBuilder<
|
|
|
75
136
|
this.signal = builder.signal
|
|
76
137
|
this.isMaybeSingle = builder.isMaybeSingle ?? false
|
|
77
138
|
this.urlLengthLimit = builder.urlLengthLimit ?? 8000
|
|
139
|
+
this.retryEnabled = builder.retry ?? true
|
|
78
140
|
|
|
79
141
|
if (builder.fetch) {
|
|
80
142
|
this.fetch = builder.fetch
|
|
@@ -107,9 +169,31 @@ export default abstract class PostgrestBuilder<
|
|
|
107
169
|
return this
|
|
108
170
|
}
|
|
109
171
|
|
|
110
|
-
/**
|
|
172
|
+
/**
|
|
111
173
|
* @category Database
|
|
174
|
+
*
|
|
175
|
+
* Configure retry behavior for this request.
|
|
176
|
+
*
|
|
177
|
+
* By default, retries are enabled for idempotent requests (GET, HEAD, OPTIONS)
|
|
178
|
+
* that fail with network errors or specific HTTP status codes (503, 520).
|
|
179
|
+
* Retries use exponential backoff (1s, 2s, 4s) with a maximum of 3 attempts.
|
|
180
|
+
*
|
|
181
|
+
* @param enabled - Whether to enable retries for this request
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* // Disable retries for a specific query
|
|
186
|
+
* const { data, error } = await supabase
|
|
187
|
+
* .from('users')
|
|
188
|
+
* .select()
|
|
189
|
+
* .retry(false)
|
|
190
|
+
* ```
|
|
112
191
|
*/
|
|
192
|
+
retry(enabled: boolean): this {
|
|
193
|
+
this.retryEnabled = enabled
|
|
194
|
+
return this
|
|
195
|
+
}
|
|
196
|
+
|
|
113
197
|
then<
|
|
114
198
|
TResult1 = ThrowOnError extends true
|
|
115
199
|
? PostgrestResponseSuccess<Result>
|
|
@@ -141,101 +225,74 @@ export default abstract class PostgrestBuilder<
|
|
|
141
225
|
// NOTE: Invoke w/o `this` to avoid illegal invocation error.
|
|
142
226
|
// https://github.com/supabase/postgrest-js/pull/247
|
|
143
227
|
const _fetch = this.fetch
|
|
144
|
-
let res = _fetch(this.url.toString(), {
|
|
145
|
-
method: this.method,
|
|
146
|
-
headers: this.headers,
|
|
147
|
-
body: JSON.stringify(this.body),
|
|
148
|
-
signal: this.signal,
|
|
149
|
-
}).then(async (res) => {
|
|
150
|
-
let error = null
|
|
151
|
-
let data = null
|
|
152
|
-
let count: number | null = null
|
|
153
|
-
let status = res.status
|
|
154
|
-
let statusText = res.statusText
|
|
155
|
-
|
|
156
|
-
if (res.ok) {
|
|
157
|
-
if (this.method !== 'HEAD') {
|
|
158
|
-
const body = await res.text()
|
|
159
|
-
if (body === '') {
|
|
160
|
-
// Prefer: return=minimal
|
|
161
|
-
} else if (this.headers.get('Accept') === 'text/csv') {
|
|
162
|
-
data = body
|
|
163
|
-
} else if (
|
|
164
|
-
this.headers.get('Accept') &&
|
|
165
|
-
this.headers.get('Accept')?.includes('application/vnd.pgrst.plan+text')
|
|
166
|
-
) {
|
|
167
|
-
data = body
|
|
168
|
-
} else {
|
|
169
|
-
data = JSON.parse(body)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
228
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
229
|
+
// Execute fetch with retry logic
|
|
230
|
+
const executeWithRetry = async (): Promise<{
|
|
231
|
+
error: any
|
|
232
|
+
data: any
|
|
233
|
+
count: number | null
|
|
234
|
+
status: number
|
|
235
|
+
statusText: string
|
|
236
|
+
}> => {
|
|
237
|
+
let attemptCount = 0
|
|
178
238
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// https://github.com/PostgREST/postgrest/blob/a867d79c42419af16c18c3fb019eba8df992626f/src/PostgREST/Error.hs#L553
|
|
184
|
-
code: 'PGRST116',
|
|
185
|
-
details: `Results contain ${data.length} rows, application/vnd.pgrst.object+json requires 1 row`,
|
|
186
|
-
hint: null,
|
|
187
|
-
message: 'JSON object requested, multiple (or no) rows returned',
|
|
188
|
-
}
|
|
189
|
-
data = null
|
|
190
|
-
count = null
|
|
191
|
-
status = 406
|
|
192
|
-
statusText = 'Not Acceptable'
|
|
193
|
-
} else if (data.length === 1) {
|
|
194
|
-
data = data[0]
|
|
195
|
-
} else {
|
|
196
|
-
data = null
|
|
197
|
-
}
|
|
239
|
+
while (true) {
|
|
240
|
+
const requestHeaders = new Headers(this.headers)
|
|
241
|
+
if (attemptCount > 0) {
|
|
242
|
+
requestHeaders.set('X-Retry-Count', String(attemptCount))
|
|
198
243
|
}
|
|
199
|
-
} else {
|
|
200
|
-
const body = await res.text()
|
|
201
244
|
|
|
245
|
+
// Only wrap the fetch call itself — processResponse errors must never trigger retries
|
|
246
|
+
let res: Response
|
|
202
247
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
248
|
+
res = await _fetch(this.url.toString(), {
|
|
249
|
+
method: this.method,
|
|
250
|
+
headers: requestHeaders,
|
|
251
|
+
body: JSON.stringify(this.body),
|
|
252
|
+
signal: this.signal,
|
|
253
|
+
})
|
|
254
|
+
} catch (fetchError: any) {
|
|
255
|
+
// Never retry aborted requests
|
|
256
|
+
if (fetchError?.name === 'AbortError' || fetchError?.code === 'ABORT_ERR') {
|
|
257
|
+
throw fetchError
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Don't retry network errors for non-idempotent methods
|
|
261
|
+
if (!RETRYABLE_METHODS.includes(this.method as (typeof RETRYABLE_METHODS)[number])) {
|
|
262
|
+
throw fetchError
|
|
211
263
|
}
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
message: body,
|
|
220
|
-
}
|
|
264
|
+
|
|
265
|
+
// Check if we should retry network errors
|
|
266
|
+
if (this.retryEnabled && attemptCount < DEFAULT_MAX_RETRIES) {
|
|
267
|
+
const delay = getRetryDelay(attemptCount)
|
|
268
|
+
attemptCount++
|
|
269
|
+
await sleep(delay, this.signal)
|
|
270
|
+
continue
|
|
221
271
|
}
|
|
272
|
+
|
|
273
|
+
// Exhausted retries or retries disabled, throw the last error
|
|
274
|
+
throw fetchError
|
|
222
275
|
}
|
|
223
276
|
|
|
224
|
-
if
|
|
225
|
-
|
|
277
|
+
// Check if we should retry this HTTP response
|
|
278
|
+
if (shouldRetry(this.method, res.status, attemptCount, this.retryEnabled)) {
|
|
279
|
+
const retryAfterHeader = res.headers?.get('Retry-After') ?? null
|
|
280
|
+
const delay =
|
|
281
|
+
retryAfterHeader !== null
|
|
282
|
+
? Math.max(0, parseInt(retryAfterHeader, 10) || 0) * 1000
|
|
283
|
+
: getRetryDelay(attemptCount)
|
|
284
|
+
await res.text()
|
|
285
|
+
attemptCount++
|
|
286
|
+
await sleep(delay, this.signal)
|
|
287
|
+
continue
|
|
226
288
|
}
|
|
227
|
-
}
|
|
228
289
|
|
|
229
|
-
|
|
230
|
-
error,
|
|
231
|
-
data,
|
|
232
|
-
count,
|
|
233
|
-
status,
|
|
234
|
-
statusText,
|
|
290
|
+
return await this.processResponse(res)
|
|
235
291
|
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let res = executeWithRetry()
|
|
236
295
|
|
|
237
|
-
return postgrestResponse
|
|
238
|
-
})
|
|
239
296
|
if (!this.shouldThrowOnError) {
|
|
240
297
|
res = res.catch((fetchError) => {
|
|
241
298
|
// Build detailed error information including cause if available
|
|
@@ -307,6 +364,104 @@ export default abstract class PostgrestBuilder<
|
|
|
307
364
|
return res.then(onfulfilled, onrejected)
|
|
308
365
|
}
|
|
309
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Process a fetch response and return the standardized postgrest response.
|
|
369
|
+
*/
|
|
370
|
+
private async processResponse(res: Response): Promise<{
|
|
371
|
+
error: any
|
|
372
|
+
data: any
|
|
373
|
+
count: number | null
|
|
374
|
+
status: number
|
|
375
|
+
statusText: string
|
|
376
|
+
}> {
|
|
377
|
+
let error = null
|
|
378
|
+
let data = null
|
|
379
|
+
let count: number | null = null
|
|
380
|
+
let status = res.status
|
|
381
|
+
let statusText = res.statusText
|
|
382
|
+
|
|
383
|
+
if (res.ok) {
|
|
384
|
+
if (this.method !== 'HEAD') {
|
|
385
|
+
const body = await res.text()
|
|
386
|
+
if (body === '') {
|
|
387
|
+
// Prefer: return=minimal
|
|
388
|
+
} else if (this.headers.get('Accept') === 'text/csv') {
|
|
389
|
+
data = body
|
|
390
|
+
} else if (
|
|
391
|
+
this.headers.get('Accept') &&
|
|
392
|
+
this.headers.get('Accept')?.includes('application/vnd.pgrst.plan+text')
|
|
393
|
+
) {
|
|
394
|
+
data = body
|
|
395
|
+
} else {
|
|
396
|
+
data = JSON.parse(body)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const countHeader = this.headers.get('Prefer')?.match(/count=(exact|planned|estimated)/)
|
|
401
|
+
const contentRange = res.headers.get('content-range')?.split('/')
|
|
402
|
+
if (countHeader && contentRange && contentRange.length > 1) {
|
|
403
|
+
count = parseInt(contentRange[1])
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Fix for https://github.com/supabase/postgrest-js/issues/361 — applies to all methods.
|
|
407
|
+
if (this.isMaybeSingle && Array.isArray(data)) {
|
|
408
|
+
if (data.length > 1) {
|
|
409
|
+
error = {
|
|
410
|
+
// https://github.com/PostgREST/postgrest/blob/a867d79c42419af16c18c3fb019eba8df992626f/src/PostgREST/Error.hs#L553
|
|
411
|
+
code: 'PGRST116',
|
|
412
|
+
details: `Results contain ${data.length} rows, application/vnd.pgrst.object+json requires 1 row`,
|
|
413
|
+
hint: null,
|
|
414
|
+
message: 'JSON object requested, multiple (or no) rows returned',
|
|
415
|
+
}
|
|
416
|
+
data = null
|
|
417
|
+
count = null
|
|
418
|
+
status = 406
|
|
419
|
+
statusText = 'Not Acceptable'
|
|
420
|
+
} else if (data.length === 1) {
|
|
421
|
+
data = data[0]
|
|
422
|
+
} else {
|
|
423
|
+
data = null
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
const body = await res.text()
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
error = JSON.parse(body)
|
|
431
|
+
|
|
432
|
+
// Workaround for https://github.com/supabase/postgrest-js/issues/295
|
|
433
|
+
if (Array.isArray(error) && res.status === 404) {
|
|
434
|
+
data = []
|
|
435
|
+
error = null
|
|
436
|
+
status = 200
|
|
437
|
+
statusText = 'OK'
|
|
438
|
+
}
|
|
439
|
+
} catch {
|
|
440
|
+
// Workaround for https://github.com/supabase/postgrest-js/issues/295
|
|
441
|
+
if (res.status === 404 && body === '') {
|
|
442
|
+
status = 204
|
|
443
|
+
statusText = 'No Content'
|
|
444
|
+
} else {
|
|
445
|
+
error = {
|
|
446
|
+
message: body,
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (error && this.shouldThrowOnError) {
|
|
452
|
+
throw new PostgrestError(error)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
error,
|
|
458
|
+
data,
|
|
459
|
+
count,
|
|
460
|
+
status,
|
|
461
|
+
statusText,
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
310
465
|
/**
|
|
311
466
|
* Override the type of the returned `data`.
|
|
312
467
|
*
|
package/src/PostgrestClient.ts
CHANGED
|
@@ -40,6 +40,9 @@ export default class PostgrestClient<
|
|
|
40
40
|
fetch?: Fetch
|
|
41
41
|
urlLengthLimit: number
|
|
42
42
|
|
|
43
|
+
// Retry configuration - enabled by default
|
|
44
|
+
retry?: boolean
|
|
45
|
+
|
|
43
46
|
// TODO: Add back shouldThrowOnError once we figure out the typings
|
|
44
47
|
/**
|
|
45
48
|
* Creates a PostgREST client.
|
|
@@ -51,6 +54,10 @@ export default class PostgrestClient<
|
|
|
51
54
|
* @param options.fetch - Custom fetch
|
|
52
55
|
* @param options.timeout - Optional timeout in milliseconds for all requests. When set, requests will automatically abort after this duration to prevent indefinite hangs.
|
|
53
56
|
* @param options.urlLengthLimit - Maximum URL length in characters before warnings/errors are triggered. Defaults to 8000.
|
|
57
|
+
* @param options.retry - Enable or disable automatic retries for transient errors.
|
|
58
|
+
* When enabled, idempotent requests (GET, HEAD, OPTIONS) that fail with network
|
|
59
|
+
* errors or HTTP 503/520 responses will be automatically retried up to 3 times
|
|
60
|
+
* with exponential backoff (1s, 2s, 4s). Defaults to `true`.
|
|
54
61
|
* @example
|
|
55
62
|
* ```ts
|
|
56
63
|
* import { PostgrestClient } from '@supabase/postgrest-js'
|
|
@@ -86,6 +93,7 @@ export default class PostgrestClient<
|
|
|
86
93
|
* headers: { apikey: 'public-anon-key' },
|
|
87
94
|
* schema: 'public',
|
|
88
95
|
* timeout: 30000, // 30 second timeout
|
|
96
|
+
* retry: false, // Disable automatic retries
|
|
89
97
|
* })
|
|
90
98
|
* ```
|
|
91
99
|
*/
|
|
@@ -97,12 +105,14 @@ export default class PostgrestClient<
|
|
|
97
105
|
fetch,
|
|
98
106
|
timeout,
|
|
99
107
|
urlLengthLimit = 8000,
|
|
108
|
+
retry,
|
|
100
109
|
}: {
|
|
101
110
|
headers?: HeadersInit
|
|
102
111
|
schema?: SchemaName
|
|
103
112
|
fetch?: Fetch
|
|
104
113
|
timeout?: number
|
|
105
114
|
urlLengthLimit?: number
|
|
115
|
+
retry?: boolean
|
|
106
116
|
} = {}
|
|
107
117
|
) {
|
|
108
118
|
this.url = url
|
|
@@ -151,6 +161,7 @@ export default class PostgrestClient<
|
|
|
151
161
|
} else {
|
|
152
162
|
this.fetch = originalFetch
|
|
153
163
|
}
|
|
164
|
+
this.retry = retry
|
|
154
165
|
}
|
|
155
166
|
from<
|
|
156
167
|
TableName extends string & keyof Schema['Tables'],
|
|
@@ -179,6 +190,7 @@ export default class PostgrestClient<
|
|
|
179
190
|
schema: this.schemaName,
|
|
180
191
|
fetch: this.fetch,
|
|
181
192
|
urlLengthLimit: this.urlLengthLimit,
|
|
193
|
+
retry: this.retry,
|
|
182
194
|
})
|
|
183
195
|
}
|
|
184
196
|
|
|
@@ -204,6 +216,7 @@ export default class PostgrestClient<
|
|
|
204
216
|
schema,
|
|
205
217
|
fetch: this.fetch,
|
|
206
218
|
urlLengthLimit: this.urlLengthLimit,
|
|
219
|
+
retry: this.retry,
|
|
207
220
|
})
|
|
208
221
|
}
|
|
209
222
|
|
|
@@ -442,6 +455,7 @@ export default class PostgrestClient<
|
|
|
442
455
|
body,
|
|
443
456
|
fetch: this.fetch ?? fetch,
|
|
444
457
|
urlLengthLimit: this.urlLengthLimit,
|
|
458
|
+
retry: this.retry,
|
|
445
459
|
})
|
|
446
460
|
}
|
|
447
461
|
}
|
|
@@ -22,18 +22,34 @@ export default class PostgrestQueryBuilder<
|
|
|
22
22
|
fetch?: Fetch
|
|
23
23
|
urlLengthLimit: number
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Enable or disable automatic retries for transient errors.
|
|
27
|
+
* When enabled, idempotent requests (GET/HEAD/OPTIONS) that fail with network
|
|
28
|
+
* errors or HTTP 503/520 responses are automatically retried with exponential
|
|
29
|
+
* backoff (1s, 2s, 4s, up to 3 attempts). Defaults to `true` when not specified.
|
|
30
|
+
*/
|
|
31
|
+
retry?: boolean
|
|
32
|
+
|
|
25
33
|
/**
|
|
26
34
|
* Creates a query builder scoped to a Postgres table or view.
|
|
27
35
|
*
|
|
28
36
|
* @category Database
|
|
29
37
|
*
|
|
38
|
+
* @param url - The URL for the query
|
|
39
|
+
* @param options - Named parameters
|
|
40
|
+
* @param options.headers - Custom headers
|
|
41
|
+
* @param options.schema - Postgres schema to use
|
|
42
|
+
* @param options.fetch - Custom fetch implementation
|
|
43
|
+
* @param options.urlLengthLimit - Maximum URL length before warning
|
|
44
|
+
* @param options.retry - Enable automatic retries for transient errors (default: true)
|
|
45
|
+
*
|
|
30
46
|
* @example Creating a Postgrest query builder
|
|
31
47
|
* ```ts
|
|
32
48
|
* import { PostgrestQueryBuilder } from '@supabase/postgrest-js'
|
|
33
49
|
*
|
|
34
50
|
* const query = new PostgrestQueryBuilder(
|
|
35
51
|
* new URL('https://xyzcompany.supabase.co/rest/v1/users'),
|
|
36
|
-
* { headers: { apikey: 'public-anon-key' } }
|
|
52
|
+
* { headers: { apikey: 'public-anon-key' }, retry: true }
|
|
37
53
|
* )
|
|
38
54
|
* ```
|
|
39
55
|
*/
|
|
@@ -44,11 +60,13 @@ export default class PostgrestQueryBuilder<
|
|
|
44
60
|
schema,
|
|
45
61
|
fetch,
|
|
46
62
|
urlLengthLimit = 8000,
|
|
63
|
+
retry,
|
|
47
64
|
}: {
|
|
48
65
|
headers?: HeadersInit
|
|
49
66
|
schema?: string
|
|
50
67
|
fetch?: Fetch
|
|
51
68
|
urlLengthLimit?: number
|
|
69
|
+
retry?: boolean
|
|
52
70
|
}
|
|
53
71
|
) {
|
|
54
72
|
this.url = url
|
|
@@ -56,6 +74,7 @@ export default class PostgrestQueryBuilder<
|
|
|
56
74
|
this.schema = schema
|
|
57
75
|
this.fetch = fetch
|
|
58
76
|
this.urlLengthLimit = urlLengthLimit
|
|
77
|
+
this.retry = retry
|
|
59
78
|
}
|
|
60
79
|
|
|
61
80
|
/**
|
|
@@ -904,6 +923,7 @@ export default class PostgrestQueryBuilder<
|
|
|
904
923
|
schema: this.schema,
|
|
905
924
|
fetch: this.fetch,
|
|
906
925
|
urlLengthLimit: this.urlLengthLimit,
|
|
926
|
+
retry: this.retry,
|
|
907
927
|
})
|
|
908
928
|
}
|
|
909
929
|
|
|
@@ -1092,6 +1112,7 @@ export default class PostgrestQueryBuilder<
|
|
|
1092
1112
|
body: values,
|
|
1093
1113
|
fetch: this.fetch ?? fetch,
|
|
1094
1114
|
urlLengthLimit: this.urlLengthLimit,
|
|
1115
|
+
retry: this.retry,
|
|
1095
1116
|
})
|
|
1096
1117
|
}
|
|
1097
1118
|
|
|
@@ -1389,6 +1410,7 @@ export default class PostgrestQueryBuilder<
|
|
|
1389
1410
|
body: values,
|
|
1390
1411
|
fetch: this.fetch ?? fetch,
|
|
1391
1412
|
urlLengthLimit: this.urlLengthLimit,
|
|
1413
|
+
retry: this.retry,
|
|
1392
1414
|
})
|
|
1393
1415
|
}
|
|
1394
1416
|
|
|
@@ -1562,6 +1584,7 @@ export default class PostgrestQueryBuilder<
|
|
|
1562
1584
|
body: values,
|
|
1563
1585
|
fetch: this.fetch ?? fetch,
|
|
1564
1586
|
urlLengthLimit: this.urlLengthLimit,
|
|
1587
|
+
retry: this.retry,
|
|
1565
1588
|
})
|
|
1566
1589
|
}
|
|
1567
1590
|
|
|
@@ -1710,6 +1733,7 @@ export default class PostgrestQueryBuilder<
|
|
|
1710
1733
|
schema: this.schema,
|
|
1711
1734
|
fetch: this.fetch ?? fetch,
|
|
1712
1735
|
urlLengthLimit: this.urlLengthLimit,
|
|
1736
|
+
retry: this.retry,
|
|
1713
1737
|
})
|
|
1714
1738
|
}
|
|
1715
1739
|
}
|
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
export type Fetch = typeof fetch
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Default number of retry attempts.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_MAX_RETRIES = 3
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default exponential backoff delay function.
|
|
12
|
+
* Delays: 1s, 2s, 4s, 8s, ... (max 30s)
|
|
13
|
+
*
|
|
14
|
+
* @param attemptIndex - Zero-based index of the retry attempt
|
|
15
|
+
* @returns Delay in milliseconds before the next retry
|
|
16
|
+
*/
|
|
17
|
+
export const getRetryDelay = (attemptIndex: number): number =>
|
|
18
|
+
Math.min(1000 * 2 ** attemptIndex, 30000)
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Status codes that are safe to retry.
|
|
22
|
+
* 520 = Cloudflare timeout/connection errors (transient)
|
|
23
|
+
* 503 = PostgREST schema cache not yet loaded (transient, signals retry via Retry-After header)
|
|
24
|
+
*/
|
|
25
|
+
export const RETRYABLE_STATUS_CODES = [520, 503] as const
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* HTTP methods that are safe to retry (idempotent operations).
|
|
29
|
+
*/
|
|
30
|
+
export const RETRYABLE_METHODS = ['GET', 'HEAD', 'OPTIONS'] as const
|
|
31
|
+
|
|
5
32
|
export type GenericRelationship = {
|
|
6
33
|
foreignKeyName: string
|
|
7
34
|
columns: string[]
|
package/src/version.ts
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
// - Debugging and support (identifying which version is running)
|
|
5
5
|
// - Telemetry and logging (version reporting in errors/analytics)
|
|
6
6
|
// - Ensuring build artifacts match the published package version
|
|
7
|
-
export const version = '2.
|
|
7
|
+
export const version = '2.102.0-canary.0'
|