alchemy 2.0.0-beta.2 → 2.0.0-beta.4
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/bin/{alchemy-effect.js → alchemy.js} +171 -171
- package/bin/alchemy.js.map +1 -0
- package/bin/{alchemy-effect.sh → alchemy.sh} +2 -2
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cli/index.js +167 -167
- package/lib/cli/index.js.map +1 -1
- package/package.json +54 -45
- package/src/AWS/AGENTS.md +10 -10
- package/src/AWS/Assets.ts +1 -1
- package/src/AWS/AuthProvider.ts +392 -0
- package/src/AWS/Credentials.ts +0 -1
- package/src/AWS/DynamoDB/Table.ts +63 -10
- package/src/AWS/EC2/VpcEndpoint.ts +4 -4
- package/src/AWS/EC2/hosted.ts +1 -1
- package/src/AWS/ECS/Task.ts +1 -1
- package/src/AWS/Kinesis/Stream.ts +44 -8
- package/src/AWS/Lambda/Function.ts +218 -10
- package/src/AWS/S3/Bucket.ts +26 -19
- package/src/AWS/S3/BucketNotifications.ts +1 -1
- package/src/AWS/SNS/Topic.ts +47 -10
- package/src/AWS/SQS/Queue.ts +66 -0
- package/src/Auth/AuthProvider.ts +33 -0
- package/src/Auth/Credentials.ts +56 -0
- package/src/Auth/Env.ts +45 -0
- package/src/Auth/Profile.ts +72 -0
- package/src/Auth/index.ts +2 -0
- package/src/Cloudflare/Auth/AuthProvider.ts +670 -0
- package/src/Cloudflare/Auth/OAuthClient.ts +315 -0
- package/src/Cloudflare/Container/Container.ts +122 -0
- package/src/Cloudflare/Container/ContainerApplication.ts +6 -3
- package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
- package/src/Cloudflare/Container/StartContainer.ts +1 -1
- package/src/Cloudflare/D1/D1Database.ts +23 -4
- package/src/Cloudflare/KV/KVNamespace.ts +25 -0
- package/src/Cloudflare/Providers.ts +1 -6
- package/src/Cloudflare/R2/R2Bucket.ts +45 -2
- package/src/Cloudflare/Website/StaticSite.ts +101 -15
- package/src/Cloudflare/Website/Vite.ts +86 -20
- package/src/Cloudflare/Workers/Assets.ts +170 -207
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +629 -0
- package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
- package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
- package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +66 -7
- package/src/Cloudflare/Workers/InferEnv.ts +11 -7
- package/src/Cloudflare/Workers/Rpc.ts +13 -2
- package/src/Cloudflare/Workers/ScheduledEvents.ts +185 -0
- package/src/Cloudflare/Workers/WebSocket.ts +1 -1
- package/src/Cloudflare/Workers/Worker.ts +572 -67
- package/src/Cloudflare/Workers/Workflow.ts +53 -11
- package/src/Cloudflare/Workers/index.ts +4 -1
- package/src/Construct.ts +2 -2
- package/src/GitHub/Comment.ts +224 -0
- package/src/GitHub/Secret.ts +257 -0
- package/src/GitHub/Variable.ts +166 -0
- package/src/GitHub/index.ts +3 -0
- package/src/Kubernetes/client.ts +2 -2
- package/src/Output.ts +1 -1
- package/src/Platform.ts +10 -5
- package/src/Provider.ts +1 -1
- package/src/Resource.ts +4 -4
- package/src/Test/Vitest.ts +4 -3
- package/src/Util/Clank.ts +77 -0
- package/src/Util/PlatformServices.ts +21 -0
- package/src/Util/dedent.ts +59 -0
- package/src/Util/index.ts +1 -0
- package/bin/alchemy-effect.js.map +0 -1
- package/src/Cloudflare/Workers/DurableObject.ts +0 -527
- package/src/Daemon/Client.ts +0 -116
- package/src/Daemon/Config.ts +0 -28
- package/src/Daemon/Errors.ts +0 -48
- package/src/Daemon/Lock.ts +0 -162
- package/src/Daemon/ProcessRegistry.ts +0 -284
- package/src/Daemon/RpcSchema.ts +0 -43
- package/src/Daemon/RpcServer.ts +0 -231
- package/src/Daemon/index.ts +0 -27
- package/src/Spawn.ts +0 -23
- /package/bin/{alchemy-effect.ts → alchemy.ts} +0 -0
|
@@ -116,25 +116,233 @@ export type FunctionShape = Main<FunctionServices>;
|
|
|
116
116
|
* An AWS Lambda host resource that combines code bundling, IAM role
|
|
117
117
|
* provisioning, and runtime binding collection.
|
|
118
118
|
*
|
|
119
|
-
* `Function` is the canonical runtime host for AWS.
|
|
120
|
-
*
|
|
121
|
-
* execution
|
|
119
|
+
* `Function` is the canonical runtime host for AWS. Alchemy automatically
|
|
120
|
+
* bundles your TypeScript entry module with Rolldown, creates an IAM
|
|
121
|
+
* execution role, and uploads the zip artifact. On subsequent deploys, the
|
|
122
|
+
* function is only updated when the bundle hash changes.
|
|
122
123
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
124
|
+
* There are two ways to define a Lambda Function:
|
|
125
|
+
*
|
|
126
|
+
* - **Async** — plain handler export, no Effect runtime in the bundle.
|
|
127
|
+
* - **Effect** — Effect implementation with typed bindings and event sources.
|
|
128
|
+
*
|
|
129
|
+
* See the {@link https://alchemy.run/guides/async-lambda | Async Lambda Guide}
|
|
130
|
+
* for plain handler patterns, or the
|
|
131
|
+
* {@link https://alchemy.run/guides/lambda | Effect Lambda Guide}
|
|
132
|
+
* for the full Effect-based approach with bindings, event sources, and sinks.
|
|
133
|
+
*
|
|
134
|
+
* @section Async Functions
|
|
135
|
+
* Point `main` at a file that exports a standard Lambda handler. No
|
|
136
|
+
* Effect runtime is included in the bundle. Useful when migrating
|
|
137
|
+
* existing Lambda functions or when you don't need Effect.
|
|
138
|
+
*
|
|
139
|
+
* @example Defining an async Lambda in your stack
|
|
125
140
|
* ```typescript
|
|
126
|
-
*
|
|
127
|
-
*
|
|
141
|
+
* // alchemy.run.ts
|
|
142
|
+
* import * as AWS from "alchemy/AWS";
|
|
143
|
+
*
|
|
144
|
+
* const func = yield* AWS.Lambda.Function("ApiFunction", {
|
|
145
|
+
* main: "./src/handler.ts",
|
|
146
|
+
* url: true,
|
|
128
147
|
* });
|
|
129
148
|
* ```
|
|
130
149
|
*
|
|
150
|
+
* @example Writing the async handler
|
|
151
|
+
* ```typescript
|
|
152
|
+
* // src/handler.ts
|
|
153
|
+
* export const handler = async (event: any) => {
|
|
154
|
+
* return {
|
|
155
|
+
* statusCode: 200,
|
|
156
|
+
* body: JSON.stringify({ message: "Hello from Lambda!" }),
|
|
157
|
+
* };
|
|
158
|
+
* };
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* @section Effect Functions
|
|
162
|
+
* Pass the Effect implementation as the third argument. Bindings
|
|
163
|
+
* attach IAM permissions and environment variables at deploy time,
|
|
164
|
+
* while the runtime execution context collects listeners and exports.
|
|
165
|
+
*
|
|
166
|
+
* @example Effect Function with HTTP handler
|
|
167
|
+
* ```typescript
|
|
168
|
+
* export default class ApiFunction extends AWS.Lambda.Function<ApiFunction>()(
|
|
169
|
+
* "ApiFunction",
|
|
170
|
+
* { main: import.meta.filename, url: true },
|
|
171
|
+
* Effect.gen(function* () {
|
|
172
|
+
* // init: bind resources
|
|
173
|
+
* const getItem = yield* DynamoDB.GetItem.bind(table);
|
|
174
|
+
*
|
|
175
|
+
* return {
|
|
176
|
+
* // runtime: use them
|
|
177
|
+
* fetch: Effect.gen(function* () {
|
|
178
|
+
* const request = yield* HttpServerRequest;
|
|
179
|
+
* const url = new URL(request.url);
|
|
180
|
+
* const id = url.searchParams.get("id");
|
|
181
|
+
* const result = yield* getItem({ Key: { pk: { S: id! } } });
|
|
182
|
+
* return yield* HttpServerResponse.json(result.Item);
|
|
183
|
+
* }),
|
|
184
|
+
* };
|
|
185
|
+
* }),
|
|
186
|
+
* ) {}
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* @section Configuration
|
|
131
190
|
* @example Function with URL
|
|
132
191
|
* ```typescript
|
|
133
|
-
* const func = yield* Function("ApiFunction", {
|
|
134
|
-
* main: "./src/
|
|
192
|
+
* const func = yield* AWS.Lambda.Function("ApiFunction", {
|
|
193
|
+
* main: "./src/handler.ts",
|
|
135
194
|
* url: true,
|
|
136
195
|
* });
|
|
137
196
|
* ```
|
|
197
|
+
*
|
|
198
|
+
* @example Function in a VPC
|
|
199
|
+
* ```typescript
|
|
200
|
+
* const func = yield* AWS.Lambda.Function("VpcFunction", {
|
|
201
|
+
* main: "./src/handler.ts",
|
|
202
|
+
* vpc: {
|
|
203
|
+
* subnetIds: ["subnet-abc123", "subnet-def456"],
|
|
204
|
+
* securityGroupIds: ["sg-xyz789"],
|
|
205
|
+
* },
|
|
206
|
+
* });
|
|
207
|
+
* ```
|
|
208
|
+
*
|
|
209
|
+
* @section S3 Bindings
|
|
210
|
+
* Bind S3 operations in the init phase to give the function IAM
|
|
211
|
+
* permissions and inject the bucket name as an environment variable.
|
|
212
|
+
*
|
|
213
|
+
* @example Read and write S3 objects
|
|
214
|
+
* ```typescript
|
|
215
|
+
* // init
|
|
216
|
+
* const getObject = yield* S3.GetObject.bind(bucket);
|
|
217
|
+
* const putObject = yield* S3.PutObject.bind(bucket);
|
|
218
|
+
*
|
|
219
|
+
* return {
|
|
220
|
+
* fetch: Effect.gen(function* () {
|
|
221
|
+
* // runtime
|
|
222
|
+
* yield* putObject({ Key: "hello.txt", Body: "Hello!" });
|
|
223
|
+
* const obj = yield* getObject({ Key: "hello.txt" });
|
|
224
|
+
* return HttpServerResponse.text("OK");
|
|
225
|
+
* }),
|
|
226
|
+
* };
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* @section DynamoDB Bindings
|
|
230
|
+
* Bind DynamoDB operations in the init phase to grant table-scoped
|
|
231
|
+
* IAM permissions.
|
|
232
|
+
*
|
|
233
|
+
* @example Get and put items
|
|
234
|
+
* ```typescript
|
|
235
|
+
* // init
|
|
236
|
+
* const getItem = yield* DynamoDB.GetItem.bind(table);
|
|
237
|
+
* const putItem = yield* DynamoDB.PutItem.bind(table);
|
|
238
|
+
*
|
|
239
|
+
* return {
|
|
240
|
+
* fetch: Effect.gen(function* () {
|
|
241
|
+
* // runtime
|
|
242
|
+
* yield* putItem({ Item: { pk: { S: "user#1" }, name: { S: "Alice" } } });
|
|
243
|
+
* const result = yield* getItem({ Key: { pk: { S: "user#1" } } });
|
|
244
|
+
* return yield* HttpServerResponse.json(result.Item);
|
|
245
|
+
* }),
|
|
246
|
+
* };
|
|
247
|
+
* ```
|
|
248
|
+
*
|
|
249
|
+
* @section SQS Bindings
|
|
250
|
+
* Bind SQS operations in the init phase to send messages to a queue.
|
|
251
|
+
*
|
|
252
|
+
* @example Send a message
|
|
253
|
+
* ```typescript
|
|
254
|
+
* // init
|
|
255
|
+
* const sendMessage = yield* SQS.SendMessage.bind(queue);
|
|
256
|
+
*
|
|
257
|
+
* return {
|
|
258
|
+
* fetch: Effect.gen(function* () {
|
|
259
|
+
* // runtime
|
|
260
|
+
* yield* sendMessage({
|
|
261
|
+
* MessageBody: JSON.stringify({ orderId: "123" }),
|
|
262
|
+
* });
|
|
263
|
+
* return HttpServerResponse.text("Queued");
|
|
264
|
+
* }),
|
|
265
|
+
* };
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* @section SNS Bindings
|
|
269
|
+
* Bind SNS operations in the init phase to publish messages to a
|
|
270
|
+
* topic.
|
|
271
|
+
*
|
|
272
|
+
* @example Publish a notification
|
|
273
|
+
* ```typescript
|
|
274
|
+
* // init
|
|
275
|
+
* const publish = yield* SNS.Publish.bind(topic);
|
|
276
|
+
*
|
|
277
|
+
* return {
|
|
278
|
+
* fetch: Effect.gen(function* () {
|
|
279
|
+
* // runtime
|
|
280
|
+
* yield* publish({
|
|
281
|
+
* Message: JSON.stringify({ event: "order.created" }),
|
|
282
|
+
* Subject: "OrderCreated",
|
|
283
|
+
* });
|
|
284
|
+
* return HttpServerResponse.text("Published");
|
|
285
|
+
* }),
|
|
286
|
+
* };
|
|
287
|
+
* ```
|
|
288
|
+
*
|
|
289
|
+
* @section Kinesis Bindings
|
|
290
|
+
* Bind Kinesis operations in the init phase to put records into a
|
|
291
|
+
* stream.
|
|
292
|
+
*
|
|
293
|
+
* @example Put a record
|
|
294
|
+
* ```typescript
|
|
295
|
+
* // init
|
|
296
|
+
* const putRecord = yield* Kinesis.PutRecord.bind(stream);
|
|
297
|
+
*
|
|
298
|
+
* return {
|
|
299
|
+
* fetch: Effect.gen(function* () {
|
|
300
|
+
* // runtime
|
|
301
|
+
* yield* putRecord({
|
|
302
|
+
* PartitionKey: "order-123",
|
|
303
|
+
* Data: new TextEncoder().encode(JSON.stringify({ orderId: "123" })),
|
|
304
|
+
* });
|
|
305
|
+
* return HttpServerResponse.text("Sent");
|
|
306
|
+
* }),
|
|
307
|
+
* };
|
|
308
|
+
* ```
|
|
309
|
+
*
|
|
310
|
+
* @section Event Sources
|
|
311
|
+
* Lambda functions can be triggered by event sources like SQS queues,
|
|
312
|
+
* DynamoDB streams, S3 notifications, SNS topics, and Kinesis streams.
|
|
313
|
+
*
|
|
314
|
+
* @example Process SQS messages
|
|
315
|
+
* ```typescript
|
|
316
|
+
* yield* SQS.messages(queue).process(
|
|
317
|
+
* Effect.fn(function* (message) {
|
|
318
|
+
* yield* Effect.log(`Received: ${message.body}`);
|
|
319
|
+
* }),
|
|
320
|
+
* );
|
|
321
|
+
* ```
|
|
322
|
+
*
|
|
323
|
+
* @example Process DynamoDB stream changes
|
|
324
|
+
* ```typescript
|
|
325
|
+
* yield* DynamoDB.streams(table, {
|
|
326
|
+
* StreamViewType: "NEW_AND_OLD_IMAGES",
|
|
327
|
+
* }).process(
|
|
328
|
+
* Effect.fn(function* (record) {
|
|
329
|
+
* yield* Effect.log(`Change: ${record.eventName}`);
|
|
330
|
+
* }),
|
|
331
|
+
* );
|
|
332
|
+
* ```
|
|
333
|
+
*
|
|
334
|
+
* @example Process S3 notifications
|
|
335
|
+
* ```typescript
|
|
336
|
+
* yield* S3.notifications(bucket, {
|
|
337
|
+
* events: ["s3:ObjectCreated:*"],
|
|
338
|
+
* }).subscribe((stream) =>
|
|
339
|
+
* stream.pipe(
|
|
340
|
+
* Stream.runForEach((event) =>
|
|
341
|
+
* Effect.log(`New object: ${event.key}`),
|
|
342
|
+
* ),
|
|
343
|
+
* ),
|
|
344
|
+
* );
|
|
345
|
+
* ```
|
|
138
346
|
*/
|
|
139
347
|
export const Function: Platform<
|
|
140
348
|
Function,
|
|
@@ -433,7 +641,7 @@ export const FunctionProvider = () =>
|
|
|
433
641
|
virtualEntryPlugin(
|
|
434
642
|
(importPath) => `
|
|
435
643
|
import { NodeServices } from "@effect/platform-node";
|
|
436
|
-
import { Stack } from "alchemy
|
|
644
|
+
import { Stack } from "alchemy/Stack";
|
|
437
645
|
import * as Config from "effect/Config";
|
|
438
646
|
import * as ConfigProvider from "effect/ConfigProvider";
|
|
439
647
|
import * as Credentials from "@distilled.cloud/aws/Credentials";
|
package/src/AWS/S3/Bucket.ts
CHANGED
|
@@ -91,7 +91,7 @@ export interface Bucket extends Resource<
|
|
|
91
91
|
* @section Creating a Bucket
|
|
92
92
|
* @example Basic Bucket
|
|
93
93
|
* ```typescript
|
|
94
|
-
* import * as S3 from "alchemy
|
|
94
|
+
* import * as S3 from "alchemy/AWS/S3";
|
|
95
95
|
*
|
|
96
96
|
* const bucket = yield* S3.Bucket("my-bucket", {});
|
|
97
97
|
* ```
|
|
@@ -110,37 +110,44 @@ export interface Bucket extends Resource<
|
|
|
110
110
|
* });
|
|
111
111
|
* ```
|
|
112
112
|
*
|
|
113
|
-
* @section
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* const response = yield* getObject({ Key: "hello.txt" });
|
|
119
|
-
* ```
|
|
113
|
+
* @section Runtime Operations
|
|
114
|
+
* Bind S3 operations in the init phase and use them in runtime
|
|
115
|
+
* handlers. Bindings inject the bucket name and grant scoped IAM
|
|
116
|
+
* permissions automatically.
|
|
120
117
|
*
|
|
121
|
-
* @
|
|
122
|
-
* @example Put an object into a bucket
|
|
118
|
+
* @example Read and write objects
|
|
123
119
|
* ```typescript
|
|
120
|
+
* // init
|
|
121
|
+
* const getObject = yield* S3.GetObject.bind(bucket);
|
|
124
122
|
* const putObject = yield* S3.PutObject.bind(bucket);
|
|
125
123
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
124
|
+
* return {
|
|
125
|
+
* fetch: Effect.gen(function* () {
|
|
126
|
+
* // runtime
|
|
127
|
+
* yield* putObject({
|
|
128
|
+
* Key: "hello.txt",
|
|
129
|
+
* Body: "Hello, World!",
|
|
130
|
+
* ContentType: "text/plain",
|
|
131
|
+
* });
|
|
132
|
+
* const response = yield* getObject({ Key: "hello.txt" });
|
|
133
|
+
* return HttpServerResponse.text("OK");
|
|
134
|
+
* }),
|
|
135
|
+
* };
|
|
131
136
|
* ```
|
|
132
137
|
*
|
|
133
|
-
* @
|
|
134
|
-
* @example Delete an object from a bucket
|
|
138
|
+
* @example Delete an object
|
|
135
139
|
* ```typescript
|
|
140
|
+
* // init
|
|
136
141
|
* const deleteObject = yield* S3.DeleteObject.bind(bucket);
|
|
137
|
-
*
|
|
138
|
-
* yield* deleteObject({ Key: "hello.txt" });
|
|
139
142
|
* ```
|
|
140
143
|
*
|
|
141
144
|
* @section Event Notifications
|
|
145
|
+
* Subscribe to bucket events from the init phase. The subscription
|
|
146
|
+
* and Lambda invoke permissions are created automatically.
|
|
147
|
+
*
|
|
142
148
|
* @example Process object creation events
|
|
143
149
|
* ```typescript
|
|
150
|
+
* // init
|
|
144
151
|
* yield* S3.notifications(bucket, {
|
|
145
152
|
* events: ["s3:ObjectCreated:*"],
|
|
146
153
|
* }).subscribe((stream) =>
|
|
@@ -35,7 +35,7 @@ export interface NotificationsProps<Events extends S3EventType[]> {
|
|
|
35
35
|
* @section Subscribing to Events
|
|
36
36
|
* @example Process all object creation events
|
|
37
37
|
* ```typescript
|
|
38
|
-
* import * as S3 from "alchemy
|
|
38
|
+
* import * as S3 from "alchemy/AWS/S3";
|
|
39
39
|
*
|
|
40
40
|
* yield* S3.notifications(bucket, {
|
|
41
41
|
* events: ["s3:ObjectCreated:*"],
|
package/src/AWS/SNS/Topic.ts
CHANGED
|
@@ -60,20 +60,30 @@ export interface Topic extends Resource<
|
|
|
60
60
|
* An Amazon SNS topic for fan-out messaging and notifications.
|
|
61
61
|
*
|
|
62
62
|
* `Topic` owns the SNS topic lifecycle while raw AWS topic attributes remain
|
|
63
|
-
* available through the `attributes` prop so the full core pub/sub surface can
|
|
64
|
-
* configured without waiting on additional typed wrappers.
|
|
63
|
+
* available through the `attributes` prop so the full core pub/sub surface can
|
|
64
|
+
* be configured without waiting on additional typed wrappers. A topic name is
|
|
65
|
+
* auto-generated unless you provide one explicitly.
|
|
65
66
|
*
|
|
66
67
|
* @section Creating Topics
|
|
67
68
|
* @example Standard Topic
|
|
68
69
|
* ```typescript
|
|
69
|
-
*
|
|
70
|
-
*
|
|
70
|
+
* import * as SNS from "alchemy/AWS/SNS";
|
|
71
|
+
*
|
|
72
|
+
* const topic = yield* SNS.Topic("OrdersTopic");
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @example Topic with Display Name
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const topic = yield* SNS.Topic("NotificationsTopic", {
|
|
78
|
+
* attributes: {
|
|
79
|
+
* DisplayName: "App Notifications",
|
|
80
|
+
* },
|
|
71
81
|
* });
|
|
72
82
|
* ```
|
|
73
83
|
*
|
|
74
84
|
* @example FIFO Topic
|
|
75
85
|
* ```typescript
|
|
76
|
-
* const topic = yield* Topic("OrdersFifoTopic", {
|
|
86
|
+
* const topic = yield* SNS.Topic("OrdersFifoTopic", {
|
|
77
87
|
* fifo: true,
|
|
78
88
|
* attributes: {
|
|
79
89
|
* ContentBasedDeduplication: "true",
|
|
@@ -82,14 +92,41 @@ export interface Topic extends Resource<
|
|
|
82
92
|
* ```
|
|
83
93
|
*
|
|
84
94
|
* @section Runtime Publishing
|
|
85
|
-
*
|
|
95
|
+
* Bind publish operations in the init phase and use them in runtime
|
|
96
|
+
* handlers.
|
|
97
|
+
*
|
|
98
|
+
* @example Publish from a handler
|
|
86
99
|
* ```typescript
|
|
100
|
+
* // init
|
|
87
101
|
* const publish = yield* SNS.Publish.bind(topic);
|
|
88
102
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
103
|
+
* return {
|
|
104
|
+
* fetch: Effect.gen(function* () {
|
|
105
|
+
* // runtime
|
|
106
|
+
* yield* publish({
|
|
107
|
+
* Message: JSON.stringify({ orderId: "123" }),
|
|
108
|
+
* Subject: "OrderCreated",
|
|
109
|
+
* });
|
|
110
|
+
* return HttpServerResponse.text("Published");
|
|
111
|
+
* }),
|
|
112
|
+
* };
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* @section Subscriptions
|
|
116
|
+
* Subscribe a Lambda function to process messages published to the
|
|
117
|
+
* topic. The subscription and invoke permissions are created
|
|
118
|
+
* automatically.
|
|
119
|
+
*
|
|
120
|
+
* @example Process topic notifications
|
|
121
|
+
* ```typescript
|
|
122
|
+
* // init
|
|
123
|
+
* yield* SNS.notifications(topic).subscribe((stream) =>
|
|
124
|
+
* stream.pipe(
|
|
125
|
+
* Stream.runForEach((message) =>
|
|
126
|
+
* Effect.log(`Received: ${message.Message}`),
|
|
127
|
+
* ),
|
|
128
|
+
* ),
|
|
129
|
+
* );
|
|
93
130
|
* ```
|
|
94
131
|
*/
|
|
95
132
|
export const Topic = Resource<Topic>("AWS.SNS.Topic");
|
package/src/AWS/SQS/Queue.ts
CHANGED
|
@@ -87,6 +87,72 @@ export interface Queue extends Resource<
|
|
|
87
87
|
Providers
|
|
88
88
|
> {}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* An Amazon SQS queue for reliable, decoupled message processing.
|
|
92
|
+
*
|
|
93
|
+
* `Queue` owns the lifecycle of a standard or FIFO SQS queue. A queue name
|
|
94
|
+
* is auto-generated from the app, stage, and logical ID unless you provide
|
|
95
|
+
* one explicitly. FIFO queues automatically append the `.fifo` suffix.
|
|
96
|
+
*
|
|
97
|
+
* @section Creating Queues
|
|
98
|
+
* @example Standard Queue
|
|
99
|
+
* ```typescript
|
|
100
|
+
* import * as SQS from "alchemy/AWS/SQS";
|
|
101
|
+
*
|
|
102
|
+
* const queue = yield* SQS.Queue("OrdersQueue");
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @example FIFO Queue
|
|
106
|
+
* ```typescript
|
|
107
|
+
* const queue = yield* SQS.Queue("OrdersFifoQueue", {
|
|
108
|
+
* fifo: true,
|
|
109
|
+
* contentBasedDeduplication: true,
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* @example Queue with Custom Settings
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const queue = yield* SQS.Queue("ProcessingQueue", {
|
|
116
|
+
* visibilityTimeout: 120,
|
|
117
|
+
* messageRetentionPeriod: 86400,
|
|
118
|
+
* receiveMessageWaitTimeSeconds: 20,
|
|
119
|
+
* });
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @section Sending Messages
|
|
123
|
+
* Bind send operations in the init phase and use them in runtime
|
|
124
|
+
* handlers.
|
|
125
|
+
*
|
|
126
|
+
* @example Send a message from a handler
|
|
127
|
+
* ```typescript
|
|
128
|
+
* // init
|
|
129
|
+
* const sendMessage = yield* SQS.SendMessage.bind(queue);
|
|
130
|
+
*
|
|
131
|
+
* return {
|
|
132
|
+
* fetch: Effect.gen(function* () {
|
|
133
|
+
* // runtime
|
|
134
|
+
* yield* sendMessage({
|
|
135
|
+
* MessageBody: JSON.stringify({ orderId: "123" }),
|
|
136
|
+
* });
|
|
137
|
+
* return HttpServerResponse.text("Queued");
|
|
138
|
+
* }),
|
|
139
|
+
* };
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* @section Event Sources
|
|
143
|
+
* Process messages from a queue using a Lambda event source mapping.
|
|
144
|
+
* Messages are automatically deleted after successful processing.
|
|
145
|
+
*
|
|
146
|
+
* @example Process queue messages
|
|
147
|
+
* ```typescript
|
|
148
|
+
* // init
|
|
149
|
+
* yield* SQS.messages(queue).process(
|
|
150
|
+
* Effect.fn(function* (message) {
|
|
151
|
+
* yield* Effect.log(`Received: ${message.body}`);
|
|
152
|
+
* }),
|
|
153
|
+
* );
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
90
156
|
export const Queue = Resource<Queue>("AWS.SQS.Queue");
|
|
91
157
|
|
|
92
158
|
export const QueueProvider = () =>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
|
|
4
|
+
export class AuthError extends Schema.TaggedErrorClass<AuthError>()(
|
|
5
|
+
"AuthError",
|
|
6
|
+
{
|
|
7
|
+
message: Schema.String,
|
|
8
|
+
cause: Schema.optional(Schema.Defect),
|
|
9
|
+
},
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
export interface AuthProvider<Config extends { method: string }, Credentials> {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
|
|
15
|
+
configure(profileName: string): Effect.Effect<Config, AuthError>;
|
|
16
|
+
|
|
17
|
+
login(
|
|
18
|
+
profileName: string,
|
|
19
|
+
config: Config,
|
|
20
|
+
): Effect.Effect<void, AuthError, never>;
|
|
21
|
+
|
|
22
|
+
logout(
|
|
23
|
+
profileName: string,
|
|
24
|
+
config: Config,
|
|
25
|
+
): Effect.Effect<void, AuthError, never>;
|
|
26
|
+
|
|
27
|
+
prettyPrint(profileName: string, config: Config): Effect.Effect<void>;
|
|
28
|
+
|
|
29
|
+
read(
|
|
30
|
+
profileName: string,
|
|
31
|
+
config: Config,
|
|
32
|
+
): Effect.Effect<Credentials, AuthError, never>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as FileSystem from "effect/FileSystem";
|
|
3
|
+
import * as Redacted from "effect/Redacted";
|
|
4
|
+
import path from "pathe";
|
|
5
|
+
import { rootDir } from "./Profile.ts";
|
|
6
|
+
|
|
7
|
+
const credentialsDirPath = path.join(rootDir, "credentials");
|
|
8
|
+
|
|
9
|
+
export const credentialsFilePath = (profile: string, provider: string) =>
|
|
10
|
+
path.join(credentialsDirPath, profile, `${provider}.json`);
|
|
11
|
+
|
|
12
|
+
export const readCredentials = Effect.fnUntraced(function* <T>(
|
|
13
|
+
profile: string,
|
|
14
|
+
provider: string,
|
|
15
|
+
) {
|
|
16
|
+
const fs = yield* FileSystem.FileSystem;
|
|
17
|
+
const data = yield* fs
|
|
18
|
+
.readFileString(credentialsFilePath(profile, provider))
|
|
19
|
+
.pipe(Effect.catch(() => Effect.succeed(undefined)));
|
|
20
|
+
if (data === undefined) return undefined as T | undefined;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(data) as T;
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined as T | undefined;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const writeCredentials = Effect.fnUntraced(function* <T>(
|
|
29
|
+
profile: string,
|
|
30
|
+
provider: string,
|
|
31
|
+
credentials: T,
|
|
32
|
+
) {
|
|
33
|
+
const fs = yield* FileSystem.FileSystem;
|
|
34
|
+
const filePath = credentialsFilePath(profile, provider);
|
|
35
|
+
yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
|
|
36
|
+
yield* fs.writeFileString(filePath, JSON.stringify(credentials, null, 2));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const deleteCredentials = Effect.fnUntraced(function* (
|
|
40
|
+
profile: string,
|
|
41
|
+
provider: string,
|
|
42
|
+
) {
|
|
43
|
+
const fs = yield* FileSystem.FileSystem;
|
|
44
|
+
yield* fs
|
|
45
|
+
.remove(credentialsFilePath(profile, provider))
|
|
46
|
+
.pipe(Effect.catch(() => Effect.void));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export function displayRedacted(
|
|
50
|
+
r: Redacted.Redacted<string>,
|
|
51
|
+
visibleChars = 4,
|
|
52
|
+
): string {
|
|
53
|
+
const raw = Redacted.value(r);
|
|
54
|
+
if (raw.length <= visibleChars) return "****";
|
|
55
|
+
return `${raw.slice(0, visibleChars)}****`;
|
|
56
|
+
}
|
package/src/Auth/Env.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as Config from "effect/Config";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Clank from "../Util/Clank.ts";
|
|
4
|
+
import { AuthError } from "./AuthProvider.ts";
|
|
5
|
+
|
|
6
|
+
export const getEnv = (key: string) =>
|
|
7
|
+
Config.string(key)
|
|
8
|
+
.asEffect()
|
|
9
|
+
.pipe(Effect.orElseSucceed(() => undefined));
|
|
10
|
+
|
|
11
|
+
export const getEnvRequired = (key: string) =>
|
|
12
|
+
Config.string(key)
|
|
13
|
+
.asEffect()
|
|
14
|
+
.pipe(
|
|
15
|
+
Effect.catch(() =>
|
|
16
|
+
Effect.fail(new AuthError({ message: `Missing required env: ${key}` })),
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const getEnvRedacted = (key: string) =>
|
|
21
|
+
Config.redacted(key)
|
|
22
|
+
.asEffect()
|
|
23
|
+
.pipe(Effect.orElseSucceed(() => undefined));
|
|
24
|
+
|
|
25
|
+
export const getEnvRedactedRequired = (key: string) =>
|
|
26
|
+
Config.redacted(key)
|
|
27
|
+
.asEffect()
|
|
28
|
+
.pipe(
|
|
29
|
+
Effect.catch(() =>
|
|
30
|
+
Effect.fail(new AuthError({ message: `Missing required env: ${key}` })),
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export const retryOnce = <A, R>(
|
|
35
|
+
self: Effect.Effect<A, Clank.PromptCancelled, R>,
|
|
36
|
+
) =>
|
|
37
|
+
self.pipe(
|
|
38
|
+
Effect.retry({
|
|
39
|
+
times: 1,
|
|
40
|
+
while: (e) => e instanceof Clank.PromptCancelled,
|
|
41
|
+
}),
|
|
42
|
+
Effect.mapError(
|
|
43
|
+
(e) => new AuthError({ message: "User cancelled prompt", cause: e }),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as FileSystem from "effect/FileSystem";
|
|
3
|
+
import type { PlatformError } from "effect/PlatformError";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "pathe";
|
|
6
|
+
|
|
7
|
+
export const rootDir = path.join(os.homedir(), ".alchemy");
|
|
8
|
+
export const configFilePath = path.join(rootDir, "profiles.json");
|
|
9
|
+
|
|
10
|
+
export const CONFIG_VERSION = 2;
|
|
11
|
+
|
|
12
|
+
export interface AlchemyProfiles {
|
|
13
|
+
version: typeof CONFIG_VERSION;
|
|
14
|
+
profiles: Record<string, AlchemyProfile>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type AlchemyProfile = Record<string, { method: string }>;
|
|
18
|
+
|
|
19
|
+
const emptyConfig = (): AlchemyProfiles => ({
|
|
20
|
+
version: CONFIG_VERSION,
|
|
21
|
+
profiles: {},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const readConfig: Effect.Effect<
|
|
25
|
+
AlchemyProfiles,
|
|
26
|
+
never,
|
|
27
|
+
FileSystem.FileSystem
|
|
28
|
+
> = Effect.gen(function* () {
|
|
29
|
+
const fs = yield* FileSystem.FileSystem;
|
|
30
|
+
const data = yield* fs
|
|
31
|
+
.readFileString(configFilePath)
|
|
32
|
+
.pipe(Effect.orElseSucceed(() => undefined));
|
|
33
|
+
if (data === undefined) return emptyConfig();
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(data);
|
|
36
|
+
if (parsed?.version !== CONFIG_VERSION) {
|
|
37
|
+
return emptyConfig();
|
|
38
|
+
}
|
|
39
|
+
return parsed as AlchemyProfiles;
|
|
40
|
+
} catch {
|
|
41
|
+
return emptyConfig();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const writeConfig = (
|
|
46
|
+
config: AlchemyProfiles,
|
|
47
|
+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
const fs = yield* FileSystem.FileSystem;
|
|
50
|
+
yield* fs.makeDirectory(path.dirname(configFilePath), {
|
|
51
|
+
recursive: true,
|
|
52
|
+
});
|
|
53
|
+
yield* fs.writeFileString(configFilePath, JSON.stringify(config, null, 2));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const getProfile = (
|
|
57
|
+
name: string,
|
|
58
|
+
): Effect.Effect<AlchemyProfile | undefined, never, FileSystem.FileSystem> =>
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
const config = yield* readConfig;
|
|
61
|
+
return config.profiles[name];
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const setProfile = (
|
|
65
|
+
name: string,
|
|
66
|
+
profile: AlchemyProfile,
|
|
67
|
+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
const config = yield* readConfig;
|
|
70
|
+
config.profiles[name] = profile;
|
|
71
|
+
yield* writeConfig(config);
|
|
72
|
+
});
|