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.
Files changed (75) hide show
  1. package/bin/alchemy.js +2 -1
  2. package/lib/cloudflare/bindings.d.ts +2 -2
  3. package/lib/cloudflare/bindings.d.ts.map +1 -1
  4. package/lib/cloudflare/bindings.js.map +1 -1
  5. package/lib/cloudflare/bound.d.ts +1 -1
  6. package/lib/cloudflare/bound.d.ts.map +1 -1
  7. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  8. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  9. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  10. package/lib/cloudflare/miniflare/build-worker-options.js +10 -0
  11. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  12. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  13. package/lib/cloudflare/miniflare/miniflare-controller.js +2 -2
  14. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  15. package/lib/cloudflare/vite/vite.d.ts.map +1 -1
  16. package/lib/cloudflare/vite/vite.js +38 -1
  17. package/lib/cloudflare/vite/vite.js.map +1 -1
  18. package/lib/cloudflare/website.d.ts +4 -0
  19. package/lib/cloudflare/website.d.ts.map +1 -1
  20. package/lib/cloudflare/website.js +36 -2
  21. package/lib/cloudflare/website.js.map +1 -1
  22. package/lib/cloudflare/worker-metadata.d.ts +1 -1
  23. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  24. package/lib/cloudflare/worker-metadata.js +16 -1
  25. package/lib/cloudflare/worker-metadata.js.map +1 -1
  26. package/lib/cloudflare/worker-subdomain.d.ts +1 -0
  27. package/lib/cloudflare/worker-subdomain.d.ts.map +1 -1
  28. package/lib/cloudflare/worker-subdomain.js +10 -2
  29. package/lib/cloudflare/worker-subdomain.js.map +1 -1
  30. package/lib/cloudflare/worker.d.ts +10 -0
  31. package/lib/cloudflare/worker.d.ts.map +1 -1
  32. package/lib/cloudflare/worker.js +8 -0
  33. package/lib/cloudflare/worker.js.map +1 -1
  34. package/lib/cloudflare/wrangler.json.d.ts +1 -1
  35. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  36. package/lib/cloudflare/wrangler.json.js +16 -3
  37. package/lib/cloudflare/wrangler.json.js.map +1 -1
  38. package/lib/docker/api.d.ts +14 -0
  39. package/lib/docker/api.d.ts.map +1 -1
  40. package/lib/docker/api.js +16 -1
  41. package/lib/docker/api.js.map +1 -1
  42. package/lib/docker/container.d.ts +5 -0
  43. package/lib/docker/container.d.ts.map +1 -1
  44. package/lib/docker/container.js +69 -49
  45. package/lib/docker/container.js.map +1 -1
  46. package/lib/docker/volume.d.ts +5 -0
  47. package/lib/docker/volume.d.ts.map +1 -1
  48. package/lib/docker/volume.js +43 -19
  49. package/lib/docker/volume.js.map +1 -1
  50. package/lib/neon/project.d.ts +30 -16
  51. package/lib/neon/project.d.ts.map +1 -1
  52. package/lib/neon/project.js +123 -1
  53. package/lib/neon/project.js.map +1 -1
  54. package/lib/util/find-open-port.d.ts +1 -0
  55. package/lib/util/find-open-port.d.ts.map +1 -1
  56. package/lib/util/find-open-port.js +15 -0
  57. package/lib/util/find-open-port.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/cloudflare/bindings.ts +3 -1
  60. package/src/cloudflare/bound.ts +19 -13
  61. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  62. package/src/cloudflare/miniflare/build-worker-options.ts +10 -0
  63. package/src/cloudflare/miniflare/miniflare-controller.ts +3 -2
  64. package/src/cloudflare/vite/vite.ts +29 -1
  65. package/src/cloudflare/website.ts +43 -2
  66. package/src/cloudflare/worker-metadata.ts +19 -1
  67. package/src/cloudflare/worker-subdomain.ts +14 -2
  68. package/src/cloudflare/worker.ts +11 -0
  69. package/src/cloudflare/wrangler.json.ts +14 -1
  70. package/src/docker/api.ts +22 -1
  71. package/src/docker/container.ts +83 -52
  72. package/src/docker/volume.ts +64 -27
  73. package/src/neon/project.ts +157 -2
  74. package/src/util/find-open-port.ts +18 -0
  75. package/workers/tunnel-proxy.js +1 -1
@@ -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++) {
@@ -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.79.0</p>
86
+ <p class="text-sm text-slate-500">Alchemy 0.80.1</p>
87
87
  </div>
88
88
  </div>
89
89
  </body>