neon-testing 1.0.1 โ 1.1.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/README.md +112 -27
- package/index.ts +150 -15
- package/package.json +17 -15
package/README.md
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
# Neon testing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/starmode-base/neon-testing/actions/workflows/test.yml)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A [Vitest](https://vitest.dev/) utility for seamless integration tests with [Neon Postgres](https://neon.com/).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Each test file runs against its own isolated PostgreSQL database (Neon branch), ensuring clean, parallel, and reproducible testing of code that interacts with a database. Because it uses a real, isolated clone of your production database, you can test code logic that depends on database features, such as transaction rollbacks, unique constraints, and more.
|
|
8
|
+
|
|
9
|
+
**Testing against a clone of your production database lets you verify functionality that mocks cannot.**
|
|
8
10
|
|
|
9
11
|
## Features
|
|
10
12
|
|
|
11
13
|
- ๐ **Isolated test environments** - Each test file runs against its own Postgres database with your actual schema and constraints
|
|
12
14
|
- ๐งน **Automatic cleanup** - Neon test branches are created and destroyed automatically
|
|
15
|
+
- ๐ **Debug friendly** - Option to preserve test branches for debugging failed tests
|
|
13
16
|
- ๐ก๏ธ **TypeScript native** - No JavaScript support
|
|
14
17
|
- ๐ฏ **ESM only** - No CommonJS support
|
|
15
18
|
|
|
@@ -20,29 +23,42 @@ Using an actual clone of your production database for integration testing lets y
|
|
|
20
23
|
1. **Test execution**: Your tests run against the isolated database
|
|
21
24
|
1. **Cleanup**: After tests complete, the branch is automatically deleted
|
|
22
25
|
|
|
26
|
+
### Test isolation
|
|
27
|
+
|
|
28
|
+
Tests within a test file share the same database instance (Neon branch), so while all test files are isolated, tests within a test file are intentionally not.
|
|
29
|
+
|
|
30
|
+
This works because Vitest runs test files in parallel, but tests within each test file run sequentially one at a time.
|
|
31
|
+
|
|
32
|
+
If you prefer individual tests within a test file to be isolated, [simply clean up the database in a beforeEach lifecycle](examples/isolated.test.ts).
|
|
33
|
+
|
|
23
34
|
## Quick start
|
|
24
35
|
|
|
25
36
|
### Prerequisites
|
|
26
37
|
|
|
27
38
|
- A [Neon project](https://console.neon.tech/app/projects) with a database
|
|
28
|
-
- A [Neon API key](https://neon.
|
|
39
|
+
- A [Neon API key](https://neon.com/docs/manage/api-keys) for programmatic access
|
|
29
40
|
|
|
30
41
|
### Install
|
|
31
42
|
|
|
32
|
-
```
|
|
43
|
+
```sh
|
|
33
44
|
bun add -d neon-testing
|
|
34
45
|
```
|
|
35
46
|
|
|
36
47
|
### Minimal example
|
|
37
48
|
|
|
38
49
|
```typescript
|
|
39
|
-
//
|
|
50
|
+
// minimal.test.ts
|
|
40
51
|
import { expect, test } from "vitest";
|
|
41
52
|
import { makeNeonTesting } from "neon-testing";
|
|
42
53
|
import { Pool } from "@neondatabase/serverless";
|
|
43
54
|
|
|
44
55
|
// Enable Neon test branch for this test file
|
|
45
|
-
makeNeonTesting({
|
|
56
|
+
makeNeonTesting({
|
|
57
|
+
apiKey: "apiKey",
|
|
58
|
+
projectId: "projectId",
|
|
59
|
+
// Recommended for Neon WebSocket drivers to automatically close connections
|
|
60
|
+
autoCloseWebSockets: true,
|
|
61
|
+
})();
|
|
46
62
|
|
|
47
63
|
test("database operations", async () => {
|
|
48
64
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
@@ -52,8 +68,6 @@ test("database operations", async () => {
|
|
|
52
68
|
|
|
53
69
|
const users = await pool.query(`SELECT * FROM users`);
|
|
54
70
|
expect(users.rows).toStrictEqual([{ id: 1, name: "Ellen Ripley" }]);
|
|
55
|
-
|
|
56
|
-
await pool.end();
|
|
57
71
|
});
|
|
58
72
|
```
|
|
59
73
|
|
|
@@ -74,20 +88,21 @@ export const withNeonTestBranch = makeNeonTesting({
|
|
|
74
88
|
});
|
|
75
89
|
```
|
|
76
90
|
|
|
77
|
-
See all available options in [NeonTestingOptions](https://github.com/starmode-base/neon-testing/blob/main/index.ts#L30-L41).
|
|
78
|
-
|
|
79
91
|
#### 2. Enable database testing
|
|
80
92
|
|
|
81
93
|
Then call the exported test lifecycle function in the test files where you need database access.
|
|
82
94
|
|
|
83
95
|
```typescript
|
|
84
|
-
//
|
|
96
|
+
// recommended.test.ts
|
|
85
97
|
import { expect, test } from "vitest";
|
|
86
98
|
import { withNeonTestBranch } from "./test-setup";
|
|
87
99
|
import { Pool } from "@neondatabase/serverless";
|
|
88
100
|
|
|
89
101
|
// Enable Neon test branch for this test file
|
|
90
|
-
withNeonTestBranch(
|
|
102
|
+
withNeonTestBranch({
|
|
103
|
+
// Recommended for Neon WebSocket drivers to automatically close connections
|
|
104
|
+
autoCloseWebSockets: true,
|
|
105
|
+
});
|
|
91
106
|
|
|
92
107
|
test("database operations", async () => {
|
|
93
108
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
@@ -97,14 +112,51 @@ test("database operations", async () => {
|
|
|
97
112
|
|
|
98
113
|
const users = await pool.query(`SELECT * FROM users`);
|
|
99
114
|
expect(users.rows).toStrictEqual([{ id: 1, name: "Ellen Ripley" }]);
|
|
115
|
+
});
|
|
116
|
+
```
|
|
100
117
|
|
|
101
|
-
|
|
118
|
+
## Drivers
|
|
119
|
+
|
|
120
|
+
This library works with any database driver that supports Neon Postgres and Vitest. The examples below demonstrate connection management, transaction support, and test isolation patterns for some popular drivers.
|
|
121
|
+
|
|
122
|
+
**IMPORTANT:** For [Neon WebSocket drivers](https://neon.com/docs/serverless/serverless-driver), enable `autoCloseWebSockets` in your `makeNeonTesting()` or `withNeonTestBranch()` configuration. This automatically closes WebSocket connections when deleting test branches, preventing connection termination errors.
|
|
123
|
+
|
|
124
|
+
### Examples
|
|
125
|
+
|
|
126
|
+
- [Neon serverless WebSocket](examples/drivers/ws-neon.test.ts)
|
|
127
|
+
- [Neon serverless WebSocket + Drizzle](examples/drivers/ws-neon-drizzle.test.ts)
|
|
128
|
+
- [Neon serverless HTTP](examples/drivers/http-neon.test.ts)
|
|
129
|
+
- [Neon serverless HTTP + Drizzle](examples/drivers/http-neon-drizzle.test.ts)
|
|
130
|
+
- [node-postgres](examples/drivers/tcp-pg.test.ts)
|
|
131
|
+
- [node-postgres + Drizzle](examples/drivers/tcp-pg-drizzle.test.ts)
|
|
132
|
+
- [Postgres.js](examples/drivers/tcp-postgres.test.ts)
|
|
133
|
+
- [Postgres.js + Drizzle](examples/drivers/tcp-postgres-drizzle.test.ts)
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
You configure neon-testing in two places:
|
|
138
|
+
|
|
139
|
+
- **Base settings** in `makeNeonTesting()`
|
|
140
|
+
- **Optional overrides** in `withNeonTestBranch()`
|
|
141
|
+
|
|
142
|
+
See all available options in [NeonTestingOptions](https://github.com/starmode-base/neon-testing/blob/main/index.ts#L33-L75).
|
|
143
|
+
|
|
144
|
+
### Base configuration
|
|
145
|
+
|
|
146
|
+
Configure the base settings in `makeNeonTesting()`:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { makeNeonTesting } from "neon-testing";
|
|
150
|
+
|
|
151
|
+
export const withNeonTestBranch = makeNeonTesting({
|
|
152
|
+
apiKey: "apiKey",
|
|
153
|
+
projectId: "projectId",
|
|
102
154
|
});
|
|
103
155
|
```
|
|
104
156
|
|
|
105
|
-
|
|
157
|
+
### Override configuration
|
|
106
158
|
|
|
107
|
-
|
|
159
|
+
Override the base configuration in specific test files with `withNeonTestBranch()`:
|
|
108
160
|
|
|
109
161
|
```typescript
|
|
110
162
|
import { withNeonTestBranch } from "./test-setup";
|
|
@@ -112,34 +164,67 @@ import { withNeonTestBranch } from "./test-setup";
|
|
|
112
164
|
withNeonTestBranch({ parentBranchId: "br-staging-123" });
|
|
113
165
|
```
|
|
114
166
|
|
|
115
|
-
|
|
167
|
+
## Utilities
|
|
168
|
+
|
|
169
|
+
### deleteAllTestBranches()
|
|
170
|
+
|
|
171
|
+
The `deleteAllTestBranches()` function is a utility that deletes all test branches from your Neon project. This is useful for cleanup when tests fail unexpectedly and leave orphaned test branches.
|
|
116
172
|
|
|
117
173
|
```typescript
|
|
118
174
|
import { withNeonTestBranch } from "./test-setup";
|
|
119
175
|
|
|
120
|
-
|
|
176
|
+
// Access the cleanup utility
|
|
177
|
+
await withNeonTestBranch.deleteAllTestBranches();
|
|
121
178
|
```
|
|
122
179
|
|
|
123
|
-
|
|
180
|
+
The function identifies test branches by looking for the `integration-test: true` annotation that neon-testing automatically adds to all test branches it creates.
|
|
181
|
+
|
|
182
|
+
### lazySingleton()
|
|
124
183
|
|
|
125
|
-
|
|
184
|
+
The `lazySingleton()` function creates a lazy singleton from a factory function. This is useful for managing database connections efficiently:
|
|
126
185
|
|
|
127
|
-
|
|
186
|
+
```typescript
|
|
187
|
+
import { lazySingleton } from "neon-testing";
|
|
188
|
+
import { neon } from "@neondatabase/serverless";
|
|
128
189
|
|
|
129
|
-
|
|
190
|
+
const sql = lazySingleton(() => neon(process.env.DATABASE_URL!));
|
|
191
|
+
|
|
192
|
+
// The connection is only created when first called
|
|
193
|
+
test("database operations", async () => {
|
|
194
|
+
const users = await sql()`SELECT * FROM users`;
|
|
195
|
+
// ...
|
|
196
|
+
});
|
|
197
|
+
```
|
|
130
198
|
|
|
131
199
|
## Contributing
|
|
132
200
|
|
|
133
201
|
Contributions are welcome! Please open issues or pull requests on [GitHub](https://github.com/starmode-base/neon-testing/pulls).
|
|
134
202
|
|
|
203
|
+
### Environment
|
|
204
|
+
|
|
205
|
+
To run tests locally, create an `.env` file in the project root with these keys:
|
|
206
|
+
|
|
207
|
+
- `NEON_API_KEY="***"`
|
|
208
|
+
- `NEON_PROJECT_ID="***"`
|
|
209
|
+
|
|
210
|
+
Create a free Neon project at [neon.com](https://neon.com/) to test with.
|
|
211
|
+
|
|
212
|
+
### Release
|
|
213
|
+
|
|
214
|
+
To make a new release, run:
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
bun run release
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The command will abort if there are uncommitted changes in the working tree, or if the `version` in [package.json](package.json) has not been incremented.
|
|
221
|
+
|
|
135
222
|
## License
|
|
136
223
|
|
|
137
224
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
138
225
|
|
|
139
|
-
## Need
|
|
140
|
-
|
|
141
|
-
I take on a few consulting projects each year where I can build, unblock, and ship.
|
|
226
|
+
## Need help?
|
|
142
227
|
|
|
143
|
-
|
|
228
|
+
Hi, Iโm [Mikael Lirbank](https://www.lirbank.com/). I help teams build reliable AI systems. I care about qualityโAI evals, robust test suites, clean data models, and clean architecture. Sometimes I draw user interfaces.
|
|
144
229
|
|
|
145
|
-
|
|
230
|
+
Want to ship faster without breaking things? Letโs talk.
|
package/index.ts
CHANGED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
type ConnectionDetails,
|
|
8
8
|
} from "@neondatabase/api-client";
|
|
9
9
|
import { afterAll, beforeAll } from "vitest";
|
|
10
|
+
import { neonConfig } from "@neondatabase/serverless";
|
|
11
|
+
|
|
12
|
+
export { lazySingleton } from "./singleton";
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Creates a PostgreSQL connection URI from connection parameters
|
|
@@ -17,7 +20,7 @@ import { afterAll, beforeAll } from "vitest";
|
|
|
17
20
|
*/
|
|
18
21
|
function createConnectionUri(
|
|
19
22
|
connectionParameters: ConnectionDetails,
|
|
20
|
-
type: "pooler" | "direct"
|
|
23
|
+
type: "pooler" | "direct",
|
|
21
24
|
) {
|
|
22
25
|
const { role, password, host, pooler_host, database } =
|
|
23
26
|
connectionParameters.connection_parameters;
|
|
@@ -28,16 +31,47 @@ function createConnectionUri(
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export interface NeonTestingOptions {
|
|
31
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The Neon API key, this is used to create and teardown test branches
|
|
36
|
+
*
|
|
37
|
+
* https://neon.com/docs/manage/api-keys#creating-api-keys
|
|
38
|
+
*/
|
|
32
39
|
apiKey: string;
|
|
33
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* The Neon project ID to operate on
|
|
42
|
+
*
|
|
43
|
+
* https://console.neon.tech/app/projects
|
|
44
|
+
*/
|
|
34
45
|
projectId: string;
|
|
35
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* The parent branch ID for the new branch. If omitted or empty, the branch
|
|
48
|
+
* will be created from the project's default branch.
|
|
49
|
+
*/
|
|
36
50
|
parentBranchId?: string;
|
|
37
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Whether to create a schema-only branch (default: false)
|
|
53
|
+
*/
|
|
38
54
|
schemaOnly?: boolean;
|
|
39
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* The type of connection to create (pooler is recommended)
|
|
57
|
+
*/
|
|
40
58
|
endpoint?: "pooler" | "direct";
|
|
59
|
+
/**
|
|
60
|
+
* Delete the test branch in afterAll (default: true)
|
|
61
|
+
*
|
|
62
|
+
* Disabling this will leave each test branch in the Neon project after the
|
|
63
|
+
* test suite runs
|
|
64
|
+
*/
|
|
65
|
+
deleteBranch?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Automatically close Neon WebSocket connections opened during tests before
|
|
68
|
+
* deleting the branch (default: false)
|
|
69
|
+
*
|
|
70
|
+
* Suppresses the specific Neon WebSocket "Connection terminated unexpectedly"
|
|
71
|
+
* error that may surface when deleting a branch with open WebSocket
|
|
72
|
+
* connections
|
|
73
|
+
*/
|
|
74
|
+
autoCloseWebSockets?: boolean;
|
|
41
75
|
}
|
|
42
76
|
|
|
43
77
|
/** Options for overriding test database setup (excludes apiKey) */
|
|
@@ -47,11 +81,12 @@ export type NeonTestingOverrides = Omit<Partial<NeonTestingOptions>, "apiKey">;
|
|
|
47
81
|
* Factory function that creates a Neon test database setup/teardown function
|
|
48
82
|
* for Vitest test suites.
|
|
49
83
|
*
|
|
50
|
-
* @param apiKey - The Neon API key
|
|
51
|
-
* @param projectId - The Neon project ID
|
|
52
|
-
* @param
|
|
53
|
-
* @param
|
|
54
|
-
* @param
|
|
84
|
+
* @param apiKey - The Neon API key, this is used to create and teardown test branches
|
|
85
|
+
* @param projectId - The Neon project ID to operate on
|
|
86
|
+
* @param parentBranchId - The parent branch ID for the new branch. If omitted or empty, the branch will be created from the project's default branch.
|
|
87
|
+
* @param schemaOnly - Whether to create a schema-only branch (default: false)
|
|
88
|
+
* @param endpoint - The type of connection to create (pooler is recommended)
|
|
89
|
+
* @param deleteBranch - Delete the test branch in afterAll (default: true). Disabling this will leave each test branch in the Neon project after the test suite runs
|
|
55
90
|
* @returns A setup/teardown function for Vitest test suites
|
|
56
91
|
*
|
|
57
92
|
* Side effects:
|
|
@@ -77,7 +112,7 @@ export function makeNeonTesting(factoryOptions: NeonTestingOptions) {
|
|
|
77
112
|
if (isTestBranch) {
|
|
78
113
|
await apiClient.deleteProjectBranch(
|
|
79
114
|
factoryOptions.projectId,
|
|
80
|
-
branch.id
|
|
115
|
+
branch.id,
|
|
81
116
|
);
|
|
82
117
|
}
|
|
83
118
|
}
|
|
@@ -85,7 +120,7 @@ export function makeNeonTesting(factoryOptions: NeonTestingOptions) {
|
|
|
85
120
|
|
|
86
121
|
const testDbSetup = (
|
|
87
122
|
/** Override any factory options except apiKey */
|
|
88
|
-
overrides?: NeonTestingOverrides
|
|
123
|
+
overrides?: NeonTestingOverrides,
|
|
89
124
|
) => {
|
|
90
125
|
// Merge factory options with overrides
|
|
91
126
|
const options = { ...factoryOptions, ...overrides };
|
|
@@ -93,6 +128,21 @@ export function makeNeonTesting(factoryOptions: NeonTestingOptions) {
|
|
|
93
128
|
// Each test file gets its own branch ID and database client
|
|
94
129
|
let branchId: string | undefined;
|
|
95
130
|
|
|
131
|
+
// List of tracked Neon WebSocket connections
|
|
132
|
+
const neonSockets = new Set<WebSocket>();
|
|
133
|
+
|
|
134
|
+
// Custom WebSocket constructor that tracks Neon WebSocket connections
|
|
135
|
+
class TrackingWebSocket extends WebSocket {
|
|
136
|
+
constructor(url: string) {
|
|
137
|
+
super(url);
|
|
138
|
+
|
|
139
|
+
// Only track Neon WebSocket connections
|
|
140
|
+
if (!url.includes(".neon.tech/")) return;
|
|
141
|
+
|
|
142
|
+
neonSockets.add(this);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
96
146
|
/**
|
|
97
147
|
* Create a new test branch
|
|
98
148
|
*
|
|
@@ -114,6 +164,7 @@ export function makeNeonTesting(factoryOptions: NeonTestingOptions) {
|
|
|
114
164
|
branchId = data.branch.id;
|
|
115
165
|
|
|
116
166
|
const [connectionUri] = data.connection_uris ?? [];
|
|
167
|
+
|
|
117
168
|
if (!connectionUri) {
|
|
118
169
|
throw new Error("No connection URI found");
|
|
119
170
|
}
|
|
@@ -134,12 +185,33 @@ export function makeNeonTesting(factoryOptions: NeonTestingOptions) {
|
|
|
134
185
|
}
|
|
135
186
|
|
|
136
187
|
beforeAll(async () => {
|
|
137
|
-
process.env.DATABASE_URL = await createBranch
|
|
188
|
+
process.env.DATABASE_URL = await withRetry(createBranch, {
|
|
189
|
+
maxRetries: 5,
|
|
190
|
+
baseDelayMs: 1000,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (options.autoCloseWebSockets) {
|
|
194
|
+
// Install a custom WebSocket constructor that tracks Neon WebSocket
|
|
195
|
+
// connections and closes them before deleting the branch
|
|
196
|
+
neonConfig.webSocketConstructor = TrackingWebSocket;
|
|
197
|
+
}
|
|
138
198
|
});
|
|
139
199
|
|
|
140
200
|
afterAll(async () => {
|
|
141
|
-
await deleteBranch();
|
|
142
201
|
process.env.DATABASE_URL = undefined;
|
|
202
|
+
|
|
203
|
+
// Close all tracked Neon WebSocket connections before deleting the branch
|
|
204
|
+
if (options.autoCloseWebSockets) {
|
|
205
|
+
// Suppress Neon WebSocket "Connection terminated unexpectedly" error
|
|
206
|
+
process.prependListener("uncaughtException", neonWsErrorHandler);
|
|
207
|
+
|
|
208
|
+
// Close tracked Neon WebSocket connections before deleting the branch
|
|
209
|
+
neonSockets.forEach((ws) => ws.close());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (options.deleteBranch !== false) {
|
|
213
|
+
await deleteBranch();
|
|
214
|
+
}
|
|
143
215
|
});
|
|
144
216
|
};
|
|
145
217
|
|
|
@@ -148,3 +220,66 @@ export function makeNeonTesting(factoryOptions: NeonTestingOptions) {
|
|
|
148
220
|
|
|
149
221
|
return testDbSetup;
|
|
150
222
|
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Error handler: Suppress Neon WebSocket "Connection terminated unexpectedly"
|
|
226
|
+
* error
|
|
227
|
+
*/
|
|
228
|
+
const neonWsErrorHandler = (error: Error) => {
|
|
229
|
+
const isNeonWsClose =
|
|
230
|
+
error.message.includes("Connection terminated unexpectedly") &&
|
|
231
|
+
error.stack?.includes("@neondatabase/serverless");
|
|
232
|
+
|
|
233
|
+
if (isNeonWsClose) {
|
|
234
|
+
// Swallow this specific Neon WS termination error
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// For any other error, detach and rethrow
|
|
239
|
+
throw error;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Reusable API call wrapper with automatic retry on 423 errors with exponential
|
|
244
|
+
* backoff
|
|
245
|
+
*
|
|
246
|
+
* https://neon.com/docs/reference/typescript-sdk#error-handling
|
|
247
|
+
* https://neon.com/docs/changelog/2022-07-20
|
|
248
|
+
*/
|
|
249
|
+
async function withRetry<T>(
|
|
250
|
+
fn: () => Promise<T>,
|
|
251
|
+
options: {
|
|
252
|
+
maxRetries: number;
|
|
253
|
+
baseDelayMs: number;
|
|
254
|
+
},
|
|
255
|
+
): Promise<T> {
|
|
256
|
+
if (!Number.isInteger(options.maxRetries) || options.maxRetries <= 0) {
|
|
257
|
+
throw new Error("maxRetries must be a positive integer");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!Number.isInteger(options.baseDelayMs) || options.baseDelayMs <= 0) {
|
|
261
|
+
throw new Error("baseDelayMs must be a positive integer");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (let attempt = 1; attempt <= options.maxRetries; attempt++) {
|
|
265
|
+
try {
|
|
266
|
+
return await fn();
|
|
267
|
+
} catch (error: any) {
|
|
268
|
+
const status = error?.response?.status;
|
|
269
|
+
|
|
270
|
+
if (status === 423 && attempt < options.maxRetries) {
|
|
271
|
+
const delay = options.baseDelayMs * Math.pow(2, attempt - 1);
|
|
272
|
+
|
|
273
|
+
console.log(
|
|
274
|
+
`API call failed with 423, retrying in ${delay}ms (attempt ${attempt}/${options.maxRetries})`,
|
|
275
|
+
);
|
|
276
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
277
|
+
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
throw new Error("apiCallWithRetry reached unexpected end");
|
|
285
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon-testing",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "A Vitest utility for
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A Vitest utility for seamless integration tests with Neon Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
7
7
|
"postgres",
|
|
@@ -16,28 +16,30 @@
|
|
|
16
16
|
"bugs": "https://github.com/starmode-base/neon-testing/issues",
|
|
17
17
|
"module": "index.ts",
|
|
18
18
|
"type": "module",
|
|
19
|
+
"files": [
|
|
20
|
+
"index.ts"
|
|
21
|
+
],
|
|
19
22
|
"scripts": {
|
|
20
23
|
"test": "vitest",
|
|
21
24
|
"format": "prettier --write .",
|
|
22
|
-
"release": "bun publish
|
|
25
|
+
"release": "bun publish",
|
|
26
|
+
"prepublishOnly": "git diff-index --quiet HEAD || (echo 'Error: You have uncommitted changes' && exit 1) && tsc && vitest run && prettier --check .",
|
|
27
|
+
"postpublish": "git tag v$(bun -p \"require('./package.json').version\") && git push --tags"
|
|
23
28
|
},
|
|
24
29
|
"dependencies": {
|
|
25
|
-
"@neondatabase/api-client": "^2.
|
|
30
|
+
"@neondatabase/api-client": "^2.2.0"
|
|
26
31
|
},
|
|
27
32
|
"peerDependencies": {
|
|
28
|
-
"
|
|
29
|
-
"vitest": "^3.2.2"
|
|
33
|
+
"vitest": "^3.2.4"
|
|
30
34
|
},
|
|
31
35
|
"devDependencies": {
|
|
32
36
|
"@neondatabase/serverless": "^1.0.1",
|
|
33
|
-
"dotenv": "^
|
|
34
|
-
"
|
|
37
|
+
"dotenv": "^17.2.1",
|
|
38
|
+
"drizzle-orm": "^0.44.4",
|
|
39
|
+
"pg": "^8.16.3",
|
|
35
40
|
"postgres": "^3.4.7",
|
|
36
|
-
"prettier": "^3.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"README.md",
|
|
41
|
-
"LICENSE"
|
|
42
|
-
]
|
|
41
|
+
"prettier": "^3.6.2",
|
|
42
|
+
"typescript": "^5.9.2",
|
|
43
|
+
"vitest": "^3.2.4"
|
|
44
|
+
}
|
|
43
45
|
}
|