import { pipe } from "effect"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as App from "./app.ts"; import { isPrimitive } from "./data.ts"; import type { From } from "./policy.ts"; import { getRefMetadata, isRef, ref as stageRef, type Ref } from "./ref.ts"; import type { AnyResource, Resource } from "./resource.ts"; import * as State from "./state.ts"; import type { IsAny, UnionToIntersection } from "./util.ts"; // a special symbol only used at runtime to probe the Output proxy const ExprSymbol = Symbol.for("alchemy/Expr"); export const isOutput = (value: any): value is Output => value && (typeof value === "object" || typeof value === "function") && ExprSymbol in value; export const of = ( resource: Ref | R, ): Output.Of> => { if (isRef(resource)) { const metadata = getRefMetadata(resource); return new RefExpr( metadata.stack, metadata.stage, metadata.resourceId, ) as any; } return new ResourceExpr(resource) as any; }; export const ref = >( resourceId: R["id"], options?: { stage?: string; stack?: string; }, ) => of(stageRef({ resourceId, ...options })); export interface Output { readonly kind: string; readonly src: Src; readonly req: Req; apply(fn: (value: A) => B): Output.Of; effect( // Outputs are not allowed to fail, so we use never for the error type fn: (value: A) => Effect.Effect, ): Output.Of; } export declare namespace Output { // TODO(sam): doesn't support disjunct unions very well export type Of = [ Extract, ] extends [never] ? Output : [Extract] extends [never] ? Object< { [attr in keyof A]: A[attr]; }, Src, Req > : Array, Src, Req>; } export type Object = Output & { [Prop in keyof Exclude]-?: Output.Of< Exclude[Prop] | Extract, Src, Req >; }; export type Array = Output< A, Src, Req > & { [i in Extract]: Output.Of; }; 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; const proxy = (self: any): any => { const proxy = new Proxy( Object.assign(() => {}, self), { has: (_, prop) => (prop === ExprSymbol ? true : prop in self), get: (_, prop) => prop === Symbol.toPrimitive ? (hint: string) => (hint === "string" ? self.toString() : self) : prop === ExprSymbol ? self : 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; }; export abstract class BaseExpr< A = any, Src extends Resource = any, Req = any, > implements Output { declare readonly kind: any; declare readonly src: Src; declare readonly req: Req; // we use a kind tag instead of instanceof to protect ourselves from duplicate alchemy-effect module imports constructor() {} public apply(fn: (value: A) => B): Output.Of { return new ApplyExpr(this as Expr, fn) as any; } public effect( fn: (value: A) => Effect.Effect, ): Output.Of { return new EffectExpr(this as any, fn) as any; } public pipe(...fns: any[]): any { // @ts-expect-error return pipe(this, ...fns); } toString(): string { return JSON.stringify(this, null, 2); } } export const isResourceExpr = < Value = any, Src extends AnyResource = AnyResource, Req = any, >( node: Expr | any, ): node is ResourceExpr => node?.kind === "ResourceExpr"; export class ResourceExpr< Value, Src extends AnyResource, Req = never, > extends BaseExpr { readonly kind = "ResourceExpr"; constructor( public readonly src: Src, readonly stables?: Record, ) { super(); return proxy(this); } } export const isPropExpr = < A = any, Prop extends keyof A = keyof A, Src extends AnyResource = AnyResource, Req = any, >( node: any, ): node is PropExpr => node?.kind === "PropExpr"; export class PropExpr< A = any, Id extends keyof A = keyof A, Src extends AnyResource = AnyResource, Req = any, > extends BaseExpr { readonly kind = "PropExpr"; constructor( public readonly expr: Expr, public readonly identifier: Id, ) { super(); return proxy(this); } } 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); } } //Output.ApplyExpr export const isApplyExpr = < In = any, Out = any, Src extends AnyResource = AnyResource, Req = any, >( node: Output, ): node is ApplyExpr => node?.kind === "ApplyExpr"; export class ApplyExpr< A, B, Src extends AnyResource, Req = never, > extends BaseExpr { readonly kind = "ApplyExpr"; constructor( public readonly expr: Expr, public readonly f: (value: A) => B, ) { super(); return proxy(this); } } export const isEffectExpr = < In = any, Out = any, Src extends AnyResource = AnyResource, Req = any, Req2 = any, >( node: any, ): node is EffectExpr => node?.kind === "EffectExpr"; export class EffectExpr< A, B, Src extends AnyResource, Req = never, Req2 = never, > extends BaseExpr { readonly kind = "EffectExpr"; constructor( public readonly expr: Expr, public readonly f: (value: A) => Effect.Effect, ) { super(); return proxy(this); } } 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); } } 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); } } 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, upstream: { [Id in Upstream["id"]]: Extract["attr"]; }, ) => Effect.Effect< A, InvalidReferenceError | MissingSourceError, State.State > = (expr, upstream) => Effect.gen(function* () { if (isResourceExpr(expr)) { const srcId = expr.src.id; 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* Effect.fail( 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 app = yield* App.App; const stack = expr.stack ?? app.name; const stage = expr.stage ?? app.stage; const resource = yield* state.get({ stack, stage, resourceId: 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; } else 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 type Upstream> = O extends Output ? { [Id in Up["id"]]: Extract; } : never; 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 {}; }; export const upstream = >( expr: E, ): { [Id in keyof Upstream]: Upstream[Id]; } => _upstream(expr); const _upstream = (expr: any): any => { if (isResourceExpr(expr)) { return { [expr.src.id]: 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 {}; }; const toObject = (acc: B, v: A) => ({ ...acc, ...v, }); export type ResolveUpstream = unknown extends A ? { [id: string]: Resource } : A extends undefined | null | boolean | number | string | symbol | bigint ? {} : IsAny extends true ? {} : A extends Output ? { [Id in Upstream["id"]]: Extract; } : A extends readonly any[] | any[] ? ResolveUpstream : A extends Record ? { [Id in keyof UnionToIntersection< ResolveUpstream >]: UnionToIntersection>[Id]; } : {}; export const resolveUpstream = (value: A): ResolveUpstream => { 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; }; export const interpolate = ( template: TemplateStringsArray, ...args: Args ): All extends Output ? Output : never => all(...args.map((arg) => (isOutput(arg) ? arg : literal(arg)))).apply( (args) => template .map((str, i) => str + (args[i] == null ? "" : String(args[i]))) .join(""), ) as any; 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; export type Tuple< Outs extends (Output | Expr)[], Values extends any[] = [], Src extends Resource = never, Req = never, > = Outs extends [infer H, ...infer Tail extends (Output | Expr)[]] ? H extends Output ? Tuple : never : Output; export const filter = (...outs: Outs) => outs.filter(isOutput) as unknown as Filter; export type Filter = number extends Outs["length"] ? Output< Extract["value"], Extract["src"], Extract["req"] > : FilterTuple; export type FilterTuple< Outs extends (Output | Expr)[], Values extends any[] = [], Src extends Resource = never, > = Outs extends [infer H, ...infer Tail extends (Output | Expr)[]] ? H extends Output ? FilterTuple : FilterTuple : Output;