import * as Data from "effect/Data"; import type { Yieldable } from "effect/Effect"; import * as Effect from "effect/Effect"; import { pipe } from "effect/Function"; import type { Pipeable } from "effect/Pipeable"; import { SingleShotGen } from "effect/Utils"; import { ExecutionContext } from "./ExecutionContext.ts"; import { getRefMetadata, isRef, ref as stageRef, type Ref } from "./Ref.ts"; import { isResource, type Resource, type ResourceLike } from "./Resource.ts"; import { Stack } from "./Stack.ts"; import { Stage } from "./Stage.ts"; import * as State from "./State/State.ts"; import { isPrimitive } from "./Util/data.ts"; const inspect = Symbol.for("nodejs.util.inspect.custom"); export const of = ( resource: Ref | R, ): R extends ResourceLike ? ResourceExpr : RefExpr => { if (isRef(resource)) { const metadata = getRefMetadata(resource); return new RefExpr(metadata.stack, metadata.stage, metadata.id) as any; } return new ResourceExpr(resource) as any; }; export const asOutput = (t: T | Output | Effect.Effect): Output => isOutput(t) ? t : Effect.isEffect(t) ? new EffectExpr(VoidExpr, () => t) : new LiteralExpr(t); export const isOutput = (value: any): value is Output => value && (typeof value === "object" || typeof value === "function") && ExprSymbol in value; export interface Output extends Pipeable { /** @internal phantom */ readonly kind: string; /** @internal phantom */ readonly A: A; /** @internal phantom */ readonly req: Req; /** @internal phantom */ [Symbol.iterator](): Iterator< Yieldable, Accessor, void >; bind(id: string): Effect.Effect, never, ExecutionContext>; asEffect(): Effect.Effect, never, Req>; as(): Output; } export interface Accessor extends Effect.Effect {} export type ToOutput = [Extract] extends [never] ? Output : [Extract] extends [never] ? ObjectExpr< { [attr in keyof A]: A[attr]; }, Req > : ArrayExpr, Req>; export const ExprSymbol = Symbol.for("alchemy/Expr"); export const isExpr = (value: any): value is Expr => value && (typeof value === "object" || typeof value === "function") && ExprSymbol in value; export type Expr = | AllExpr[]> | ApplyExpr | EffectExpr | LiteralExpr | PropExpr | ResourceExpr | RefExpr; export abstract class BaseExpr implements Output { declare readonly kind: any; declare readonly A: A; declare readonly src: ResourceLike; declare readonly req: Req; // we use a kind tag instead of instanceof to protect ourselves from duplicate alchemy-effect module imports constructor() {} as(): Output { return this as any; } [Symbol.iterator](): Iterator< Yieldable, Accessor, void > { // @ts-expect-error - TODO(sam): fix this (works at runtime, but maybe indicates a bad assumption) return new SingleShotGen(this); } asEffect(): any { return this.bind(this.toString()); } public bind(id: string): any { return ExecutionContext.asEffect().pipe( Effect.flatMap((ctx) => Effect.map(ctx.set(id, this), (key) => ctx.get(key)), ), ); } public pipe(...fns: any[]): any { // @ts-expect-error return pipe(this, ...fns); } public abstract [inspect](): string; public toString(): string { return this[inspect](); } } export type ObjectExpr = Output & { [Prop in keyof Exclude]-?: ToOutput< Exclude[Prop] | Extract, Req >; }; export type ArrayExpr = Output & { [i in Extract]: ToOutput; }; export const isResourceExpr = ( node: Expr | any, ): node is ResourceExpr => node?.kind === "ResourceExpr"; export class ResourceExpr extends BaseExpr { readonly kind = "ResourceExpr"; constructor( readonly src: ResourceLike, readonly stables?: Record, ) { super(); return proxy(this); } [inspect](): string { return this.src.LogicalId; } } export const isPropExpr = ( node: any, ): node is PropExpr => node?.kind === "PropExpr"; export class PropExpr< A = any, Id extends keyof A = keyof A, Req = any, > extends BaseExpr { readonly kind = "PropExpr"; constructor( public readonly expr: Expr, public readonly identifier: Id, ) { super(); return proxy(this); } [inspect](): string { return `${this.expr[inspect]()}.${this.identifier.toString()}`; } } export const literal = (value: A) => new LiteralExpr(value); export const isLiteralExpr = (node: any): node is LiteralExpr => node?.kind === "LiteralExpr"; export class LiteralExpr extends BaseExpr { readonly kind = "LiteralExpr"; constructor(public readonly value: A) { super(); return proxy(this); } [inspect](): string { return String(this.value); } } export const VoidExpr = new LiteralExpr(void 0); export const map: { ( fn: (value: A) => B, ): (output: Output) => ToOutput; (output: Output, fn: (value: A) => B): ToOutput; } = (( ...args: [fn: (value: A) => B] | [output: Output, fn: (value: A) => B] ) => args.length === 1 ? (output: Output): ToOutput => new ApplyExpr(output as Expr, args[0]) as any : new ApplyExpr(args[0] as any, args[1])) as any; type _ = Parameters; //Output.ApplyExpr export const isApplyExpr = ( node: Output, ): node is ApplyExpr => node?.kind === "ApplyExpr"; export class ApplyExpr extends BaseExpr { readonly kind = "ApplyExpr"; constructor( public readonly expr: Expr, public readonly f: (value: A) => B, ) { super(); return proxy(this); } [inspect](): string { return `${this.expr[inspect]()}.map(${this.f.toString()})`; } } export const mapEffect = (fn: (value: A) => Effect.Effect) => (output: Output): ToOutput => new EffectExpr(output as Expr, fn) as any; export const isEffectExpr = ( node: any, ): node is EffectExpr => node?.kind === "EffectExpr"; export class EffectExpr extends BaseExpr< B, Req > { readonly kind = "EffectExpr"; constructor( public readonly expr: Expr, public readonly f: (value: A) => Effect.Effect, ) { super(); return proxy(this); } [inspect](): string { return `${this.expr[inspect]()}.mapEffect(${this.f.toString()})`; } } export const all = (...outs: Outs) => new AllExpr(outs as any) as unknown as All; export type All = number extends Outs["length"] ? [Outs[number]] extends [ Output | Expr, ] ? Output : never : Tuple; type Tuple< Outs extends (Output | Expr)[], Values extends any[] = [], Req = never, > = Outs extends [infer H, ...infer Tail extends (Output | Expr)[]] ? H extends Output ? Tuple : never : Output; export const isAllExpr = ( node: any, ): node is AllExpr => node?.kind === "AllExpr"; export class AllExpr extends BaseExpr { readonly kind = "AllExpr"; constructor(public readonly outs: Outs) { super(); return proxy(this); } [inspect](): string { return `all(${this.outs.map((out) => out[inspect]()).join(", ")})`; } } export const isRefExpr = (node: any): node is RefExpr => node?.kind === "RefExpr"; export class RefExpr extends BaseExpr { readonly kind = "RefExpr"; constructor( public readonly stack: string | undefined, public readonly stage: string | undefined, public readonly resourceId: string, ) { super(); return proxy(this); } [inspect](): string { return `ref(${this.resourceId}, { stack: ${this.stack}, stage: ${this.stage} })`; } } export const filter = (...outs: Outs) => outs.filter(isOutput) as unknown as Filter; export type Filter = number extends Outs["length"] ? Output< Extract["value"], Extract["req"] > : FilterTuple; export type FilterTuple< Outs extends (Output | Expr)[], Values extends any[] = [], > = Outs extends [infer H, ...infer Tail extends (Output | Expr)[]] ? H extends Output ? FilterTuple : FilterTuple : Output; export const interpolate = ( template: TemplateStringsArray, ...args: Args ): All extends Output ? Output : never => all(...args.map((arg) => (isOutput(arg) ? arg : literal(arg)))).pipe( map((args) => template .map((str, i) => str + (args[i] == null ? "" : String(args[i]))) .join(""), ), ) as any; function proxy(self: any): any { const target = Object.assign(() => {}, self); if (inspect in self) { Object.defineProperty(target, inspect, { value: self[inspect].bind(self), configurable: true, }); } const proxy = new Proxy(target, { has: (_, prop) => prop === ExprSymbol || prop === inspect ? true : prop in self, get: (target, prop) => prop === Symbol.toPrimitive ? (hint: string) => (hint === "string" ? self.toString() : self) : prop === ExprSymbol ? self : prop === inspect ? target[inspect] : isResourceExpr(self) && self.stables && prop in self.stables ? self.stables[prop as keyof typeof self.stables] : prop === "apply" ? self[prop] : prop in self ? typeof self[prop as keyof typeof self] === "function" && !("kind" in self) ? new PropExpr(proxy, prop as never) : self[prop as keyof typeof self] : new PropExpr(proxy, prop as never), apply: (_, thisArg, args) => { if (isPropExpr(self)) { if (self.identifier === "apply") { return new ApplyExpr(self.expr, args[0]); } else if (self.identifier === "effect") { return new EffectExpr(self.expr, args[0]); } } return undefined; }, }); return proxy; } /// Evaluation export class MissingSourceError extends Data.TaggedError("MissingSourceError")<{ message: string; srcId: string; }> {} export class InvalidReferenceError extends Data.TaggedError( "InvalidReferenceError", )<{ message: string; stack: string; stage: string; resourceId: string; }> {} export const evaluate: ( expr: Output | A, upstream: { [Id in string]: any; }, ) => Effect.Effect< A, InvalidReferenceError | MissingSourceError, State.State | Req > = (expr, upstream) => Effect.gen(function* () { if (isResource(expr)) { const srcId = expr.FQN; const src = upstream[srcId as keyof typeof upstream]; if (!src) { // type-safety should prevent this but let the caller decide how to handle it return yield* new MissingSourceError({ message: `Source ${srcId} not found`, srcId, }); } return src; } else if (isOutput(expr)) { if (isResourceExpr(expr)) { const srcId = expr.src.FQN; const src = upstream[srcId as keyof typeof upstream]; if (!src) { // type-safety should prevent this but let the caller decide how to handle it return yield* new MissingSourceError({ message: `Source ${srcId} not found`, srcId, }); } return src; } else if (isLiteralExpr(expr)) { return expr.value; } else if (isApplyExpr(expr)) { return expr.f(yield* evaluate(expr.expr, upstream)); } else if (isEffectExpr(expr)) { // TODO(sam): the same effect shoudl be memoized so that it's not run multiple times return yield* expr.f(yield* evaluate(expr.expr, upstream)); } else if (isAllExpr(expr)) { return yield* Effect.all( expr.outs.map((out) => evaluate(out, upstream)), ); } else if (isPropExpr(expr)) { return (yield* evaluate(expr.expr, upstream))?.[expr.identifier]; } else if (isRefExpr(expr)) { const state = yield* State.State; const stack = expr.stack ?? (yield* Stack).name; const stage = expr.stage ?? (yield* Stage); const resource = yield* state.get({ stack, stage, fqn: expr.resourceId, }); if (!resource) { return yield* Effect.fail( new InvalidReferenceError({ message: `Reference to '${expr.resourceId}' in stack '${stack}' and stage '${stage}' not found. Have you deployed '${stage}' or '${stack}'?`, stack, stage, resourceId: expr.resourceId, }), ); } return resource.attr; } } if (Array.isArray(expr)) { return yield* Effect.all(expr.map((item) => evaluate(item, upstream))); } else if (typeof expr === "object" && expr !== null) { return Object.fromEntries( yield* Effect.all( Object.entries(expr).map(([key, value]) => evaluate(value, upstream).pipe(Effect.map((value) => [key, value])), ), ), ); } return expr; }) as Effect.Effect; export const ref = ( resourceId: R["LogicalId"], options?: { stage?: string; stack?: string; }, ) => of(stageRef({ id: resourceId, ...options })); export const hasOutputs = (value: any): value is Output => Object.keys(upstreamAny(value)).length > 0; export const upstreamAny = ( value: any, ): { [ID in string]: Resource; } => { if (isExpr(value)) { return upstream(value); } else if (Array.isArray(value)) { return Object.assign({}, ...value.map(resolveUpstream)); } else if ( value && (typeof value === "object" || typeof value === "function") ) { return Object.assign( {}, ...Object.values(value).map((value) => resolveUpstream(value)), ); } return {}; }; // TODO(sam): add a type export const upstream = >(expr: E): any => { if (isResourceExpr(expr)) { return { [expr.src.FQN]: expr.src, }; } else if (isPropExpr(expr)) { return upstream(expr.expr); } else if (isAllExpr(expr)) { return Object.assign({}, ...expr.outs.map((out) => upstream(out))); } else if (isEffectExpr(expr) || isApplyExpr(expr)) { return upstream(expr.expr); } else if (Array.isArray(expr)) { return expr.map(upstream).reduce(toObject, {}); } else if (typeof expr === "object" && expr !== null) { return Object.values(expr) .map((v) => upstream(v)) .reduce(toObject, {}); } return {}; }; // TODO(sam): add a type export const resolveUpstream = (value: A): any => { if (isPrimitive(value)) { return {} as any; } else if (isOutput(value)) { return upstream(value) as any; } else if (Array.isArray(value)) { return Object.fromEntries( value.map((v) => resolveUpstream(v)).flatMap(Object.entries), ) as any; } else if (typeof value === "object" || typeof value === "function") { return Object.fromEntries( Object.values(value as any) .map(resolveUpstream) .flatMap(Object.entries), ) as any; } return {} as any; }; const toObject = (acc: B, v: A) => ({ ...acc, ...v, }); export const log = (_value: A) => Effect.gen(function* () { // TODO(sam): implement a log effect }); export const toEnvKey = ( id: ID, suffix: Suffix, ) => `${replace(toUpper(id))}_${replace(toUpper(suffix))}` as const; export const toUpper = (str: S) => str.toUpperCase() as string extends S ? S : Uppercase; const replace = (str: S) => str.replace(/-/g, "_") as Replace; type Replace = string extends S ? S : S extends "" ? Accum : S extends `${infer S}${infer Rest}` ? S extends "-" ? Replace : Replace : Accum;