@neondatabase/config 0.0.0 → 0.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.
Files changed (90) hide show
  1. package/LICENSE.md +178 -0
  2. package/dist/index.d.ts +10 -0
  3. package/dist/index.js +8 -0
  4. package/dist/lib/auth.d.ts +63 -0
  5. package/dist/lib/auth.d.ts.map +1 -0
  6. package/dist/lib/auth.js +93 -0
  7. package/dist/lib/auth.js.map +1 -0
  8. package/dist/lib/define-config.d.ts +43 -0
  9. package/dist/lib/define-config.d.ts.map +1 -0
  10. package/dist/lib/define-config.js +111 -0
  11. package/dist/lib/define-config.js.map +1 -0
  12. package/dist/lib/diff.d.ts +109 -0
  13. package/dist/lib/diff.d.ts.map +1 -0
  14. package/dist/lib/diff.js +205 -0
  15. package/dist/lib/diff.js.map +1 -0
  16. package/dist/lib/duration.d.ts +46 -0
  17. package/dist/lib/duration.d.ts.map +1 -0
  18. package/dist/lib/duration.js +96 -0
  19. package/dist/lib/duration.js.map +1 -0
  20. package/dist/lib/errors.d.ts +129 -0
  21. package/dist/lib/errors.d.ts.map +1 -0
  22. package/dist/lib/errors.js +168 -0
  23. package/dist/lib/errors.js.map +1 -0
  24. package/dist/lib/loader.d.ts +44 -0
  25. package/dist/lib/loader.d.ts.map +1 -0
  26. package/dist/lib/loader.js +119 -0
  27. package/dist/lib/loader.js.map +1 -0
  28. package/dist/lib/neon-api-real.d.ts +45 -0
  29. package/dist/lib/neon-api-real.d.ts.map +1 -0
  30. package/dist/lib/neon-api-real.js +582 -0
  31. package/dist/lib/neon-api-real.js.map +1 -0
  32. package/dist/lib/neon-api.d.ts +262 -0
  33. package/dist/lib/neon-api.d.ts.map +1 -0
  34. package/dist/lib/neon-api.js +1 -0
  35. package/dist/lib/patterns.d.ts +43 -0
  36. package/dist/lib/patterns.d.ts.map +1 -0
  37. package/dist/lib/patterns.js +76 -0
  38. package/dist/lib/patterns.js.map +1 -0
  39. package/dist/lib/schema.d.ts +109 -0
  40. package/dist/lib/schema.d.ts.map +1 -0
  41. package/dist/lib/schema.js +199 -0
  42. package/dist/lib/schema.js.map +1 -0
  43. package/dist/lib/types.d.ts +259 -0
  44. package/dist/lib/types.d.ts.map +1 -0
  45. package/dist/lib/types.js +1 -0
  46. package/dist/lib/wrap-neon-error.d.ts +30 -0
  47. package/dist/lib/wrap-neon-error.d.ts.map +1 -0
  48. package/dist/lib/wrap-neon-error.js +139 -0
  49. package/dist/lib/wrap-neon-error.js.map +1 -0
  50. package/dist/v1.d.ts +132 -0
  51. package/dist/v1.d.ts.map +1 -0
  52. package/dist/v1.js +69 -0
  53. package/dist/v1.js.map +1 -0
  54. package/package.json +67 -17
  55. package/.env.example +0 -5
  56. package/e2e/errors.e2e.test.ts +0 -52
  57. package/e2e/helpers.ts +0 -205
  58. package/e2e/load-env.ts +0 -29
  59. package/e2e/setup.ts +0 -24
  60. package/src/index.ts +0 -5
  61. package/src/lib/auth.test.ts +0 -166
  62. package/src/lib/auth.ts +0 -124
  63. package/src/lib/define-config.test.ts +0 -161
  64. package/src/lib/define-config.ts +0 -152
  65. package/src/lib/diff.test.ts +0 -142
  66. package/src/lib/diff.ts +0 -391
  67. package/src/lib/duration.test.ts +0 -105
  68. package/src/lib/duration.ts +0 -147
  69. package/src/lib/errors.test.ts +0 -26
  70. package/src/lib/errors.ts +0 -220
  71. package/src/lib/fake-neon-api.ts +0 -782
  72. package/src/lib/loader.test.ts +0 -35
  73. package/src/lib/loader.ts +0 -215
  74. package/src/lib/neon-api-real.test.ts +0 -72
  75. package/src/lib/neon-api-real.ts +0 -1123
  76. package/src/lib/neon-api.ts +0 -356
  77. package/src/lib/patterns.test.ts +0 -80
  78. package/src/lib/patterns.ts +0 -98
  79. package/src/lib/schema.test.ts +0 -88
  80. package/src/lib/schema.ts +0 -252
  81. package/src/lib/test-utils.ts +0 -83
  82. package/src/lib/types.ts +0 -268
  83. package/src/lib/wrap-neon-error.test.ts +0 -145
  84. package/src/lib/wrap-neon-error.ts +0 -204
  85. package/src/v1.test.ts +0 -33
  86. package/src/v1.ts +0 -148
  87. package/tsconfig.json +0 -4
  88. package/tsdown.config.ts +0 -19
  89. package/vitest.config.ts +0 -19
  90. package/vitest.e2e.config.ts +0 -29
