alchemy 0.79.0 → 0.80.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/bin/alchemy.js +2 -1
- package/lib/cloudflare/bindings.d.ts +2 -2
- package/lib/cloudflare/bindings.d.ts.map +1 -1
- package/lib/cloudflare/bindings.js.map +1 -1
- package/lib/cloudflare/bound.d.ts +1 -1
- package/lib/cloudflare/bound.d.ts.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.js +10 -0
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.js +2 -2
- package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
- package/lib/cloudflare/vite/vite.d.ts.map +1 -1
- package/lib/cloudflare/vite/vite.js +38 -1
- package/lib/cloudflare/vite/vite.js.map +1 -1
- package/lib/cloudflare/website.d.ts +4 -0
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +36 -2
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts +1 -1
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +16 -1
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker-subdomain.d.ts +1 -0
- package/lib/cloudflare/worker-subdomain.d.ts.map +1 -1
- package/lib/cloudflare/worker-subdomain.js +10 -2
- package/lib/cloudflare/worker-subdomain.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +10 -0
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +8 -0
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +1 -1
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +16 -3
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/docker/api.d.ts +14 -0
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +16 -1
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/container.d.ts +5 -0
- package/lib/docker/container.d.ts.map +1 -1
- package/lib/docker/container.js +69 -49
- package/lib/docker/container.js.map +1 -1
- package/lib/docker/volume.d.ts +5 -0
- package/lib/docker/volume.d.ts.map +1 -1
- package/lib/docker/volume.js +43 -19
- package/lib/docker/volume.js.map +1 -1
- package/lib/neon/project.d.ts +30 -16
- package/lib/neon/project.d.ts.map +1 -1
- package/lib/neon/project.js +123 -1
- package/lib/neon/project.js.map +1 -1
- package/lib/util/find-open-port.d.ts +1 -0
- package/lib/util/find-open-port.d.ts.map +1 -1
- package/lib/util/find-open-port.js +15 -0
- package/lib/util/find-open-port.js.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/bindings.ts +3 -1
- package/src/cloudflare/bound.ts +19 -13
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/miniflare/build-worker-options.ts +10 -0
- package/src/cloudflare/miniflare/miniflare-controller.ts +3 -2
- package/src/cloudflare/vite/vite.ts +29 -1
- package/src/cloudflare/website.ts +43 -2
- package/src/cloudflare/worker-metadata.ts +19 -1
- package/src/cloudflare/worker-subdomain.ts +14 -2
- package/src/cloudflare/worker.ts +11 -0
- package/src/cloudflare/wrangler.json.ts +14 -1
- package/src/docker/api.ts +22 -1
- package/src/docker/container.ts +83 -52
- package/src/docker/volume.ts +64 -27
- package/src/neon/project.ts +157 -2
- package/src/util/find-open-port.ts +18 -0
- package/workers/tunnel-proxy.js +1 -1
package/src/neon/project.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { alchemy } from "../alchemy.ts";
|
|
1
2
|
import type { Context } from "../context.ts";
|
|
2
3
|
import { Resource } from "../resource.ts";
|
|
3
4
|
import { createNeonApi, type NeonApiOptions } from "./api.ts";
|
|
5
|
+
import type { NeonClient } from "./api/sdk.gen.ts";
|
|
4
6
|
import type * as neon from "./api/types.gen.ts";
|
|
5
7
|
import {
|
|
6
8
|
formatConnectionUri,
|
|
7
9
|
formatRole,
|
|
8
|
-
waitForOperations,
|
|
9
10
|
type NeonConnectionUri,
|
|
10
11
|
type NeonRole,
|
|
12
|
+
waitForOperations,
|
|
11
13
|
} from "./utils.ts";
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -32,6 +34,18 @@ export type NeonPgVersion = 14 | 15 | 16 | 17 | 18;
|
|
|
32
34
|
* Properties for creating or updating a Neon project
|
|
33
35
|
*/
|
|
34
36
|
export interface NeonProjectProps extends NeonApiOptions {
|
|
37
|
+
/**
|
|
38
|
+
* When `true`, will adopt an existing project by `name`
|
|
39
|
+
*/
|
|
40
|
+
adopt?: true;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether to delete the database when the resource is destroyed.
|
|
44
|
+
* When false, the database will only be removed from the state but not deleted via API.
|
|
45
|
+
* @default true, unless the resource was adopted
|
|
46
|
+
*/
|
|
47
|
+
delete?: boolean;
|
|
48
|
+
|
|
35
49
|
/**
|
|
36
50
|
* Name of the project
|
|
37
51
|
*
|
|
@@ -165,6 +179,13 @@ export interface NeonProject {
|
|
|
165
179
|
* });
|
|
166
180
|
*
|
|
167
181
|
* @example
|
|
182
|
+
* // Adopt an existing Neon project by name:
|
|
183
|
+
* const project = await NeonProject("my-project", {
|
|
184
|
+
* adopt: true
|
|
185
|
+
* name: "adjective-noun-123",
|
|
186
|
+
* });
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
168
189
|
* // Create a Neon project in a specific region with a specific PostgreSQL version:
|
|
169
190
|
* const euProject = await NeonProject("my-eu-project", {
|
|
170
191
|
* name: "My EU Project",
|
|
@@ -193,6 +214,17 @@ export const NeonProject = Resource(
|
|
|
193
214
|
|
|
194
215
|
switch (this.phase) {
|
|
195
216
|
case "create": {
|
|
217
|
+
if (props.adopt) {
|
|
218
|
+
try {
|
|
219
|
+
return await fetchProject(api, { ...props, name });
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (!(error instanceof NeonProjectNotFound)) {
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
// project not found, continue with creation
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
196
228
|
const { data } = await api.createProject({
|
|
197
229
|
body: {
|
|
198
230
|
project: {
|
|
@@ -276,8 +308,9 @@ export const NeonProject = Resource(
|
|
|
276
308
|
history_retention_seconds: data.project.history_retention_seconds,
|
|
277
309
|
};
|
|
278
310
|
}
|
|
311
|
+
|
|
279
312
|
case "delete": {
|
|
280
|
-
if (this.output?.id) {
|
|
313
|
+
if (props.delete !== false && this.output?.id) {
|
|
281
314
|
const response = await api.deleteProject({
|
|
282
315
|
path: {
|
|
283
316
|
project_id: this.output.id,
|
|
@@ -298,3 +331,125 @@ export const NeonProject = Resource(
|
|
|
298
331
|
}
|
|
299
332
|
},
|
|
300
333
|
);
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* References an existing Neon project by name. You can use this to reference an existing NeonProject without adopting it.
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* const project = await NeonProjectRef({
|
|
340
|
+
* name: "my-project"
|
|
341
|
+
* });
|
|
342
|
+
*
|
|
343
|
+
* @warning Project names must be unique when referencing existing projects. If multiple projects are found with the same name, an error will be thrown.
|
|
344
|
+
*/
|
|
345
|
+
export async function NeonProjectRef(
|
|
346
|
+
props: NeonProjectProps & { name: string },
|
|
347
|
+
): Promise<NeonProject> {
|
|
348
|
+
return await fetchProject(createNeonApi(props), props);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function fetchProject(
|
|
352
|
+
api: NeonClient,
|
|
353
|
+
props: NeonProjectProps & { name: string },
|
|
354
|
+
): Promise<NeonProject> {
|
|
355
|
+
const project = await api
|
|
356
|
+
.listProjects({
|
|
357
|
+
query: { search: props.name },
|
|
358
|
+
})
|
|
359
|
+
.then(({ data: { projects } }) => {
|
|
360
|
+
if (!projects.length) {
|
|
361
|
+
throw new NeonProjectNotFound(props.name);
|
|
362
|
+
}
|
|
363
|
+
if (projects.length > 1) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Multiple projects found with name "${props.name}". Name must be unique when referencing existing projects.`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return projects[0];
|
|
369
|
+
});
|
|
370
|
+
const endpointsPromise = api
|
|
371
|
+
.listProjectEndpoints({
|
|
372
|
+
path: { project_id: project.id },
|
|
373
|
+
})
|
|
374
|
+
.then((res) => res.data.endpoints as [neon.Endpoint, ...neon.Endpoint[]]);
|
|
375
|
+
const branch = await api
|
|
376
|
+
.listProjectBranches({
|
|
377
|
+
path: { project_id: project.id },
|
|
378
|
+
query: { search: props.default_branch_name ?? "main" },
|
|
379
|
+
})
|
|
380
|
+
.then((res) => {
|
|
381
|
+
if (!res.data.branches.length) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`Branch ${props.default_branch_name ?? "main"} does not exist in Neon project "${props.name}" (${project.id}).`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return res.data.branches[0];
|
|
387
|
+
});
|
|
388
|
+
const rolesPromise = api
|
|
389
|
+
.listProjectBranchRoles({
|
|
390
|
+
path: { project_id: project.id, branch_id: branch.id },
|
|
391
|
+
})
|
|
392
|
+
.then((res) => res.data.roles.map(formatRole) as [NeonRole, ...NeonRole[]]);
|
|
393
|
+
const databases = await api
|
|
394
|
+
.listProjectBranchDatabases({
|
|
395
|
+
path: { project_id: project.id, branch_id: branch.id },
|
|
396
|
+
})
|
|
397
|
+
.then((res) => res.data.databases as [neon.Database, ...neon.Database[]]);
|
|
398
|
+
const [connection_uris, endpoints, roles] = await Promise.all([
|
|
399
|
+
Promise.all(databases.map(fetchConnectionUri)) as Promise<
|
|
400
|
+
[NeonConnectionUri, ...NeonConnectionUri[]]
|
|
401
|
+
>,
|
|
402
|
+
endpointsPromise,
|
|
403
|
+
rolesPromise,
|
|
404
|
+
]);
|
|
405
|
+
return {
|
|
406
|
+
id: project.id,
|
|
407
|
+
name: project.name,
|
|
408
|
+
created_at: project.created_at,
|
|
409
|
+
updated_at: project.updated_at,
|
|
410
|
+
proxy_host: project.proxy_host,
|
|
411
|
+
region_id: project.region_id as NeonRegion,
|
|
412
|
+
pg_version: project.pg_version as NeonPgVersion,
|
|
413
|
+
settings: project.settings,
|
|
414
|
+
default_endpoint_settings: project.default_endpoint_settings,
|
|
415
|
+
history_retention_seconds: project.history_retention_seconds ?? 86400,
|
|
416
|
+
connection_uris,
|
|
417
|
+
roles,
|
|
418
|
+
databases,
|
|
419
|
+
branch,
|
|
420
|
+
endpoints,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
async function fetchConnectionUri(
|
|
424
|
+
database: neon.Database,
|
|
425
|
+
): Promise<NeonConnectionUri> {
|
|
426
|
+
const {
|
|
427
|
+
data: { uri },
|
|
428
|
+
} = await api.getConnectionUri({
|
|
429
|
+
path: { project_id: project.id },
|
|
430
|
+
query: {
|
|
431
|
+
branch_id: database.branch_id,
|
|
432
|
+
database_name: database.name,
|
|
433
|
+
role_name: database.owner_name,
|
|
434
|
+
pooled: false, // match default behavior from create endpoint (should this be true?)
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
const url = new URL(uri);
|
|
438
|
+
return {
|
|
439
|
+
connection_uri: alchemy.secret(uri),
|
|
440
|
+
connection_parameters: {
|
|
441
|
+
database: database.name,
|
|
442
|
+
host: url.host,
|
|
443
|
+
port: 5432,
|
|
444
|
+
user: url.username,
|
|
445
|
+
password: alchemy.secret(url.password),
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
class NeonProjectNotFound extends Error {
|
|
452
|
+
constructor(name: string) {
|
|
453
|
+
super(`Failed to find existing project "${name}".`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
|
+
import { AsyncMutex } from "./mutex.ts";
|
|
3
|
+
|
|
4
|
+
const owners = new Map<string, number>();
|
|
5
|
+
const ports = new Map<number, string>();
|
|
6
|
+
|
|
7
|
+
const mutex = new AsyncMutex();
|
|
8
|
+
|
|
9
|
+
export async function reservePort(key: string) {
|
|
10
|
+
if (owners.has(key)) {
|
|
11
|
+
return owners.get(key)!;
|
|
12
|
+
}
|
|
13
|
+
return mutex.lock(async () => {
|
|
14
|
+
const port = await findOpenPort();
|
|
15
|
+
owners.set(key, port);
|
|
16
|
+
ports.set(port, key);
|
|
17
|
+
return port;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
2
20
|
|
|
3
21
|
export async function findOpenPort(min = 1337, max = 65535): Promise<number> {
|
|
4
22
|
for (let port = min; port <= max; port++) {
|
package/workers/tunnel-proxy.js
CHANGED
|
@@ -83,7 +83,7 @@ const renderErrorHtml = (props) => `
|
|
|
83
83
|
Alchemy</a>.</p>
|
|
84
84
|
</div>
|
|
85
85
|
<div class="bg-slate-200 px-5 py-3 flex flex-col w-full max-w-lg">
|
|
86
|
-
<p class="text-sm text-slate-500">Alchemy 0.
|
|
86
|
+
<p class="text-sm text-slate-500">Alchemy 0.80.1</p>
|
|
87
87
|
</div>
|
|
88
88
|
</div>
|
|
89
89
|
</body>
|