Skip to content

Commit 9e8fc77

Browse files
committed
feat: use triples in Policy to support overriden tags for bindings
1 parent 88a594a commit 9e8fc77

File tree

11 files changed

+146
-113
lines changed

11 files changed

+146
-113
lines changed

@alchemy.run/effect-aws/src/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { TagInstance } from "@alchemy.run/effect";
21
import type * as Context from "effect/Context";
32
import * as Effect from "effect/Effect";
43
import * as Layer from "effect/Layer";
@@ -7,6 +6,8 @@ import type { AWSClientConfig } from "itty-aws";
76
import { Credentials } from "./credentials.ts";
87
import { Region } from "./region.ts";
98

9+
export type TagInstance<T> = T extends new (_: never) => infer R ? R : never;
10+
1011
export const createAWSServiceClientLayer =
1112
<Tag extends Context.Tag<any, any>, Client>(
1213
tag: Tag,

@alchemy.run/effect-aws/src/lambda/function.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import {
2-
Binding,
32
Provider,
43
Runtime,
5-
type Capability,
64
type RuntimeHandler,
75
type RuntimeProps,
86
} from "@alchemy.run/effect";
97
import type { Context as LambdaContext } from "aws-lambda";
10-
import * as IAM from "../iam.ts";
118

129
export interface FunctionProps<Req = any> extends RuntimeProps<Function, Req> {
1310
main: string;
@@ -37,21 +34,10 @@ export interface Function<
3734
> extends Runtime<"AWS.Lambda.Function", Handler, Props> {
3835
readonly Constructor: Function;
3936
readonly Provider: FunctionProvider;
40-
readonly Binding: FunctionBinding<this["capability"]>;
4137
readonly Instance: Function<this["handler"], this["props"]>;
4238

4339
readonly attr: FunctionAttr<Extract<this["props"], FunctionProps>>;
4440
}
4541
export const Function = Runtime("AWS.Lambda.Function")<Function>();
4642

47-
export interface FunctionBinding<Cap extends Capability>
48-
extends Binding<
49-
Function,
50-
Cap,
51-
{
52-
env: Record<string, string>;
53-
policyStatements: IAM.PolicyStatement[];
54-
}
55-
> {}
56-
5743
export type FunctionProvider = Provider<Function>;

@alchemy.run/effect-aws/src/lambda/serve.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { $ } from "@alchemy.run/effect";
21
import type {
32
Context as LambdaContext,
43
LambdaFunctionURLEvent,
@@ -19,10 +18,5 @@ export const serve =
1918
) => Effect.Effect<LambdaFunctionURLResult, never, Req>;
2019
},
2120
) =>
22-
<const Props extends Lambda.FunctionProps<Req>>(props: Props) => {
23-
const f = Lambda.Function(id, fetch);
24-
return f({
25-
main: "",
26-
bindings: $(),
27-
});
28-
};
21+
<const Props extends Lambda.FunctionProps<Req>>(props: Props) =>
22+
Lambda.Function(id, { handle: fetch })(props);

@alchemy.run/effect-aws/src/sqs/queue.consume.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import * as Effect from "effect/Effect";
44
import { Function, type FunctionBinding } from "../lambda/index.ts";
55
import { Queue } from "./queue.ts";
66

7+
export type QueueRecord<Data> = Omit<lambda.SQSRecord, "body"> & {
8+
body: Data;
9+
};
10+
11+
export type QueueEvent<Data> = Omit<lambda.SQSEvent, "Records"> & {
12+
Records: QueueRecord<Data>[];
13+
};
14+
715
export interface Consume<Q> extends Capability<"AWS.SQS.Consume", Q> {}
816

917
export const Consume = Function.binding<
@@ -18,14 +26,6 @@ export const Consume = Function.binding<
1826
) => FunctionBinding<Consume<Q>>
1927
>("AWS.SQS.Consume", Queue);
2028

21-
export type QueueRecord<Data> = Omit<lambda.SQSRecord, "body"> & {
22-
body: Data;
23-
};
24-
25-
export type QueueEvent<Data> = Omit<lambda.SQSEvent, "Records"> & {
26-
Records: QueueRecord<Data>[];
27-
};
28-
2929
export const consumeFromLambdaFunction = () =>
3030
Consume.layer.succeed({
3131
// oxlint-disable-next-line require-yield

@alchemy.run/effect-aws/src/sqs/queue.send-message.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import * as Effect from "effect/Effect";
22

3-
import { Policy, type Capability, type Declared } from "@alchemy.run/effect";
4-
import { Function, type FunctionBinding } from "../lambda/index.ts";
3+
import {
4+
Binding,
5+
Policy,
6+
type Capability,
7+
type Declared,
8+
} from "@alchemy.run/effect";
9+
import { Function } from "../lambda/index.ts";
510
import { QueueClient } from "./queue.client.ts";
611
import { Queue } from "./queue.ts";
712

@@ -22,9 +27,16 @@ export const sendMessage = <Q extends Queue>(
2227
});
2328
});
2429

