neon-testing 2.0.1-beta.1 → 2.0.1-beta.13

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.
@@ -0,0 +1,66 @@
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
+ };
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":"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"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Create a lazy singleton from a factory function
3
+ */
4
+ export declare function lazySingleton<T>(factory: () => T): () => T;
@@ -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,2 @@
1
+ export { lazySingleton } from "./singleton.js";
2
+ export { neonTesting } from "./vite-plugin.js";
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,2 @@
1
+ import type { Plugin } from "vite";
2
+ export declare function neonTesting(): Plugin;
@@ -0,0 +1,17 @@
1
+ import { fileURLToPath } from "node:url";
2
+ export function neonTesting() {
3
+ return {
4
+ name: "neon-testing-plugin",
5
+ enforce: "pre",
6
+ config(user) {
7
+ const setupPath = fileURLToPath(new URL("./vitest-setup.js", import.meta.url));
8
+ const setup = new Set([...(user.test?.setupFiles ?? []), setupPath]);
9
+ return {
10
+ test: {
11
+ setupFiles: Array.from(setup),
12
+ },
13
+ };
14
+ },
15
+ };
16
+ }
17
+ //# 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,OAAO,EAAE,KAAK;QACd,MAAM,CAAC,IAAI;YACT,MAAM,SAAS,GAAG,aAAa,CAC7B,IAAI,GAAG,CAAC,mBAAmB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAC9C,CAAC;YAEF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,IAAI,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;YAErE,OAAO;gBACL,IAAI,EAAE;oBACJ,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;iBAC9B;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1 @@
1
+ declare const isVitest: string | undefined;
@@ -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.1",
3
+ "version": "2.0.1-beta.13",
4
4
  "description": "A Vitest utility for seamless integration tests with Neon Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -16,32 +16,32 @@
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"
23
+ "types": "./dist/utils.d.ts",
24
+ "import": "./dist/utils.js"
25
+ },
26
+ "./vite-plugin": {
27
+ "types": "./dist/vite-plugin.d.ts",
28
+ "import": "./dist/vite-plugin.js"
27
29
  }
28
30
  },
31
+ "types": "./dist/index.d.ts",
29
32
  "files": [
30
- "index.ts",
31
- "utils.ts",
32
- "singleton.ts",
33
- "vite-plugin.ts",
34
- "vitest-setup.ts"
33
+ "dist"
35
34
  ],
36
35
  "scripts": {
37
- "test": "vitest",
36
+ "test": "bun run build && vitest run",
38
37
  "format": "prettier --write .",
39
- "release:patch": "bun pm version patch && bun publish --tag latest",
40
- "release:minor": "bun pm version minor && bun publish --tag latest",
41
- "release:major": "bun pm version major && bun publish --tag latest",
42
- "release:beta": "bun pm version prerelease --preid=beta && bun publish --tag beta",
43
- "prepublishOnly": "tsc && vitest run && prettier --check .",
44
- "postpublish": "git push --tags"
38
+ "build": "rm -rf dist && tsc",
39
+ "preversion": "bun run build && vitest run && prettier --check .",
40
+ "release:patch": "bun run preversion && bun pm version patch && bun publish --tag latest",
41
+ "release:minor": "bun run preversion && bun pm version minor && bun publish --tag latest",
42
+ "release:major": "bun run preversion && bun pm version major && bun publish --tag latest",
43
+ "release:beta": "bun run preversion && bun pm version prerelease --preid=beta && bun publish --tag beta",
44
+ "postpublish": "git push --follow-tags"
45
45
  },
46
46
  "dependencies": {
47
47
  "@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
- }