Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions alchemy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
"@iarna/toml": "^2.2.5",
"@smithy/node-config-provider": "^4.0.0",
"aws4fetch": "^1.0.20",
"effect": "^3.17.8",
"env-paths": "^3.0.0",
"esbuild": "^0.25.1",
"execa": "^9.6.0",
Expand Down
22 changes: 7 additions & 15 deletions alchemy/src/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,6 @@ import { formatFQN } from "./util/cli.ts";
import { logger } from "./util/logger.ts";
import type { Telemetry } from "./util/telemetry/index.ts";

export interface ApplyOptions {
quiet?: boolean;
alwaysUpdate?: boolean;
noop?: boolean;
}

export function apply<Out extends Resource>(
resource: PendingResource<Out>,
props: ResourceProps | undefined,
options?: ApplyOptions,
): Promise<Awaited<Out>> {
return _apply(resource, props, options);
}

export function isReplacedSignal(error: any): error is ReplacedSignal {
return error instanceof Error && (error as any).kind === "ReplacedSignal";
}
Expand All @@ -47,7 +33,13 @@ export class ReplacedSignal extends Error {
}
}

async function _apply<Out extends Resource>(
export interface ApplyOptions {
quiet?: boolean;
alwaysUpdate?: boolean;
noop?: boolean;
}

export async function apply<Out extends Resource>(
resource: PendingResource<Out>,
props: ResourceProps | undefined,
options?: ApplyOptions,
Expand Down
10 changes: 4 additions & 6 deletions alchemy/src/cloudflare/bucket.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isDeepStrictEqual } from "node:util";
import type { Context } from "../context.ts";
import { Resource, ResourceKind } from "../resource.ts";
import type { Rune } from "../rune.ts";
import { Scope } from "../scope.ts";
import { withExponentialBackoff } from "../util/retry.ts";
import { CloudflareApiError } from "./api-error.ts";
Expand Down Expand Up @@ -334,17 +335,14 @@ export function isBucket(resource: Resource): resource is R2Bucket {
*
* @see https://developers.cloudflare.com/r2/buckets/
*/
export async function R2Bucket(
id: string,
props: BucketProps = {},
): Promise<R2Bucket> {
return await _R2Bucket(id, {
export function R2Bucket(id: string, props: BucketProps = {}) {
return _R2Bucket(id, {
...props,
dev: {
...(props.dev ?? {}),
force: Scope.current.local,
},
});
}) as Rune.of<R2Bucket>;
}

const _R2Bucket = Resource(
Expand Down
64 changes: 33 additions & 31 deletions alchemy/src/cloudflare/hyperdrive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,44 +192,43 @@ export interface HyperdriveProps extends CloudflareApiOptions {
*/
export type Hyperdrive = Resource<"cloudflare::Hyperdrive"> &
Omit<HyperdriveProps, "origin" | "dev"> & {
/**
* The ID of the resource
*/
id: string;

/**
* Name of the Hyperdrive configuration
*/
name: string;

/**
* The Cloudflare-generated UUID of the hyperdrive
*/
hyperdriveId: string;
/**
* The ID of the resource
*/
id: string;

/**
* Database connection origin configuration
*/
origin: HyperdrivePublicOrigin | HyperdriveOriginWithAccess;
/**
* Name of the Hyperdrive configuration
*/
name: string;

/**
* Local development configuration
* @internal
*/
dev: {
/**
* The connection string to use for local development
*/
origin: Secret;
};
/**
* The Cloudflare-generated UUID of the hyperdrive
*/
hyperdriveId: string;

/**
* Database connection origin configuration
*/
origin: HyperdrivePublicOrigin | HyperdriveOriginWithAccess;
/**
* Local development configuration
* @internal
*/
dev: {
/**
* Resource type identifier for binding.
* @internal
* The connection string to use for local development
*/
type: "hyperdrive";
origin: Secret;
};

/**
* Resource type identifier for binding.
* @internal
*/
type: "hyperdrive";
}

/**
* Represents a Cloudflare Hyperdrive configuration.
*
Expand Down Expand Up @@ -355,6 +354,9 @@ const _Hyperdrive = Resource(
const name =
props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);

const name =
props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);

if (this.scope.local) {
return this({
id,
Expand Down
10 changes: 4 additions & 6 deletions alchemy/src/cloudflare/queue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Context } from "../context.ts";
import { Resource, ResourceKind } from "../resource.ts";
import type { Rune } from "../rune.ts";
import { Scope } from "../scope.ts";
import { CloudflareApiError, handleApiError } from "./api-error.ts";
import {
Expand Down Expand Up @@ -230,17 +231,14 @@ export type Queue<Body = unknown> = Resource<"cloudflare::Queue"> &
*
* @see https://developers.cloudflare.com/queues/
*/
export async function Queue<T = unknown>(
id: string,
props: QueueProps = {},
): Promise<Queue<T>> {
return await _Queue(id, {
export function Queue<T = unknown>(id: string, props: QueueProps = {}) {
return _Queue(id, {
...props,
dev: {
...(props.dev ?? {}),
force: Scope.current.local,
},
});
}) as Rune.of<Queue<T>>;
}

const _Queue = Resource("cloudflare::Queue", async function <
Expand Down
17 changes: 7 additions & 10 deletions alchemy/src/cloudflare/secret.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Context } from "../context.ts";
import { Resource, ResourceKind } from "../resource.ts";
import type { Rune } from "../rune.ts";
import {
secret as alchemySecret,
type Secret as AlchemySecret,
Expand Down Expand Up @@ -138,19 +139,15 @@ export type Secret = Resource<"cloudflare::Secret"> &
* delete: false
* });
*/
export async function Secret(
name: string,
props: SecretProps,
): Promise<Secret> {
// Convert string value to AlchemySecret if needed to prevent plain text serialization
const secretValue =
typeof props.value === "string" ? alchemySecret(props.value) : props.value;

export function Secret(name: string, props: SecretProps) {
// Call the internal resource with secure props
return _Secret(name, {
...props,
value: secretValue,
});
value:
typeof props.value === "string"
? alchemySecret(props.value)
: props.value,
}) as Rune.of<Secret>;
}

const _Secret = Resource(
Expand Down
1 change: 0 additions & 1 deletion alchemy/src/cloudflare/tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,6 @@ export const Tunnel = Resource(
props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);

if (this.phase === "update" && this.output.name !== name) {
console.log("replacing tunnel", this.output.name, name);
this.replace(true);
}

Expand Down
40 changes: 28 additions & 12 deletions alchemy/src/cloudflare/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { Workflow, isWorkflow, upsertWorkflow } from "./workflow.ts";
// Previous versions of `Worker` used the `Bundle` resource.
// This import is here to avoid errors when destroying the `Bundle` resource.
import "../esbuild/bundle.ts";
import type { Rune } from "../rune.ts";

/**
* Configuration options for static assets
Expand Down Expand Up @@ -677,28 +678,44 @@ export type Worker<
*
* @example
* // Create a worker version for testing with a preview URL:
* const previewWorker = await Worker("my-worker", {
* const previewWorker = Worker("my-worker", {
* name: "my-worker",
* entrypoint: "./src/worker.ts",
* version: "pr-123"
* });
*
* // The worker will have a preview URL for testing:
* console.log(`Preview URL: ${previewWorker.url}`);
* console.log(`Preview URL: ${await previewWorker.url}`);
* // Output: Preview URL: https://pr-123-my-worker.subdomain.workers.dev
*/
export function Worker<
const B extends Bindings,
RPC extends Rpc.WorkerEntrypointBranded,
>(id: string, props: WorkerProps<B, RPC>): Promise<Worker<B, RPC>>;

export function Worker<const B extends Bindings>(
export function Worker<const Props extends Resource.input<WorkerProps>>(
id: string,
props: WorkerProps<B>,
): Promise<Worker<B>> {
return _Worker(id, props as WorkerProps<B>);
props: Props,
) {
return _Worker(id, props) as Rune.of<
Worker<awaitBindings<Props>, awaitRpc<Props>>
> & {
// TODO(sam): we have Env and _Env - consolidate
_Env: Worker.Env<Worker<awaitBindings<Props>, awaitRpc<Props>>>;
};
}

export declare namespace Worker {
export type Env<W extends { bindings: any }> = Bindings.Runtime<
Rune.await<W["bindings"]>
>;
}

type awaitRpc<Props extends Resource.input<WorkerProps>> =
Rune.await<Props>["rpc"] extends type<infer U>
? U & Rpc.WorkerEntrypointBranded
: never;

type awaitBindings<Props extends Resource.input<WorkerProps>> = Extract<
Rune.await<Props>["bindings"],
Bindings | undefined
>;

const _Worker = Resource(
"cloudflare::Worker",
{
Expand Down Expand Up @@ -731,7 +748,6 @@ const _Worker = Resource(
logger.warn("projectRoot is deprecated, use cwd instead");
props.cwd = props.projectRoot;
}

const cwd = path.resolve(props.cwd ?? process.cwd());
const compatibilityDate =
props.compatibilityDate ?? DEFAULT_COMPATIBILITY_DATE;
Expand Down
8 changes: 4 additions & 4 deletions alchemy/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import {
import type { Scope } from "./scope.ts";
import type { State } from "./state.ts";

export type Context<
Out extends Resource,
Props extends ResourceProps = ResourceProps,
> = CreateContext<Out> | UpdateContext<Out, Props> | DeleteContext<Out, Props>;
export type Context<Out, Props = ResourceProps> =
| CreateContext<Out>
| UpdateContext<Out, Props>
| DeleteContext<Out, Props>;

export interface CreateContext<Out extends Resource> extends BaseContext<Out> {
phase: "create";
Expand Down
1 change: 1 addition & 0 deletions alchemy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type { AlchemyOptions, Phase } from "./alchemy.ts";
export type * from "./context.ts";

export * from "./resource.ts";
export * from "./rune.ts";
export * from "./scope.ts";
export * from "./secret.ts";
export * from "./serde.ts";
Expand Down
7 changes: 4 additions & 3 deletions alchemy/src/planetscale/branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ export const Branch = Resource(
// TODO(sam): maybe we don't need to replace? just branch again? or rename?
this.replace();
}

if (this.phase === "delete") {
if (this.output?.name) {
const response = await api.organizations.databases.branches.delete({
Expand Down Expand Up @@ -194,6 +193,7 @@ export const Branch = Resource(
? props.parentBranch
: props.parentBranch.name;


if (typeof props.parentBranch !== "string" && props.parentBranch) {
await waitForDatabaseReady(
api,
Expand Down Expand Up @@ -228,8 +228,8 @@ export const Branch = Resource(
);
}

const data = getResponse.data;
const currentParentBranch = data.parent_branch || "main";
const data = getResponse.data;
const currentParentBranch = data.parent_branch || "main";

// Check immutable properties
if (props.parentBranch && parentBranchName !== currentParentBranch) {
Expand Down Expand Up @@ -262,6 +262,7 @@ export const Branch = Resource(
});
}


if (props.clusterSize && data.cluster_name !== props.clusterSize) {
await api.organizations.databases.branches.cluster.patch({
path: {
Expand Down
2 changes: 1 addition & 1 deletion alchemy/src/planetscale/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,6 @@ export const Database = Resource(
body: { new_name: databaseName },
});
}

if (this.phase === "delete") {
if (this.output?.name) {
const response = await api.organizations.databases.delete({
Expand All @@ -212,6 +211,7 @@ export const Database = Resource(
return this.destroy();
}


// Check if database exists
const getResponse = await api.organizations.databases.get({
path: {
Expand Down
Loading
Loading