25-
export const SendMessage = Function.binding<
26-
<Q extends Queue>(queue: Declared<Q>) => FunctionBinding<SendMessage<Q>>
27-
>("AWS.SQS.SendMessage", Queue);
30+
// provide a custom tag to uniquely identify your binding implementation of Function<SendMessage<Q>>
31+
export const SendMessage2 = Binding<
32+
<Q extends Queue>(
33+
queue: Declared<Q>,
34+
) => Binding<Function, SendMessage<Q>, "Hyperdrive">
35+
>(Function, Queue, "Hyperdrive");
36+
37+
export const SendMessage = Binding<
38+
<Q extends Queue>(queue: Declared<Q>) => Binding<Function, SendMessage<Q>>
39+
>(Function, Queue, "AWS.SQS.SendMessage");
2840

2941
export const sendMessageFromLambdaFunction = () =>
3042
SendMessage.layer.succeed({

@alchemy.run/effect-example/src/api.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,34 @@ import { Message, Messages } from "./messages.ts";
88
// SQS.SendMessage<Messages>
99
// -> FunctionBinding<SQS.SendMessage<Messages>>
1010
// -> FunctionBinding<SQS.SendMessage<Queue>>
11+
// -> "AWS.Lambda.Function(SQS.SendMessage(AWS.SQS.Queue))"
12+
13+
const ____ = $(SQS.SendMessage(Messages));
14+
const _____ = $(SQS.SendMessage2(Messages));
15+
16+
const ___ = Lambda.serve("Api", {
17+
fetch: Effect.fn(function* (event) {
18+
const msg = yield* S.validate(Message)(event.body).pipe(
19+
Effect.catchAll(Effect.die),
20+
);
21+
yield* SQS.sendMessage(Messages, msg).pipe(
22+
Effect.catchAll(() => Effect.void),
23+
);
24+
return {
25+
body: JSON.stringify(null),
26+
};
27+
}),
28+
})({
29+
main: import.meta.filename,
30+
bindings: $(SQS.SendMessage(Messages)),
31+
});
1132

1233
export class Api extends Lambda.serve("Api", {
1334
fetch: Effect.fn(function* (event) {
1435
const msg = yield* S.validate(Message)(event.body).pipe(
1536
Effect.catchAll(Effect.die),
1637
);
17-
const ___ = SQS.sendMessage(Messages, msg).pipe(
38+
yield* SQS.sendMessage(Messages, msg).pipe(
1839
Effect.catchAll(() => Effect.void),
1940
);
2041
return {

@alchemy.run/effect/src/binding.ts

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,82 @@
1-
import * as Context from "effect/Context";
2-
import type * as Effect from "effect/Effect";
1+
import type { Effect } from "effect/Effect";
2+
import type { Layer } from "effect/Layer";
33
import type { Capability } from "./capability.ts";
4-
import type { Policy } from "./policy.ts";
54
import type { Resource } from "./resource.ts";
65
import type { Runtime } from "./runtime.ts";
76

8-
export type Bindings = ReturnType<typeof Bindings>;
9-
10-
export const Bindings = <S extends any[]>(
11-
...capabilities: S
12-
): Policy<S[number]> => ({
13-
capabilities,
14-
and: <C extends Capability[]>(...caps: C): Policy<C[number] | S[number]> =>
15-
Bindings(...capabilities, ...caps),
16-
});
17-
18-
export type $ = typeof $;
19-
export const $ = Bindings;
20-
217
export interface BindingProps {
228
[key: string]: any;
239
}
2410

11+
export const isBinding = (b: any): b is Binding<any, any, any> =>
12+
"runtime" in b && "capability" in b && "tag" in b && "output" in b;
13+
14+
export type AnyBinding<F extends Runtime = any> = Binding<F, any, any>;
15+
2516
export interface Binding<
2617
Run extends Runtime,
2718
Cap extends Capability = Capability,
28-
Output = any,
29-
> extends Context.TagClass<
30-
Runtime.Binding<Run, Cap>,
31-
`${Cap["action"]}(${Cap["resource"]["type"]}, ${Run["type"]})`,
32-
BindingService<Cap["resource"], Output>
33-
> {
19+
Tag = Cap["type"],
20+
> {
3421
runtime: Run;
3522
capability: Cap;
36-
output: Output;
23+
tag: Tag;
3724
}
3825

39-
export const Binding =
40-
<
41-
const Runtime extends string,
42-
Cap extends Capability,
43-
Props extends BindingProps,
44-
>(
45-
runtime: Runtime,
46-
capability: Cap,
47-
) =>
48-
<Self>(): Self =>
49-
Object.assign(
50-
Context.Tag(
51-
`${capability.action}(${capability.resource.type}, ${runtime})` as `${Cap["action"]}(${Cap["resource"]["type"]}, ${Runtime})`,
52-
)<Self, BindingService<Cap["resource"], Props>>(),
53-
{
54-
Kind: "Binding",
55-
Capability: capability,
26+
export const Binding = <F extends (resource: any, props?: any) => AnyBinding>(
27+
runtime: ReturnType<F>["runtime"],
28+
resource: new () => ReturnType<F>["capability"]["resource"],
29+
tag: ReturnType<F>["tag"],
30+
): F & BindingDeclaration<ReturnType<F>["runtime"], F> => {
31+
type Runtime = ReturnType<F>["runtime"];
32+
type Tag = ReturnType<F>["tag"];
33+
type Resource = new () => ReturnType<F>["capability"]["resource"];
34+
35+
const handler = (() => {
36+
throw new Error(`Should never be called`);
37+
}) as unknown as F;
38+
39+
return Object.assign(handler, {
40+
layer: {
41+
effect: () => {
42+
throw new Error(`Not implemented`);
5643
},
57-
) as Self;
44+
succeed: () => {
45+
throw new Error(`Not implemented`);
46+
},
47+
},
48+
});
49+
};
50+
51+
export interface BindingDeclaration<
52+
Run extends Runtime,
53+
F extends (target: any, props?: any) => Binding<Run, any>,
54+
Tag = ReturnType<F>["tag"],
55+
> {
56+
layer: {
57+
effect<Err, Req>(
58+
eff: Effect<
59+
BindingService<Run["props"], Parameters<F>[0], Parameters<F>[1]>,
60+
Err,
61+
Req
62+
>,
63+
): Layer<Tag, Err, Req>;
64+
succeed(
65+
service: BindingService<Run["props"], Parameters<F>[0], Parameters<F>[1]>,
66+
): Layer<BindingService<Run["props"], Parameters<F>[0], Parameters<F>[1]>>;
67+
};
68+
}
69+
70+
// <Self>(): Self =>
71+
// Object.assign(
72+
// Context.Tag(
73+
// `${capability.action}(${tag}, ${runtime})` as `${Cap["action"]}(${Tag}, ${Runtime})`,
74+
// )<Self, BindingService<Cap["resource"], Props>>(),
75+
// {
76+
// Kind: "Binding",
77+
// Capability: capability,
78+
// },
79+
// ) as Self;
5880

5981
export type BindingService<
6082
Target = any,

@alchemy.run/effect/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,3 @@ export * from "./provider.ts";
1515
export * from "./resource.ts";
1616
export * from "./runtime.ts";
1717
export * as State from "./state.ts";
18-
export * from "./tag-instance.ts";

@alchemy.run/effect/src/policy.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
11
import * as Effect from "effect/Effect";
2+
import { type AnyBinding } from "./binding.ts";
23
import type { Capability } from "./capability.ts";
4+
import type { Runtime } from "./runtime.ts";
35

46
// A policy is invariant over its allowed actions
5-
export interface Policy<in out Caps = any> {
6-
readonly capabilities: Caps[];
7+
export interface Policy<
8+
in out F extends Runtime,
9+
in out Capabilities = any,
10+
Tags = unknown,
11+
> {
12+
readonly runtime: F;
13+
readonly tags: Tags[];
14+
readonly capabilities: Capabilities[];
715
/** Add more Capabilities to a Policy */
8-
and<C extends any[]>(...caps: C): Policy<C[number] | Caps>;
16+
and<B extends AnyBinding[]>(
17+
...bindings: B
18+
): Policy<F, B[number]["capability"] | Capabilities, Tags>;
19+
}
20+
21+
export type $ = typeof $;
22+
export const $ = Policy;
23+
24+
export function Policy<F extends Runtime>(): Policy<F, never, never>;
25+
export function Policy<B extends AnyBinding[]>(
26+
...capabilities: B
27+
): Policy<B[number]["runtime"], B[number]["capability"], B[number]["tag"]>;
28+
export function Policy(...bindings: AnyBinding[]) {
29+
return {
30+
runtime: bindings[0]["runtime"],
31+
capabilities: bindings.map((b) => b.capability),
32+
tags: bindings.map((b) => b.tag),
33+
and: (...b2: AnyBinding[]) => Policy(...bindings, ...b2),
34+
};
935
}
1036

1137
export namespace Policy {

0 commit comments

Comments
 (0)