alchemy 0.70.0 → 0.70.2

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 (80) hide show
  1. package/bin/alchemy.js +97 -60
  2. package/bin/alchemy.ts +2 -0
  3. package/bin/commands/telemetry.ts +33 -0
  4. package/bin/services/execute-alchemy.ts +7 -0
  5. package/lib/alchemy.d.ts +4 -0
  6. package/lib/alchemy.d.ts.map +1 -1
  7. package/lib/alchemy.js +1 -0
  8. package/lib/alchemy.js.map +1 -1
  9. package/lib/clickhouse/organization.d.ts +1 -1
  10. package/lib/clickhouse/organization.js +2 -2
  11. package/lib/clickhouse/organization.js.map +1 -1
  12. package/lib/clickhouse/service.d.ts +28 -18
  13. package/lib/clickhouse/service.d.ts.map +1 -1
  14. package/lib/clickhouse/service.js +61 -9
  15. package/lib/clickhouse/service.js.map +1 -1
  16. package/lib/cloudflare/account-api-token.js +1 -1
  17. package/lib/cloudflare/account-api-token.js.map +1 -1
  18. package/lib/cloudflare/api.d.ts.map +1 -1
  19. package/lib/cloudflare/api.js +2 -0
  20. package/lib/cloudflare/api.js.map +1 -1
  21. package/lib/cloudflare/bun-spa/bun-spa.d.ts +6 -8
  22. package/lib/cloudflare/bun-spa/bun-spa.d.ts.map +1 -1
  23. package/lib/cloudflare/bun-spa/bun-spa.js +3 -3
  24. package/lib/cloudflare/bun-spa/bun-spa.js.map +1 -1
  25. package/lib/cloudflare/d1-database.d.ts +1 -1
  26. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  27. package/lib/cloudflare/d1-database.js +1 -1
  28. package/lib/cloudflare/d1-database.js.map +1 -1
  29. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  30. package/lib/cloudflare/miniflare/build-worker-options.js +30 -3
  31. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  32. package/lib/cloudflare/permission-groups.d.ts +1489 -26
  33. package/lib/cloudflare/permission-groups.d.ts.map +1 -1
  34. package/lib/cloudflare/permission-groups.js +1787 -6
  35. package/lib/cloudflare/permission-groups.js.map +1 -1
  36. package/lib/cloudflare/worker-metadata.d.ts +14 -2
  37. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  38. package/lib/cloudflare/worker-metadata.js +4 -1
  39. package/lib/cloudflare/worker-metadata.js.map +1 -1
  40. package/lib/cloudflare/worker-stub.d.ts +3 -1
  41. package/lib/cloudflare/worker-stub.d.ts.map +1 -1
  42. package/lib/cloudflare/worker-stub.js +19 -7
  43. package/lib/cloudflare/worker-stub.js.map +1 -1
  44. package/lib/cloudflare/worker.d.ts +74 -10
  45. package/lib/cloudflare/worker.d.ts.map +1 -1
  46. package/lib/cloudflare/worker.js +16 -7
  47. package/lib/cloudflare/worker.js.map +1 -1
  48. package/lib/scope.d.ts +5 -0
  49. package/lib/scope.d.ts.map +1 -1
  50. package/lib/scope.js +3 -1
  51. package/lib/scope.js.map +1 -1
  52. package/lib/util/camel-to-snake.d.ts +12 -0
  53. package/lib/util/camel-to-snake.d.ts.map +1 -0
  54. package/lib/util/camel-to-snake.js +16 -0
  55. package/lib/util/camel-to-snake.js.map +1 -0
  56. package/lib/util/idempotent-spawn.js +1 -1
  57. package/lib/util/idempotent-spawn.js.map +1 -1
  58. package/lib/util/telemetry.d.ts +3 -2
  59. package/lib/util/telemetry.d.ts.map +1 -1
  60. package/lib/util/telemetry.js +34 -10
  61. package/lib/util/telemetry.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/alchemy.ts +5 -0
  64. package/src/clickhouse/organization.ts +2 -2
  65. package/src/clickhouse/service.ts +124 -33
  66. package/src/cloudflare/account-api-token.ts +1 -1
  67. package/src/cloudflare/api.ts +2 -0
  68. package/src/cloudflare/bun-spa/bun-spa.ts +5 -9
  69. package/src/cloudflare/d1-database.ts +2 -2
  70. package/src/cloudflare/miniflare/build-worker-options.ts +32 -4
  71. package/src/cloudflare/permission-groups.ts +1832 -45
  72. package/src/cloudflare/worker-metadata.ts +20 -3
  73. package/src/cloudflare/worker-stub.ts +28 -11
  74. package/src/cloudflare/worker.ts +98 -17
  75. package/src/scope.ts +7 -0
  76. package/src/util/camel-to-snake.ts +88 -0
  77. package/src/util/idempotent-spawn.ts +1 -1
  78. package/src/util/telemetry.ts +41 -10
  79. package/templates/tanstack-start/package.json +2 -2
  80. package/workers/tunnel-proxy.js +1 -1
