import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import { pipeArguments, type Pipeable } from "effect/Pipeable"; import { SingleShotGen } from "effect/Utils"; import { toFqn } from "./FQN.ts"; import type { Input } from "./Input.ts"; import { CurrentNamespace, type NamespaceNode } from "./Namespace.ts"; import * as Output from "./Output.ts"; import { Provider } from "./Provider.ts"; import { RemovalPolicy } from "./RemovalPolicy.ts"; import { Self } from "./Self.ts"; import { Stack } from "./Stack.ts"; export type ResourceConstructor = { Type: R["Type"]; Props: R["Props"]; ( methods: Methods, ): ResourceClassWithMethods; ( id: string, ...args: {} extends R["Props"] ? [ props?: { [prop in keyof R["Props"]]: Input; }, ] : [ props: { [prop in keyof R["Props"]]: Input; }, ] ): Effect.Effect; // ( // id: string, // props: Effect.Effect, never, PropsReq>, // ): Effect.Effect; }; export type ResourceClassWithMethods< R extends ResourceLike, Methods extends { [key: string]: any }, > = ResourceConstructor> & Effect.Effect> & { Self: Self; Provider: Provider; } & Methods; export type ResourceClass = ResourceConstructor< R, Provider > & Effect.Effect> & { Self: Self; Provider: Provider; }; export type LogicalId = string; export interface ResourceBinding { sid: string; data: Data; } export interface ResourceLike< Type extends string = string, Props extends object | undefined = any, Attributes extends object = object, Binding = any, > { /** * Namespace containing this Resource. */ Namespace: NamespaceNode | undefined; /** * Fully Qualified Name (namespace path + logical ID). * Used as the unique key for state storage. */ FQN: string; /** * Type of the Resource (e.g. AWS.Lambda.Function) */ Type: Type; /** * Logical ID of the Resource (e.g. MyFunction) */ LogicalId: LogicalId; /** * Properties of the Resource. */ Props: Props; /** * Removal Policy of the Resource. */ RemovalPolicy: RemovalPolicy["Service"]; /** @internal phantom */ Attributes: Attributes; /** @internal phantom */ Binding: Binding; } export const isResource = (value: any): value is ResourceLike => { return typeof value === "object" && value !== null && "Type" in value; }; export type Resource< Type extends string = any, Props extends object | undefined = any, Attributes extends object = any, Binding = never, > = Pipeable & ResourceLike & { bind(sid: string, binding: Input): Effect.Effect; bind( template: TemplateStringsArray, ...args: any[] ): (binding: Input) => Effect.Effect; } & { [attr in keyof Attributes]-?: Output.Output; }; /** * Creates a resource constructor for a concrete resource type. * * The returned constructor registers the resource on the current stack, * resolves input props, exposes output attributes as `Output` expressions, and * records bindings contributed by policies and event sources. Resource * providers are attached separately through `.provider`. */ export function Resource( type: R["Type"], ): ResourceClass { type Props = Input; const self = Self(type); const constructor = ( id: string, props: Props | Effect.Effect | undefined, ) => Effect.gen(function* () { const stack = yield* Stack; const namespace = yield* CurrentNamespace; const fqn = toFqn(namespace, id); const existing = stack.resources[fqn]; if (existing) { // // TODO(sam): check if props are different and die return existing; } const bind = ( ...args: | [sid: string, data: R["Binding"]] | [template: TemplateStringsArray, ...args: any[]] ) => typeof args[0] === "string" ? Effect.gen(function* () { const [sid, data] = args as [sid: string, data: R["Binding"]]; (stack.bindings[fqn] ??= []).push({ sid, data, }); return undefined; }) : (data: R["Binding"]) => { const stringifyBindArg = (arg: any): string | undefined => { if (arg === undefined) { return undefined; } if (Array.isArray(arg)) { return arg .flatMap((item) => { const stringified = stringifyBindArg(item); return stringified === undefined ? [] : [stringified]; }) .join(", "); } if ( arg && (typeof arg === "object" || typeof arg === "function") ) { if ("LogicalId" in arg && typeof arg.LogicalId === "string") { return arg.LogicalId; } if ("id" in arg && typeof arg.id === "string") { return arg.id; } } return String(arg); }; return bind( `${(args[0] as TemplateStringsArray) .flatMap((text, i) => { const stringified = stringifyBindArg(args[i + 1]); return stringified !== undefined ? [text, stringified] : [text]; }) .join("")}`, data, ); }; const target: any = { Type: type, Namespace: namespace, FQN: fqn, LogicalId: id, Props: props, Provider: ProviderTag as Provider, RemovalPolicy: yield* Effect.serviceOption(RemovalPolicy).pipe( Effect.map(Option.getOrElse(() => "destroy" as const)), ), bind, toString(this: typeof target) { return `Resource<${this.Type}>(${this.LogicalId})`; }, [Symbol.toPrimitive](this: typeof target, hint: string) { return hint === "number" ? NaN : this.toString(); }, // TODO(sam): figure out a better way to log things in cloudflare, this breaks indentation and is bloated // [nodeInspect]( // depth: number, // options: { depth?: number | null } & Record, // inspect: (value: unknown, opts?: unknown) => string, // ) { // if (depth < 0) { // return target.toString(); // } // const nextDepth = // options.depth == null ? null : Math.max(0, options.depth - 1); // return inspect( // { // Type: target.Type, // Namespace: target.Namespace, // FQN: target.FQN, // LogicalId: target.LogicalId, // Props: target.Props, // RemovalPolicy: target.RemovalPolicy, // }, // { ...options, depth: nextDepth }, // ); // }, }; const Resource: R = (stack.resources[fqn] = new Proxy(target, { set: (t, prop, value) => { t[prop as keyof typeof t] = value; return true; }, get: (t, prop) => typeof prop === "symbol" || prop in t ? t[prop as keyof typeof t] : new Output.PropExpr(Output.of(Resource), prop), })) as R; Resource.Props = Effect.isEffect(props) ? yield* props.pipe( Effect.provideService(Self, Resource), Effect.provideService(Self(type), Resource), ) : props; return Resource; }); const ProviderTag = Provider(type); const Service = { [Symbol.iterator]() { return new SingleShotGen(this); }, pipe() { return pipeArguments(this.asEffect(), arguments); }, asEffect() { return Effect.succeed((id: string, props: R["Props"]) => constructor(id, props), ); }, Type: type, Provider: ProviderTag, Self: self, }; const ResourceClass = Object.assign( (...args: [id: string, props: R["Props"]] | [methods: object]) => typeof args[0] === "object" ? Object.assign(ResourceClass, args[0]) : constructor(...(args as [string, R["Props"]])), Service, ) as any; return ResourceClass; }