@neondatabase/config 0.0.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/.env.example +5 -0
- package/README.md +92 -0
- package/e2e/errors.e2e.test.ts +52 -0
- package/e2e/helpers.ts +205 -0
- package/e2e/load-env.ts +29 -0
- package/e2e/setup.ts +24 -0
- package/package.json +18 -0
- package/src/index.ts +5 -0
- package/src/lib/auth.test.ts +166 -0
- package/src/lib/auth.ts +124 -0
- package/src/lib/define-config.test.ts +161 -0
- package/src/lib/define-config.ts +152 -0
- package/src/lib/diff.test.ts +142 -0
- package/src/lib/diff.ts +391 -0
- package/src/lib/duration.test.ts +105 -0
- package/src/lib/duration.ts +147 -0
- package/src/lib/errors.test.ts +26 -0
- package/src/lib/errors.ts +220 -0
- package/src/lib/fake-neon-api.ts +782 -0
- package/src/lib/loader.test.ts +35 -0
- package/src/lib/loader.ts +215 -0
- package/src/lib/neon-api-real.test.ts +72 -0
- package/src/lib/neon-api-real.ts +1123 -0
- package/src/lib/neon-api.ts +356 -0
- package/src/lib/patterns.test.ts +80 -0
- package/src/lib/patterns.ts +98 -0
- package/src/lib/schema.test.ts +88 -0
- package/src/lib/schema.ts +252 -0
- package/src/lib/test-utils.ts +83 -0
- package/src/lib/types.ts +268 -0
- package/src/lib/wrap-neon-error.test.ts +145 -0
- package/src/lib/wrap-neon-error.ts +204 -0
- package/src/v1.test.ts +33 -0
- package/src/v1.ts +148 -0
- package/tsconfig.json +4 -0
- package/tsdown.config.ts +19 -0
- package/vitest.config.ts +19 -0
- package/vitest.e2e.config.ts +29 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { ConflictReport } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Every code a {@link PlatformError} can carry. Stable identifiers — consumers can rely on
|
|
5
|
+
* these for `instanceof PlatformError && err.code === ErrorCode.…` style checks instead of
|
|
6
|
+
* matching on free-text messages.
|
|
7
|
+
*
|
|
8
|
+
* Grouped by source:
|
|
9
|
+
* - `PLATFORM_INVALID_CONFIG` — `defineConfig` / `configSchema` rejected the input.
|
|
10
|
+
* - `PLATFORM_MISSING_CONTEXT` — no project / branch context could be resolved.
|
|
11
|
+
* - `PLATFORM_PUSH_CONFLICT` — local config conflicts with remote and the caller did not
|
|
12
|
+
* opt in to apply.
|
|
13
|
+
* - `PLATFORM_CONFIG_LOAD_FAILED` — `neon.ts` could not be found or evaluated.
|
|
14
|
+
* - `PLATFORM_MISSING_API_KEY` — no `NEON_API_KEY` and no explicit `apiKey` was provided.
|
|
15
|
+
* - `PLATFORM_MISSING_PARENT_BRANCH` — push tried to create a child of a non-existent
|
|
16
|
+
* branch.
|
|
17
|
+
* - `PLATFORM_UNAUTHORIZED` / `PLATFORM_FORBIDDEN` / `PLATFORM_NOT_FOUND` /
|
|
18
|
+
* `PLATFORM_CONFLICT` / `PLATFORM_RATE_LIMITED` / `PLATFORM_LOCKED` /
|
|
19
|
+
* `PLATFORM_SERVER_ERROR` — wrappings of Neon HTTP failures.
|
|
20
|
+
* - `PLATFORM_NETWORK_ERROR` — transport-level failure (no HTTP response at all).
|
|
21
|
+
* - `PLATFORM_INTERNAL_ERROR` — invariant violations. Should never happen in production;
|
|
22
|
+
* if you see one, please open an issue.
|
|
23
|
+
*/
|
|
24
|
+
export const ErrorCode = {
|
|
25
|
+
InvalidConfig: "PLATFORM_INVALID_CONFIG",
|
|
26
|
+
EnvNotInjected: "PLATFORM_ENV_NOT_INJECTED",
|
|
27
|
+
MissingContext: "PLATFORM_MISSING_CONTEXT",
|
|
28
|
+
PushConflict: "PLATFORM_PUSH_CONFLICT",
|
|
29
|
+
PushAborted: "PLATFORM_PUSH_ABORTED",
|
|
30
|
+
ConfigLoadFailed: "PLATFORM_CONFIG_LOAD_FAILED",
|
|
31
|
+
MissingApiKey: "PLATFORM_MISSING_API_KEY",
|
|
32
|
+
AmbiguousBranchAuth: "PLATFORM_AMBIGUOUS_BRANCH_AUTH",
|
|
33
|
+
BranchNotFound: "PLATFORM_BRANCH_NOT_FOUND",
|
|
34
|
+
MissingParentBranch: "PLATFORM_MISSING_PARENT_BRANCH",
|
|
35
|
+
Unauthorized: "PLATFORM_UNAUTHORIZED",
|
|
36
|
+
Forbidden: "PLATFORM_FORBIDDEN",
|
|
37
|
+
NotFound: "PLATFORM_NOT_FOUND",
|
|
38
|
+
Conflict: "PLATFORM_CONFLICT",
|
|
39
|
+
RateLimited: "PLATFORM_RATE_LIMITED",
|
|
40
|
+
Locked: "PLATFORM_LOCKED",
|
|
41
|
+
ServerError: "PLATFORM_SERVER_ERROR",
|
|
42
|
+
NetworkError: "PLATFORM_NETWORK_ERROR",
|
|
43
|
+
InternalError: "PLATFORM_INTERNAL_ERROR",
|
|
44
|
+
} as const;
|
|
45
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
46
|
+
|
|
47
|
+
const ISSUE_URL = "https://github.com/neondatabase/neon-pkgs/issues/new";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Base class for all errors thrown by `@neondatabase/config`. Always extend this so callers
|
|
51
|
+
* can catch every package-thrown error with a single `instanceof` check.
|
|
52
|
+
*
|
|
53
|
+
* Optional `details` carries structured context that the CLI prints under `--debug` and
|
|
54
|
+
* that programmatic consumers can read (e.g. `details.status` for HTTP wrappings,
|
|
55
|
+
* `details.requestId` for Neon API failures).
|
|
56
|
+
*/
|
|
57
|
+
export class PlatformError extends Error {
|
|
58
|
+
override readonly name: string = "PlatformError";
|
|
59
|
+
readonly code: string;
|
|
60
|
+
readonly details: Readonly<Record<string, unknown>>;
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
code: string,
|
|
64
|
+
message: string,
|
|
65
|
+
options?: { cause?: unknown; details?: Record<string, unknown> },
|
|
66
|
+
) {
|
|
67
|
+
super(message, options);
|
|
68
|
+
this.code = code;
|
|
69
|
+
this.details = Object.freeze({ ...(options?.details ?? {}) });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Append a "report-a-bug" footer to an error message. Used only on truly unreachable
|
|
75
|
+
* internal errors — never on user-facing validation / configuration errors where the user
|
|
76
|
+
* is supposed to fix something on their end.
|
|
77
|
+
*/
|
|
78
|
+
export function bugReportFooter(): string {
|
|
79
|
+
return `\nThis indicates a bug in @neondatabase/config. Please file an issue: ${ISSUE_URL}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Thrown by {@link defineConfig} when the user-provided configuration object is invalid.
|
|
84
|
+
*
|
|
85
|
+
* The class collects every validation failure rather than throwing on the first one so that
|
|
86
|
+
* users get a complete picture of what is wrong with their `neon.ts`.
|
|
87
|
+
*/
|
|
88
|
+
export class ConfigValidationError extends PlatformError {
|
|
89
|
+
override readonly name = "ConfigValidationError";
|
|
90
|
+
readonly issues: readonly string[];
|
|
91
|
+
|
|
92
|
+
constructor(issues: readonly string[]) {
|
|
93
|
+
super(
|
|
94
|
+
"PLATFORM_INVALID_CONFIG",
|
|
95
|
+
`Invalid Neon platform config:\n - ${issues.join("\n - ")}`,
|
|
96
|
+
);
|
|
97
|
+
this.issues = issues;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Thrown when the package cannot resolve which Neon project to operate on.
|
|
103
|
+
*
|
|
104
|
+
* Per the package's read-only-filesystem contract, we never create a `.neon` context file;
|
|
105
|
+
* callers must either pass `projectId`/`orgId` explicitly or rely on an existing context file
|
|
106
|
+
* (`.neon/project.json` or neonctl's `.neon`).
|
|
107
|
+
*/
|
|
108
|
+
export class MissingContextError extends PlatformError {
|
|
109
|
+
override readonly name = "MissingContextError";
|
|
110
|
+
|
|
111
|
+
constructor(message: string) {
|
|
112
|
+
super("PLATFORM_MISSING_CONTEXT", message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Thrown by {@link pushConfig} when it detects differences between the local config and
|
|
118
|
+
* the remote project that the caller hasn't opted in to apply.
|
|
119
|
+
*
|
|
120
|
+
* The message lists every conflict with both the current and desired value plus a
|
|
121
|
+
* per-conflict hint. Mutable branch drift is applied by passing `updateExisting: true`.
|
|
122
|
+
*/
|
|
123
|
+
export class PushConflictError extends PlatformError {
|
|
124
|
+
override readonly name = "PushConflictError";
|
|
125
|
+
readonly conflicts: readonly ConflictReport[];
|
|
126
|
+
|
|
127
|
+
constructor(conflicts: readonly ConflictReport[]) {
|
|
128
|
+
const lines: string[] = [
|
|
129
|
+
"pushConfig refused to apply: local config conflicts with remote state.",
|
|
130
|
+
"",
|
|
131
|
+
];
|
|
132
|
+
for (const c of conflicts) {
|
|
133
|
+
lines.push(
|
|
134
|
+
` - [${c.kind}:${c.identifier}] ${c.field}: ${c.reason}`,
|
|
135
|
+
` current : ${formatValue(c.current)}`,
|
|
136
|
+
` desired : ${formatValue(c.desired)}`,
|
|
137
|
+
` fix : ${suggestFix(c)}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const hasMutable = conflicts.some((c) => !isImmutableConflict(c));
|
|
141
|
+
lines.push("");
|
|
142
|
+
if (hasMutable) {
|
|
143
|
+
lines.push(
|
|
144
|
+
"For mutable conflicts, pass `updateExisting: true` (SDK) / `--update-existing` (CLI) to apply.",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
super("PLATFORM_PUSH_CONFLICT", lines.join("\n"), {
|
|
149
|
+
details: { conflicts: conflicts.map((c) => ({ ...c })) },
|
|
150
|
+
});
|
|
151
|
+
this.conflicts = conflicts;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isImmutableConflict(_c: ConflictReport): boolean {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function suggestFix(c: ConflictReport): string {
|
|
160
|
+
if (isImmutableConflict(c)) {
|
|
161
|
+
return "immutable on Neon — recreate the project, or change your `neon.ts` to match the remote.";
|
|
162
|
+
}
|
|
163
|
+
if (c.kind === "branch" && c.field === "parent") {
|
|
164
|
+
return "create the parent branch on Neon first, or change the `parent` reference to an existing branch.";
|
|
165
|
+
}
|
|
166
|
+
return "pass `updateExisting: true` (SDK) / `--update-existing` (CLI) to apply.";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Thrown by {@link pushConfig} when the caller-supplied `confirm` callback declines a
|
|
171
|
+
* push that requires confirmation (protected branch and/or mutable drift overriding
|
|
172
|
+
* existing remote settings).
|
|
173
|
+
*
|
|
174
|
+
* The CLI maps this to a non-zero exit so users see "aborted" rather than a stack trace.
|
|
175
|
+
*/
|
|
176
|
+
export class PushAbortedError extends PlatformError {
|
|
177
|
+
override readonly name = "PushAbortedError";
|
|
178
|
+
readonly branchName: string;
|
|
179
|
+
readonly reasons: readonly ("protected-branch" | "override-updates")[];
|
|
180
|
+
|
|
181
|
+
constructor(
|
|
182
|
+
branchName: string,
|
|
183
|
+
reasons: readonly ("protected-branch" | "override-updates")[],
|
|
184
|
+
) {
|
|
185
|
+
super(
|
|
186
|
+
"PLATFORM_PUSH_ABORTED",
|
|
187
|
+
[
|
|
188
|
+
`Aborted push to branch ${JSON.stringify(branchName)}.`,
|
|
189
|
+
reasons.length > 0
|
|
190
|
+
? `Reason${reasons.length === 1 ? "" : "s"}: ${reasons.join(", ")}.`
|
|
191
|
+
: undefined,
|
|
192
|
+
"Re-run with `--update-existing` (override existing settings) or `--allow-protected-branch` (push to a protected branch) to skip the prompt.",
|
|
193
|
+
]
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.join(" "),
|
|
196
|
+
{ details: { branchName, reasons: [...reasons] } },
|
|
197
|
+
);
|
|
198
|
+
this.branchName = branchName;
|
|
199
|
+
this.reasons = reasons;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Thrown when the SDK fails to find or load a `neon.ts` config file.
|
|
205
|
+
*/
|
|
206
|
+
export class ConfigLoadError extends PlatformError {
|
|
207
|
+
override readonly name = "ConfigLoadError";
|
|
208
|
+
|
|
209
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
210
|
+
super("PLATFORM_CONFIG_LOAD_FAILED", message, options);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function formatValue(value: unknown): string {
|
|
215
|
+
if (value === undefined) return "<unset>";
|
|
216
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
217
|
+
if (typeof value === "object" && value !== null)
|
|
218
|
+
return JSON.stringify(value);
|
|
219
|
+
return String(value);
|
|
220
|
+
}
|