@@ -5,6 +5,26 @@ import { diff } from "../util/diff.ts";
5
5
  import { createClickhouseApi } from "./api.ts";
6
6
  import type { ClickhouseClient } from "./api/sdk.gen.ts";
7
7
  import type { Service as ApiService, Organization } from "./api/types.gen.ts";
8
+ import { OrganizationRef } from "./organization.ts";
9
+
10
+ type MysqlEndpoint = {
11
+ protocol: "mysql";
12
+ host: string;
13
+ port: number;
14
+ username: string;
15
+ };
16
+
17
+ type HttpsEndpoint = {
18
+ protocol: "https";
19
+ host: string;
20
+ port: number;
21
+ };
22
+
23
+ type NativesecureEndpoint = {
24
+ protocol: "nativesecure";
25
+ host: string;
26
+ port: number;
27
+ };
8
28
 
9
29
  export interface ServiceProps {
10
30
  /**
@@ -18,7 +38,7 @@ export interface ServiceProps {
18
38
  secret?: string | Secret<string>;
19
39
 
20
40
  /**
21
- * The id of Clickhouse cloud organization to create the service in.
41
+ * The id, name, or OrganizationRef of Clickhouse cloud organization to create the service in.
22
42
  */
23
43
  organization: string | Organization;
24
44
 
@@ -128,6 +148,13 @@ export interface ServiceProps {
128
148
  */
129
149
  complianceType?: ApiService["complianceType"];
130
150
 
151
+ /**
152
+ * wait for http service to be ready before marking the resource as created
153
+ *
154
+ * @default true
155
+ */
156
+ waitForHttpEndpointReady?: boolean;
157
+
131
158
  //todo(michael): I need to understand more about what these properties do before documenting
132
159
  //todo(michael): support linking to BYOC infrastructure directly
133
160
  byocId?: ApiService["byocId"];
@@ -269,30 +296,17 @@ export interface Service {
269
296
  /**
270
297
  * The mysql endpoint details of the Clickhouse service.
271
298
  */
272
- mysqlEndpoint?: {
273
- protocol: "mysql";
274
- host: string;
275
- port: number;
276
- username: string;
277
- };
299
+ mysqlEndpoint?: MysqlEndpoint;
278
300
 
279
301
  /**
280
302
  * The https endpoint details of the Clickhouse service.
281
303
  */
282
- httpsEndpoint?: {
283
- protocol: "https";
284
- host: string;
285
- port: number;
286
- };
304
+ httpsEndpoint?: HttpsEndpoint;
287
305
 
288
306
  /**
289
307
  * The nativesecure endpoint details of the Clickhouse service.
290
308
  */
291
- nativesecureEndpoint?: {
292
- protocol: "nativesecure";
293
- host: string;
294
- port: number;
295
- };
309
+ nativesecureEndpoint?: NativesecureEndpoint;
296
310
 
297
311
  /**
298
312
  * The desired state of the Clickhouse service.
@@ -310,7 +324,7 @@ export interface Service {
310
324
  *
311
325
  * @example
312
326
  * // Create a basic Clickhouse service on aws
313
- * const organization = await getOrganizationByName("Alchemy's Organization");
327
+ * const organization = await OrganizationRef("Alchemy's Organization");
314
328
  * const service = await Service("clickhouse", {
315
329
  * organization,
316
330
  * provider: "aws",
@@ -348,6 +362,7 @@ export const Service = Resource(
348
362
  if (enableMysqlEndpoint) {
349
363
  endpoints.push({ protocol: "mysql", enabled: true });
350
364
  }
365
+ const waitForHttpEndpointReady = props.waitForHttpEndpointReady ?? true;
351
366
  //todo(michael): comment these in when disabling is supported
352
367
  // const enableHttpsEndpoint = props.enableHttpsEndpoint ?? true;
353
368
  // if (enableHttpsEndpoint) {
@@ -374,7 +389,9 @@ export const Service = Resource(
374
389
 
375
390
  const organizationId =
376
391
  typeof props.organization === "string"
377
- ? props.organization
392
+ ? await OrganizationRef(props.organization)
393
+ .then((organization) => organization.id!)
394
+ .catch(() => props.organization as string)
378
395
  : props.organization.id!;
379
396
 
380
397
  if (this.phase === "delete") {
@@ -393,7 +410,6 @@ export const Service = Resource(
393
410
  organizationId,
394
411
  this.output.clickhouseId,
395
412
  (state) => state === "stopped",
396
- 10 * 60,
397
413
  );
398
414
 
399
415
  await api.deleteService({
@@ -403,6 +419,12 @@ export const Service = Resource(
403
419
  },
404
420
  });
405
421
 
422
+ await waitForServiceDeletion(
423
+ api,
424
+ organizationId,
425
+ this.output.clickhouseId,
426
+ );
427
+
406
428
  return this.destroy();
407
429
  }
408
430
  if (this.phase === "update") {
@@ -481,13 +503,13 @@ export const Service = Resource(
481
503
  updates.releaseChannel = response.releaseChannel!;
482
504
  updates.mysqlEndpoint = response!.endpoints!.find(
483
505
  (endpoint) => endpoint.protocol === "mysql",
484
- ) as any;
506
+ ) as MysqlEndpoint;
485
507
  updates.httpsEndpoint = response!.endpoints!.find(
486
508
  (endpoint) => endpoint.protocol === "https",
487
- ) as any;
509
+ ) as HttpsEndpoint;
488
510
  updates.nativesecureEndpoint = response!.endpoints!.find(
489
511
  (endpoint) => endpoint.protocol === "nativesecure",
490
- ) as any;
512
+ ) as NativesecureEndpoint;
491
513
  }
492
514
 
493
515
  if (stateTarget !== this.output.stateTarget) {
@@ -579,14 +601,24 @@ export const Service = Resource(
579
601
  const password = response.password!;
580
602
  const service = response.service!;
581
603
 
604
+ const httpEndpoint = service.endpoints!.find(
605
+ (endpoint) => endpoint.protocol === "https",
606
+ ) as HttpsEndpoint;
607
+
582
608
  await waitForServiceState(
583
609
  api,
584
610
  organizationId,
585
611
  response.service!.id!,
586
612
  (state) => state === "running" || state === "idle",
587
- 10 * 60,
588
613
  );
589
614
 
615
+ if (waitForHttpEndpointReady && httpEndpoint) {
616
+ await waitForHttpServiceReady(httpEndpoint as HttpsEndpoint, {
617
+ password: password!,
618
+ username: "default",
619
+ });
620
+ }
621
+
590
622
  return {
591
623
  organizationId: organizationId,
592
624
  name: service.name!,
@@ -614,13 +646,11 @@ export const Service = Resource(
614
646
  state: service.state,
615
647
  mysqlEndpoint: service.endpoints!.find(
616
648
  (endpoint) => endpoint.protocol === "mysql",
617
- ) as any,
618
- httpsEndpoint: service.endpoints!.find(
619
- (endpoint) => endpoint.protocol === "https",
620
- ) as any,
649
+ ) as MysqlEndpoint,
650
+ httpsEndpoint: httpEndpoint,
621
651
  nativesecureEndpoint: service.endpoints!.find(
622
652
  (endpoint) => endpoint.protocol === "nativesecure",
623
- ) as any,
653
+ ) as NativesecureEndpoint,
624
654
  };
625
655
  },
626
656
  );
@@ -630,9 +660,8 @@ async function waitForServiceState(
630
660
  organizationId: string,
631
661
  serviceId: string,
632
662
  stateChecker: (state: string) => boolean,
633
- maxWaitSeconds: number,
634
663
  ) {
635
- const checkState = async (): Promise<void> => {
664
+ async function checkState(): Promise<void> {
636
665
  const service = await api.getServiceDetails({
637
666
  path: {
638
667
  organizationId: organizationId,
@@ -646,8 +675,70 @@ async function waitForServiceState(
646
675
  }
647
676
 
648
677
  throw new Error(`Service ${serviceId} is in state ${serviceState}`);
649
- };
678
+ }
679
+
680
+ await waitFor(checkState, 10 * 60);
681
+ }
650
682
 
683
+ async function waitForHttpServiceReady(
684
+ endpoint: {
685
+ protocol: "https";
686
+ host: string;
687
+ port: number;
688
+ },
689
+ credentials: { password: string; username: string },
690
+ ) {
691
+ async function checkHttpEndpoint() {
692
+ await fetch(
693
+ `https://${endpoint.host}:${endpoint.port}/?query=SELECT%20count%28%29%20FROM%20system.databases%20WHERE%20name%20%3D%20%27default%27`,
694
+ {
695
+ headers: {
696
+ Authorization: `Basic ${btoa(`${credentials.username}:${credentials.password}`)}`,
697
+ },
698
+ },
699
+ )
700
+ .then((res) => res.text())
701
+ .then((text) => {
702
+ if (text === "0") {
703
+ throw new Error("Service is not ready");
704
+ }
705
+ return;
706
+ });
707
+ }
708
+
709
+ await waitFor(checkHttpEndpoint, 10 * 60);
710
+ }
711
+
712
+ async function waitForServiceDeletion(
713
+ api: ClickhouseClient,
714
+ organizationId: string,
715
+ serviceId: string,
716
+ ) {
717
+ async function checkDeletion() {
718
+ const status = await api
719
+ .getServiceDetails({
720
+ path: {
721
+ organizationId: organizationId,
722
+ serviceId: serviceId,
723
+ },
724
+ })
725
+ .then((service) => service.response.status)
726
+ .catch((error) => {
727
+ return (error.status as number) ?? 500;
728
+ });
729
+ if (status === 404) {
730
+ return;
731
+ }
732
+ throw new Error(`Service ${serviceId} is not deleted`);
733
+ }
734
+
735
+ await waitFor(checkDeletion, 10 * 60);
736
+ }
737
+
738
+ async function waitFor(
739
+ checkFunction: () => Promise<void>,
740
+ maxWaitSeconds: number,
741
+ ) {
651
742
  if (maxWaitSeconds < 5) {
652
743
  maxWaitSeconds = 5;
653
744
  }
@@ -656,7 +747,7 @@ async function waitForServiceState(
656
747
 
657
748
  for (let attempt = 0; attempt < maxRetries; attempt++) {
658
749
  try {
659
- await checkState();
750
+ await checkFunction();
660
751
  return;
661
752
  } catch (error) {
662
753
  if (attempt === maxRetries - 1) {
@@ -282,7 +282,7 @@ export const AccountApiToken = Resource(
282
282
  return this.destroy();
283
283
  }
284
284
 
285
- const permissionGroups = await PermissionGroups(props);
285
+ const permissionGroups = await PermissionGroups();
286
286
 
287
287
  // Transform our properties to API format
288
288
  const apiPayload = {
@@ -1,4 +1,5 @@
1
1
  import { Provider, type Credentials } from "../auth.ts";
2
+ import { Scope } from "../scope.ts";
2
3
  import type { Secret } from "../secret.ts";
3
4
  import { isBinary } from "../serde.ts";
4
5
  import { memoize } from "../util/memoize.ts";
@@ -99,6 +100,7 @@ export const createCloudflareApi = memoize(
99
100
  try {
100
101
  const profile =
101
102
  options.profile ??
103
+ Scope.getScope()?.profile ??
102
104
  process.env.CLOUDFLARE_PROFILE ??
103
105
  process.env.ALCHEMY_PROFILE ??
104
106
  "default";
@@ -2,7 +2,6 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { Scope } from "../../scope.ts";
4
4
  import { exists } from "../../util/exists.ts";
5
- import type { Assets } from "../assets.ts";
6
5
  import type { Bindings } from "../bindings.ts";
7
6
  import {
8
7
  extractStringAndSecretBindings,
@@ -14,21 +13,18 @@ import {
14
13
  Website,
15
14
  type WebsiteProps,
16
15
  } from "../website.ts";
17
- import type { Worker } from "../worker.ts";
18
16
 
19
17
  export interface BunSPAProps<B extends Bindings> extends WebsiteProps<B> {
20
18
  frontend: string;
21
19
  outDir?: string;
22
20
  }
23
21
 
24
- export type BunSPA<B extends Bindings> = B extends { ASSETS: any }
25
- ? never
26
- : Worker<B & { ASSETS: Assets }>;
22
+ export type BunSPA<B extends Bindings> = Website<B> & { apiUrl: string };
27
23
 
28
24
  export async function BunSPA<B extends Bindings>(
29
25
  id: string,
30
26
  props: BunSPAProps<B>,
31
- ): Promise<BunSPA<B>> {
27
+ ): Promise<BunSPA<B> & { apiUrl: string }> {
32
28
  const frontendPath = path.resolve(props.frontend);
33
29
  if (!(await exists(frontendPath))) {
34
30
  throw new Error(`Frontend path ${frontendPath} does not exist`);
@@ -66,6 +62,7 @@ export async function BunSPA<B extends Bindings>(
66
62
  ),
67
63
  });
68
64
 
65
+ let apiUrl = website.url!;
69
66
  // in dev
70
67
  if (scope.local) {
71
68
  const cwd = props.cwd ?? process.cwd();
@@ -74,7 +71,6 @@ export async function BunSPA<B extends Bindings>(
74
71
  props,
75
72
  `bun '${path.relative(cwd, frontendPath)}'`,
76
73
  );
77
- console.log("backend url", website.url);
78
74
  const secrets = props.wrangler?.secrets ?? !props.wrangler?.path;
79
75
  const env = {
80
76
  ...(process.env ?? {}),
@@ -101,11 +97,11 @@ export async function BunSPA<B extends Bindings>(
101
97
  ...process.env,
102
98
  NODE_ENV: "development",
103
99
  ALCHEMY_ROOT: Scope.current.rootDir,
104
- PUBLIC_BACKEND_URL: website.url!,
100
+ PUBLIC_BACKEND_URL: apiUrl,
105
101
  },
106
102
  });
107
103
  }
108
- return website;
104
+ return { ...website, apiUrl } as BunSPA<B>;
109
105
  }
110
106
 
111
107
  async function validateBunfigToml(cwd: string): Promise<void> {
@@ -113,7 +113,7 @@ export interface D1DatabaseProps extends CloudflareApiOptions {
113
113
  * Set when `Scope.local` is true to force update to the database even if it was already deployed live.
114
114
  * @internal
115
115
  */
116
- force?: boolean;
116
+ force?: boolean | number;
117
117
  };
118
118
  }
119
119
 
@@ -250,7 +250,7 @@ export async function D1Database(
250
250
  ...(props.dev ?? {}),
251
251
  // force local migrations to run even if the database was already deployed live
252
252
  // this property will oscillate from true to false depending on the dev vs live deployment
253
- force: Scope.current.local,
253
+ force: Scope.current.local ? Date.now() : false,
254
254
  },
255
255
  });
