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/plan.ts
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import * as Data from "effect/Data";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import type { AnyBinding } from "./binding.ts";
|
|
4
|
+
import type { Capability } from "./capability.ts";
|
|
5
|
+
import type { Phase } from "./phase.ts";
|
|
6
|
+
import type { Instance } from "./policy.ts";
|
|
7
|
+
import { type ProviderService } from "./provider.ts";
|
|
8
|
+
import type { Resource, ResourceTags } from "./resource.ts";
|
|
9
|
+
import { isService, type Service } from "./service.ts";
|
|
10
|
+
import { State, type ResourceState } from "./state.ts";
|
|
11
|
+
|
|
12
|
+
export type PlanError = never;
|
|
13
|
+
|
|
14
|
+
export const isBindNode = (node: any): node is BindNode => {
|
|
15
|
+
return (
|
|
16
|
+
node &&
|
|
17
|
+
typeof node === "object" &&
|
|
18
|
+
(node.action === "attach" ||
|
|
19
|
+
node.action === "detach" ||
|
|
20
|
+
node.action === "noop")
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A node in the plan that represents a binding operation acting on a resource.
|
|
26
|
+
*/
|
|
27
|
+
export type BindNode<B extends AnyBinding = AnyBinding> =
|
|
28
|
+
| Attach<B>
|
|
29
|
+
| Detach<B>
|
|
30
|
+
| NoopBind<B>;
|
|
31
|
+
|
|
32
|
+
export type Attach<B extends AnyBinding = AnyBinding> = {
|
|
33
|
+
action: "attach";
|
|
34
|
+
binding: B;
|
|
35
|
+
olds?: BindNode;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type Detach<B extends AnyBinding = AnyBinding> = {
|
|
39
|
+
action: "detach";
|
|
40
|
+
binding: B;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type NoopBind<B extends AnyBinding = AnyBinding> = {
|
|
44
|
+
action: "noop";
|
|
45
|
+
binding: B;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const isCRUD = (node: any): node is CRUD => {
|
|
49
|
+
return (
|
|
50
|
+
node &&
|
|
51
|
+
typeof node === "object" &&
|
|
52
|
+
(node.action === "create" ||
|
|
53
|
+
node.action === "update" ||
|
|
54
|
+
node.action === "replace" ||
|
|
55
|
+
node.action === "noop")
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A node in the plan that represents a resource CRUD operation.
|
|
61
|
+
*/
|
|
62
|
+
export type CRUD<R extends Resource = Resource> =
|
|
63
|
+
| Create<R>
|
|
64
|
+
| Update<R>
|
|
65
|
+
| Delete<R>
|
|
66
|
+
| Replace<R>
|
|
67
|
+
| NoopUpdate<R>;
|
|
68
|
+
|
|
69
|
+
export type Apply<R extends Resource = Resource> =
|
|
70
|
+
| Create<R>
|
|
71
|
+
| Update<R>
|
|
72
|
+
| Replace<R>
|
|
73
|
+
| NoopUpdate<R>;
|
|
74
|
+
|
|
75
|
+
const Node = <T extends Apply>(node: T) => ({
|
|
76
|
+
...node,
|
|
77
|
+
toString(): string {
|
|
78
|
+
return `${this.action.charAt(0).toUpperCase()}${this.action.slice(1)}(${this.resource})`;
|
|
79
|
+
},
|
|
80
|
+
[Symbol.toStringTag]() {
|
|
81
|
+
return this.toString();
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export type Create<R extends Resource> = {
|
|
86
|
+
action: "create";
|
|
87
|
+
resource: R;
|
|
88
|
+
news: any;
|
|
89
|
+
provider: ProviderService;
|
|
90
|
+
attributes: R["attr"];
|
|
91
|
+
bindings: BindNode[];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type Update<R extends Resource> = {
|
|
95
|
+
action: "update";
|
|
96
|
+
resource: R;
|
|
97
|
+
olds: any;
|
|
98
|
+
news: any;
|
|
99
|
+
output: any;
|
|
100
|
+
provider: ProviderService;
|
|
101
|
+
attributes: R["attr"];
|
|
102
|
+
bindings: BindNode[];
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type Delete<R extends Resource> = {
|
|
106
|
+
action: "delete";
|
|
107
|
+
resource: R;
|
|
108
|
+
olds: any;
|
|
109
|
+
output: any;
|
|
110
|
+
provider: ProviderService;
|
|
111
|
+
bindings: BindNode[];
|
|
112
|
+
attributes: R["attr"];
|
|
113
|
+
downstream: string[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type NoopUpdate<R extends Resource> = {
|
|
117
|
+
action: "noop";
|
|
118
|
+
resource: R;
|
|
119
|
+
attributes: R["attr"];
|
|
120
|
+
bindings: BindNode[];
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type Replace<R extends Resource> = {
|
|
124
|
+
action: "replace";
|
|
125
|
+
resource: R;
|
|
126
|
+
olds: any;
|
|
127
|
+
news: any;
|
|
128
|
+
output: any;
|
|
129
|
+
provider: ProviderService;
|
|
130
|
+
bindings: BindNode[];
|
|
131
|
+
attributes: R["attr"];
|
|
132
|
+
deleteFirst?: boolean;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export type Plan = {
|
|
136
|
+
phase: Phase;
|
|
137
|
+
resources: {
|
|
138
|
+
[id in string]: CRUD;
|
|
139
|
+
};
|
|
140
|
+
deletions: {
|
|
141
|
+
[id in string]?: Delete<Resource>;
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const plan = <
|
|
146
|
+
const Phase extends "update" | "destroy",
|
|
147
|
+
const Services extends Service[],
|
|
148
|
+
>({
|
|
149
|
+
phase,
|
|
150
|
+
services,
|
|
151
|
+
}: {
|
|
152
|
+
phase: Phase;
|
|
153
|
+
services: Services;
|
|
154
|
+
}) => {
|
|
155
|
+
type ServiceIDs = Services[number]["id"];
|
|
156
|
+
type ServiceHosts = {
|
|
157
|
+
[ID in ServiceIDs]: Extract<Services[number], Service<Extract<ID, string>>>;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
type UpstreamTags = {
|
|
161
|
+
[ID in ServiceIDs]: ServiceHosts[ID]["props"]["bindings"]["tags"][number];
|
|
162
|
+
}[ServiceIDs];
|
|
163
|
+
type UpstreamResources = {
|
|
164
|
+
[ID in ServiceIDs]: Extract<
|
|
165
|
+
ServiceHosts[ID]["props"]["bindings"]["capabilities"][number]["resource"],
|
|
166
|
+
Resource
|
|
167
|
+
>;
|
|
168
|
+
}[ServiceIDs];
|
|
169
|
+
type Resources = {
|
|
170
|
+
[ID in ServiceIDs]: Apply<Extract<Instance<ServiceHosts[ID]>, Resource>>;
|
|
171
|
+
} & {
|
|
172
|
+
[ID in UpstreamResources["id"]]: Apply<
|
|
173
|
+
Extract<UpstreamResources, { id: ID }>
|
|
174
|
+
>;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return Effect.gen(function* () {
|
|
178
|
+
const state = yield* State;
|
|
179
|
+
|
|
180
|
+
const resourceIds = yield* state.list();
|
|
181
|
+
const resourcesState = yield* Effect.all(
|
|
182
|
+
resourceIds.map((id) => state.get(id)),
|
|
183
|
+
);
|
|
184
|
+
// map of resource ID -> its downstream dependencies (resources that depend on it)
|
|
185
|
+
const downstream = resourcesState
|
|
186
|
+
.filter(
|
|
187
|
+
(
|
|
188
|
+
resource,
|
|
189
|
+
): resource is ResourceState & {
|
|
190
|
+
bindings: BindNode[];
|
|
191
|
+
} => !!resource?.bindings,
|
|
192
|
+
)
|
|
193
|
+
.flatMap(
|
|
194
|
+
(resource) =>
|
|
195
|
+
resource.bindings.flatMap(({ binding }) => [
|
|
196
|
+
[binding.capability.resource.id, binding.capability.resource],
|
|
197
|
+
]),
|
|
198
|
+
// resource.bindings.flatMap(({ binding }) => {
|
|
199
|
+
// const capability: Capability = binding.capability;
|
|
200
|
+
// const resource = capability.resource;
|
|
201
|
+
// if (!resource) {
|
|
202
|
+
// return [];
|
|
203
|
+
// }
|
|
204
|
+
// return [
|
|
205
|
+
// [binding.capability.resource.id, binding.capability.resource],
|
|
206
|
+
// ];
|
|
207
|
+
// }),
|
|
208
|
+
)
|
|
209
|
+
.reduce(
|
|
210
|
+
(acc, [id, resourceId]) => ({
|
|
211
|
+
...acc,
|
|
212
|
+
[id]: [...(acc[id] ?? []), resourceId],
|
|
213
|
+
}),
|
|
214
|
+
{} as Record<string, string[]>,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const resources =
|
|
218
|
+
phase === "update"
|
|
219
|
+
? (Object.fromEntries(
|
|
220
|
+
(yield* Effect.all(
|
|
221
|
+
services
|
|
222
|
+
.flatMap((service) => [
|
|
223
|
+
...service.props.bindings.capabilities.map(
|
|
224
|
+
(cap: Capability) => cap.resource as Resource,
|
|
225
|
+
),
|
|
226
|
+
service,
|
|
227
|
+
])
|
|
228
|
+
.filter(
|
|
229
|
+
(node, i, arr) =>
|
|
230
|
+
arr.findIndex((n) => n.id === node.id) === i,
|
|
231
|
+
)
|
|
232
|
+
.map(
|
|
233
|
+
Effect.fn(function* (node) {
|
|
234
|
+
const id = node.id;
|
|
235
|
+
const resource = node as Resource & {
|
|
236
|
+
provider: ResourceTags<Resource>;
|
|
237
|
+
};
|
|
238
|
+
const news = resource.props;
|
|
239
|
+
|
|
240
|
+
const oldState = yield* state.get(id);
|
|
241
|
+
const provider = yield* resource.provider.tag;
|
|
242
|
+
|
|
243
|
+
const bindings = diffBindings(
|
|
244
|
+
oldState,
|
|
245
|
+
isService(node)
|
|
246
|
+
? (
|
|
247
|
+
node.props.bindings as unknown as {
|
|
248
|
+
bindings: AnyBinding[];
|
|
249
|
+
}
|
|
250
|
+
).bindings
|
|
251
|
+
: [],
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
oldState === undefined ||
|
|
256
|
+
oldState.status === "creating"
|
|
257
|
+
) {
|
|
258
|
+
return Node<Create<Resource>>({
|
|
259
|
+
action: "create",
|
|
260
|
+
news,
|
|
261
|
+
provider,
|
|
262
|
+
resource,
|
|
263
|
+
bindings,
|
|
264
|
+
// phantom
|
|
265
|
+
attributes: undefined!,
|
|
266
|
+
});
|
|
267
|
+
} else if (provider.diff) {
|
|
268
|
+
const diff = yield* provider.diff({
|
|
269
|
+
id,
|
|
270
|
+
olds: oldState.props,
|
|
271
|
+
news,
|
|
272
|
+
output: oldState.output,
|
|
273
|
+
});
|
|
274
|
+
if (diff.action === "noop") {
|
|
275
|
+
return Node<NoopUpdate<Resource>>({
|
|
276
|
+
action: "noop",
|
|
277
|
+
resource,
|
|
278
|
+
bindings,
|
|
279
|
+
// phantom
|
|
280
|
+
attributes: undefined!,
|
|
281
|
+
});
|
|
282
|
+
} else if (diff.action === "replace") {
|
|
283
|
+
return Node<Replace<Resource>>({
|
|
284
|
+
action: "replace",
|
|
285
|
+
olds: oldState.props,
|
|
286
|
+
news,
|
|
287
|
+
output: oldState.output,
|
|
288
|
+
provider,
|
|
289
|
+
resource,
|
|
290
|
+
bindings,
|
|
291
|
+
// phantom
|
|
292
|
+
attributes: undefined!,
|
|
293
|
+
});
|
|
294
|
+
} else {
|
|
295
|
+
return Node<Update<Resource>>({
|
|
296
|
+
action: "update",
|
|
297
|
+
olds: oldState.props,
|
|
298
|
+
news,
|
|
299
|
+
output: oldState.output,
|
|
300
|
+
provider,
|
|
301
|
+
resource,
|
|
302
|
+
bindings,
|
|
303
|
+
// phantom
|
|
304
|
+
attributes: undefined!,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} else if (compare(oldState, resource.props)) {
|
|
308
|
+
return Node<Update<Resource>>({
|
|
309
|
+
action: "update",
|
|
310
|
+
olds: oldState.props,
|
|
311
|
+
news,
|
|
312
|
+
output: oldState.output,
|
|
313
|
+
provider,
|
|
314
|
+
resource,
|
|
315
|
+
bindings,
|
|
316
|
+
// phantom
|
|
317
|
+
attributes: undefined!,
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
return Node<NoopUpdate<Resource>>({
|
|
321
|
+
action: "noop",
|
|
322
|
+
resource,
|
|
323
|
+
bindings,
|
|
324
|
+
// phantom
|
|
325
|
+
attributes: undefined!,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}),
|
|
329
|
+
),
|
|
330
|
+
)).map((update) => [update.resource.id, update]),
|
|
331
|
+
) as Plan["resources"])
|
|
332
|
+
: ({} as Plan["resources"]);
|
|
333
|
+
|
|
334
|
+
const deletions = Object.fromEntries(
|
|
335
|
+
(yield* Effect.all(
|
|
336
|
+
(yield* state.list()).map(
|
|
337
|
+
Effect.fn(function* (id) {
|
|
338
|
+
if (id in resources) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const oldState = yield* state.get(id);
|
|
342
|
+
const context = yield* Effect.context<never>();
|
|
343
|
+
if (oldState) {
|
|
344
|
+
const provider: ProviderService = context.unsafeMap.get(
|
|
345
|
+
oldState?.type,
|
|
346
|
+
);
|
|
347
|
+
if (!provider) {
|
|
348
|
+
yield* Effect.die(
|
|
349
|
+
new Error(`Provider not found for ${oldState?.type}`),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return [
|
|
353
|
+
id,
|
|
354
|
+
{
|
|
355
|
+
action: "delete",
|
|
356
|
+
olds: oldState.props,
|
|
357
|
+
output: oldState.output,
|
|
358
|
+
provider,
|
|
359
|
+
attributes: oldState?.output,
|
|
360
|
+
// TODO(sam): Support Detach Bindings
|
|
361
|
+
bindings: [],
|
|
362
|
+
resource: {
|
|
363
|
+
id: id,
|
|
364
|
+
parent: undefined,
|
|
365
|
+
type: oldState.type,
|
|
366
|
+
attr: oldState.output,
|
|
367
|
+
props: oldState.props,
|
|
368
|
+
} as Resource,
|
|
369
|
+
downstream: downstream[id] ?? [],
|
|
370
|
+
} satisfies Delete<Resource>,
|
|
371
|
+
] as const;
|
|
372
|
+
}
|
|
373
|
+
}),
|
|
374
|
+
),
|
|
375
|
+
)).filter((v) => !!v),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
for (const [resourceId, deletion] of Object.entries(deletions)) {
|
|
379
|
+
const dependencies = deletion.downstream.filter((d) => d in resources);
|
|
380
|
+
if (dependencies.length > 0) {
|
|
381
|
+
return yield* Effect.fail(
|
|
382
|
+
new DeleteResourceHasDownstreamDependencies({
|
|
383
|
+
message: `Resource ${resourceId} has downstream dependencies`,
|
|
384
|
+
resourceId,
|
|
385
|
+
dependencies,
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
phase,
|
|
393
|
+
resources,
|
|
394
|
+
deletions,
|
|
395
|
+
} satisfies Plan as Plan;
|
|
396
|
+
}) as Effect.Effect<
|
|
397
|
+
{
|
|
398
|
+
phase: Phase;
|
|
399
|
+
resources: {
|
|
400
|
+
[ID in keyof Resources]: Resources[ID];
|
|
401
|
+
};
|
|
402
|
+
deletions: {
|
|
403
|
+
[id in Exclude<string, keyof Resources>]?: Delete<Resource>;
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
never,
|
|
407
|
+
UpstreamTags | State
|
|
408
|
+
>;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
class DeleteResourceHasDownstreamDependencies extends Data.TaggedError(
|
|
412
|
+
"DeleteResourceHasDownstreamDependencies",
|
|
413
|
+
)<{
|
|
414
|
+
message: string;
|
|
415
|
+
resourceId: string;
|
|
416
|
+
dependencies: string[];
|
|
417
|
+
}> {}
|
|
418
|
+
|
|
419
|
+
const compare = <R extends Resource>(
|
|
420
|
+
oldState: ResourceState | undefined,
|
|
421
|
+
newState: R["props"],
|
|
422
|
+
) => JSON.stringify(oldState?.props) === JSON.stringify(newState);
|
|
423
|
+
|
|
424
|
+
const diffBindings = (
|
|
425
|
+
oldState: ResourceState | undefined,
|
|
426
|
+
bindings: AnyBinding[],
|
|
427
|
+
) => {
|
|
428
|
+
const actions: BindNode[] = [];
|
|
429
|
+
const oldBindings = oldState?.bindings;
|
|
430
|
+
const oldSids = new Set(
|
|
431
|
+
oldBindings?.map(({ binding }) => binding.capability.sid),
|
|
432
|
+
);
|
|
433
|
+
for (const binding of bindings) {
|
|
434
|
+
const cap = binding.capability;
|
|
435
|
+
const sid = cap.sid ?? `${cap.action}:${cap.resource.ID}`;
|
|
436
|
+
oldSids.delete(sid);
|
|
437
|
+
|
|
438
|
+
const oldBinding = oldBindings?.find(
|
|
439
|
+
({ binding }) => binding.capability.sid === sid,
|
|
440
|
+
);
|
|
441
|
+
if (!oldBinding) {
|
|
442
|
+
actions.push({
|
|
443
|
+
action: "attach",
|
|
444
|
+
binding,
|
|
445
|
+
});
|
|
446
|
+
} else if (isBindingDiff(oldBinding, binding)) {
|
|
447
|
+
actions.push({
|
|
448
|
+
action: "attach",
|
|
449
|
+
binding,
|
|
450
|
+
olds: oldBinding,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// for (const sid of oldSids) {
|
|
455
|
+
// actions.push({
|
|
456
|
+
// action: "detach",
|
|
457
|
+
// cap: oldBindings?.find((binding) => binding.sid === sid)!,
|
|
458
|
+
// });
|
|
459
|
+
// }
|
|
460
|
+
return actions;
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const isBindingDiff = (
|
|
464
|
+
{ binding: oldBinding }: BindNode,
|
|
465
|
+
newBinding: AnyBinding,
|
|
466
|
+
) =>
|
|
467
|
+
oldBinding.capability.action !== newBinding.capability.action ||
|
|
468
|
+
oldBinding.capability?.resource?.id !== newBinding.capability?.resource?.id;
|
|
469
|
+
// TODO(sam): compare props
|
|
470
|
+
// oldBinding.props !== newBinding.props;
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { type AnyBinding, type Bind } from "./binding.ts";
|
|
4
|
+
import type { Capability } from "./capability.ts";
|
|
5
|
+
import type { Runtime } from "./runtime.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A Policy binds a set of Capbilities (e.g SQS.SendMessage, SQS.Consume, etc.) to a
|
|
9
|
+
* specific Runtime (e.g. AWS Lambda Function, Cloudflare Worker, etc.).
|
|
10
|
+
*
|
|
11
|
+
* It brings with it a set of upstream Tags containing the required Provider services
|
|
12
|
+
* to deploy the infrastructure, e.g. (BindingTag<AWS.Lambda.Function, SendMessage<Queue>>)
|
|
13
|
+
*
|
|
14
|
+
* A Policy is invariant over the set of Capabilities to ensure least-privilege.
|
|
15
|
+
*/
|
|
16
|
+
export interface Policy<
|
|
17
|
+
F extends Runtime,
|
|
18
|
+
in out Capabilities,
|
|
19
|
+
Tags = unknown,
|
|
20
|
+
> {
|
|
21
|
+
readonly runtime: F;
|
|
22
|
+
readonly tags: Tags[];
|
|
23
|
+
readonly capabilities: Capabilities[];
|
|
24
|
+
// phantom property (exists at runtime but not in types)
|
|
25
|
+
// readonly bindings: AnyBinding[];
|
|
26
|
+
/** Add more Capabilities to a Policy */
|
|
27
|
+
and<B extends AnyBinding[]>(
|
|
28
|
+
...bindings: B
|
|
29
|
+
): Policy<F, B[number]["capability"] | Capabilities, Tags>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type $<T> = Instance<T>;
|
|
33
|
+
export const $ = Policy;
|
|
34
|
+
|
|
35
|
+
type BindingTags<B extends AnyBinding> = B extends any
|
|
36
|
+
? Bind<B["runtime"], B["capability"], Extract<B["tag"], string>>
|
|
37
|
+
: never;
|
|
38
|
+
|
|
39
|
+
export function Policy<F extends Runtime>(): Policy<F, never, never>;
|
|
40
|
+
export function Policy<B extends AnyBinding[]>(
|
|
41
|
+
...capabilities: B
|
|
42
|
+
): Policy<
|
|
43
|
+
B[number]["runtime"],
|
|
44
|
+
B[number]["capability"],
|
|
45
|
+
BindingTags<B[number]>
|
|
46
|
+
>;
|
|
47
|
+
export function Policy(...bindings: AnyBinding[]): any {
|
|
48
|
+
return {
|
|
49
|
+
runtime: bindings[0]["runtime"],
|
|
50
|
+
capabilities: bindings.map((b) => b.capability),
|
|
51
|
+
tags: bindings.map((b) => Context.Tag(b.tag as any)()),
|
|
52
|
+
bindings,
|
|
53
|
+
and: (...b2: AnyBinding[]) => Policy(...bindings, ...b2),
|
|
54
|
+
} as Policy<any, any, any> & {
|
|
55
|
+
// add the phantom property
|
|
56
|
+
bindings: AnyBinding[];
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** declare a Policy requiring Capabilities in some context */
|
|
61
|
+
export const declare = <S extends Capability>() =>
|
|
62
|
+
Effect.gen(function* () {}) as Effect.Effect<void, never, S>;
|
|
63
|
+
|
|
64
|
+
export type Instance<T> = T extends { id: string }
|
|
65
|
+
? string extends T["id"]
|
|
66
|
+
? T
|
|
67
|
+
: T extends new (...args: any) => infer I
|
|
68
|
+
? I
|
|
69
|
+
: never
|
|
70
|
+
: never;
|
|
71
|
+
// syntactic sugar for mapping `typeof Messages` -> Messages, e.g. so it's SQS.SendMessage<Messages> instead of SQS.SendMessage<typeof Messages>
|
|
72
|
+
// e.g. <Q extends SQS.Queue>(queue: Q) => SQS.SendMessage<To<Q>>
|
|
73
|
+
export type From<T> = Instance<T>;
|
|
74
|
+
export type To<T> = Instance<T>;
|
|
75
|
+
export type In<T> = Instance<T>;
|
|
76
|
+
export type Into<T> = Instance<T>;
|
|
77
|
+
export type On<T> = Instance<T>;
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
import type { ScopedPlanStatusSession } from "./apply.ts";
|
|
4
|
+
import type { Resource } from "./resource.ts";
|
|
5
|
+
import type { Runtime } from "./runtime.ts";
|
|
6
|
+
|
|
7
|
+
export type Provider<R extends Resource> = Context.TagClass<
|
|
8
|
+
Provider<R>,
|
|
9
|
+
R["type"],
|
|
10
|
+
ProviderService<R>
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
export type Diff =
|
|
14
|
+
| {
|
|
15
|
+
action: "update" | "noop";
|
|
16
|
+
deleteFirst?: undefined;
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
action: "replace";
|
|
20
|
+
deleteFirst?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type BindingData<Res extends Resource> = [Res] extends [Runtime]
|
|
24
|
+
? Res["binding"][]
|
|
25
|
+
: any[];
|
|
26
|
+
|
|
27
|
+
export interface ProviderService<Res extends Resource = Resource> {
|
|
28
|
+
// tail();
|
|
29
|
+
// watch();
|
|
30
|
+
// replace(): Effect.Effect<void, never, never>;
|
|
31
|
+
|
|
32
|
+
// different interface that is persistent, watching, reloads
|
|
33
|
+
// run?() {}
|
|
34
|
+
read?(input: {
|
|
35
|
+
id: string;
|
|
36
|
+
olds: Res["props"] | undefined;
|
|
37
|
+
// what is the ARN?
|
|
38
|
+
output: Res["attr"] | undefined; // current state -> synced state
|
|
39
|
+
session: ScopedPlanStatusSession;
|
|
40
|
+
bindings: BindingData<Res>;
|
|
41
|
+
}): Effect.Effect<Res["attr"] | undefined, any, never>;
|
|
42
|
+
diff?(input: {
|
|
43
|
+
id: string;
|
|
44
|
+
olds: Res["props"];
|
|
45
|
+
news: Res["props"];
|
|
46
|
+
output: Res["attr"];
|
|
47
|
+
}): Effect.Effect<Diff, never, never>;
|
|
48
|
+
stub?(input: {
|
|
49
|
+
id: string;
|
|
50
|
+
news: Res["props"];
|
|
51
|
+
session: ScopedPlanStatusSession;
|
|
52
|
+
}): Effect.Effect<Res["attr"], any, never>;
|
|
53
|
+
create(input: {
|
|
54
|
+
id: string;
|
|
55
|
+
news: Res["props"];
|
|
56
|
+
session: ScopedPlanStatusSession;
|
|
57
|
+
bindings: BindingData<Res>;
|
|
58
|
+
}): Effect.Effect<Res["attr"], any, never>;
|
|
59
|
+
update(input: {
|
|
60
|
+
id: string;
|
|
61
|
+
news: Res["props"];
|
|
62
|
+
olds: Res["props"];
|
|
63
|
+
output: Res["attr"];
|
|
64
|
+
session: ScopedPlanStatusSession;
|
|
65
|
+
bindings: BindingData<Res>;
|
|
66
|
+
}): Effect.Effect<Res["attr"], any, never>;
|
|
67
|
+
delete(input: {
|
|
68
|
+
id: string;
|
|
69
|
+
olds: Res["props"];
|
|
70
|
+
output: Res["attr"];
|
|
71
|
+
session: ScopedPlanStatusSession;
|
|
72
|
+
bindings: BindingData<Res>;
|
|
73
|
+
}): Effect.Effect<void, any, never>;
|
|
74
|
+
}
|
package/src/reporter.ts
ADDED
package/src/resource.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import type { Effect } from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import type { Provider, ProviderService } from "./provider.ts";
|
|
5
|
+
|
|
6
|
+
export const isResource = (r: any): r is Resource => {
|
|
7
|
+
return (
|
|
8
|
+
r && typeof r === "function" && "id" in r && "type" in r && "props" in r
|
|
9
|
+
);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface Resource<
|
|
13
|
+
Type extends string = string,
|
|
14
|
+
ID extends string = string,
|
|
15
|
+
Props = unknown,
|
|
16
|
+
Attrs = unknown,
|
|
17
|
+
> {
|
|
18
|
+
id: ID;
|
|
19
|
+
type: Type;
|
|
20
|
+
props: Props;
|
|
21
|
+
attr: Attrs;
|
|
22
|
+
parent: unknown;
|
|
23
|
+
// oxlint-disable-next-line no-misused-new
|
|
24
|
+
new (): Resource<Type, ID, Props, Attrs>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ResourceTags<R extends Resource> {
|
|
28
|
+
of<S extends ProviderService<R>>(service: S): S;
|
|
29
|
+
tag: Context.TagClass<Provider<R>, R["type"], ProviderService<R>>;
|
|
30
|
+
effect<Err, Req>(
|
|
31
|
+
eff: Effect<ProviderService<R>, Err, Req>,
|
|
32
|
+
): Layer.Layer<Provider<R>, Err, Req>;
|
|
33
|
+
succeed(service: ProviderService<R>): Layer.Layer<Provider<R>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Resource = <Ctor extends (id: string, props: any) => Resource>(
|
|
37
|
+
type: ReturnType<Ctor>["type"],
|
|
38
|
+
) => {
|
|
39
|
+
const Tag = Context.Tag(type)();
|
|
40
|
+
const provider = {
|
|
41
|
+
tag: Tag,
|
|
42
|
+
effect: <Err, Req>(
|
|
43
|
+
eff: Effect<ProviderService<ReturnType<Ctor>>, Err, Req>,
|
|
44
|
+
) => Layer.effect(Tag, eff),
|
|
45
|
+
succeed: (service: ProviderService<ReturnType<Ctor>>) =>
|
|
46
|
+
Layer.succeed(Tag, service),
|
|
47
|
+
};
|
|
48
|
+
return Object.assign(
|
|
49
|
+
function (id: string, props: any) {
|
|
50
|
+
return class Resource {
|
|
51
|
+
static readonly id = id;
|
|
52
|
+
static readonly type = type;
|
|
53
|
+
static readonly props = props;
|
|
54
|
+
static readonly provider = provider;
|
|
55
|
+
};
|
|
56
|
+
} as unknown as Ctor & {
|
|
57
|
+
type: ReturnType<Ctor>["type"];
|
|
58
|
+
new (): ReturnType<Ctor> & {
|
|
59
|
+
parent: ReturnType<Ctor>;
|
|
60
|
+
};
|
|
61
|
+
provider: typeof provider;
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: type,
|
|
65
|
+
provider,
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
};
|