@neondatabase/config 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +178 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/lib/auth.d.ts +63 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +93 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/define-config.d.ts +43 -0
- package/dist/lib/define-config.d.ts.map +1 -0
- package/dist/lib/define-config.js +112 -0
- package/dist/lib/define-config.js.map +1 -0
- package/dist/lib/diff.d.ts +109 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/diff.js +205 -0
- package/dist/lib/diff.js.map +1 -0
- package/dist/lib/duration.d.ts +46 -0
- package/dist/lib/duration.d.ts.map +1 -0
- package/dist/lib/duration.js +96 -0
- package/dist/lib/duration.js.map +1 -0
- package/dist/lib/errors.d.ts +129 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +168 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/loader.d.ts +44 -0
- package/dist/lib/loader.d.ts.map +1 -0
- package/dist/lib/loader.js +119 -0
- package/dist/lib/loader.js.map +1 -0
- package/dist/lib/neon-api-real.d.ts +45 -0
- package/dist/lib/neon-api-real.d.ts.map +1 -0
- package/dist/lib/neon-api-real.js +582 -0
- package/dist/lib/neon-api-real.js.map +1 -0
- package/dist/lib/neon-api.d.ts +262 -0
- package/dist/lib/neon-api.d.ts.map +1 -0
- package/dist/lib/neon-api.js +1 -0
- package/dist/lib/patterns.d.ts +43 -0
- package/dist/lib/patterns.d.ts.map +1 -0
- package/dist/lib/patterns.js +76 -0
- package/dist/lib/patterns.js.map +1 -0
- package/dist/lib/schema.d.ts +130 -0
- package/dist/lib/schema.d.ts.map +1 -0
- package/dist/lib/schema.js +218 -0
- package/dist/lib/schema.js.map +1 -0
- package/dist/lib/types.d.ts +289 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/wrap-neon-error.d.ts +30 -0
- package/dist/lib/wrap-neon-error.d.ts.map +1 -0
- package/dist/lib/wrap-neon-error.js +139 -0
- package/dist/lib/wrap-neon-error.js.map +1 -0
- package/dist/v1.d.ts +153 -0
- package/dist/v1.d.ts.map +1 -0
- package/dist/v1.js +69 -0
- package/dist/v1.js.map +1 -0
- package/package.json +67 -17
- package/.env.example +0 -5
- package/e2e/errors.e2e.test.ts +0 -52
- package/e2e/helpers.ts +0 -205
- package/e2e/load-env.ts +0 -29
- package/e2e/setup.ts +0 -24
- package/src/index.ts +0 -5
- package/src/lib/auth.test.ts +0 -166
- package/src/lib/auth.ts +0 -124
- package/src/lib/define-config.test.ts +0 -161
- package/src/lib/define-config.ts +0 -152
- package/src/lib/diff.test.ts +0 -142
- package/src/lib/diff.ts +0 -391
- package/src/lib/duration.test.ts +0 -105
- package/src/lib/duration.ts +0 -147
- package/src/lib/errors.test.ts +0 -26
- package/src/lib/errors.ts +0 -220
- package/src/lib/fake-neon-api.ts +0 -782
- package/src/lib/loader.test.ts +0 -35
- package/src/lib/loader.ts +0 -215
- package/src/lib/neon-api-real.test.ts +0 -72
- package/src/lib/neon-api-real.ts +0 -1123
- package/src/lib/neon-api.ts +0 -356
- package/src/lib/patterns.test.ts +0 -80
- package/src/lib/patterns.ts +0 -98
- package/src/lib/schema.test.ts +0 -88
- package/src/lib/schema.ts +0 -252
- package/src/lib/test-utils.ts +0 -83
- package/src/lib/types.ts +0 -268
- package/src/lib/wrap-neon-error.test.ts +0 -145
- package/src/lib/wrap-neon-error.ts +0 -204
- package/src/v1.test.ts +0 -33
- package/src/v1.ts +0 -148
- package/tsconfig.json +0 -4
- package/tsdown.config.ts +0 -19
- package/vitest.config.ts +0 -19
- package/vitest.e2e.config.ts +0 -29
package/src/lib/loader.test.ts
DELETED
|
@@ -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
|
-
});
|