256
256
  }
@@ -1,6 +1,7 @@
1
1
  import * as miniflare from "miniflare";
2
2
  import { assertNever } from "../../util/assert-never.ts";
3
3
  import type { HTTPServer } from "../../util/http.ts";
4
+ import { logger } from "../../util/logger.ts";
4
5
  import type { CloudflareApi } from "../api.ts";
5
6
  import type {
6
7
  Binding,
@@ -79,10 +80,7 @@ export const buildWorkerOptions = async (
79
80
  ],
80
81
  containerEngine: {
81
82
  localDocker: {
82
- socketPath:
83
- process.platform === "win32"
84
- ? "//./pipe/docker_engine"
85
- : "unix:///var/run/docker.sock",
83
+ socketPath: await getDockerSocketPath(),
86
84
  },
87
85
  },
88
86
  // This exposes the worker as a route that can be accessed by setting the MF-Route-Override header.
@@ -480,3 +478,33 @@ const isRemoteBinding = (binding: Binding) => {
480
478
  !!binding.dev.remote
481
479
  );
482
480
  };
481
+
482
+ /**
483
+ * DOCKER_HOST env is standardized
484
+ * docker has an option to expose on tcp://localhost:2375; so we check 2375 if the user has it enabled
485
+ * the pipe on windows doesn't work half the time(even though the pipe exists). This seems like a strange error on how miniflare parses the pipe
486
+ * @returns The Docker path
487
+ */
488
+ async function getDockerSocketPath() {
489
+ if (process.env.DOCKER_HOST) {
490
+ return process.env.DOCKER_HOST;
491
+ }
492
+ // Check if docker is running on tcp://localhost:2375 using fetch
493
+ try {
494
+ const url = "http://localhost:2375/_ping";
495
+ const res = await fetch(url, { method: "GET" });
496
+ if (res.ok) {
497
+ const text = await res.text();
498
+ if (text.trim() === "OK") {
499
+ return "localhost:2375";
500
+ }
501
+ }
502
+ } catch {}
503
+ if (process.platform === "win32") {
504
+ logger.warn(
505
+ "Using the pipe on Windows is unstable. If you have issues, try setting DOCKER_HOST or enabling 'Expose daemon on tcp://localhost:2375 without TLS' in docker desktop",
506
+ );
507
+ return "//./pipe/docker_engine";
508
+ }
509
+ return "unix:///var/run/docker.sock";
510
+ }