@@ -1,35 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { loadConfigFromFile } from "./loader.js";
3
- import { makeTempRepo } from "./test-utils.js";
4
-
5
- const PLATFORM_SRC = new URL("../v1.ts", import.meta.url).pathname;
6
-
7
- describe("loadConfigFromFile", () => {
8
- test("loads a neon.ts branch policy", async () => {
9
- const repo = makeTempRepo({
10
- "neon.ts": `import { defineConfig } from "${PLATFORM_SRC}"; export default defineConfig((branch) => ({ parent: branch.name === "main" ? undefined : "main" }));`,
11
- });
12
- try {
13
- const { config, resolvedPath } = await loadConfigFromFile({
14
- cwd: repo.root,
15
- });
16
- expect(resolvedPath.endsWith("neon.ts")).toBe(true);
17
- expect(config({ name: "dev", exists: false })).toEqual({
18
- parent: "main",
19
- });
20
- } finally {
21
- repo.cleanup();
22
- }
23
- });
24
-
25
- test("fails when config is missing", async () => {
26
- const repo = makeTempRepo({ "package.json": "{}" });
27
- try {
28
- await expect(
29
- loadConfigFromFile({ cwd: repo.root }),
30
- ).rejects.toThrow("Could not find");
31
- } finally {
32
- repo.cleanup();
33
- }
34
- });
35
- });
package/src/lib/loader.ts DELETED
@@ -1,215 +0,0 @@
1
- import { existsSync, statSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, isAbsolute, resolve } from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- import { defineConfig } from "./define-config.js";
6
- import { ConfigLoadError } from "./errors.js";
7
- import type { Config } from "./types.js";
8
-
9
- /**
10
- * Default file names tried (in order) when {@link loadConfigFromFile} is called without an
11
- * explicit path. We accept `.ts` first because that's the documented format; `.mjs` and `.js`
12
- * fall out for free since jiti handles all of them.
13
- */
14
- export const DEFAULT_CONFIG_FILENAMES = [
15
- "neon.ts",
16
- "neon.mts",
17
- "neon.js",
18
- "neon.mjs",
19
- ] as const;
20
-
21
- export interface LoadConfigOptions {
22
- /** Explicit absolute or cwd-relative path to a config file. Takes precedence over the search. */
23
- path?: string;
24
- /** Starting directory for the upward search. Defaults to `process.cwd()`. */
25
- cwd?: string;
26
- /**
27
- * Hard ceiling for the upward walk — once `current === stopAt` the search returns
28
- * `null` even if no `.git` boundary was hit. Defaults to the OS home directory so
29
- * stray runs from outside any repo never leak into the user's `~` files.
30
- */
31
- stopAt?: string;
32
- }
33
-
34
- /**
35
- * Load a `neon.ts` (or any other supported extension) and return the validated {@link Config}.
36
- *
37
- * Behavior:
38
- * - When `path` is set, that file is loaded directly. The file must exist and must default-export
39
- * a value produced by `defineConfig()`.
40
- * - When `path` is omitted, we walk up from `cwd` picking the **closest** file matching
41
- * {@link DEFAULT_CONFIG_FILENAMES}. The walk is monorepo-friendly: intermediate
42
- * `package.json` files do **not** stop it, so a single `neon.ts` lifted to the workspace
43
- * root keeps working when invoked from inside any sub-package. The walk terminates at the
44
- * first directory containing `.git`, at `stopAt`, or at the filesystem root.
45
- *
46
- * jiti is loaded lazily so that callers who pass an already-resolved `Config` to `pushConfig`
47
- * never pay the import cost.
48
- */
49
- export async function loadConfigFromFile(
50
- options: LoadConfigOptions = {},
51
- ): Promise<{
52
- config: Config;
53
- resolvedPath: string;
54
- }> {
55
- const resolvedPath = options.path
56
- ? resolveExplicitPath(options.path, options.cwd)
57
- : findDefaultConfig(options.cwd, options.stopAt);
58
-
59
- if (!resolvedPath) {
60
- throw new ConfigLoadError(
61
- [
62
- `Could not find a Neon config file while walking up from ${resolve(options.cwd ?? process.cwd())}.`,
63
- `Looked for: ${DEFAULT_CONFIG_FILENAMES.join(", ")} (stopping at the first directory with a \`.git\`).`,
64
- `Create one at your repository root (or anywhere on the path from cwd up to .git), or pass an explicit \`configPath\` (SDK) / \`--config <path>\` (CLI).`,
65
- ].join("\n"),
66
- );
67
- }
68
-
69
- let mod: unknown;
70
- try {
71
- mod = await importModule(resolvedPath);
72
- } catch (cause) {
73
- throw new ConfigLoadError(
74
- [
75
- `Failed to evaluate ${resolvedPath}.`,
76
- `Underlying error: ${(cause as Error)?.message ?? String(cause)}`,
77
- "This is usually a TypeScript syntax error, a missing dependency, or a runtime exception inside the config file. Run the file directly (e.g. `npx tsx neon.ts`) to reproduce.",
78
- ].join("\n"),
79
- { cause },
80
- );
81
- }
82
-
83
- const exported = extractDefaultExport(mod);
84
- if (exported === undefined) {
85
- throw new ConfigLoadError(
86
- [
87
- `${resolvedPath} loaded successfully but did not default-export a config.`,
88
- "Add `export default defineConfig({ ... })` at the bottom of the file. (Named exports are ignored.)",
89
- ].join("\n"),
90
- );
91
- }
92
-
93
- // Run through defineConfig to validate any function the user might have constructed manually.
94
- const config = defineConfig(exported as Config);
95
- return { config, resolvedPath };
96
- }
97
-
98
- function resolveExplicitPath(input: string, cwd?: string): string {
99
- const base = resolve(cwd ?? process.cwd());
100
- const abs = isAbsolute(input) ? input : resolve(base, input);
101
- if (!existsSync(abs)) {
102
- throw new ConfigLoadError(
103
- `Config file not found at ${abs}. The path was resolved from \`${input}\` against ${base}.`,
104
- );
105
- }
106
- const s = statSync(abs);
107
- if (!s.isFile()) {
108
- throw new ConfigLoadError(
109
- `Config path ${abs} is a directory, not a file. Pass a path to the file itself (e.g. ./neon.ts).`,
110
- );
111
- }
112
- return abs;
113
- }
114
-
115
- function findDefaultConfig(
116
- cwd: string | undefined,
117
- stopAt: string | undefined,
118
- ): string | null {
119
- let current = resolve(cwd ?? process.cwd());
120
- const stop = resolve(stopAt ?? homedir());
121
- let lastSeen: string | null = null;
122
-
123
- while (true) {
124
- for (const name of DEFAULT_CONFIG_FILENAMES) {
125
- const candidate = resolve(current, name);
126
- if (existsSync(candidate) && safeIsFile(candidate))
127
- return candidate;
128
- }
129
-
130
- // `.git` is the canonical repo-root marker. `package.json` is deliberately *not*
131
- // a stop: monorepos lift `neon.ts` above sub-package package.jsons.
132
- if (existsSync(resolve(current, ".git"))) return null;
133
- if (current === stop) return null;
134
-
135
- const parent = dirname(current);
136
- if (parent === current || parent === lastSeen) return null;
137
- lastSeen = current;
138
- current = parent;
139
- }
140
- }
141
-
142
- async function importModule(absPath: string): Promise<unknown> {
143
- const lower = absPath.toLowerCase();
144
- const needsJiti =
145
- lower.endsWith(".ts") ||
146
- lower.endsWith(".mts") ||
147
- lower.endsWith(".cts");
148
-
149
- if (!needsJiti) {
150
- return import(pathToFileURL(absPath).href);
151
- }
152
-
153
- const jitiModule: unknown = await import("jiti");
154
- const createJiti = extractCreateJiti(jitiModule);
155
- if (!createJiti) {
156
- throw new ConfigLoadError(
157
- [
158
- "jiti is required to load TypeScript config files but could not be initialised.",
159
- "Reinstall the package dependencies (`pnpm install` / `npm install`) — jiti is a runtime dependency of @neondatabase/config.",
160
- ].join(" "),
161
- );
162
- }
163
- const jiti = createJiti(pathToFileURL(absPath).href, {
164
- interopDefault: true,
165
- moduleCache: false,
166
- });
167
- return jiti.import(absPath);
168
- }
169
-
170
- function extractCreateJiti(
171
- mod: unknown,
172
- ): ((id: string, options?: unknown) => JitiInstance) | null {
173
- if (mod === null || typeof mod !== "object") return null;
174
- const obj = mod as Record<string, unknown>;
175
- if (typeof obj.createJiti === "function") {
176
- return obj.createJiti as (
177
- id: string,
178
- options?: unknown,
179
- ) => JitiInstance;
180
- }
181
- const def = obj.default;
182
- if (def !== null && typeof def === "object") {
183
- const defObj = def as Record<string, unknown>;
184
- if (typeof defObj.createJiti === "function") {
185
- return defObj.createJiti as (
186
- id: string,
187
- options?: unknown,
188
- ) => JitiInstance;
189
- }
190
- }
191
- return null;
192
- }
193
-
194
- interface JitiInstance {
195
- import(id: string): Promise<unknown>;
196
- }
197
-
198
- function extractDefaultExport(mod: unknown): unknown {
199
- if (mod === null || typeof mod !== "object") return mod;
200
- const obj = mod as Record<string, unknown>;
201
- if ("default" in obj && obj.default !== undefined) return obj.default;
202
- // No `default` export. If the module itself is a function, treat it as the config —
203
- // that lets tests and advanced users skip the wrapper.
204
- // Otherwise, return `undefined` so the caller surfaces a clear ConfigLoadError.
205
- if (typeof mod === "function") return mod;
206
- return undefined;
207
- }
208
-
209
- function safeIsFile(path: string): boolean {
210
- try {
211
- return statSync(path).isFile();
212
- } catch {
213
- return false;
214
- }
215
- }
@@ -1,72 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { createNeonAuthRestInput, retryOnLocked } from "./neon-api-real.js";
3
-
4
- const FAST_CONFIG = { maxAttempts: 5, initialDelayMs: 1, maxDelayMs: 4 };
5
-
6
- describe("retryOnLocked", () => {
7
- test("returns the value when the call succeeds on the first try", async () => {
8
- let calls = 0;
9
- const result = await retryOnLocked(async () => {
10
- calls += 1;
11
- return "ok";
12
- }, FAST_CONFIG);
13
- expect(result).toBe("ok");
14
- expect(calls).toBe(1);
15
- });
16
-
17
- test("retries on HTTP 423 and eventually succeeds", async () => {
18
- let calls = 0;
19
- const result = await retryOnLocked(async () => {
20
- calls += 1;
21
- if (calls < 3) {
22
- throw Object.assign(new Error("locked"), {
23
- response: { status: 423 },
24
- });
25
- }
26
- return "after-retries";
27
- }, FAST_CONFIG);
28
- expect(result).toBe("after-retries");
29
- expect(calls).toBe(3);
30
- });
31
-
32
- test("does not retry on non-423 errors", async () => {
33
- let calls = 0;
34
- await expect(
35
- retryOnLocked(async () => {
36
- calls += 1;
37
- throw Object.assign(new Error("bad request"), {
38
- response: { status: 400 },
39
- });
40
- }, FAST_CONFIG),
41
- ).rejects.toMatchObject({ message: "bad request" });
42
- expect(calls).toBe(1);
43
- });
44
-
45
- test("rethrows the last 423 after maxAttempts", async () => {
46
- let calls = 0;
47
- await expect(
48
- retryOnLocked(async () => {
49
- calls += 1;
50
- throw Object.assign(new Error("still locked"), {
51
- response: { status: 423 },
52
- });
53
- }, FAST_CONFIG),
54
- ).rejects.toMatchObject({ message: "still locked" });
55
- expect(calls).toBe(FAST_CONFIG.maxAttempts);
56
- });
57
- });
58
-
59
- describe("createNeonAuthRestInput", () => {
60
- test("uses the documented Better Auth provider value", () => {
61
- expect(createNeonAuthRestInput({})).toEqual({
62
- auth_provider: "better_auth",
63
- });
64
- });
65
-
66
- test("includes the database name when one is selected", () => {
67
- expect(createNeonAuthRestInput({ databaseName: "app" })).toEqual({
68
- auth_provider: "better_auth",
69
- database_name: "app",
70
- });
71
- });
72
- });