neon-testing 2.0.1-beta.2 โ†’ 2.0.1-beta.20

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 CHANGED
@@ -13,7 +13,7 @@ Each test file runs against its own isolated PostgreSQL database (Neon branch),
13
13
  - ๐Ÿ”„ **Isolated test environments** - Each test file runs against its own Postgres database with your actual schema and constraints
14
14
  - ๐Ÿงน **Automatic cleanup** - Neon test branches are created and destroyed automatically
15
15
  - ๐Ÿ› **Debug friendly** - Option to preserve test branches for debugging failed tests
16
- - ๐Ÿ›ก๏ธ **TypeScript native** - No JavaScript support
16
+ - ๐Ÿ›ก๏ธ **TypeScript native** - With JavaScript support
17
17
  - ๐ŸŽฏ **ESM only** - No CommonJS support
18
18
 
19
19
  ## How it works
@@ -73,7 +73,23 @@ test("database operations", async () => {
73
73
 
74
74
  ### Recommended usage
75
75
 
76
- #### 1. Configuration
76
+ #### 1. Plugin setup
77
+
78
+ First, add the Vite plugin to clear any existing `DATABASE_URL` environment variable before tests run, ensuring tests use isolated test databases.
79
+
80
+ ```typescript
81
+ // vitest.config.ts or vite.config.ts
82
+ import { defineConfig } from "vitest/config";
83
+ import { neonTesting } from "neon-testing/utils";
84
+
85
+ export default defineConfig({
86
+ plugins: [neonTesting()],
87
+ });
88
+ ```
89
+
90
+ This plugin is recommended but not required. Without it, tests might accidentally use your existing `DATABASE_URL` (from `.env` files or environment variables) instead of the isolated test databases that neon-testing creates. This can happen if you forget to call `withNeonTestBranch()` in a test file where database writes happen.
91
+
92
+ #### 2. Configuration
77
93
 
78
94
  Use the `makeNeonTesting` factory to generate a lifecycle function for your tests.
79
95
 
@@ -88,7 +104,7 @@ export const withNeonTestBranch = makeNeonTesting({
88
104
  });
89
105
  ```
90
106
 
91
- #### 2. Enable database testing
107
+ #### 3. Enable database testing
92
108
 
93
109
  Then call the exported test lifecycle function in the test files where you need database access.
94
110
 
@@ -164,6 +180,21 @@ import { withNeonTestBranch } from "./test-setup";
164
180
  withNeonTestBranch({ parentBranchId: "br-staging-123" });
165
181
  ```
166
182
 
183
+ ## CI/CD
184
+
185
+ It is easy to run Neon integration in CI/CD
186
+
187
+ ### GitHub Actions
188
+
189
+ [Example](.github/workflows/test.yml)
190
+
191
+ ### Vercel
192
+
193
+ Two options:
194
+
195
+ - Add `vitest run` to the `build` script in [package.json](https://github.com/starmode-base/template-tanstack-start/blob/83c784e164b55fd8d59c5b57b907251e5eb03de1/app/package.json#L11l)
196
+ - Add `vitest run` to the _Build Command_ in the Vercel dashboard
197
+
167
198
  ## Utilities
168
199
 
169
200
  ### deleteAllTestBranches()
@@ -0,0 +1,67 @@
1
+ export interface NeonTestingOptions {
2
+ /**
3
+ * The Neon API key, this is used to create and teardown test branches
4
+ *
5
+ * https://neon.com/docs/manage/api-keys#creating-api-keys
6
+ */
7
+ apiKey: string;
8
+ /**
9
+ * The Neon project ID to operate on
10
+ *
11
+ * https://console.neon.tech/app/projects
12
+ */
13
+ projectId: string;
14
+ /**
15
+ * The parent branch ID for the new branch. If omitted or empty, the branch
16
+ * will be created from the project's default branch.
17
+ */
18
+ parentBranchId?: string;
19
+ /**
20
+ * Whether to create a schema-only branch (default: false)
21
+ */
22
+ schemaOnly?: boolean;
23
+ /**
24
+ * The type of connection to create (pooler is recommended)
25
+ */
26
+ endpoint?: "pooler" | "direct";
27
+ /**
28
+ * Delete the test branch in afterAll (default: true)
29
+ *
30
+ * Disabling this will leave each test branch in the Neon project after the
31
+ * test suite runs
32
+ */
33
+ deleteBranch?: boolean;
34
+ /**
35
+ * Automatically close Neon WebSocket connections opened during tests before
36
+ * deleting the branch (default: false)
37
+ *
38
+ * Suppresses the specific Neon WebSocket "Connection terminated unexpectedly"
39
+ * error that may surface when deleting a branch with open WebSocket
40
+ * connections
41
+ */
42
+ autoCloseWebSockets?: boolean;
43
+ }
44
+ /** Options for overriding test database setup (excludes apiKey) */
45
+ export type NeonTestingOverrides = Omit<Partial<NeonTestingOptions>, "apiKey">;
46
+ /**
47
+ * Factory function that creates a Neon test database setup/teardown function
48
+ * for Vitest test suites.
49
+ *
50
+ * @param apiKey - The Neon API key, this is used to create and teardown test branches
51
+ * @param projectId - The Neon project ID to operate on
52
+ * @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.
53
+ * @param schemaOnly - Whether to create a schema-only branch (default: false)
54
+ * @param endpoint - The type of connection to create (pooler is recommended)
55
+ * @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
56
+ * @returns A setup/teardown function for Vitest test suites
57
+ *
58
+ * Side effects:
59
+ * - Sets the `DATABASE_URL` environment variable to the connection URI for the
60
+ * new branch
61
+ * - Deletes the test branch after the test suite runs
62
+ */
63
+ export declare function makeNeonTesting(factoryOptions: NeonTestingOptions): {
64
+ (overrides?: NeonTestingOverrides): void;
65
+ deleteAllTestBranches: () => Promise<void>;
66
+ };
67
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AA8BA,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,mEAAmE;AACnE,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,CAAC;AAE/E;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,cAAc,EAAE,kBAAkB;iBA0BlD,oBAAoB;;EAmGnC"}
package/dist/index.js ADDED
@@ -0,0 +1,179 @@
1
+ /**
2
+ * https://neon.com/docs/reference/typescript-sdk
3
+ */
4
+ import { createApiClient, EndpointType, } from "@neondatabase/api-client";
5
+ import { afterAll, beforeAll } from "vitest";
6
+ import { neonConfig } from "@neondatabase/serverless";
7
+ /**
8
+ * Creates a PostgreSQL connection URI from connection parameters
9
+ *
10
+ * @param connectionParameters - The connection parameters object
11
+ * @param type - The type of connection to create (pooler or direct)
12
+ * @returns A PostgreSQL connection URI string
13
+ */
14
+ function createConnectionUri(connectionParameters, type) {
15
+ const { role, password, host, pooler_host, database } = connectionParameters.connection_parameters;
16
+ const hostname = type === "pooler" ? pooler_host : host;
17
+ return `postgresql://${role}:${password}@${hostname}/${database}?sslmode=require`;
18
+ }
19
+ /**
20
+ * Factory function that creates a Neon test database setup/teardown function
21
+ * for Vitest test suites.
22
+ *
23
+ * @param apiKey - The Neon API key, this is used to create and teardown test branches
24
+ * @param projectId - The Neon project ID to operate on
25
+ * @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.
26
+ * @param schemaOnly - Whether to create a schema-only branch (default: false)
27
+ * @param endpoint - The type of connection to create (pooler is recommended)
28
+ * @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
29
+ * @returns A setup/teardown function for Vitest test suites
30
+ *
31
+ * Side effects:
32
+ * - Sets the `DATABASE_URL` environment variable to the connection URI for the
33
+ * new branch
34
+ * - Deletes the test branch after the test suite runs
35
+ */
36
+ export function makeNeonTesting(factoryOptions) {
37
+ const apiClient = createApiClient({ apiKey: factoryOptions.apiKey });
38
+ /**
39
+ * Delete all test branches
40
+ */
41
+ async function deleteAllTestBranches() {
42
+ const { data } = await apiClient.listProjectBranches({
43
+ projectId: factoryOptions.projectId,
44
+ });
45
+ for (const branch of data.branches) {
46
+ const isTestBranch = data.annotations[branch.id]?.value["integration-test"] === "true";
47
+ if (isTestBranch) {
48
+ await apiClient.deleteProjectBranch(factoryOptions.projectId, branch.id);
49
+ }
50
+ }
51
+ }
52
+ const testDbSetup = (
53
+ /** Override any factory options except apiKey */
54
+ overrides) => {
55
+ // Merge factory options with overrides
56
+ const options = { ...factoryOptions, ...overrides };
57
+ // Each test file gets its own branch ID and database client
58
+ let branchId;
59
+ // List of tracked Neon WebSocket connections
60
+ const neonSockets = new Set();
61
+ // Custom WebSocket constructor that tracks Neon WebSocket connections
62
+ class TrackingWebSocket extends WebSocket {
63
+ constructor(url) {
64
+ super(url);
65
+ // Only track Neon WebSocket connections
66
+ if (!url.includes(".neon.tech/"))
67
+ return;
68
+ neonSockets.add(this);
69
+ }
70
+ }
71
+ /**
72
+ * Create a new test branch
73
+ *
74
+ * @returns The connection URI for the new branch
75
+ */
76
+ async function createBranch() {
77
+ const { data } = await apiClient.createProjectBranch(options.projectId, {
78
+ branch: {
79
+ name: `test/${crypto.randomUUID()}`,
80
+ parent_id: options.parentBranchId,
81
+ init_source: options.schemaOnly ? "schema-only" : undefined,
82
+ },
83
+ endpoints: [{ type: EndpointType.ReadWrite }],
84
+ annotation_value: {
85
+ "integration-test": "true",
86
+ },
87
+ });
88
+ branchId = data.branch.id;
89
+ const [connectionUri] = data.connection_uris ?? [];
90
+ if (!connectionUri) {
91
+ throw new Error("No connection URI found");
92
+ }
93
+ return createConnectionUri(connectionUri, options.endpoint ?? "pooler");
94
+ }
95
+ /**
96
+ * Delete the test branch
97
+ */
98
+ async function deleteBranch() {
99
+ if (!branchId) {
100
+ throw new Error("No branch to delete");
101
+ }
102
+ await apiClient.deleteProjectBranch(options.projectId, branchId);
103
+ branchId = undefined;
104
+ }
105
+ beforeAll(async () => {
106
+ process.env.DATABASE_URL = await withRetry(createBranch, {
107
+ maxRetries: 5,
108
+ baseDelayMs: 1000,
109
+ });
110
+ if (options.autoCloseWebSockets) {
111
+ // Install a custom WebSocket constructor that tracks Neon WebSocket
112
+ // connections and closes them before deleting the branch
113
+ neonConfig.webSocketConstructor = TrackingWebSocket;
114
+ }
115
+ });
116
+ afterAll(async () => {
117
+ process.env.DATABASE_URL = undefined;
118
+ // Close all tracked Neon WebSocket connections before deleting the branch
119
+ if (options.autoCloseWebSockets) {
120
+ // Suppress Neon WebSocket "Connection terminated unexpectedly" error
121
+ process.prependListener("uncaughtException", neonWsErrorHandler);
122
+ // Close tracked Neon WebSocket connections before deleting the branch
123
+ neonSockets.forEach((ws) => ws.close());
124
+ }
125
+ if (options.deleteBranch !== false) {
126
+ await deleteBranch();
127
+ }
128
+ });
129
+ };
130
+ // Attach the utility
131
+ testDbSetup.deleteAllTestBranches = deleteAllTestBranches;
132
+ return testDbSetup;
133
+ }
134
+ /**
135
+ * Error handler: Suppress Neon WebSocket "Connection terminated unexpectedly"
136
+ * error
137
+ */
138
+ const neonWsErrorHandler = (error) => {
139
+ const isNeonWsClose = error.message.includes("Connection terminated unexpectedly") &&
140
+ error.stack?.includes("@neondatabase/serverless");
141
+ if (isNeonWsClose) {
142
+ // Swallow this specific Neon WS termination error
143
+ return;
144
+ }
145
+ // For any other error, detach and rethrow
146
+ throw error;
147
+ };
148
+ /**
149
+ * Reusable API call wrapper with automatic retry on 423 errors with exponential
150
+ * backoff
151
+ *
152
+ * https://neon.com/docs/reference/typescript-sdk#error-handling
153
+ * https://neon.com/docs/changelog/2022-07-20
154
+ */
155
+ async function withRetry(fn, options) {
156
+ if (!Number.isInteger(options.maxRetries) || options.maxRetries <= 0) {
157
+ throw new Error("maxRetries must be a positive integer");
158
+ }
159
+ if (!Number.isInteger(options.baseDelayMs) || options.baseDelayMs <= 0) {
160
+ throw new Error("baseDelayMs must be a positive integer");
161
+ }
162
+ for (let attempt = 1; attempt <= options.maxRetries; attempt++) {
163
+ try {
164
+ return await fn();
165
+ }
166
+ catch (error) {
167
+ const status = error?.response?.status;
168
+ if (status === 423 && attempt < options.maxRetries) {
169
+ const delay = options.baseDelayMs * Math.pow(2, attempt - 1);
170
+ console.log(`API call failed with 423, retrying in ${delay}ms (attempt ${attempt}/${options.maxRetries})`);
171
+ await new Promise((resolve) => setTimeout(resolve, delay));
172
+ continue;
173
+ }
174
+ throw error;
175
+ }
176
+ }
177
+ throw new Error("apiCallWithRetry reached unexpected end");
178
+ }
179
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EACL,eAAe,EACf,YAAY,GAEb,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEtD;;;;;;GAMG;AACH,SAAS,mBAAmB,CAC1B,oBAAuC,EACvC,IAAyB;IAEzB,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,GACnD,oBAAoB,CAAC,qBAAqB,CAAC;IAE7C,MAAM,QAAQ,GAAG,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;IAExD,OAAO,gBAAgB,IAAI,IAAI,QAAQ,IAAI,QAAQ,IAAI,QAAQ,kBAAkB,CAAC;AACpF,CAAC;AAiDD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,eAAe,CAAC,cAAkC;IAChE,MAAM,SAAS,GAAG,eAAe,CAAC,EAAE,MAAM,EAAE,cAAc,CAAC,MAAM,EAAE,CAAC,CAAC;IAErE;;OAEG;IACH,KAAK,UAAU,qBAAqB;QAClC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,SAAS,CAAC,mBAAmB,CAAC;YACnD,SAAS,EAAE,cAAc,CAAC,SAAS;SACpC,CAAC,CAAC;QAEH,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,MAAM,YAAY,GAChB,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,kBAAkB,CAAC,KAAK,MAAM,CAAC;YAEpE,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,SAAS,CAAC,mBAAmB,CACjC,cAAc,CAAC,SAAS,EACxB,MAAM,CAAC,EAAE,CACV,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,WAAW,GAAG;IAClB,iDAAiD;IACjD,SAAgC,EAChC,EAAE;QACF,uCAAuC;QACvC,MAAM,OAAO,GAAG,EAAE,GAAG,cAAc,EAAE,GAAG,SAAS,EAAE,CAAC;QAEpD,4DAA4D;QAC5D,IAAI,QAA4B,CAAC;QAEjC,6CAA6C;QAC7C,MAAM,WAAW,GAAG,IAAI,GAAG,EAAa,CAAC;QAEzC,sEAAsE;QACtE,MAAM,iBAAkB,SAAQ,SAAS;YACvC,YAAY,GAAW;gBACrB,KAAK,CAAC,GAAG,CAAC,CAAC;gBAEX,wCAAwC;gBACxC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC;oBAAE,OAAO;gBAEzC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACxB,CAAC;SACF;QAED;;;;WAIG;QACH,KAAK,UAAU,YAAY;YACzB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,SAAS,CAAC,mBAAmB,CAAC,OAAO,CAAC,SAAS,EAAE;gBACtE,MAAM,EAAE;oBACN,IAAI,EAAE,QAAQ,MAAM,CAAC,UAAU,EAAE,EAAE;oBACnC,SAAS,EAAE,OAAO,CAAC,cAAc;oBACjC,WAAW,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS;iBAC5D;gBACD,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,SAAS,EAAE,CAAC;gBAC7C,gBAAgB,EAAE;oBAChB,kBAAkB,EAAE,MAAM;iBAC3B;aACF,CAAC,CAAC;YAEH,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAE1B,MAAM,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC,eAAe,IAAI,EAAE,CAAC;YAEnD,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;YAC7C,CAAC;YAED,OAAO,mBAAmB,CAAC,aAAa,EAAE,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,CAAC;QAC1E,CAAC;QAED;;WAEG;QACH,KAAK,UAAU,YAAY;YACzB,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;YACzC,CAAC;YAED,MAAM,SAAS,CAAC,mBAAmB,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;YACjE,QAAQ,GAAG,SAAS,CAAC;QACvB,CAAC;QAED,SAAS,CAAC,KAAK,IAAI,EAAE;YACnB,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,MAAM,SAAS,CAAC,YAAY,EAAE;gBACvD,UAAU,EAAE,CAAC;gBACb,WAAW,EAAE,IAAI;aAClB,CAAC,CAAC;YAEH,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;gBAChC,oEAAoE;gBACpE,yDAAyD;gBACzD,UAAU,CAAC,oBAAoB,GAAG,iBAAiB,CAAC;YACtD,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,KAAK,IAAI,EAAE;YAClB,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,SAAS,CAAC;YAErC,0EAA0E;YAC1E,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;gBAChC,qEAAqE;gBACrE,OAAO,CAAC,eAAe,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,CAAC;gBAEjE,sEAAsE;gBACtE,WAAW,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;YAC1C,CAAC;YAED,IAAI,OAAO,CAAC,YAAY,KAAK,KAAK,EAAE,CAAC;gBACnC,MAAM,YAAY,EAAE,CAAC;YACvB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,qBAAqB;IACrB,WAAW,CAAC,qBAAqB,GAAG,qBAAqB,CAAC;IAE1D,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,kBAAkB,GAAG,CAAC,KAAY,EAAE,EAAE;IAC1C,MAAM,aAAa,GACjB,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,oCAAoC,CAAC;QAC5D,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC,0BAA0B,CAAC,CAAC;IAEpD,IAAI,aAAa,EAAE,CAAC;QAClB,kDAAkD;QAClD,OAAO;IACT,CAAC;IAED,0CAA0C;IAC1C,MAAM,KAAK,CAAC;AACd,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,KAAK,UAAU,SAAS,CACtB,EAAoB,EACpB,OAGC;IAED,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,UAAU,IAAI,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,OAAO,CAAC,WAAW,IAAI,CAAC,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,CAAC,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QAC/D,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,MAAM,MAAM,GAAG,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC;YAEvC,IAAI,MAAM,KAAK,GAAG,IAAI,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;gBACnD,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC;gBAE7D,OAAO,CAAC,GAAG,CACT,yCAAyC,KAAK,eAAe,OAAO,IAAI,OAAO,CAAC,UAAU,GAAG,CAC9F,CAAC;gBACF,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;gBAE3D,SAAS;YACX,CAAC;YAED,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;AAC7D,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Create a lazy singleton from a factory function
3
+ */
4
+ export declare function lazySingleton<T>(factory: () => T): () => T;
5
+ //# sourceMappingURL=singleton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton.d.ts","sourceRoot":"","sources":["../singleton.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,CAM1D"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Create a lazy singleton from a factory function
3
+ */
4
+ export function lazySingleton(factory) {
5
+ let instance;
6
+ return () => {
7
+ instance ??= factory();
8
+ return instance;
9
+ };
10
+ }
11
+ //# sourceMappingURL=singleton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton.js","sourceRoot":"","sources":["../singleton.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,aAAa,CAAI,OAAgB;IAC/C,IAAI,QAAuB,CAAC;IAC5B,OAAO,GAAG,EAAE;QACV,QAAQ,KAAK,OAAO,EAAE,CAAC;QACvB,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { lazySingleton } from "./singleton.js";
2
+ export { neonTesting } from "./vite-plugin.js";
3
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/utils.js ADDED
@@ -0,0 +1,3 @@
1
+ export { lazySingleton } from "./singleton.js";
2
+ export { neonTesting } from "./vite-plugin.js";
3
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "vite";
2
+ export declare function neonTesting(): Plugin;
3
+ //# sourceMappingURL=vite-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../vite-plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,wBAAgB,WAAW,IAAI,MAAM,CAqBpC"}
@@ -0,0 +1,19 @@
1
+ import { fileURLToPath } from "node:url";
2
+ export function neonTesting() {
3
+ return {
4
+ name: "neon-testing-plugin",
5
+ // Run as late as possible to reduce the risk of other plugins restoring
6
+ // DATABASE_URL after we clear it
7
+ enforce: "post",
8
+ config(user) {
9
+ const setupPath = fileURLToPath(new URL("./vitest-setup.js", import.meta.url));
10
+ return {
11
+ test: {
12
+ // Register the vitest-setup.js file to run after other setup files
13
+ setupFiles: Array.from(new Set([...(user.test?.setupFiles ?? []), setupPath])),
14
+ },
15
+ };
16
+ },
17
+ };
18
+ }
19
+ //# sourceMappingURL=vite-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,MAAM,UAAU,WAAW;IACzB,OAAO;QACL,IAAI,EAAE,qBAAqB;QAC3B,wEAAwE;QACxE,iCAAiC;QACjC,OAAO,EAAE,MAAM;QACf,MAAM,CAAC,IAAI;YACT,MAAM,SAAS,GAAG,aAAa,CAC7B,IAAI,GAAG,CAAC,mBAAmB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAC9C,CAAC;YAEF,OAAO;gBACL,IAAI,EAAE;oBACJ,mEAAmE;oBACnE,UAAU,EAAE,KAAK,CAAC,IAAI,CACpB,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,IAAI,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,CACvD;iBACF;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ declare const isVitest: string | undefined;
2
+ //# sourceMappingURL=vitest-setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest-setup.d.ts","sourceRoot":"","sources":["../vitest-setup.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,QAAQ,oBAAqB,CAAC"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ const isVitest = process.env.VITEST;
3
+ if (isVitest) {
4
+ if (process.env.DATABASE_URL) {
5
+ console.warn("[neon-testing] Clearing existing DATABASE_URL in test environment");
6
+ }
7
+ delete process.env.DATABASE_URL;
8
+ }
9
+ //# sourceMappingURL=vitest-setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest-setup.js","sourceRoot":"","sources":["../vitest-setup.ts"],"names":[],"mappings":";AAAA,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;AAEpC,IAAI,QAAQ,EAAE,CAAC;IACb,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,CACV,mEAAmE,CACpE,CAAC;IACJ,CAAC;IAED,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;AAClC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon-testing",
3
- "version": "2.0.1-beta.2",
3
+ "version": "2.0.1-beta.20",
4
4
  "description": "A Vitest utility for seamless integration tests with Neon Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -16,42 +16,27 @@
16
16
  "type": "module",
17
17
  "exports": {
18
18
  ".": {
19
- "types": "./index.ts",
20
- "import": "./index.ts",
21
- "default": "./index.ts"
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
22
21
  },
23
22
  "./utils": {
24
- "types": "./utils.ts",
25
- "import": "./utils.ts",
26
- "default": "./utils.ts"
27
- },
28
- "./vite-plugin": {
29
- "types": "./vite-plugin.ts",
30
- "import": "./vite-plugin.ts",
31
- "default": "./vite-plugin.ts"
32
- },
33
- "./vitest-setup": {
34
- "types": "./vitest-setup.ts",
35
- "import": "./vitest-setup.ts",
36
- "default": "./vitest-setup.ts"
23
+ "types": "./dist/utils.d.ts",
24
+ "import": "./dist/utils.js"
37
25
  }
38
26
  },
39
27
  "files": [
40
- "index.ts",
41
- "utils.ts",
42
- "singleton.ts",
43
- "vite-plugin.ts",
44
- "vitest-setup.ts"
28
+ "dist"
45
29
  ],
46
30
  "scripts": {
31
+ "build": "rm -rf dist && tsc",
47
32
  "test": "vitest",
48
33
  "format": "prettier --write .",
49
- "release:patch": "bun pm version patch && bun publish --tag latest",
50
- "release:minor": "bun pm version minor && bun publish --tag latest",
51
- "release:major": "bun pm version major && bun publish --tag latest",
52
- "release:beta": "bun pm version prerelease --preid=beta && bun publish --tag beta",
53
- "prepublishOnly": "tsc && vitest run && prettier --check .",
54
- "postpublish": "git push --tags"
34
+ "check": "bun run build && bunx vitest run && bunx prettier --check .",
35
+ "release:patch": "bun run check && bun pm version patch && bun publish --tag latest",
36
+ "release:minor": "bun run check && bun pm version minor && bun publish --tag latest",
37
+ "release:major": "bun run check && bun pm version major && bun publish --tag latest",
38
+ "release:beta": "bun run check && bun pm version prerelease --preid=beta && bun publish --tag beta",
39
+ "postpublish": "git push --follow-tags"
55
40
  },
56
41
  "dependencies": {
57
42
  "@neondatabase/api-client": "^2.2.0"
package/index.ts DELETED
@@ -1,285 +0,0 @@
1
- /**
2
- * https://neon.com/docs/reference/typescript-sdk
3
- */
4
- import {
5
- createApiClient,
6
- EndpointType,
7
- type ConnectionDetails,
8
- } from "@neondatabase/api-client";
9
- import { afterAll, beforeAll } from "vitest";
10
- import { neonConfig } from "@neondatabase/serverless";
11
-
12
- export * from "./utils";
13
-
14
- /**
15
- * Creates a PostgreSQL connection URI from connection parameters
16
- *
17
- * @param connectionParameters - The connection parameters object
18
- * @param type - The type of connection to create (pooler or direct)
19
- * @returns A PostgreSQL connection URI string
20
- */
21
- function createConnectionUri(
22
- connectionParameters: ConnectionDetails,
23
- type: "pooler" | "direct",
24
- ) {
25
- const { role, password, host, pooler_host, database } =
26
- connectionParameters.connection_parameters;
27
-
28
- const hostname = type === "pooler" ? pooler_host : host;
29
-
30
- return `postgresql://${role}:${password}@${hostname}/${database}?sslmode=require`;
31
- }
32
-
33
- export interface NeonTestingOptions {
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
- */
39
- apiKey: string;
40
- /**
41
- * The Neon project ID to operate on
42
- *
43
- * https://console.neon.tech/app/projects
44
- */
45
- projectId: string;
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
- */
50
- parentBranchId?: string;
51
- /**
52
- * Whether to create a schema-only branch (default: false)
53
- */
54
- schemaOnly?: boolean;
55
- /**
56
- * The type of connection to create (pooler is recommended)
57
- */
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;
75
- }
76
-
77
- /** Options for overriding test database setup (excludes apiKey) */
78
- export type NeonTestingOverrides = Omit<Partial<NeonTestingOptions>, "apiKey">;
79
-
80
- /**
81
- * Factory function that creates a Neon test database setup/teardown function
82
- * for Vitest test suites.
83
- *
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
90
- * @returns A setup/teardown function for Vitest test suites
91
- *
92
- * Side effects:
93
- * - Sets the `DATABASE_URL` environment variable to the connection URI for the
94
- * new branch
95
- * - Deletes the test branch after the test suite runs
96
- */
97
- export function makeNeonTesting(factoryOptions: NeonTestingOptions) {
98
- const apiClient = createApiClient({ apiKey: factoryOptions.apiKey });
99
-
100
- /**
101
- * Delete all test branches
102
- */
103
- async function deleteAllTestBranches() {
104
- const { data } = await apiClient.listProjectBranches({
105
- projectId: factoryOptions.projectId,
106
- });
107
-
108
- for (const branch of data.branches) {
109
- const isTestBranch =
110
- data.annotations[branch.id]?.value["integration-test"] === "true";
111
-
112
- if (isTestBranch) {
113
- await apiClient.deleteProjectBranch(
114
- factoryOptions.projectId,
115
- branch.id,
116
- );
117
- }
118
- }
119
- }
120
-
121
- const testDbSetup = (
122
- /** Override any factory options except apiKey */
123
- overrides?: NeonTestingOverrides,
124
- ) => {
125
- // Merge factory options with overrides
126
- const options = { ...factoryOptions, ...overrides };
127
-
128
- // Each test file gets its own branch ID and database client
129
- let branchId: string | undefined;
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
-
146
- /**
147
- * Create a new test branch
148
- *
149
- * @returns The connection URI for the new branch
150
- */
151
- async function createBranch() {
152
- const { data } = await apiClient.createProjectBranch(options.projectId, {
153
- branch: {
154
- name: `test/${crypto.randomUUID()}`,
155
- parent_id: options.parentBranchId,
156
- init_source: options.schemaOnly ? "schema-only" : undefined,
157
- },
158
- endpoints: [{ type: EndpointType.ReadWrite }],
159
- annotation_value: {
160
- "integration-test": "true",
161
- },
162
- });
163
-
164
- branchId = data.branch.id;
165
-
166
- const [connectionUri] = data.connection_uris ?? [];
167
-
168
- if (!connectionUri) {
169
- throw new Error("No connection URI found");
170
- }
171
-
172
- return createConnectionUri(connectionUri, options.endpoint ?? "pooler");
173
- }
174
-
175
- /**
176
- * Delete the test branch
177
- */
178
- async function deleteBranch() {
179
- if (!branchId) {
180
- throw new Error("No branch to delete");
181
- }
182
-
183
- await apiClient.deleteProjectBranch(options.projectId, branchId);
184
- branchId = undefined;
185
- }
186
-
187
- beforeAll(async () => {
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
- }
198
- });
199
-
200
- afterAll(async () => {
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
- }
215
- });
216
- };
217
-
218
- // Attach the utility
219
- testDbSetup.deleteAllTestBranches = deleteAllTestBranches;
220
-
221
- return testDbSetup;
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/singleton.ts DELETED
@@ -1,10 +0,0 @@
1
- /**
2
- * Create a lazy singleton from a factory function
3
- */
4
- export function lazySingleton<T>(factory: () => T): () => T {
5
- let instance: T | undefined;
6
- return () => {
7
- instance ??= factory();
8
- return instance;
9
- };
10
- }
package/utils.ts DELETED
@@ -1,2 +0,0 @@
1
- export { lazySingleton } from "./singleton";
2
- export { neonTesting } from "./vite-plugin";
package/vite-plugin.ts DELETED
@@ -1,22 +0,0 @@
1
- import { fileURLToPath } from "node:url";
2
- import type { Plugin, UserConfig } from "vite";
3
-
4
- export function neonTesting() {
5
- return {
6
- name: "neon-testing-plugin",
7
- enforce: "pre",
8
- config(user: any) {
9
- const setupPath = fileURLToPath(
10
- new URL("./vitest-setup.ts", import.meta.url),
11
- );
12
-
13
- const setup = new Set([...(user.test?.setupFiles ?? []), setupPath]);
14
-
15
- return {
16
- test: {
17
- setupFiles: Array.from(setup),
18
- },
19
- };
20
- },
21
- } satisfies Plugin;
22
- }
package/vitest-setup.ts DELETED
@@ -1,12 +0,0 @@
1
- const isVitest =
2
- process.env.VITEST === "true" || !!process.env.VITEST_WORKER_ID;
3
-
4
- if (isVitest) {
5
- if (process.env.DATABASE_URL) {
6
- console.warn(
7
- "[neon-testing] Clearing existing DATABASE_URL in test environment",
8
- );
9
- }
10
-
11
- delete process.env.DATABASE_URL;
12
- }