alchemy-effect 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app.d.ts +23 -0
- package/lib/app.d.ts.map +1 -0
- package/lib/app.js +26 -0
- package/lib/app.js.map +1 -0
- package/lib/apply.d.ts +22 -0
- package/lib/apply.d.ts.map +1 -0
- package/lib/apply.js +191 -0
- package/lib/apply.js.map +1 -0
- package/lib/approve.d.ts +15 -0
- package/lib/approve.d.ts.map +1 -0
- package/lib/approve.js +7 -0
- package/lib/approve.js.map +1 -0
- package/lib/binding.d.ts +58 -0
- package/lib/binding.d.ts.map +1 -0
- package/lib/binding.js +27 -0
- package/lib/binding.js.map +1 -0
- package/lib/capability.d.ts +11 -0
- package/lib/capability.d.ts.map +1 -0
- package/lib/capability.js +1 -0
- package/lib/capability.js.map +1 -0
- package/lib/destroy.d.ts +4 -0
- package/lib/destroy.d.ts.map +1 -0
- package/lib/destroy.js +1 -0
- package/lib/destroy.js.map +1 -0
- package/lib/dot-alchemy.d.ts +9 -0
- package/lib/dot-alchemy.d.ts.map +1 -0
- package/lib/dot-alchemy.js +14 -0
- package/lib/dot-alchemy.js.map +1 -0
- package/lib/env.d.ts +4 -0
- package/lib/env.d.ts.map +1 -0
- package/lib/env.js +33 -0
- package/lib/env.js.map +1 -0
- package/lib/event.d.ts +16 -0
- package/lib/event.d.ts.map +1 -0
- package/lib/event.js +1 -0
- package/lib/event.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/phase.d.ts +2 -0
- package/lib/phase.d.ts.map +1 -0
- package/lib/phase.js +1 -0
- package/lib/phase.js.map +1 -0
- package/lib/physical-name.d.ts +4 -0
- package/lib/physical-name.d.ts.map +1 -0
- package/lib/physical-name.js +7 -0
- package/lib/physical-name.js.map +1 -0
- package/lib/plan.d.ts +98 -0
- package/lib/plan.d.ts.map +1 -0
- package/lib/plan.js +228 -0
- package/lib/plan.js.map +1 -0
- package/lib/policy.d.ts +37 -0
- package/lib/policy.d.ts.map +1 -0
- package/lib/policy.js +16 -0
- package/lib/policy.js.map +1 -0
- package/lib/provider.d.ts +57 -0
- package/lib/provider.d.ts.map +1 -0
- package/lib/provider.js +2 -0
- package/lib/provider.js.map +1 -0
- package/lib/reporter.d.ts +8 -0
- package/lib/reporter.d.ts.map +1 -0
- package/lib/reporter.js +4 -0
- package/lib/reporter.js.map +1 -0
- package/lib/resource.d.ts +38 -0
- package/lib/resource.d.ts.map +1 -0
- package/lib/resource.js +25 -0
- package/lib/resource.js.map +1 -0
- package/lib/runtime.d.ts +39 -0
- package/lib/runtime.d.ts.map +1 -0
- package/lib/runtime.js +54 -0
- package/lib/runtime.js.map +1 -0
- package/lib/service.d.ts +33 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +4 -0
- package/lib/service.js.map +1 -0
- package/lib/state.d.ts +38 -0
- package/lib/state.d.ts.map +1 -0
- package/lib/state.js +66 -0
- package/lib/state.js.map +1 -0
- package/package.json +49 -0
- package/src/app.ts +43 -0
- package/src/apply.ts +318 -0
- package/src/approve.ts +13 -0
- package/src/binding.ts +144 -0
- package/src/capability.ts +19 -0
- package/src/destroy.ts +4 -0
- package/src/dot-alchemy.ts +17 -0
- package/src/env.ts +51 -0
- package/src/event.ts +27 -0
- package/src/index.ts +17 -0
- package/src/phase.ts +1 -0
- package/src/physical-name.ts +7 -0
- package/src/plan.ts +470 -0
- package/src/policy.ts +77 -0
- package/src/provider.ts +74 -0
- package/src/reporter.ts +8 -0
- package/src/resource.ts +68 -0
- package/src/runtime.ts +145 -0
- package/src/service.ts +55 -0
- package/src/state.ts +196 -0
package/src/apply.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import type { Simplify } from "effect/Types";
|
|
5
|
+
import { PlanReviewer, type PlanRejected } from "./approve.ts";
|
|
6
|
+
import type { AnyBinding, BindingService } from "./binding.ts";
|
|
7
|
+
import type { ApplyEvent, ApplyStatus } from "./event.ts";
|
|
8
|
+
import { type BindNode, type CRUD, type Delete, type Plan } from "./plan.ts";
|
|
9
|
+
import type { Resource } from "./resource.ts";
|
|
10
|
+
import { State } from "./state.ts";
|
|
11
|
+
|
|
12
|
+
export interface PlanStatusSession {
|
|
13
|
+
emit: (event: ApplyEvent) => Effect.Effect<void>;
|
|
14
|
+
done: () => Effect.Effect<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ScopedPlanStatusSession extends PlanStatusSession {
|
|
18
|
+
note: (note: string) => Effect.Effect<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class PlanStatusReporter extends Context.Tag("PlanStatusReporter")<
|
|
22
|
+
PlanStatusReporter,
|
|
23
|
+
{
|
|
24
|
+
start(plan: Plan): Effect.Effect<PlanStatusSession, never>;
|
|
25
|
+
}
|
|
26
|
+
>() {}
|
|
27
|
+
|
|
28
|
+
export const apply = <P extends Plan, Err, Req>(
|
|
29
|
+
plan: Effect.Effect<P, Err, Req>,
|
|
30
|
+
) =>
|
|
31
|
+
plan.pipe(
|
|
32
|
+
Effect.flatMap((plan) =>
|
|
33
|
+
Effect.gen(function* () {
|
|
34
|
+
const state = yield* State;
|
|
35
|
+
const outputs = {} as Record<string, Effect.Effect<any, any>>;
|
|
36
|
+
const reviewer = yield* Effect.serviceOption(PlanReviewer);
|
|
37
|
+
|
|
38
|
+
if (Option.isSome(reviewer)) {
|
|
39
|
+
yield* reviewer.value.approve(plan);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const events = yield* Effect.serviceOption(PlanStatusReporter);
|
|
43
|
+
|
|
44
|
+
const session = Option.isSome(events)
|
|
45
|
+
? yield* events.value.start(plan)
|
|
46
|
+
: ({
|
|
47
|
+
emit: () => Effect.void,
|
|
48
|
+
done: () => Effect.void,
|
|
49
|
+
} satisfies PlanStatusSession);
|
|
50
|
+
const { emit, done } = session;
|
|
51
|
+
|
|
52
|
+
const applyBindings = Effect.fn(function* (
|
|
53
|
+
resource: Resource,
|
|
54
|
+
bindings: BindNode[],
|
|
55
|
+
target: {
|
|
56
|
+
id: string;
|
|
57
|
+
props: any;
|
|
58
|
+
attr: any;
|
|
59
|
+
},
|
|
60
|
+
) {
|
|
61
|
+
return yield* Effect.all(
|
|
62
|
+
bindings.map(
|
|
63
|
+
Effect.fn(function* (node) {
|
|
64
|
+
const binding = node.binding as AnyBinding & {
|
|
65
|
+
// smuggled property (because it interacts poorly with inference)
|
|
66
|
+
Tag: Context.Tag<never, BindingService>;
|
|
67
|
+
};
|
|
68
|
+
const provider = yield* binding.Tag;
|
|
69
|
+
|
|
70
|
+
const resourceId: string = node.binding.capability.resource.id;
|
|
71
|
+
const upstreamNode = plan.resources[resourceId];
|
|
72
|
+
const upstreamAttr = resource
|
|
73
|
+
? yield* apply(upstreamNode)
|
|
74
|
+
: yield* Effect.dieMessage(
|
|
75
|
+
`Resource ${resourceId} not found`,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const attach = provider.attach({
|
|
79
|
+
source: {
|
|
80
|
+
id: resourceId,
|
|
81
|
+
attr: upstreamAttr,
|
|
82
|
+
props: upstreamNode.resource.props,
|
|
83
|
+
},
|
|
84
|
+
props: node.binding.props,
|
|
85
|
+
target,
|
|
86
|
+
});
|
|
87
|
+
return Effect.isEffect(attach)
|
|
88
|
+
? yield* attach as Effect.Effect<any, never, never>
|
|
89
|
+
: attach;
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const apply: (node: CRUD) => Effect.Effect<any, never, never> = (
|
|
96
|
+
node,
|
|
97
|
+
) =>
|
|
98
|
+
Effect.gen(function* () {
|
|
99
|
+
const checkpoint = <Out, Err>(
|
|
100
|
+
effect: Effect.Effect<Out, Err, never>,
|
|
101
|
+
) =>
|
|
102
|
+
effect.pipe(
|
|
103
|
+
Effect.flatMap((output) =>
|
|
104
|
+
state
|
|
105
|
+
.set(node.resource.id, {
|
|
106
|
+
id: node.resource.id,
|
|
107
|
+
type: node.resource.type,
|
|
108
|
+
status: node.action === "create" ? "created" : "updated",
|
|
109
|
+
props: node.resource.props,
|
|
110
|
+
output,
|
|
111
|
+
bindings: node.bindings,
|
|
112
|
+
})
|
|
113
|
+
.pipe(Effect.map(() => output)),
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const id = node.resource.id;
|
|
118
|
+
const resource = node.resource;
|
|
119
|
+
|
|
120
|
+
const scopedSession = {
|
|
121
|
+
...session,
|
|
122
|
+
note: (note: string) =>
|
|
123
|
+
session.emit({
|
|
124
|
+
id,
|
|
125
|
+
kind: "annotate",
|
|
126
|
+
message: note,
|
|
127
|
+
}),
|
|
128
|
+
} satisfies ScopedPlanStatusSession;
|
|
129
|
+
|
|
130
|
+
return yield* (outputs[id] ??= yield* Effect.cached(
|
|
131
|
+
Effect.gen(function* () {
|
|
132
|
+
const report = (status: ApplyStatus) =>
|
|
133
|
+
emit({
|
|
134
|
+
kind: "status-change",
|
|
135
|
+
id,
|
|
136
|
+
type: node.resource.type,
|
|
137
|
+
status,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (node.action === "noop") {
|
|
141
|
+
return (yield* state.get(id))?.output;
|
|
142
|
+
} else if (node.action === "create") {
|
|
143
|
+
let attr: any;
|
|
144
|
+
if (node.provider.stub) {
|
|
145
|
+
// stub the resource prior to resolving upstream resources or bindings if a stub is available
|
|
146
|
+
attr = yield* node.provider.stub({
|
|
147
|
+
id,
|
|
148
|
+
news: node.news,
|
|
149
|
+
session: scopedSession,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const bindings = yield* applyBindings(
|
|
154
|
+
resource,
|
|
155
|
+
node.bindings,
|
|
156
|
+
{
|
|
157
|
+
id,
|
|
158
|
+
props: node.news,
|
|
159
|
+
attr,
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
yield* report("creating");
|
|
164
|
+
|
|
165
|
+
return yield* node.provider
|
|
166
|
+
.create({
|
|
167
|
+
id,
|
|
168
|
+
news: node.news,
|
|
169
|
+
bindings,
|
|
170
|
+
session: scopedSession,
|
|
171
|
+
})
|
|
172
|
+
.pipe(
|
|
173
|
+
checkpoint,
|
|
174
|
+
Effect.tap(() => report("created")),
|
|
175
|
+
);
|
|
176
|
+
} else if (node.action === "update") {
|
|
177
|
+
const bindings = yield* applyBindings(
|
|
178
|
+
resource,
|
|
179
|
+
node.bindings,
|
|
180
|
+
{
|
|
181
|
+
id,
|
|
182
|
+
props: node.news,
|
|
183
|
+
attr: node.attributes,
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
yield* report("updating");
|
|
188
|
+
|
|
189
|
+
// const bindings = yield* applyDependencies(node.bindings);
|
|
190
|
+
|
|
191
|
+
return yield* node.provider
|
|
192
|
+
.update({
|
|
193
|
+
id,
|
|
194
|
+
news: node.news,
|
|
195
|
+
olds: node.olds,
|
|
196
|
+
output: node.output,
|
|
197
|
+
bindings,
|
|
198
|
+
session: scopedSession,
|
|
199
|
+
})
|
|
200
|
+
.pipe(
|
|
201
|
+
checkpoint,
|
|
202
|
+
Effect.tap(() => report("updated")),
|
|
203
|
+
);
|
|
204
|
+
} else if (node.action === "delete") {
|
|
205
|
+
yield* Effect.all(
|
|
206
|
+
node.downstream.map((dep) =>
|
|
207
|
+
dep in plan.resources
|
|
208
|
+
? apply(
|
|
209
|
+
plan.resources[
|
|
210
|
+
dep
|
|
211
|
+
] as P["resources"][keyof P["resources"]],
|
|
212
|
+
)
|
|
213
|
+
: Effect.void,
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
yield* report("deleting");
|
|
217
|
+
|
|
218
|
+
return yield* node.provider
|
|
219
|
+
.delete({
|
|
220
|
+
id,
|
|
221
|
+
olds: node.olds,
|
|
222
|
+
output: node.output,
|
|
223
|
+
session: scopedSession,
|
|
224
|
+
bindings: [],
|
|
225
|
+
})
|
|
226
|
+
.pipe(
|
|
227
|
+
Effect.flatMap(() => state.delete(id)),
|
|
228
|
+
Effect.tap(() => report("deleted")),
|
|
229
|
+
);
|
|
230
|
+
} else if (node.action === "replace") {
|
|
231
|
+
const destroy = Effect.gen(function* () {
|
|
232
|
+
yield* report("deleting");
|
|
233
|
+
return yield* node.provider.delete({
|
|
234
|
+
id,
|
|
235
|
+
olds: node.olds,
|
|
236
|
+
output: node.output,
|
|
237
|
+
session: scopedSession,
|
|
238
|
+
bindings: [],
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
const create = Effect.gen(function* () {
|
|
242
|
+
yield* report("creating");
|
|
243
|
+
return yield* (
|
|
244
|
+
node.provider
|
|
245
|
+
.create({
|
|
246
|
+
id,
|
|
247
|
+
news: node.news,
|
|
248
|
+
// TODO(sam): these need to only include attach actions
|
|
249
|
+
bindings: yield* applyBindings(
|
|
250
|
+
resource,
|
|
251
|
+
node.bindings,
|
|
252
|
+
{
|
|
253
|
+
id,
|
|
254
|
+
props: node.news,
|
|
255
|
+
attr: node.attributes,
|
|
256
|
+
},
|
|
257
|
+
),
|
|
258
|
+
session: scopedSession,
|
|
259
|
+
})
|
|
260
|
+
// TODO(sam): delete and create will conflict here, we need to extend the state store for replace
|
|
261
|
+
.pipe(
|
|
262
|
+
checkpoint,
|
|
263
|
+
Effect.tap(() => report("created")),
|
|
264
|
+
)
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
if (!node.deleteFirst) {
|
|
268
|
+
const outputs = yield* create;
|
|
269
|
+
yield* destroy;
|
|
270
|
+
return outputs;
|
|
271
|
+
} else {
|
|
272
|
+
yield* destroy;
|
|
273
|
+
return yield* create;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}),
|
|
277
|
+
));
|
|
278
|
+
}) as Effect.Effect<any, never, never>;
|
|
279
|
+
|
|
280
|
+
const nodes = [
|
|
281
|
+
...Object.entries(plan.resources),
|
|
282
|
+
...Object.entries(plan.deletions),
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
const resources: any = Object.fromEntries(
|
|
286
|
+
yield* Effect.all(
|
|
287
|
+
nodes.map(
|
|
288
|
+
Effect.fn(function* ([id, node]) {
|
|
289
|
+
return [id, yield* apply(node as CRUD)];
|
|
290
|
+
}),
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
yield* done();
|
|
295
|
+
if (Object.keys(plan.resources).length === 0) {
|
|
296
|
+
// all resources are deleted, return undefined
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
return resources;
|
|
300
|
+
}),
|
|
301
|
+
),
|
|
302
|
+
) as Effect.Effect<
|
|
303
|
+
"update" extends P["phase"]
|
|
304
|
+
?
|
|
305
|
+
| {
|
|
306
|
+
[id in keyof P["resources"]]: P["resources"][id] extends
|
|
307
|
+
| Delete<Resource>
|
|
308
|
+
| undefined
|
|
309
|
+
| never
|
|
310
|
+
? never
|
|
311
|
+
: Simplify<P["resources"][id]["resource"]["attr"]>;
|
|
312
|
+
}
|
|
313
|
+
// union distribution isn't happening, so we gotta add this additional void here just in case
|
|
314
|
+
| ("destroy" extends P["phase"] ? void : never)
|
|
315
|
+
: void,
|
|
316
|
+
Err | PlanRejected,
|
|
317
|
+
Req
|
|
318
|
+
>;
|
package/src/approve.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Data from "effect/Data";
|
|
3
|
+
import type * as Effect from "effect/Effect";
|
|
4
|
+
import type { Plan } from "./plan.ts";
|
|
5
|
+
|
|
6
|
+
export class PlanRejected extends Data.TaggedError("PlanRejected")<{}> {}
|
|
7
|
+
|
|
8
|
+
export class PlanReviewer extends Context.Tag("PlanReviewer")<
|
|
9
|
+
PlanReviewer,
|
|
10
|
+
{
|
|
11
|
+
approve: <P extends Plan>(plan: P) => Effect.Effect<void, PlanRejected>;
|
|
12
|
+
}
|
|
13
|
+
>() {}
|
package/src/binding.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import type { Effect } from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import type { Capability, ICapability } from "./capability.ts";
|
|
5
|
+
import type { Resource } from "./resource.ts";
|
|
6
|
+
import type { Runtime } from "./runtime.ts";
|
|
7
|
+
|
|
8
|
+
export interface BindingProps {
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const isBinding = (b: any): b is AnyBinding =>
|
|
13
|
+
"runtime" in b && "capability" in b && "tag" in b && "output" in b;
|
|
14
|
+
|
|
15
|
+
export type AnyBinding<F extends Runtime = any> = Binding<
|
|
16
|
+
F,
|
|
17
|
+
any,
|
|
18
|
+
any,
|
|
19
|
+
string,
|
|
20
|
+
boolean
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
export interface Binding<
|
|
24
|
+
Run extends Runtime,
|
|
25
|
+
Cap extends Capability = Capability,
|
|
26
|
+
Props = any,
|
|
27
|
+
Tag extends string = Cap["type"],
|
|
28
|
+
IsCustom extends boolean = Cap["type"] extends Tag ? false : true,
|
|
29
|
+
> {
|
|
30
|
+
runtime: Run;
|
|
31
|
+
capability: Cap;
|
|
32
|
+
tag: Tag;
|
|
33
|
+
props: Props;
|
|
34
|
+
isCustom: IsCustom;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Tag for a Service that can bind a Capability to a Runtime */
|
|
38
|
+
export interface Bind<
|
|
39
|
+
F extends Runtime,
|
|
40
|
+
Cap extends Capability,
|
|
41
|
+
Tag extends string,
|
|
42
|
+
> extends Context.Tag<
|
|
43
|
+
`${F["type"]}(${Cap["type"]}, ${Tag})`,
|
|
44
|
+
BindingService<
|
|
45
|
+
F,
|
|
46
|
+
Extract<Extract<Cap["resource"], Resource>["parent"], Resource>,
|
|
47
|
+
F["props"]
|
|
48
|
+
>
|
|
49
|
+
> {
|
|
50
|
+
/** @internal phantom */
|
|
51
|
+
name: Tag;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const Binding: {
|
|
55
|
+
<F extends (resource: any, props?: any) => AnyBinding & { isCustom: true }>(
|
|
56
|
+
runtime: ReturnType<F>["runtime"],
|
|
57
|
+
// resource: new () => ReturnType<F>["capability"]["resource"],
|
|
58
|
+
type: ReturnType<F>["capability"]["type"],
|
|
59
|
+
tag: ReturnType<F>["tag"],
|
|
60
|
+
): F & BindingDeclaration<ReturnType<F>["runtime"], F>;
|
|
61
|
+
<F extends (resource: any, props?: any) => AnyBinding & { isCustom: false }>(
|
|
62
|
+
runtime: ReturnType<F>["runtime"],
|
|
63
|
+
// resource: new () => ReturnType<F>["capability"]["resource"],
|
|
64
|
+
type: ReturnType<F>["capability"]["type"],
|
|
65
|
+
): F & BindingDeclaration<ReturnType<F>["runtime"], F>;
|
|
66
|
+
} = (runtime: any, cap: string, tag?: string) => {
|
|
67
|
+
const Tag = Context.Tag(`${runtime.type}(${cap}, ${tag ?? cap})`)();
|
|
68
|
+
return Object.assign(
|
|
69
|
+
(resource: any, props?: any) =>
|
|
70
|
+
({
|
|
71
|
+
runtime,
|
|
72
|
+
capability: {
|
|
73
|
+
type: cap,
|
|
74
|
+
resource,
|
|
75
|
+
constraint: undefined!,
|
|
76
|
+
sid: `${cap}${resource.id}`.replace(/[^a-zA-Z0-9]/g, ""),
|
|
77
|
+
label: `${cap}(${resource.id})`,
|
|
78
|
+
} satisfies ICapability,
|
|
79
|
+
props,
|
|
80
|
+
isCustom: false,
|
|
81
|
+
tag: tag ?? cap,
|
|
82
|
+
// @ts-expect-error - we smuggle this property because it interacts poorly with inference
|
|
83
|
+
Tag,
|
|
84
|
+
}) satisfies Binding<any, any, any, string, false>,
|
|
85
|
+
{
|
|
86
|
+
provider: {
|
|
87
|
+
effect: (eff) => Layer.effect(Tag, eff),
|
|
88
|
+
succeed: (service) => Layer.succeed(Tag, service),
|
|
89
|
+
},
|
|
90
|
+
} satisfies BindingDeclaration<Runtime, any>,
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export interface BindingDeclaration<
|
|
95
|
+
Run extends Runtime,
|
|
96
|
+
F extends (target: any, props?: any) => AnyBinding<Run>,
|
|
97
|
+
Tag extends string = ReturnType<F>["tag"],
|
|
98
|
+
Cap extends Capability = ReturnType<F>["capability"],
|
|
99
|
+
> {
|
|
100
|
+
provider: {
|
|
101
|
+
effect<Err, Req>(
|
|
102
|
+
eff: Effect<
|
|
103
|
+
BindingService<Run, Parameters<F>[0], Parameters<F>[1]>,
|
|
104
|
+
Err,
|
|
105
|
+
Req
|
|
106
|
+
>,
|
|
107
|
+
): Layer.Layer<Bind<Run, Cap, Tag>, Err, Req>;
|
|
108
|
+
succeed(
|
|
109
|
+
service: BindingService<Run, Parameters<F>[0], Parameters<F>[1]>,
|
|
110
|
+
): Layer.Layer<Bind<Run, Cap, Tag>>;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type BindingService<
|
|
115
|
+
Target extends Runtime = any,
|
|
116
|
+
Source extends Resource = Resource,
|
|
117
|
+
Props = any,
|
|
118
|
+
AttachReq = never,
|
|
119
|
+
DetachReq = never,
|
|
120
|
+
> = {
|
|
121
|
+
attach: (props: {
|
|
122
|
+
source: {
|
|
123
|
+
id: string;
|
|
124
|
+
attr: Source["attr"];
|
|
125
|
+
props: Source["props"];
|
|
126
|
+
};
|
|
127
|
+
props: Props;
|
|
128
|
+
target: {
|
|
129
|
+
id: string;
|
|
130
|
+
props: Target["props"];
|
|
131
|
+
attr: Target["attr"];
|
|
132
|
+
};
|
|
133
|
+
}) =>
|
|
134
|
+
| Effect<Partial<Target["binding"]> | void, never, AttachReq>
|
|
135
|
+
| Partial<Target["binding"]>;
|
|
136
|
+
detach?: (
|
|
137
|
+
resource: {
|
|
138
|
+
id: string;
|
|
139
|
+
attr: Source["attr"];
|
|
140
|
+
props: Source["props"];
|
|
141
|
+
},
|
|
142
|
+
from: Target["binding"],
|
|
143
|
+
) => Effect<void, never, DetachReq> | void;
|
|
144
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ICapability<
|
|
2
|
+
Type extends string = string,
|
|
3
|
+
Resource = unknown,
|
|
4
|
+
Constraint = unknown,
|
|
5
|
+
> {
|
|
6
|
+
type: Type;
|
|
7
|
+
resource: Resource;
|
|
8
|
+
constraint: Constraint;
|
|
9
|
+
sid: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Capability<
|
|
14
|
+
Type extends string = string,
|
|
15
|
+
Resource = unknown,
|
|
16
|
+
Constraint = unknown,
|
|
17
|
+
> extends ICapability<Type, Resource, Constraint> {
|
|
18
|
+
new (): {};
|
|
19
|
+
}
|
package/src/destroy.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FileSystem } from "@effect/platform";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export class DotAlchemy extends Context.Tag(".alchemy")<DotAlchemy, string>() {}
|
|
8
|
+
|
|
9
|
+
export const dotAlchemy = Layer.effect(
|
|
10
|
+
DotAlchemy,
|
|
11
|
+
Effect.gen(function* () {
|
|
12
|
+
const fs = yield* FileSystem.FileSystem;
|
|
13
|
+
const dir = path.join(process.cwd(), ".alchemy");
|
|
14
|
+
yield* fs.makeDirectory(dir, { recursive: true });
|
|
15
|
+
return dir;
|
|
16
|
+
}),
|
|
17
|
+
);
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const toEnvKey = <const ID extends string, const Suffix extends string>(
|
|
2
|
+
id: ID,
|
|
3
|
+
suffix: Suffix,
|
|
4
|
+
) => `${replace(toUpper(id))}_${replace(toUpper(suffix))}` as const;
|
|
5
|
+
|
|
6
|
+
const toUpper = <const S extends string>(str: S) =>
|
|
7
|
+
str.toUpperCase() as string extends S ? S : Uppercase<S>;
|
|
8
|
+
|
|
9
|
+
const replace = <const S extends string>(str: S) =>
|
|
10
|
+
str.replace(/-/g, "_") as Replace<S>;
|
|
11
|
+
|
|
12
|
+
type Replace<S extends string, Accum extends string = ""> = string extends S
|
|
13
|
+
? S
|
|
14
|
+
: S extends ""
|
|
15
|
+
? Accum
|
|
16
|
+
: S extends `${infer S}${infer Rest}`
|
|
17
|
+
? S extends "-"
|
|
18
|
+
? Replace<Rest, `${Accum}_`>
|
|
19
|
+
: Replace<Rest, `${Accum}${S}`>
|
|
20
|
+
: Accum;
|
|
21
|
+
|
|
22
|
+
function _test_both_literals() {
|
|
23
|
+
const key = toEnvKey("my-id", "my-suffix");
|
|
24
|
+
const _: typeof key = "MY_ID_MY_SUFFIX";
|
|
25
|
+
// @ts-expect-error
|
|
26
|
+
const _err: typeof key = "MY_ID_MY_SUFFIX2";
|
|
27
|
+
}
|
|
28
|
+
function _test_replace_wide_string() {
|
|
29
|
+
const ___ = toUpper(undefined! as string);
|
|
30
|
+
const id: string = "my-id";
|
|
31
|
+
const key = toEnvKey(id, "my-suffix");
|
|
32
|
+
const _: typeof key = "MY_ID_MY_SUFFIX";
|
|
33
|
+
const _2: typeof key = `${id}_MY_SUFFIX` as const;
|
|
34
|
+
// @ts-expect-error
|
|
35
|
+
const _err: typeof key = "MY_ID_MY_SUFFIX2";
|
|
36
|
+
// @ts-expect-error
|
|
37
|
+
const _err2: typeof key = `${id}_MY_SUFFIX2` as const;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _test_replace_wide_suffix() {
|
|
41
|
+
const ___ = toUpper(undefined! as string);
|
|
42
|
+
const id = "my-id";
|
|
43
|
+
const suffix = "my-suffix" as string;
|
|
44
|
+
const key = toEnvKey(id, suffix);
|
|
45
|
+
const _: typeof key = "MY_ID_MY_SUFFIX";
|
|
46
|
+
const _2: typeof key = `MY_ID_${suffix}` as const;
|
|
47
|
+
// @ts-expect-error
|
|
48
|
+
const _err: typeof key = "WRONG_PREFIX_MY_SUFFIX";
|
|
49
|
+
// @ts-expect-error
|
|
50
|
+
const _err2: typeof key = `WRONG_PREFIX_${suffix}`;
|
|
51
|
+
}
|
package/src/event.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type ApplyStatus =
|
|
2
|
+
| "pending"
|
|
3
|
+
| "creating"
|
|
4
|
+
| "created"
|
|
5
|
+
| "updating"
|
|
6
|
+
| "updated"
|
|
7
|
+
| "deleting"
|
|
8
|
+
| "deleted"
|
|
9
|
+
| "success"
|
|
10
|
+
| "fail";
|
|
11
|
+
|
|
12
|
+
export type ApplyEvent = AnnotateEvent | StatusChangeEvent;
|
|
13
|
+
|
|
14
|
+
export interface AnnotateEvent {
|
|
15
|
+
kind: "annotate";
|
|
16
|
+
id: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface StatusChangeEvent {
|
|
21
|
+
kind: "status-change";
|
|
22
|
+
id: string; // resource id (e.g. "messages", "api")
|
|
23
|
+
type: string; // resource type (e.g. "AWS::Lambda::Function", "Cloudflare::Worker")
|
|
24
|
+
status: ApplyStatus;
|
|
25
|
+
message?: string; // optional details
|
|
26
|
+
bindingId?: string; // if this event is for a binding
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "./app.ts";
|
|
2
|
+
export * from "./apply.ts";
|
|
3
|
+
export * from "./approve.ts";
|
|
4
|
+
export * from "./binding.ts";
|
|
5
|
+
export * from "./capability.ts";
|
|
6
|
+
export * from "./destroy.ts";
|
|
7
|
+
export * from "./dot-alchemy.ts";
|
|
8
|
+
export * from "./env.ts";
|
|
9
|
+
export * from "./event.ts";
|
|
10
|
+
export * from "./phase.ts";
|
|
11
|
+
export * from "./plan.ts";
|
|
12
|
+
export * from "./policy.ts";
|
|
13
|
+
export * from "./provider.ts";
|
|
14
|
+
export * from "./resource.ts";
|
|
15
|
+
export * from "./runtime.ts";
|
|
16
|
+
export * from "./service.ts";
|
|
17
|
+
export * as State from "./state.ts";
|
package/src/phase.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Phase = "update" | "destroy";
|