neon-testing 2.0.1-beta.6 → 2.0.1-beta.8

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":"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,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"}
package/dist/utils.js ADDED
@@ -0,0 +1,3 @@
1
+ export { lazySingleton } from "./singleton";
2
+ export { neonTesting } from "./vite-plugin";
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,aAAa,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,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,11 @@
1
+ const isVitest = process.env.VITEST;
2
+
3
+ if (isVitest) {
4
+ if (process.env.DATABASE_URL) {
5
+ console.warn(
6
+ "[neon-testing] Clearing existing DATABASE_URL in test environment",
7
+ );
8
+ }
9
+
10
+ delete process.env.DATABASE_URL;
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon-testing",
3
- "version": "2.0.1-beta.6",
3
+ "version": "2.0.1-beta.8",
4
4
  "description": "A Vitest utility for seamless integration tests with Neon Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -15,24 +15,33 @@
15
15
  "bugs": "https://github.com/starmode-base/neon-testing/issues",
16
16
  "type": "module",
17
17
  "exports": {
18
- ".": "./index.ts",
19
- "./utils": "./utils.ts"
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./utils": {
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"
29
+ }
20
30
  },
31
+ "types": "./dist/index.d.ts",
21
32
  "files": [
22
- "index.ts",
23
- "singleton.ts",
24
- "utils.ts",
25
- "vite-plugin.ts",
33
+ "dist",
26
34
  "vitest-setup.js"
27
35
  ],
28
36
  "scripts": {
29
37
  "test": "vitest",
30
38
  "format": "prettier --write .",
31
- "release:patch": "bun pm version patch && bun publish --tag latest",
32
- "release:minor": "bun pm version minor && bun publish --tag latest",
33
- "release:major": "bun pm version major && bun publish --tag latest",
34
- "release:beta": "bun pm version prerelease --preid=beta && bun publish --tag beta",
35
- "prepublishOnly": "tsc && vitest run && prettier --check .",
39
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && cp vitest-setup.js dist/",
40
+ "release:patch": "bun pm version patch && bun run build && bun publish --tag latest",
41
+ "release:minor": "bun pm version minor && bun run build && bun publish --tag latest",
42
+ "release:major": "bun pm version major && bun run build && bun publish --tag latest",
43
+ "release:beta": "bun pm version prerelease --preid=beta && bun run build && bun publish --tag beta",
44
+ "prepublishOnly": "bun run build && vitest run && prettier --check .",
36
45
  "postpublish": "git push --follow-tags"
37
46
  },
