@neondatabase/env 0.0.0 → 0.1.1
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/cli.d.ts +1 -0
- package/dist/cli.js +61 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/dist/lib/neon-api.d.ts +264 -0
- package/dist/config/dist/lib/neon-api.d.ts.map +1 -0
- package/dist/config/dist/lib/types.d.ts +209 -0
- package/dist/config/dist/lib/types.d.ts.map +1 -0
- package/dist/config/dist/v1.d.ts +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/lib/cli/commands.d.ts +46 -0
- package/dist/lib/cli/commands.d.ts.map +1 -0
- package/dist/lib/cli/commands.js +181 -0
- package/dist/lib/cli/commands.js.map +1 -0
- package/dist/lib/cli/resolve-context.d.ts +35 -0
- package/dist/lib/cli/resolve-context.d.ts.map +1 -0
- package/dist/lib/cli/resolve-context.js +90 -0
- package/dist/lib/cli/resolve-context.js.map +1 -0
- package/dist/lib/env.d.ts +194 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +263 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/v1.d.ts +2 -0
- package/dist/v1.js +2 -0
- package/package.json +72 -21
- package/.env.example +0 -5
- package/e2e/env.e2e.test.ts +0 -36
- package/e2e/helpers.ts +0 -188
- package/e2e/load-env.ts +0 -29
- package/e2e/setup.ts +0 -24
- package/src/cli.ts +0 -107
- package/src/index.ts +0 -5
- package/src/lib/cli/commands.test.ts +0 -101
- package/src/lib/cli/commands.ts +0 -267
- package/src/lib/cli/resolve-context.test.ts +0 -242
- package/src/lib/cli/resolve-context.ts +0 -142
- package/src/lib/env.test.ts +0 -172
- package/src/lib/env.ts +0 -610
- package/src/lib/fake-neon-api.ts +0 -782
- package/src/lib/test-utils.ts +0 -83
- package/src/v1.ts +0 -32
- package/tsconfig.json +0 -4
- package/tsdown.config.ts +0 -20
- package/vitest.config.ts +0 -19
- package/vitest.e2e.config.ts +0 -29
package/src/lib/fake-neon-api.ts
DELETED
|
@@ -1,782 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ComputeSettings,
|
|
3
|
-
CreateBranchInput,
|
|
4
|
-
CreateBucketInput,
|
|
5
|
-
CreateProjectInput,
|
|
6
|
-
DeployFunctionInput,
|
|
7
|
-
GetConnectionUriInput,
|
|
8
|
-
NeonApi,
|
|
9
|
-
NeonAuthSnapshot,
|
|
10
|
-
NeonBranchSnapshot,
|
|
11
|
-
NeonBucketSnapshot,
|
|
12
|
-
NeonDataApiSnapshot,
|
|
13
|
-
NeonDatabaseSnapshot,
|
|
14
|
-
NeonEndpointSnapshot,
|
|
15
|
-
NeonFunctionDeploymentSnapshot,
|
|
16
|
-
NeonFunctionSnapshot,
|
|
17
|
-
NeonProjectSnapshot,
|
|
18
|
-
NeonRoleSnapshot,
|
|
19
|
-
UpdateBranchInput,
|
|
20
|
-
} from "@neondatabase/config/v1";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Test-only branch seed shape. Permits omitting `protected` (defaults to `false`) so the
|
|
24
|
-
* many tests that pre-date the field don't have to spell it out on every entry.
|
|
25
|
-
*/
|
|
26
|
-
type SeedBranch = Omit<NeonBranchSnapshot, "protected"> & {
|
|
27
|
-
protected?: boolean;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* In-memory NeonApi implementation used by tests. **Not** exported from `dist/`.
|
|
32
|
-
*
|
|
33
|
-
* Models the subset of Neon's data model that {@link Config} actually exercises:
|
|
34
|
-
*
|
|
35
|
-
* - Each project has a single read-write endpoint per branch (Neon's default), an `orgId`
|
|
36
|
-
* on the project, default endpoint settings, and a fixed region.
|
|
37
|
-
* - Branches inherit endpoint defaults from the project when no explicit settings are set
|
|
38
|
-
* at create-time. This mirrors the real Neon platform.
|
|
39
|
-
*
|
|
40
|
-
* The fake records a call log (`history`) so tests can assert on the exact sequence of API
|
|
41
|
-
* operations that `pushConfig` performs.
|
|
42
|
-
*/
|
|
43
|
-
export class FakeNeonApi implements NeonApi {
|
|
44
|
-
private nextId = 1;
|
|
45
|
-
private readonly projects = new Map<string, NeonProjectSnapshot>();
|
|
46
|
-
private readonly branches = new Map<string, NeonBranchSnapshot[]>();
|
|
47
|
-
private readonly endpoints = new Map<string, NeonEndpointSnapshot[]>();
|
|
48
|
-
private readonly roles = new Map<string, NeonRoleSnapshot[]>();
|
|
49
|
-
private readonly databases = new Map<string, NeonDatabaseSnapshot[]>();
|
|
50
|
-
/** Keyed by `${projectId}:${branchId}` so a project can have per-branch integrations. */
|
|
51
|
-
private readonly neonAuth = new Map<string, NeonAuthSnapshot>();
|
|
52
|
-
/** Keyed by `${projectId}:${branchId}:${databaseName}`. */
|
|
53
|
-
private readonly neonDataApi = new Map<string, NeonDataApiSnapshot>();
|
|
54
|
-
/** Preview buckets, keyed by `${projectId}:${branchId}`. */
|
|
55
|
-
private readonly buckets = new Map<string, NeonBucketSnapshot[]>();
|
|
56
|
-
/** Preview functions, keyed by `${projectId}:${branchId}`. */
|
|
57
|
-
private readonly functions = new Map<string, NeonFunctionSnapshot[]>();
|
|
58
|
-
/** Monotonic per-function deployment counter, keyed by `${projectId}:${branchId}:${slug}`. */
|
|
59
|
-
private readonly functionDeployments = new Map<string, number>();
|
|
60
|
-
/** AI Gateway enabled set, keyed by `${projectId}:${branchId}`. */
|
|
61
|
-
private readonly aiGateway = new Set<string>();
|
|
62
|
-
readonly history: Array<{ method: string; args: unknown[] }> = [];
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Seed the fake with a fully-formed project (and optionally extra branches), bypassing
|
|
66
|
-
* the public mutation API. Used by tests that want to assert on diff/update behaviour
|
|
67
|
-
* without first calling `createProject`.
|
|
68
|
-
*
|
|
69
|
-
* Each branch is seeded with a default `neondb_owner` role and `neondb` database unless
|
|
70
|
-
* overridden — matching what Neon's real `createProject` does. Pass `roles` / `databases`
|
|
71
|
-
* on a branch entry to model non-default setups (custom roles, multiple databases).
|
|
72
|
-
*/
|
|
73
|
-
seedProject(input: {
|
|
74
|
-
project: NeonProjectSnapshot;
|
|
75
|
-
branches?: Array<{
|
|
76
|
-
branch: SeedBranch;
|
|
77
|
-
endpoint?: Partial<NeonEndpointSnapshot>;
|
|
78
|
-
roles?: Array<Partial<NeonRoleSnapshot> & { name: string }>;
|
|
79
|
-
databases?: Array<Partial<NeonDatabaseSnapshot> & { name: string }>;
|
|
80
|
-
}>;
|
|
81
|
-
}): void {
|
|
82
|
-
const project = { ...input.project };
|
|
83
|
-
this.projects.set(project.id, project);
|
|
84
|
-
const branches: NeonBranchSnapshot[] = [];
|
|
85
|
-
const endpoints: NeonEndpointSnapshot[] = [];
|
|
86
|
-
|
|
87
|
-
for (const entry of input.branches ?? []) {
|
|
88
|
-
branches.push({ protected: false, ...entry.branch });
|
|
89
|
-
endpoints.push(
|
|
90
|
-
this.makeEndpoint(entry.branch.id, entry.endpoint, project),
|
|
91
|
-
);
|
|
92
|
-
this.seedBranchAuth(entry.branch.id, entry.roles, entry.databases);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (branches.length === 0) {
|
|
96
|
-
// Auto-create a default `production` branch so tests can rely on it existing.
|
|
97
|
-
const defaultBranch: NeonBranchSnapshot = {
|
|
98
|
-
id: this.allocateId("br"),
|
|
99
|
-
name: "production",
|
|
100
|
-
isDefault: true,
|
|
101
|
-
protected: false,
|
|
102
|
-
};
|
|
103
|
-
branches.push(defaultBranch);
|
|
104
|
-
endpoints.push(
|
|
105
|
-
this.makeEndpoint(defaultBranch.id, undefined, project),
|
|
106
|
-
);
|
|
107
|
-
this.seedBranchAuth(defaultBranch.id);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
this.branches.set(project.id, branches);
|
|
111
|
-
this.endpoints.set(project.id, endpoints);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
private seedBranchAuth(
|
|
115
|
-
branchId: string,
|
|
116
|
-
roles?: Array<Partial<NeonRoleSnapshot> & { name: string }>,
|
|
117
|
-
databases?: Array<Partial<NeonDatabaseSnapshot> & { name: string }>,
|
|
118
|
-
): void {
|
|
119
|
-
const resolvedRoles: NeonRoleSnapshot[] = (
|
|
120
|
-
roles ?? [{ name: "neondb_owner" }]
|
|
121
|
-
).map((r) => ({
|
|
122
|
-
name: r.name,
|
|
123
|
-
branchId,
|
|
124
|
-
protected: r.protected ?? false,
|
|
125
|
-
}));
|
|
126
|
-
this.roles.set(branchId, resolvedRoles);
|
|
127
|
-
|
|
128
|
-
const resolvedDatabases: NeonDatabaseSnapshot[] = (
|
|
129
|
-
databases ?? [{ name: "neondb" }]
|
|
130
|
-
).map((d) => ({
|
|
131
|
-
name: d.name,
|
|
132
|
-
branchId,
|
|
133
|
-
ownerName: d.ownerName ?? resolvedRoles[0]?.name ?? "neondb_owner",
|
|
134
|
-
}));
|
|
135
|
-
this.databases.set(branchId, resolvedDatabases);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async listProjects(filter: {
|
|
139
|
-
orgId?: string;
|
|
140
|
-
}): Promise<NeonProjectSnapshot[]> {
|
|
141
|
-
this.history.push({ method: "listProjects", args: [filter] });
|
|
142
|
-
const all = Array.from(this.projects.values());
|
|
143
|
-
if (filter.orgId !== undefined) {
|
|
144
|
-
return all.filter((p) => p.orgId === filter.orgId).map(clone);
|
|
145
|
-
}
|
|
146
|
-
return all.map(clone);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async getProject(projectId: string): Promise<NeonProjectSnapshot> {
|
|
150
|
-
this.history.push({ method: "getProject", args: [projectId] });
|
|
151
|
-
const found = this.projects.get(projectId);
|
|
152
|
-
if (!found)
|
|
153
|
-
throw new Error(`Fake Neon: project ${projectId} not found`);
|
|
154
|
-
return clone(found);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async createProject(
|
|
158
|
-
input: CreateProjectInput,
|
|
159
|
-
): Promise<NeonProjectSnapshot> {
|
|
160
|
-
this.history.push({ method: "createProject", args: [input] });
|
|
161
|
-
const id = this.allocateId("proj");
|
|
162
|
-
const project: NeonProjectSnapshot = {
|
|
163
|
-
id,
|
|
164
|
-
name: input.name,
|
|
165
|
-
regionId: input.regionId,
|
|
166
|
-
pgVersion: input.pgVersion ?? 17,
|
|
167
|
-
};
|
|
168
|
-
if (input.orgId) project.orgId = input.orgId;
|
|
169
|
-
if (input.defaultEndpointSettings)
|
|
170
|
-
project.defaultEndpointSettings = {
|
|
171
|
-
...input.defaultEndpointSettings,
|
|
172
|
-
};
|
|
173
|
-
this.projects.set(id, project);
|
|
174
|
-
|
|
175
|
-
const defaultBranch: NeonBranchSnapshot = {
|
|
176
|
-
id: this.allocateId("br"),
|
|
177
|
-
name: input.defaultBranchName ?? "main",
|
|
178
|
-
isDefault: true,
|
|
179
|
-
protected: false,
|
|
180
|
-
};
|
|
181
|
-
const defaultEndpoint = this.makeEndpoint(
|
|
182
|
-
defaultBranch.id,
|
|
183
|
-
undefined,
|
|
184
|
-
project,
|
|
185
|
-
);
|
|
186
|
-
this.branches.set(id, [defaultBranch]);
|
|
187
|
-
this.endpoints.set(id, [defaultEndpoint]);
|
|
188
|
-
this.seedBranchAuth(defaultBranch.id);
|
|
189
|
-
|
|
190
|
-
return clone(project);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async updateProject(
|
|
194
|
-
projectId: string,
|
|
195
|
-
input: { name?: string; defaultEndpointSettings?: ComputeSettings },
|
|
196
|
-
): Promise<NeonProjectSnapshot> {
|
|
197
|
-
this.history.push({
|
|
198
|
-
method: "updateProject",
|
|
199
|
-
args: [projectId, input],
|
|
200
|
-
});
|
|
201
|
-
const existing = this.projects.get(projectId);
|
|
202
|
-
if (!existing)
|
|
203
|
-
throw new Error(`Fake Neon: project ${projectId} not found`);
|
|
204
|
-
const updated: NeonProjectSnapshot = { ...existing };
|
|
205
|
-
if (input.name !== undefined) updated.name = input.name;
|
|
206
|
-
if (input.defaultEndpointSettings)
|
|
207
|
-
updated.defaultEndpointSettings = {
|
|
208
|
-
...input.defaultEndpointSettings,
|
|
209
|
-
};
|
|
210
|
-
this.projects.set(projectId, updated);
|
|
211
|
-
return clone(updated);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async listBranches(projectId: string): Promise<NeonBranchSnapshot[]> {
|
|
215
|
-
this.history.push({ method: "listBranches", args: [projectId] });
|
|
216
|
-
return (this.branches.get(projectId) ?? []).map(clone);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async createBranch(
|
|
220
|
-
projectId: string,
|
|
221
|
-
input: CreateBranchInput,
|
|
222
|
-
): Promise<{
|
|
223
|
-
branch: NeonBranchSnapshot;
|
|
224
|
-
endpoints: NeonEndpointSnapshot[];
|
|
225
|
-
}> {
|
|
226
|
-
this.history.push({ method: "createBranch", args: [projectId, input] });
|
|
227
|
-
const project = this.projects.get(projectId);
|
|
228
|
-
if (!project)
|
|
229
|
-
throw new Error(`Fake Neon: project ${projectId} not found`);
|
|
230
|
-
const branchList = this.branches.get(projectId) ?? [];
|
|
231
|
-
|
|
232
|
-
if (branchList.some((b) => b.name === input.name)) {
|
|
233
|
-
throw new Error(
|
|
234
|
-
`Fake Neon: branch '${input.name}' already exists in project ${projectId}`,
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const branch: NeonBranchSnapshot = {
|
|
239
|
-
id: this.allocateId("br"),
|
|
240
|
-
name: input.name,
|
|
241
|
-
isDefault: false,
|
|
242
|
-
protected: input.protected === true,
|
|
243
|
-
};
|
|
244
|
-
if (input.parentId) branch.parentId = input.parentId;
|
|
245
|
-
else if (branchList[0])
|
|
246
|
-
branch.parentId =
|
|
247
|
-
branchList.find((b) => b.isDefault)?.id ?? branchList[0].id;
|
|
248
|
-
if (input.expiresAt) branch.expiresAt = input.expiresAt;
|
|
249
|
-
|
|
250
|
-
branchList.push(branch);
|
|
251
|
-
this.branches.set(projectId, branchList);
|
|
252
|
-
|
|
253
|
-
const endpointSettings =
|
|
254
|
-
input.computeSettings ?? project.defaultEndpointSettings;
|
|
255
|
-
const endpoint = this.makeEndpoint(
|
|
256
|
-
branch.id,
|
|
257
|
-
endpointSettings,
|
|
258
|
-
project,
|
|
259
|
-
);
|
|
260
|
-
const endpoints = this.endpoints.get(projectId) ?? [];
|
|
261
|
-
endpoints.push(endpoint);
|
|
262
|
-
this.endpoints.set(projectId, endpoints);
|
|
263
|
-
|
|
264
|
-
// Inherit the roles/databases from the parent branch — Neon does the same
|
|
265
|
-
// (every branch starts as a copy-on-write clone of its parent).
|
|
266
|
-
const parentRoles =
|
|
267
|
-
(branch.parentId ? this.roles.get(branch.parentId) : undefined) ??
|
|
268
|
-
[];
|
|
269
|
-
const parentDatabases =
|
|
270
|
-
(branch.parentId
|
|
271
|
-
? this.databases.get(branch.parentId)
|
|
272
|
-
: undefined) ?? [];
|
|
273
|
-
this.seedBranchAuth(
|
|
274
|
-
branch.id,
|
|
275
|
-
parentRoles.length > 0 ? parentRoles : undefined,
|
|
276
|
-
parentDatabases.length > 0 ? parentDatabases : undefined,
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
return { branch: clone(branch), endpoints: [clone(endpoint)] };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async updateBranch(
|
|
283
|
-
projectId: string,
|
|
284
|
-
branchId: string,
|
|
285
|
-
input: UpdateBranchInput,
|
|
286
|
-
): Promise<NeonBranchSnapshot> {
|
|
287
|
-
this.history.push({
|
|
288
|
-
method: "updateBranch",
|
|
289
|
-
args: [projectId, branchId, input],
|
|
290
|
-
});
|
|
291
|
-
const branchList = this.branches.get(projectId);
|
|
292
|
-
if (!branchList)
|
|
293
|
-
throw new Error(`Fake Neon: project ${projectId} not found`);
|
|
294
|
-
const idx = branchList.findIndex((b) => b.id === branchId);
|
|
295
|
-
if (idx === -1)
|
|
296
|
-
throw new Error(`Fake Neon: branch ${branchId} not found`);
|
|
297
|
-
const current = branchList[idx];
|
|
298
|
-
const updated: NeonBranchSnapshot = { ...current };
|
|
299
|
-
if (input.name !== undefined) updated.name = input.name;
|
|
300
|
-
if (input.expiresAt === null) delete updated.expiresAt;
|
|
301
|
-
else if (input.expiresAt !== undefined)
|
|
302
|
-
updated.expiresAt = input.expiresAt;
|
|
303
|
-
if (input.protected !== undefined) updated.protected = input.protected;
|
|
304
|
-
branchList[idx] = updated;
|
|
305
|
-
return clone(updated);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async listEndpoints(projectId: string): Promise<NeonEndpointSnapshot[]> {
|
|
309
|
-
this.history.push({ method: "listEndpoints", args: [projectId] });
|
|
310
|
-
return (this.endpoints.get(projectId) ?? []).map(clone);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async updateEndpoint(
|
|
314
|
-
projectId: string,
|
|
315
|
-
endpointId: string,
|
|
316
|
-
settings: ComputeSettings,
|
|
317
|
-
): Promise<NeonEndpointSnapshot> {
|
|
318
|
-
this.history.push({
|
|
319
|
-
method: "updateEndpoint",
|
|
320
|
-
args: [projectId, endpointId, settings],
|
|
321
|
-
});
|
|
322
|
-
const endpoints = this.endpoints.get(projectId);
|
|
323
|
-
if (!endpoints)
|
|
324
|
-
throw new Error(`Fake Neon: project ${projectId} not found`);
|
|
325
|
-
const idx = endpoints.findIndex((e) => e.id === endpointId);
|
|
326
|
-
if (idx === -1)
|
|
327
|
-
throw new Error(`Fake Neon: endpoint ${endpointId} not found`);
|
|
328
|
-
const updated: NeonEndpointSnapshot = { ...endpoints[idx] };
|
|
329
|
-
if (settings.autoscalingLimitMinCu !== undefined)
|
|
330
|
-
updated.autoscalingLimitMinCu = settings.autoscalingLimitMinCu;
|
|
331
|
-
if (settings.autoscalingLimitMaxCu !== undefined)
|
|
332
|
-
updated.autoscalingLimitMaxCu = settings.autoscalingLimitMaxCu;
|
|
333
|
-
if (settings.suspendTimeout !== undefined)
|
|
334
|
-
updated.suspendTimeout = settings.suspendTimeout;
|
|
335
|
-
endpoints[idx] = updated;
|
|
336
|
-
return clone(updated);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
async listBranchRoles(
|
|
340
|
-
projectId: string,
|
|
341
|
-
branchId: string,
|
|
342
|
-
): Promise<NeonRoleSnapshot[]> {
|
|
343
|
-
this.history.push({
|
|
344
|
-
method: "listBranchRoles",
|
|
345
|
-
args: [projectId, branchId],
|
|
346
|
-
});
|
|
347
|
-
this.requireProject(projectId);
|
|
348
|
-
this.requireBranch(projectId, branchId);
|
|
349
|
-
return (this.roles.get(branchId) ?? []).map(clone);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async listBranchDatabases(
|
|
353
|
-
projectId: string,
|
|
354
|
-
branchId: string,
|
|
355
|
-
): Promise<NeonDatabaseSnapshot[]> {
|
|
356
|
-
this.history.push({
|
|
357
|
-
method: "listBranchDatabases",
|
|
358
|
-
args: [projectId, branchId],
|
|
359
|
-
});
|
|
360
|
-
this.requireProject(projectId);
|
|
361
|
-
this.requireBranch(projectId, branchId);
|
|
362
|
-
return (this.databases.get(branchId) ?? []).map(clone);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
async getConnectionUri(
|
|
366
|
-
projectId: string,
|
|
367
|
-
input: GetConnectionUriInput,
|
|
368
|
-
): Promise<{ uri: string }> {
|
|
369
|
-
this.history.push({
|
|
370
|
-
method: "getConnectionUri",
|
|
371
|
-
args: [projectId, input],
|
|
372
|
-
});
|
|
373
|
-
this.requireProject(projectId);
|
|
374
|
-
|
|
375
|
-
const branchId = input.branchId ?? this.defaultBranchId(projectId);
|
|
376
|
-
if (!branchId) {
|
|
377
|
-
throw new Error(
|
|
378
|
-
`Fake Neon: project ${projectId} has no default branch`,
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
this.requireBranch(projectId, branchId);
|
|
382
|
-
|
|
383
|
-
const roles = this.roles.get(branchId) ?? [];
|
|
384
|
-
if (!roles.some((r) => r.name === input.roleName)) {
|
|
385
|
-
throw new Error(
|
|
386
|
-
`Fake Neon: role '${input.roleName}' not found on branch ${branchId}`,
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
const databases = this.databases.get(branchId) ?? [];
|
|
390
|
-
if (!databases.some((d) => d.name === input.databaseName)) {
|
|
391
|
-
throw new Error(
|
|
392
|
-
`Fake Neon: database '${input.databaseName}' not found on branch ${branchId}`,
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const project = this.projects.get(projectId);
|
|
397
|
-
const region = project?.regionId ?? "aws-us-east-1";
|
|
398
|
-
const hostPart = input.pooled
|
|
399
|
-
? `${branchId}-pooler.${region}.fake.neon.tech`
|
|
400
|
-
: `${branchId}.${region}.fake.neon.tech`;
|
|
401
|
-
const uri = `postgresql://${input.roleName}:fake-password-for-${branchId}@${hostPart}/${input.databaseName}?sslmode=require`;
|
|
402
|
-
return { uri };
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
async getNeonAuth(
|
|
406
|
-
projectId: string,
|
|
407
|
-
branchId: string,
|
|
408
|
-
): Promise<NeonAuthSnapshot | null> {
|
|
409
|
-
this.history.push({
|
|
410
|
-
method: "getNeonAuth",
|
|
411
|
-
args: [projectId, branchId],
|
|
412
|
-
});
|
|
413
|
-
this.requireProject(projectId);
|
|
414
|
-
this.requireBranch(projectId, branchId);
|
|
415
|
-
const found = this.neonAuth.get(`${projectId}:${branchId}`);
|
|
416
|
-
return found ? clone(publicNeonAuthSnapshot(found)) : null;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
async enableNeonAuth(
|
|
420
|
-
projectId: string,
|
|
421
|
-
branchId: string,
|
|
422
|
-
input: { databaseName?: string } = {},
|
|
423
|
-
): Promise<NeonAuthSnapshot> {
|
|
424
|
-
this.history.push({
|
|
425
|
-
method: "enableNeonAuth",
|
|
426
|
-
args: [projectId, branchId, input],
|
|
427
|
-
});
|
|
428
|
-
this.requireProject(projectId);
|
|
429
|
-
this.requireBranch(projectId, branchId);
|
|
430
|
-
const key = `${projectId}:${branchId}`;
|
|
431
|
-
const existing = this.neonAuth.get(key);
|
|
432
|
-
if (existing) return clone(publicNeonAuthSnapshot(existing));
|
|
433
|
-
const snapshot: NeonAuthSnapshot = {
|
|
434
|
-
projectId: `auth-${branchId}`,
|
|
435
|
-
publishableClientKey: `pub-${branchId}`,
|
|
436
|
-
secretServerKey: `secret-${branchId}`,
|
|
437
|
-
jwksUrl: `https://api.fake.neon.tech/auth/${projectId}/${branchId}/.well-known/jwks.json`,
|
|
438
|
-
baseUrl: `https://api.fake.neon.tech/auth/${projectId}/${branchId}`,
|
|
439
|
-
};
|
|
440
|
-
this.neonAuth.set(key, snapshot);
|
|
441
|
-
return clone(snapshot);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async getNeonDataApi(
|
|
445
|
-
projectId: string,
|
|
446
|
-
branchId: string,
|
|
447
|
-
databaseName: string,
|
|
448
|
-
): Promise<NeonDataApiSnapshot | null> {
|
|
449
|
-
this.history.push({
|
|
450
|
-
method: "getNeonDataApi",
|
|
451
|
-
args: [projectId, branchId, databaseName],
|
|
452
|
-
});
|
|
453
|
-
this.requireProject(projectId);
|
|
454
|
-
this.requireBranch(projectId, branchId);
|
|
455
|
-
const found = this.neonDataApi.get(
|
|
456
|
-
`${projectId}:${branchId}:${databaseName}`,
|
|
457
|
-
);
|
|
458
|
-
return found ? clone(found) : null;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
async enableProjectBranchDataApi(
|
|
462
|
-
projectId: string,
|
|
463
|
-
branchId: string,
|
|
464
|
-
databaseName: string,
|
|
465
|
-
): Promise<NeonDataApiSnapshot> {
|
|
466
|
-
this.history.push({
|
|
467
|
-
method: "enableProjectBranchDataApi",
|
|
468
|
-
args: [projectId, branchId, databaseName],
|
|
469
|
-
});
|
|
470
|
-
this.requireProject(projectId);
|
|
471
|
-
this.requireBranch(projectId, branchId);
|
|
472
|
-
const key = `${projectId}:${branchId}:${databaseName}`;
|
|
473
|
-
const existing = this.neonDataApi.get(key);
|
|
474
|
-
if (existing) return clone(existing);
|
|
475
|
-
const snapshot: NeonDataApiSnapshot = {
|
|
476
|
-
url: `https://${branchId}.fake.neon.tech/data-api/${databaseName}`,
|
|
477
|
-
};
|
|
478
|
-
this.neonDataApi.set(key, snapshot);
|
|
479
|
-
return clone(snapshot);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/** Test helper: attach a Neon Auth integration to a branch. */
|
|
483
|
-
seedNeonAuth(
|
|
484
|
-
projectId: string,
|
|
485
|
-
branchId: string,
|
|
486
|
-
snapshot: NeonAuthSnapshot,
|
|
487
|
-
): void {
|
|
488
|
-
this.neonAuth.set(`${projectId}:${branchId}`, { ...snapshot });
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/** Test helper: attach a Neon Data API integration to a branch + database. */
|
|
492
|
-
seedNeonDataApi(
|
|
493
|
-
projectId: string,
|
|
494
|
-
branchId: string,
|
|
495
|
-
databaseName: string,
|
|
496
|
-
snapshot: NeonDataApiSnapshot,
|
|
497
|
-
): void {
|
|
498
|
-
this.neonDataApi.set(`${projectId}:${branchId}:${databaseName}`, {
|
|
499
|
-
...snapshot,
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// ─── Preview: buckets ──────────────────────────────────────────────────────
|
|
504
|
-
|
|
505
|
-
async listBranchBuckets(
|
|
506
|
-
projectId: string,
|
|
507
|
-
branchId: string,
|
|
508
|
-
): Promise<NeonBucketSnapshot[]> {
|
|
509
|
-
this.history.push({
|
|
510
|
-
method: "listBranchBuckets",
|
|
511
|
-
args: [projectId, branchId],
|
|
512
|
-
});
|
|
513
|
-
this.requireProject(projectId);
|
|
514
|
-
this.requireBranch(projectId, branchId);
|
|
515
|
-
return (this.buckets.get(`${projectId}:${branchId}`) ?? []).map(clone);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
async createBranchBucket(
|
|
519
|
-
projectId: string,
|
|
520
|
-
branchId: string,
|
|
521
|
-
input: CreateBucketInput,
|
|
522
|
-
): Promise<NeonBucketSnapshot> {
|
|
523
|
-
this.history.push({
|
|
524
|
-
method: "createBranchBucket",
|
|
525
|
-
args: [projectId, branchId, input],
|
|
526
|
-
});
|
|
527
|
-
this.requireProject(projectId);
|
|
528
|
-
this.requireBranch(projectId, branchId);
|
|
529
|
-
const key = `${projectId}:${branchId}`;
|
|
530
|
-
const list = this.buckets.get(key) ?? [];
|
|
531
|
-
if (list.some((b) => b.name === input.name)) {
|
|
532
|
-
throw new Error(
|
|
533
|
-
`Fake Neon: bucket '${input.name}' already exists on branch ${branchId}`,
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
const snapshot: NeonBucketSnapshot = {
|
|
537
|
-
name: input.name,
|
|
538
|
-
accessLevel: input.accessLevel ?? "private",
|
|
539
|
-
};
|
|
540
|
-
list.push(snapshot);
|
|
541
|
-
this.buckets.set(key, list);
|
|
542
|
-
return clone(snapshot);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
async deleteBranchBucket(
|
|
546
|
-
projectId: string,
|
|
547
|
-
branchId: string,
|
|
548
|
-
bucketName: string,
|
|
549
|
-
): Promise<void> {
|
|
550
|
-
this.history.push({
|
|
551
|
-
method: "deleteBranchBucket",
|
|
552
|
-
args: [projectId, branchId, bucketName],
|
|
553
|
-
});
|
|
554
|
-
this.requireProject(projectId);
|
|
555
|
-
this.requireBranch(projectId, branchId);
|
|
556
|
-
const key = `${projectId}:${branchId}`;
|
|
557
|
-
const list = this.buckets.get(key) ?? [];
|
|
558
|
-
this.buckets.set(
|
|
559
|
-
key,
|
|
560
|
-
list.filter((b) => b.name !== bucketName),
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// ─── Preview: functions ────────────────────────────────────────────────────
|
|
565
|
-
|
|
566
|
-
async listBranchFunctions(
|
|
567
|
-
projectId: string,
|
|
568
|
-
branchId: string,
|
|
569
|
-
): Promise<NeonFunctionSnapshot[]> {
|
|
570
|
-
this.history.push({
|
|
571
|
-
method: "listBranchFunctions",
|
|
572
|
-
args: [projectId, branchId],
|
|
573
|
-
});
|
|
574
|
-
this.requireProject(projectId);
|
|
575
|
-
this.requireBranch(projectId, branchId);
|
|
576
|
-
return (this.functions.get(`${projectId}:${branchId}`) ?? []).map(
|
|
577
|
-
clone,
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
async createBranchFunction(
|
|
582
|
-
projectId: string,
|
|
583
|
-
branchId: string,
|
|
584
|
-
input: { slug: string; name: string },
|
|
585
|
-
): Promise<NeonFunctionSnapshot> {
|
|
586
|
-
this.history.push({
|
|
587
|
-
method: "createBranchFunction",
|
|
588
|
-
args: [projectId, branchId, input],
|
|
589
|
-
});
|
|
590
|
-
this.requireProject(projectId);
|
|
591
|
-
this.requireBranch(projectId, branchId);
|
|
592
|
-
const key = `${projectId}:${branchId}`;
|
|
593
|
-
const list = this.functions.get(key) ?? [];
|
|
594
|
-
if (list.some((f) => f.slug === input.slug)) {
|
|
595
|
-
throw new Error(
|
|
596
|
-
`Fake Neon: function '${input.slug}' already exists on branch ${branchId}`,
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
const snapshot: NeonFunctionSnapshot = {
|
|
600
|
-
id: this.allocateId("fn"),
|
|
601
|
-
slug: input.slug,
|
|
602
|
-
name: input.name,
|
|
603
|
-
invocationUrl: `https://${branchId}.fake.neon.tech/functions/${input.slug}`,
|
|
604
|
-
};
|
|
605
|
-
list.push(snapshot);
|
|
606
|
-
this.functions.set(key, list);
|
|
607
|
-
return clone(snapshot);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
async deleteBranchFunction(
|
|
611
|
-
projectId: string,
|
|
612
|
-
branchId: string,
|
|
613
|
-
slug: string,
|
|
614
|
-
): Promise<void> {
|
|
615
|
-
this.history.push({
|
|
616
|
-
method: "deleteBranchFunction",
|
|
617
|
-
args: [projectId, branchId, slug],
|
|
618
|
-
});
|
|
619
|
-
this.requireProject(projectId);
|
|
620
|
-
this.requireBranch(projectId, branchId);
|
|
621
|
-
const key = `${projectId}:${branchId}`;
|
|
622
|
-
const list = this.functions.get(key) ?? [];
|
|
623
|
-
this.functions.set(
|
|
624
|
-
key,
|
|
625
|
-
list.filter((f) => f.slug !== slug),
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
async deployBranchFunction(
|
|
630
|
-
projectId: string,
|
|
631
|
-
branchId: string,
|
|
632
|
-
slug: string,
|
|
633
|
-
input: DeployFunctionInput,
|
|
634
|
-
): Promise<NeonFunctionDeploymentSnapshot> {
|
|
635
|
-
this.history.push({
|
|
636
|
-
method: "deployBranchFunction",
|
|
637
|
-
args: [projectId, branchId, slug, input],
|
|
638
|
-
});
|
|
639
|
-
this.requireProject(projectId);
|
|
640
|
-
this.requireBranch(projectId, branchId);
|
|
641
|
-
const key = `${projectId}:${branchId}`;
|
|
642
|
-
const list = this.functions.get(key) ?? [];
|
|
643
|
-
const fn = list.find((f) => f.slug === slug);
|
|
644
|
-
if (!fn) {
|
|
645
|
-
throw new Error(
|
|
646
|
-
`Fake Neon: function '${slug}' not found on branch ${branchId}`,
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
const deployKey = `${projectId}:${branchId}:${slug}`;
|
|
650
|
-
const id = (this.functionDeployments.get(deployKey) ?? 0) + 1;
|
|
651
|
-
this.functionDeployments.set(deployKey, id);
|
|
652
|
-
fn.activeDeploymentId = id;
|
|
653
|
-
return { id, status: "completed" };
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// ─── Preview: AI Gateway ───────────────────────────────────────────────────
|
|
657
|
-
|
|
658
|
-
async getAiGatewayEnabled(
|
|
659
|
-
projectId: string,
|
|
660
|
-
branchId: string,
|
|
661
|
-
): Promise<boolean> {
|
|
662
|
-
this.history.push({
|
|
663
|
-
method: "getAiGatewayEnabled",
|
|
664
|
-
args: [projectId, branchId],
|
|
665
|
-
});
|
|
666
|
-
this.requireProject(projectId);
|
|
667
|
-
this.requireBranch(projectId, branchId);
|
|
668
|
-
return this.aiGateway.has(`${projectId}:${branchId}`);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
async enableAiGateway(projectId: string, branchId: string): Promise<void> {
|
|
672
|
-
this.history.push({
|
|
673
|
-
method: "enableAiGateway",
|
|
674
|
-
args: [projectId, branchId],
|
|
675
|
-
});
|
|
676
|
-
this.requireProject(projectId);
|
|
677
|
-
this.requireBranch(projectId, branchId);
|
|
678
|
-
this.aiGateway.add(`${projectId}:${branchId}`);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
async disableAiGateway(projectId: string, branchId: string): Promise<void> {
|
|
682
|
-
this.history.push({
|
|
683
|
-
method: "disableAiGateway",
|
|
684
|
-
args: [projectId, branchId],
|
|
685
|
-
});
|
|
686
|
-
this.requireProject(projectId);
|
|
687
|
-
this.requireBranch(projectId, branchId);
|
|
688
|
-
this.aiGateway.delete(`${projectId}:${branchId}`);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/** Test helper: attach a bucket to a branch. */
|
|
692
|
-
seedBucket(
|
|
693
|
-
projectId: string,
|
|
694
|
-
branchId: string,
|
|
695
|
-
snapshot: NeonBucketSnapshot,
|
|
696
|
-
): void {
|
|
697
|
-
const key = `${projectId}:${branchId}`;
|
|
698
|
-
const list = this.buckets.get(key) ?? [];
|
|
699
|
-
list.push({ ...snapshot });
|
|
700
|
-
this.buckets.set(key, list);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/** Test helper: attach a function to a branch. */
|
|
704
|
-
seedFunction(
|
|
705
|
-
projectId: string,
|
|
706
|
-
branchId: string,
|
|
707
|
-
snapshot: NeonFunctionSnapshot,
|
|
708
|
-
): void {
|
|
709
|
-
const key = `${projectId}:${branchId}`;
|
|
710
|
-
const list = this.functions.get(key) ?? [];
|
|
711
|
-
list.push({ ...snapshot });
|
|
712
|
-
this.functions.set(key, list);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/** Test helper: mark the AI Gateway enabled on a branch. */
|
|
716
|
-
seedAiGateway(projectId: string, branchId: string): void {
|
|
717
|
-
this.aiGateway.add(`${projectId}:${branchId}`);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
private requireProject(projectId: string): void {
|
|
721
|
-
if (!this.projects.has(projectId))
|
|
722
|
-
throw new Error(`Fake Neon: project ${projectId} not found`);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
private requireBranch(projectId: string, branchId: string): void {
|
|
726
|
-
const branchList = this.branches.get(projectId) ?? [];
|
|
727
|
-
if (!branchList.some((b) => b.id === branchId)) {
|
|
728
|
-
throw new Error(
|
|
729
|
-
`Fake Neon: branch ${branchId} not found in project ${projectId}`,
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
private defaultBranchId(projectId: string): string | undefined {
|
|
735
|
-
const branchList = this.branches.get(projectId) ?? [];
|
|
736
|
-
return (branchList.find((b) => b.isDefault) ?? branchList[0])?.id;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
private allocateId(prefix: string): string {
|
|
740
|
-
const id = `${prefix}-fake-${this.nextId.toString(36)}`;
|
|
741
|
-
this.nextId += 1;
|
|
742
|
-
return id;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
private makeEndpoint(
|
|
746
|
-
branchId: string,
|
|
747
|
-
override: Partial<NeonEndpointSnapshot> | undefined,
|
|
748
|
-
project: NeonProjectSnapshot,
|
|
749
|
-
): NeonEndpointSnapshot {
|
|
750
|
-
const projectDefaults = project.defaultEndpointSettings;
|
|
751
|
-
return {
|
|
752
|
-
id: override?.id ?? this.allocateId("ep"),
|
|
753
|
-
branchId,
|
|
754
|
-
type: override?.type ?? "read_write",
|
|
755
|
-
autoscalingLimitMinCu:
|
|
756
|
-
override?.autoscalingLimitMinCu ??
|
|
757
|
-
projectDefaults?.autoscalingLimitMinCu ??
|
|
758
|
-
0.25,
|
|
759
|
-
autoscalingLimitMaxCu:
|
|
760
|
-
override?.autoscalingLimitMaxCu ??
|
|
761
|
-
projectDefaults?.autoscalingLimitMaxCu ??
|
|
762
|
-
0.25,
|
|
763
|
-
suspendTimeout:
|
|
764
|
-
override?.suspendTimeout ??
|
|
765
|
-
projectDefaults?.suspendTimeout ??
|
|
766
|
-
undefined,
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function clone<T>(value: T): T {
|
|
772
|
-
return JSON.parse(JSON.stringify(value)) as T;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function publicNeonAuthSnapshot(snapshot: NeonAuthSnapshot): NeonAuthSnapshot {
|
|
776
|
-
const publicSnapshot: NeonAuthSnapshot = {
|
|
777
|
-
projectId: snapshot.projectId,
|
|
778
|
-
jwksUrl: snapshot.jwksUrl,
|
|
779
|
-
};
|
|
780
|
-
if (snapshot.baseUrl) publicSnapshot.baseUrl = snapshot.baseUrl;
|
|
781
|
-
return publicSnapshot;
|
|
782
|
-
}
|