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.
Files changed (101) hide show
  1. package/lib/app.d.ts +23 -0
  2. package/lib/app.d.ts.map +1 -0
  3. package/lib/app.js +26 -0
  4. package/lib/app.js.map +1 -0
  5. package/lib/apply.d.ts +22 -0
  6. package/lib/apply.d.ts.map +1 -0
  7. package/lib/apply.js +191 -0
  8. package/lib/apply.js.map +1 -0
  9. package/lib/approve.d.ts +15 -0
  10. package/lib/approve.d.ts.map +1 -0
  11. package/lib/approve.js +7 -0
  12. package/lib/approve.js.map +1 -0
  13. package/lib/binding.d.ts +58 -0
  14. package/lib/binding.d.ts.map +1 -0
  15. package/lib/binding.js +27 -0
  16. package/lib/binding.js.map +1 -0
  17. package/lib/capability.d.ts +11 -0
  18. package/lib/capability.d.ts.map +1 -0
  19. package/lib/capability.js +1 -0
  20. package/lib/capability.js.map +1 -0
  21. package/lib/destroy.d.ts +4 -0
  22. package/lib/destroy.d.ts.map +1 -0
  23. package/lib/destroy.js +1 -0
  24. package/lib/destroy.js.map +1 -0
  25. package/lib/dot-alchemy.d.ts +9 -0
  26. package/lib/dot-alchemy.d.ts.map +1 -0
  27. package/lib/dot-alchemy.js +14 -0
  28. package/lib/dot-alchemy.js.map +1 -0
  29. package/lib/env.d.ts +4 -0
  30. package/lib/env.d.ts.map +1 -0
  31. package/lib/env.js +33 -0
  32. package/lib/env.js.map +1 -0
  33. package/lib/event.d.ts +16 -0
  34. package/lib/event.d.ts.map +1 -0
  35. package/lib/event.js +1 -0
  36. package/lib/event.js.map +1 -0
  37. package/lib/index.d.ts +18 -0
  38. package/lib/index.d.ts.map +1 -0
  39. package/lib/index.js +18 -0
  40. package/lib/index.js.map +1 -0
  41. package/lib/phase.d.ts +2 -0
  42. package/lib/phase.d.ts.map +1 -0
  43. package/lib/phase.js +1 -0
  44. package/lib/phase.js.map +1 -0
  45. package/lib/physical-name.d.ts +4 -0
  46. package/lib/physical-name.d.ts.map +1 -0
  47. package/lib/physical-name.js +7 -0
  48. package/lib/physical-name.js.map +1 -0
  49. package/lib/plan.d.ts +98 -0
  50. package/lib/plan.d.ts.map +1 -0
  51. package/lib/plan.js +228 -0
  52. package/lib/plan.js.map +1 -0
  53. package/lib/policy.d.ts +37 -0
  54. package/lib/policy.d.ts.map +1 -0
  55. package/lib/policy.js +16 -0
  56. package/lib/policy.js.map +1 -0
  57. package/lib/provider.d.ts +57 -0
  58. package/lib/provider.d.ts.map +1 -0
  59. package/lib/provider.js +2 -0
  60. package/lib/provider.js.map +1 -0
  61. package/lib/reporter.d.ts +8 -0
  62. package/lib/reporter.d.ts.map +1 -0
  63. package/lib/reporter.js +4 -0
  64. package/lib/reporter.js.map +1 -0
  65. package/lib/resource.d.ts +38 -0
  66. package/lib/resource.d.ts.map +1 -0
  67. package/lib/resource.js +25 -0
  68. package/lib/resource.js.map +1 -0
  69. package/lib/runtime.d.ts +39 -0
  70. package/lib/runtime.d.ts.map +1 -0
  71. package/lib/runtime.js +54 -0
  72. package/lib/runtime.js.map +1 -0
  73. package/lib/service.d.ts +33 -0
  74. package/lib/service.d.ts.map +1 -0
  75. package/lib/service.js +4 -0
  76. package/lib/service.js.map +1 -0
  77. package/lib/state.d.ts +38 -0
  78. package/lib/state.d.ts.map +1 -0
  79. package/lib/state.js +66 -0
  80. package/lib/state.js.map +1 -0
  81. package/package.json +49 -0
  82. package/src/app.ts +43 -0
  83. package/src/apply.ts +318 -0
  84. package/src/approve.ts +13 -0
  85. package/src/binding.ts +144 -0
  86. package/src/capability.ts +19 -0
  87. package/src/destroy.ts +4 -0
  88. package/src/dot-alchemy.ts +17 -0
  89. package/src/env.ts +51 -0
  90. package/src/event.ts +27 -0
  91. package/src/index.ts +17 -0
  92. package/src/phase.ts +1 -0
  93. package/src/physical-name.ts +7 -0
  94. package/src/plan.ts +470 -0
  95. package/src/policy.ts +77 -0
  96. package/src/provider.ts +74 -0
  97. package/src/reporter.ts +8 -0
  98. package/src/resource.ts +68 -0
  99. package/src/runtime.ts +145 -0
  100. package/src/service.ts +55 -0
  101. 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>;
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ import * as Context from "effect/Context";
2
+
3
+ export class Reporter extends Context.Tag("Reporter")<
4
+ Reporter,
5
+ {
6
+ report: (event: Event) => void;
7
+ }
8
+ >() {}
@@ -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
+ };