38
47
  "dependencies": {
package/index.ts DELETED
@@ -1,283 +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
- /**
13
- * Creates a PostgreSQL connection URI from connection parameters
14
- *
15
- * @param connectionParameters - The connection parameters object
16
- * @param type - The type of connection to create (pooler or direct)
17
- * @returns A PostgreSQL connection URI string
18
- */
19
- function createConnectionUri(
20
- connectionParameters: ConnectionDetails,
21
- type: "pooler" | "direct",
22
- ) {
23
- const { role, password, host, pooler_host, database } =
24
- connectionParameters.connection_parameters;
25
-
26
- const hostname = type === "pooler" ? pooler_host : host;
27
-
28
- return `postgresql://${role}:${password}@${hostname}/${database}?sslmode=require`;
29
- }
30
-
31
- export interface NeonTestingOptions {
32
- /**
33
- * The Neon API key, this is used to create and teardown test branches
34
- *
35
- * https://neon.com/docs/manage/api-keys#creating-api-keys
36
- */
37
- apiKey: string;
38
- /**
39
- * The Neon project ID to operate on
40
- *
41
- * https://console.neon.tech/app/projects
42
- */
43
- projectId: string;
44
- /**
45
- * The parent branch ID for the new branch. If omitted or empty, the branch
46
- * will be created from the project's default branch.
47
- */
48
- parentBranchId?: string;
49
- /**
50
- * Whether to create a schema-only branch (default: false)
51
- */
52
- schemaOnly?: boolean;
53
- /**
54
- * The type of connection to create (pooler is recommended)
55
- */
56
- endpoint?: "pooler" | "direct";
57
- /**
58
- * Delete the test branch in afterAll (default: true)
59
- *
60
- * Disabling this will leave each test branch in the Neon project after the
61
- * test suite runs
62
- */
63
- deleteBranch?: boolean;
64
- /**
65
- * Automatically close Neon WebSocket connections opened during tests before
66
- * deleting the branch (default: false)
67
- *
68
- * Suppresses the specific Neon WebSocket "Connection terminated unexpectedly"
69
- * error that may surface when deleting a branch with open WebSocket
70
- * connections
71
- */
72
- autoCloseWebSockets?: boolean;
73
- }
74
-
75
- /** Options for overriding test database setup (excludes apiKey) */
76
- export type NeonTestingOverrides = Omit<Partial<NeonTestingOptions>, "apiKey">;
77
-
78
- /**
79
- * Factory function that creates a Neon test database setup/teardown function
80
- * for Vitest test suites.
81
- *
82
- * @param apiKey - The Neon API key, this is used to create and teardown test branches
83
- * @param projectId - The Neon project ID to operate on
84
- * @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.
85
- * @param schemaOnly - Whether to create a schema-only branch (default: false)
86
- * @param endpoint - The type of connection to create (pooler is recommended)
87
- * @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
88
- * @returns A setup/teardown function for Vitest test suites
89
- *
90
- * Side effects:
91
- * - Sets the `DATABASE_URL` environment variable to the connection URI for the
92
- * new branch
93
- * - Deletes the test branch after the test suite runs
94
- */
95
- export function makeNeonTesting(factoryOptions: NeonTestingOptions) {
96
- const apiClient = createApiClient({ apiKey: factoryOptions.apiKey });
97
-
98
- /**
99
- * Delete all test branches
100
- */
101
- async function deleteAllTestBranches() {
102
- const { data } = await apiClient.listProjectBranches({
103
- projectId: factoryOptions.projectId,
104
- });
105
-
106
- for (const branch of data.branches) {
107
- const isTestBranch =
108
- data.annotations[branch.id]?.value["integration-test"] === "true";
109
-
110
- if (isTestBranch) {
111
- await apiClient.deleteProjectBranch(
112
- factoryOptions.projectId,
113
- branch.id,
114
- );
115
- }
116
- }
117
- }
118
-
119
- const testDbSetup = (
120
- /** Override any factory options except apiKey */
121
- overrides?: NeonTestingOverrides,
122
- ) => {
123
- // Merge factory options with overrides
124
- const options = { ...factoryOptions, ...overrides };
125
-
126
- // Each test file gets its own branch ID and database client
127
- let branchId: string | undefined;
128
-
129
- // List of tracked Neon WebSocket connections
130
- const neonSockets = new Set<WebSocket>();
131
-
132
- // Custom WebSocket constructor that tracks Neon WebSocket connections
133
- class TrackingWebSocket extends WebSocket {
134
- constructor(url: string) {
135
- super(url);
136
-
137
- // Only track Neon WebSocket connections
138
- if (!url.includes(".neon.tech/")) return;
139
-
140
- neonSockets.add(this);
141
- }
142
- }
143
-
144
- /**
145
- * Create a new test branch
146
- *
147
- * @returns The connection URI for the new branch
148
- */
149
- async function createBranch() {
150
- const { data } = await apiClient.createProjectBranch(options.projectId, {
151
- branch: {
152
- name: `test/${crypto.randomUUID()}`,
153
- parent_id: options.parentBranchId,
154
- init_source: options.schemaOnly ? "schema-only" : undefined,
155
- },
156
- endpoints: [{ type: EndpointType.ReadWrite }],
157
- annotation_value: {
158
- "integration-test": "true",
159
- },
160
- });
161
-
162
- branchId = data.branch.id;
163
-
164
- const [connectionUri] = data.connection_uris ?? [];
165
-
166
- if (!connectionUri) {
167
- throw new Error("No connection URI found");
168
- }
169
-
170
- return createConnectionUri(connectionUri, options.endpoint ?? "pooler");
171
- }
172
-
173
- /**
174
- * Delete the test branch
175
- */
176
- async function deleteBranch() {
177
- if (!branchId) {
178
- throw new Error("No branch to delete");
179
- }
180
-
181
- await apiClient.deleteProjectBranch(options.projectId, branchId);
182
- branchId = undefined;
183
- }
184
-
185
- beforeAll(async () => {
186
- process.env.DATABASE_URL = await withRetry(createBranch, {
187
- maxRetries: 5,
188
- baseDelayMs: 1000,
189
- });
190
-
191
- if (options.autoCloseWebSockets) {
192
- // Install a custom WebSocket constructor that tracks Neon WebSocket
193
- // connections and closes them before deleting the branch
194
- neonConfig.webSocketConstructor = TrackingWebSocket;
195
- }
196
- });
197
-
198
- afterAll(async () => {
199
- process.env.DATABASE_URL = undefined;
200
-
201
- // Close all tracked Neon WebSocket connections before deleting the branch
202
- if (options.autoCloseWebSockets) {
203
- // Suppress Neon WebSocket "Connection terminated unexpectedly" error
204
- process.prependListener("uncaughtException", neonWsErrorHandler);
205
-
206
- // Close tracked Neon WebSocket connections before deleting the branch
207
- neonSockets.forEach((ws) => ws.close());
208
- }
209
-
210
- if (options.deleteBranch !== false) {
211
- await deleteBranch();
212
- }
213
- });
214
- };
215
-
216
- // Attach the utility
217
- testDbSetup.deleteAllTestBranches = deleteAllTestBranches;
218
-
219
- return testDbSetup;
220
- }
221
-
222
- /**
223
- * Error handler: Suppress Neon WebSocket "Connection terminated unexpectedly"
224
- * error
225
- */
226
- const neonWsErrorHandler = (error: Error) => {
227
- const isNeonWsClose =
228
- error.message.includes("Connection terminated unexpectedly") &&
229
- error.stack?.includes("@neondatabase/serverless");
230
-
231
- if (isNeonWsClose) {
232
- // Swallow this specific Neon WS termination error
233
- return;
234
- }
235
-
236
- // For any other error, detach and rethrow
237
- throw error;
238
- };
239
-
240
- /**
241
- * Reusable API call wrapper with automatic retry on 423 errors with exponential
242
- * backoff
243
- *
244
- * https://neon.com/docs/reference/typescript-sdk#error-handling
245
- * https://neon.com/docs/changelog/2022-07-20
246
- */
247
- async function withRetry<T>(
248
- fn: () => Promise<T>,
249
- options: {
250
- maxRetries: number;
251
- baseDelayMs: number;
252
- },
253
- ): Promise<T> {
254
- if (!Number.isInteger(options.maxRetries) || options.maxRetries <= 0) {
255
- throw new Error("maxRetries must be a positive integer");
256
- }
257
-
258
- if (!Number.isInteger(options.baseDelayMs) || options.baseDelayMs <= 0) {
259
- throw new Error("baseDelayMs must be a positive integer");
260
- }
261
-
262
- for (let attempt = 1; attempt <= options.maxRetries; attempt++) {
263
- try {
264
- return await fn();
265
- } catch (error: any) {
266
- const status = error?.response?.status;
267
-
268
- if (status === 423 && attempt < options.maxRetries) {
269
- const delay = options.baseDelayMs * Math.pow(2, attempt - 1);
270
-
271
- console.log(
272
- `API call failed with 423, retrying in ${delay}ms (attempt ${attempt}/${options.maxRetries})`,
273
- );
274
- await new Promise((resolve) => setTimeout(resolve, delay));
275
-
276
- continue;
277
- }
278
-
279
- throw error;
280
- }
281
- }
282
- throw new Error("apiCallWithRetry reached unexpected end");
283
- }
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/vite-plugin.ts DELETED
@@ -1,22 +0,0 @@
1
- import { fileURLToPath } from "node:url";
2
- import type { Plugin } from "vite";
3
-
4
- export function neonTesting(): Plugin {
5
- return {
6
- name: "neon-testing-plugin",
7
- enforce: "pre",
8
- config(user) {
9
- const setupPath = fileURLToPath(
10
- new URL("./vitest-setup.js", 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
- };
22
- }
File without changes