alchemy-effect 0.0.0 → 0.1.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/README.md +179 -0
- package/bin/alchemy-effect.js +15 -0
- package/bin/alchemy-effect.js.map +1 -0
- package/bin/alchemy-effect.ts +10 -0
- package/lib/aws/account.d.ts +17 -0
- package/lib/aws/account.d.ts.map +1 -0
- package/lib/aws/account.js +18 -0
- package/lib/aws/account.js.map +1 -0
- package/lib/aws/arn.d.ts +6 -0
- package/lib/aws/arn.d.ts.map +1 -0
- package/lib/aws/arn.js +1 -0
- package/lib/aws/arn.js.map +1 -0
- package/lib/aws/assets.d.ts +8 -0
- package/lib/aws/assets.d.ts.map +1 -0
- package/lib/aws/assets.js +4 -0
- package/lib/aws/assets.js.map +1 -0
- package/lib/aws/bundle.d.ts +4 -0
- package/lib/aws/bundle.d.ts.map +1 -0
- package/lib/aws/bundle.js +4 -0
- package/lib/aws/bundle.js.map +1 -0
- package/lib/aws/client.d.ts +8 -0
- package/lib/aws/client.d.ts.map +1 -0
- package/lib/aws/client.js +20 -0
- package/lib/aws/client.js.map +1 -0
- package/lib/aws/credentials.d.ts +146 -0
- package/lib/aws/credentials.d.ts.map +1 -0
- package/lib/aws/credentials.js +170 -0
- package/lib/aws/credentials.js.map +1 -0
- package/lib/aws/iam.d.ts +23 -0
- package/lib/aws/iam.d.ts.map +1 -0
- package/lib/aws/iam.js +7 -0
- package/lib/aws/iam.js.map +1 -0
- package/lib/aws/index.d.ts +22 -0
- package/lib/aws/index.d.ts.map +1 -0
- package/lib/aws/index.js +28 -0
- package/lib/aws/index.js.map +1 -0
- package/lib/aws/lambda/consume.d.ts +17 -0
- package/lib/aws/lambda/consume.d.ts.map +1 -0
- package/lib/aws/lambda/consume.js +30 -0
- package/lib/aws/lambda/consume.js.map +1 -0
- package/lib/aws/lambda/function.client.d.ts +8 -0
- package/lib/aws/lambda/function.client.d.ts.map +1 -0
- package/lib/aws/lambda/function.client.js +7 -0
- package/lib/aws/lambda/function.client.js.map +1 -0
- package/lib/aws/lambda/function.d.ts +38 -0
- package/lib/aws/lambda/function.d.ts.map +1 -0
- package/lib/aws/lambda/function.handler.d.ts +7 -0
- package/lib/aws/lambda/function.handler.d.ts.map +1 -0
- package/lib/aws/lambda/function.handler.js +8 -0
- package/lib/aws/lambda/function.handler.js.map +1 -0
- package/lib/aws/lambda/function.invoke.d.ts +10 -0
- package/lib/aws/lambda/function.invoke.d.ts.map +1 -0
- package/lib/aws/lambda/function.invoke.js +31 -0
- package/lib/aws/lambda/function.invoke.js.map +1 -0
- package/lib/aws/lambda/function.js +3 -0
- package/lib/aws/lambda/function.js.map +1 -0
- package/lib/aws/lambda/function.provider.d.ts +9 -0
- package/lib/aws/lambda/function.provider.d.ts.map +1 -0
- package/lib/aws/lambda/function.provider.js +375 -0
- package/lib/aws/lambda/function.provider.js.map +1 -0
- package/lib/aws/lambda/index.d.ts +9 -0
- package/lib/aws/lambda/index.d.ts.map +1 -0
- package/lib/aws/lambda/index.js +9 -0
- package/lib/aws/lambda/index.js.map +1 -0
- package/lib/aws/lambda/serve.d.ts +14 -0
- package/lib/aws/lambda/serve.d.ts.map +1 -0
- package/lib/aws/lambda/serve.js +7 -0
- package/lib/aws/lambda/serve.js.map +1 -0
- package/lib/aws/parse-ini.d.ts +4 -0
- package/lib/aws/parse-ini.d.ts.map +1 -0
- package/lib/aws/parse-ini.js +67 -0
- package/lib/aws/parse-ini.js.map +1 -0
- package/lib/aws/profile.d.ts +6 -0
- package/lib/aws/profile.d.ts.map +1 -0
- package/lib/aws/profile.js +4 -0
- package/lib/aws/profile.js.map +1 -0
- package/lib/aws/region.d.ts +18 -0
- package/lib/aws/region.d.ts.map +1 -0
- package/lib/aws/region.js +21 -0
- package/lib/aws/region.js.map +1 -0
- package/lib/aws/s3.d.ts +8 -0
- package/lib/aws/s3.d.ts.map +1 -0
- package/lib/aws/s3.js +7 -0
- package/lib/aws/s3.js.map +1 -0
- package/lib/aws/sqs/index.d.ts +6 -0
- package/lib/aws/sqs/index.d.ts.map +1 -0
- package/lib/aws/sqs/index.js +6 -0
- package/lib/aws/sqs/index.js.map +1 -0
- package/lib/aws/sqs/queue.client.d.ts +12 -0
- package/lib/aws/sqs/queue.client.d.ts.map +1 -0
- package/lib/aws/sqs/queue.client.js +11 -0
- package/lib/aws/sqs/queue.client.js.map +1 -0
- package/lib/aws/sqs/queue.consume.d.ts +21 -0
- package/lib/aws/sqs/queue.consume.d.ts.map +1 -0
- package/lib/aws/sqs/queue.consume.js +21 -0
- package/lib/aws/sqs/queue.consume.js.map +1 -0
- package/lib/aws/sqs/queue.d.ts +87 -0
- package/lib/aws/sqs/queue.d.ts.map +1 -0
- package/lib/aws/sqs/queue.js +3 -0
- package/lib/aws/sqs/queue.js.map +1 -0
- package/lib/aws/sqs/queue.provider.d.ts +6 -0
- package/lib/aws/sqs/queue.provider.d.ts.map +1 -0
- package/lib/aws/sqs/queue.provider.js +79 -0
- package/lib/aws/sqs/queue.provider.js.map +1 -0
- package/lib/aws/sqs/queue.send-message.d.ts +11 -0
- package/lib/aws/sqs/queue.send-message.d.ts.map +1 -0
- package/lib/aws/sqs/queue.send-message.js +32 -0
- package/lib/aws/sqs/queue.send-message.js.map +1 -0
- package/lib/aws/sts.d.ts +8 -0
- package/lib/aws/sts.d.ts.map +1 -0
- package/lib/aws/sts.js +7 -0
- package/lib/aws/sts.js.map +1 -0
- package/lib/aws/zip.d.ts +3 -0
- package/lib/aws/zip.d.ts.map +1 -0
- package/lib/aws/zip.js +12 -0
- package/lib/aws/zip.js.map +1 -0
- package/lib/cli/approve.d.ts +4 -0
- package/lib/cli/approve.d.ts.map +1 -0
- package/lib/cli/approve.js +18 -0
- package/lib/cli/approve.js.map +1 -0
- package/lib/cli/clack.d.ts +14 -0
- package/lib/cli/clack.d.ts.map +1 -0
- package/lib/cli/clack.js +12 -0
- package/lib/cli/clack.js.map +1 -0
- package/lib/cli/components/ApprovePlan.d.ts +8 -0
- package/lib/cli/components/ApprovePlan.d.ts.map +1 -0
- package/lib/cli/components/ApprovePlan.js +30 -0
- package/lib/cli/components/ApprovePlan.js.map +1 -0
- package/lib/cli/components/Plan.d.ts +7 -0
- package/lib/cli/components/Plan.d.ts.map +1 -0
- package/lib/cli/components/Plan.js +98 -0
- package/lib/cli/components/Plan.js.map +1 -0
- package/lib/cli/components/PlanProgress.d.ts +9 -0
- package/lib/cli/components/PlanProgress.d.ts.map +1 -0
- package/lib/cli/components/PlanProgress.js +166 -0
- package/lib/cli/components/PlanProgress.js.map +1 -0
- package/lib/cli/index.d.ts +361 -0
- package/lib/cli/index.d.ts.map +1 -0
- package/lib/cli/index.js +25260 -0
- package/lib/cli/index.js.map +1 -0
- package/lib/cli/main.d.ts +2 -0
- package/lib/cli/main.d.ts.map +1 -0
- package/lib/cli/main.js +1 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/cli/plan.d.ts +13 -0
- package/lib/cli/plan.d.ts.map +1 -0
- package/lib/cli/plan.js +1 -0
- package/lib/cli/plan.js.map +1 -0
- package/lib/cli/progress.d.ts +7 -0
- package/lib/cli/progress.d.ts.map +1 -0
- package/lib/cli/progress.js +29 -0
- package/lib/cli/progress.js.map +1 -0
- package/lib/cli/spinner.d.ts +2 -0
- package/lib/cli/spinner.d.ts.map +1 -0
- package/lib/cli/spinner.js +13 -0
- package/lib/cli/spinner.js.map +1 -0
- package/lib/cloudflare/api.d.ts +24 -0
- package/lib/cloudflare/api.d.ts.map +1 -0
- package/lib/cloudflare/api.js +34 -0
- package/lib/cloudflare/api.js.map +1 -0
- package/lib/cloudflare/index.d.ts +5 -0
- package/lib/cloudflare/index.d.ts.map +1 -0
- package/lib/cloudflare/index.js +5 -0
- package/lib/cloudflare/index.js.map +1 -0
- package/lib/cloudflare/kv.d.ts +29 -0
- package/lib/cloudflare/kv.d.ts.map +1 -0
- package/lib/cloudflare/kv.js +3 -0
- package/lib/cloudflare/kv.js.map +1 -0
- package/lib/cloudflare/kv.provider.d.ts +4 -0
- package/lib/cloudflare/kv.provider.d.ts.map +1 -0
- package/lib/cloudflare/kv.provider.js +39 -0
- package/lib/cloudflare/kv.provider.js.map +1 -0
- package/lib/cloudflare/worker.d.ts +33 -0
- package/lib/cloudflare/worker.d.ts.map +1 -0
- package/lib/cloudflare/worker.js +4 -0
- package/lib/cloudflare/worker.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/package.json +70 -3
- package/src/aws/account.ts +35 -0
- package/src/aws/arn.ts +7 -0
- package/src/aws/assets.ts +8 -0
- package/src/aws/bundle.ts +5 -0
- package/src/aws/client.ts +33 -0
- package/src/aws/credentials.ts +408 -0
- package/src/aws/iam.ts +30 -0
- package/src/aws/index.ts +55 -0
- package/src/aws/lambda/consume.ts +64 -0
- package/src/aws/lambda/function.client.ts +14 -0
- package/src/aws/lambda/function.handler.ts +30 -0
- package/src/aws/lambda/function.invoke.ts +40 -0
- package/src/aws/lambda/function.provider.ts +563 -0
- package/src/aws/lambda/function.ts +39 -0
- package/src/aws/lambda/index.ts +12 -0
- package/src/aws/lambda/serve.ts +29 -0
- package/src/aws/parse-ini.ts +80 -0
- package/src/aws/profile.ts +6 -0
- package/src/aws/region.ts +35 -0
- package/src/aws/s3.ts +10 -0
- package/src/aws/sqs/index.ts +5 -0
- package/src/aws/sqs/queue.client.ts +20 -0
- package/src/aws/sqs/queue.consume.ts +46 -0
- package/src/aws/sqs/queue.provider.ts +94 -0
- package/src/aws/sqs/queue.send-message.ts +51 -0
- package/src/aws/sqs/queue.ts +86 -0
- package/src/aws/sts.ts +13 -0
- package/src/aws/zip.ts +17 -0
- package/src/cli/approve.tsx +30 -0
- package/src/cli/clack.ts +22 -0
- package/src/cli/components/ApprovePlan.tsx +44 -0
- package/src/cli/components/Plan.tsx +152 -0
- package/src/cli/components/PlanProgress.tsx +206 -0
- package/src/cli/index.ts +6 -0
- package/src/cli/main.ts +0 -0
- package/src/cli/plan.ts +16 -0
- package/src/cli/progress.tsx +45 -0
- package/src/cli/spinner.ts +14 -0
- package/src/cloudflare/api.ts +72 -0
- package/src/cloudflare/index.ts +4 -0
- package/src/cloudflare/kv.provider.ts +51 -0
- package/src/cloudflare/kv.ts +20 -0
- package/src/cloudflare/worker.ts +34 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ParsedIniData } from "@smithy/types";
|
|
2
|
+
import { IniSectionType } from "@smithy/types";
|
|
3
|
+
|
|
4
|
+
const CONFIG_PREFIX_SEPARATOR = ".";
|
|
5
|
+
|
|
6
|
+
const prefixKeyRegex = /^([\w-]+)\s(["'])?([\w-@\+\.%:/]+)\2$/;
|
|
7
|
+
const profileNameBlockList = ["__proto__", "profile __proto__"];
|
|
8
|
+
|
|
9
|
+
export const parseIni = (iniData: string): ParsedIniData => {
|
|
10
|
+
const map: ParsedIniData = {};
|
|
11
|
+
|
|
12
|
+
let currentSection: string | undefined;
|
|
13
|
+
let currentSubSection: string | undefined;
|
|
14
|
+
|
|
15
|
+
for (const iniLine of iniData.split(/\r?\n/)) {
|
|
16
|
+
const trimmedLine = iniLine.split(/(^|\s)[;#]/)[0].trim(); // remove comments and trim
|
|
17
|
+
const isSection: boolean =
|
|
18
|
+
trimmedLine[0] === "[" && trimmedLine[trimmedLine.length - 1] === "]";
|
|
19
|
+
if (isSection) {
|
|
20
|
+
// New section found. Reset currentSection and currentSubSection.
|
|
21
|
+
currentSection = undefined;
|
|
22
|
+
currentSubSection = undefined;
|
|
23
|
+
|
|
24
|
+
const sectionName = trimmedLine.substring(1, trimmedLine.length - 1);
|
|
25
|
+
const matches = prefixKeyRegex.exec(sectionName);
|
|
26
|
+
if (matches) {
|
|
27
|
+
const [, prefix, , name] = matches;
|
|
28
|
+
// Add prefix, if the section name starts with `profile`, `sso-session` or `services`.
|
|
29
|
+
if (Object.values(IniSectionType).includes(prefix as IniSectionType)) {
|
|
30
|
+
currentSection = [prefix, name].join(CONFIG_PREFIX_SEPARATOR);
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
// If the section name does not match the regex, use the section name as is.
|
|
34
|
+
currentSection = sectionName;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (profileNameBlockList.includes(sectionName)) {
|
|
38
|
+
throw new Error(`Found invalid profile name "${sectionName}"`);
|
|
39
|
+
}
|
|
40
|
+
} else if (currentSection) {
|
|
41
|
+
const indexOfEqualsSign = trimmedLine.indexOf("=");
|
|
42
|
+
if (![0, -1].includes(indexOfEqualsSign)) {
|
|
43
|
+
const [name, value]: [string, string] = [
|
|
44
|
+
trimmedLine.substring(0, indexOfEqualsSign).trim(),
|
|
45
|
+
trimmedLine.substring(indexOfEqualsSign + 1).trim(),
|
|
46
|
+
];
|
|
47
|
+
if (value === "") {
|
|
48
|
+
currentSubSection = name;
|
|
49
|
+
} else {
|
|
50
|
+
if (currentSubSection && iniLine.trimStart() === iniLine) {
|
|
51
|
+
// Reset currentSubSection if there is no whitespace
|
|
52
|
+
currentSubSection = undefined;
|
|
53
|
+
}
|
|
54
|
+
map[currentSection] = map[currentSection] || {};
|
|
55
|
+
const key = currentSubSection
|
|
56
|
+
? [currentSubSection, name].join(CONFIG_PREFIX_SEPARATOR)
|
|
57
|
+
: name;
|
|
58
|
+
map[currentSection][key] = value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return map;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const parseSSOSessionData = (data: ParsedIniData): ParsedIniData =>
|
|
68
|
+
Object.entries(data)
|
|
69
|
+
// filter out non sso-session keys
|
|
70
|
+
.filter(([key]) =>
|
|
71
|
+
key.startsWith(IniSectionType.SSO_SESSION + CONFIG_PREFIX_SEPARATOR),
|
|
72
|
+
)
|
|
73
|
+
// replace sso-session key with sso-session name
|
|
74
|
+
.reduce(
|
|
75
|
+
(acc, [key, value]) => ({
|
|
76
|
+
...acc,
|
|
77
|
+
[key.substring(key.indexOf(CONFIG_PREFIX_SEPARATOR) + 1)]: value,
|
|
78
|
+
}),
|
|
79
|
+
{},
|
|
80
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Data from "effect/Data";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
|
|
6
|
+
export class Region extends Context.Tag("AWS::Region")<Region, string>() {}
|
|
7
|
+
|
|
8
|
+
export const of = (region: string) => Layer.succeed(Region, region);
|
|
9
|
+
|
|
10
|
+
export const fromEnvOrElse = (region: string) =>
|
|
11
|
+
Layer.succeed(Region, process.env.AWS_REGION ?? region);
|
|
12
|
+
|
|
13
|
+
export class EnvironmentVariableNotSet extends Data.TaggedError(
|
|
14
|
+
"EnvironmentVariableNotSet",
|
|
15
|
+
)<{
|
|
16
|
+
message: string;
|
|
17
|
+
variable: string;
|
|
18
|
+
}> {}
|
|
19
|
+
|
|
20
|
+
export const fromEnv = () =>
|
|
21
|
+
Layer.effect(
|
|
22
|
+
Region,
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
const region = process.env.AWS_REGION;
|
|
25
|
+
if (!region) {
|
|
26
|
+
return yield* Effect.fail(
|
|
27
|
+
new EnvironmentVariableNotSet({
|
|
28
|
+
message: "AWS_REGION is not set",
|
|
29
|
+
variable: "AWS_REGION",
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return region;
|
|
34
|
+
}),
|
|
35
|
+
);
|
package/src/aws/s3.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import { S3 } from "itty-aws/s3";
|
|
3
|
+
import { createAWSServiceClientLayer } from "./client.ts";
|
|
4
|
+
|
|
5
|
+
export class S3Client extends Context.Tag("AWS::S3::Client")<S3Client, S3>() {}
|
|
6
|
+
|
|
7
|
+
export const client = createAWSServiceClientLayer<typeof S3Client, S3>(
|
|
8
|
+
S3Client,
|
|
9
|
+
S3,
|
|
10
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
|
|
4
|
+
import { SQS as SQSClient } from "itty-aws/sqs";
|
|
5
|
+
import { createAWSServiceClientLayer } from "../client.ts";
|
|
6
|
+
import * as Credentials from "../credentials.ts";
|
|
7
|
+
import * as Region from "../region.ts";
|
|
8
|
+
|
|
9
|
+
export class QueueClient extends Context.Tag("AWS::SQS::Queue.Client")<
|
|
10
|
+
QueueClient,
|
|
11
|
+
SQSClient
|
|
12
|
+
>() {}
|
|
13
|
+
|
|
14
|
+
export const client = createAWSServiceClientLayer<
|
|
15
|
+
typeof QueueClient,
|
|
16
|
+
SQSClient
|
|
17
|
+
>(QueueClient, SQSClient);
|
|
18
|
+
|
|
19
|
+
export const clientFromEnv = () =>
|
|
20
|
+
Layer.provide(client(), Layer.merge(Credentials.fromEnv(), Region.fromEnv()));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Binding, type Capability, type From } from "alchemy-effect";
|
|
2
|
+
import type * as lambda from "aws-lambda";
|
|
3
|
+
import { Function } from "../lambda/index.ts";
|
|
4
|
+
import { Queue } from "./queue.ts";
|
|
5
|
+
|
|
6
|
+
export type QueueRecord<Data> = Omit<lambda.SQSRecord, "body"> & {
|
|
7
|
+
body: Data;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type QueueEvent<Data> = Omit<lambda.SQSEvent, "Records"> & {
|
|
11
|
+
Records: QueueRecord<Data>[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface Consume<Q = Queue> extends Capability<"AWS.SQS.Consume", Q> {}
|
|
15
|
+
|
|
16
|
+
export interface QueueEventSourceProps {
|
|
17
|
+
batchSize?: number;
|
|
18
|
+
maxBatchingWindow?: number;
|
|
19
|
+
maxConcurrency?: number;
|
|
20
|
+
reportBatchItemFailures?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const QueueEventSource = Binding<
|
|
24
|
+
<Q extends Queue, const Props extends QueueEventSourceProps>(
|
|
25
|
+
queue: Q,
|
|
26
|
+
props?: Props,
|
|
27
|
+
) => Binding<Function, Consume<From<Q>>, Props, "QueueEventSource">
|
|
28
|
+
>(Function, "AWS.SQS.Consume", "QueueEventSource");
|
|
29
|
+
|
|
30
|
+
export const consumeFromLambdaFunction = () =>
|
|
31
|
+
QueueEventSource.provider.succeed({
|
|
32
|
+
attach: ({ source: queue }) => ({
|
|
33
|
+
policyStatements: [
|
|
34
|
+
{
|
|
35
|
+
Sid: "AWS.SQS.Consume",
|
|
36
|
+
Effect: "Allow",
|
|
37
|
+
Action: [
|
|
38
|
+
"sqs:ReceiveMessage",
|
|
39
|
+
"sqs:DeleteMessage",
|
|
40
|
+
"sqs:ChangeMessageVisibility",
|
|
41
|
+
],
|
|
42
|
+
Resource: [queue.attr.queueArn],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schedule from "effect/Schedule";
|
|
3
|
+
|
|
4
|
+
import { App, type ProviderService } from "alchemy-effect";
|
|
5
|
+
import { AccountID } from "../account.ts";
|
|
6
|
+
import { Region } from "../region.ts";
|
|
7
|
+
import { QueueClient } from "./queue.client.ts";
|
|
8
|
+
import { Queue, type QueueProps } from "./queue.ts";
|
|
9
|
+
|
|
10
|
+
export const queueProvider = () =>
|
|
11
|
+
Queue.provider.effect(
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const sqs = yield* QueueClient;
|
|
14
|
+
const app = yield* App;
|
|
15
|
+
const region = yield* Region;
|
|
16
|
+
const accountId = yield* AccountID;
|
|
17
|
+
const createQueueName = (id: string, props: QueueProps) =>
|
|
18
|
+
props.queueName ??
|
|
19
|
+
`${app.name}-${id}-${app.stage}${props.fifo ? ".fifo" : ""}`;
|
|
20
|
+
const createAttributes = (props: QueueProps) => ({
|
|
21
|
+
FifoQueue: props.fifo ? "true" : "false",
|
|
22
|
+
FifoThroughputLimit: props.fifoThroughputLimit,
|
|
23
|
+
ContentBasedDeduplication: props.contentBasedDeduplication
|
|
24
|
+
? "true"
|
|
25
|
+
: "false",
|
|
26
|
+
DeduplicationScope: props.deduplicationScope,
|
|
27
|
+
DelaySeconds: props.delaySeconds?.toString(),
|
|
28
|
+
MaximumMessageSize: props.maximumMessageSize?.toString(),
|
|
29
|
+
MessageRetentionPeriod: props.messageRetentionPeriod?.toString(),
|
|
30
|
+
ReceiveMessageWaitTimeSeconds:
|
|
31
|
+
props.receiveMessageWaitTimeSeconds?.toString(),
|
|
32
|
+
VisibilityTimeout: props.visibilityTimeout?.toString(),
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
diff: Effect.fn(function* ({ id, news, olds }) {
|
|
36
|
+
const oldFifo = olds.fifo ?? false;
|
|
37
|
+
const newFifo = news.fifo ?? false;
|
|
38
|
+
if (oldFifo !== newFifo) {
|
|
39
|
+
return { action: "replace" } as const;
|
|
40
|
+
}
|
|
41
|
+
const oldQueueName = createQueueName(id, olds);
|
|
42
|
+
const newQueueName = createQueueName(id, news);
|
|
43
|
+
if (oldQueueName !== newQueueName) {
|
|
44
|
+
return { action: "replace" } as const;
|
|
45
|
+
}
|
|
46
|
+
return { action: "noop" } as const;
|
|
47
|
+
}),
|
|
48
|
+
create: Effect.fn(function* ({ id, news, session }) {
|
|
49
|
+
const queueName = createQueueName(id, news);
|
|
50
|
+
const response = yield* sqs
|
|
51
|
+
.createQueue({
|
|
52
|
+
QueueName: queueName,
|
|
53
|
+
Attributes: createAttributes(news),
|
|
54
|
+
})
|
|
55
|
+
.pipe(
|
|
56
|
+
Effect.retry({
|
|
57
|
+
while: (e) => e.name === "QueueDeletedRecently",
|
|
58
|
+
schedule: Schedule.fixed(1000).pipe(
|
|
59
|
+
Schedule.tapOutput((i) =>
|
|
60
|
+
session.note(
|
|
61
|
+
`Queue was deleted recently, retrying... ${i + 1}s`,
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
const queueArn =
|
|
68
|
+
`arn:aws:sqs:${region}:${accountId}:${queueName}` as const;
|
|
69
|
+
const queueUrl = response.QueueUrl!;
|
|
70
|
+
yield* session.note(queueUrl);
|
|
71
|
+
return {
|
|
72
|
+
queueName,
|
|
73
|
+
queueUrl,
|
|
74
|
+
queueArn: queueArn,
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
update: Effect.fn(function* ({ news, output, session }) {
|
|
78
|
+
yield* sqs.setQueueAttributes({
|
|
79
|
+
QueueUrl: output.queueUrl,
|
|
80
|
+
Attributes: createAttributes(news),
|
|
81
|
+
});
|
|
82
|
+
yield* session.note(output.queueUrl);
|
|
83
|
+
return output;
|
|
84
|
+
}),
|
|
85
|
+
delete: Effect.fn(function* (input) {
|
|
86
|
+
yield* sqs
|
|
87
|
+
.deleteQueue({
|
|
88
|
+
QueueUrl: input.output.queueUrl,
|
|
89
|
+
})
|
|
90
|
+
.pipe(Effect.catchTag("QueueDoesNotExist", () => Effect.void));
|
|
91
|
+
}),
|
|
92
|
+
} satisfies ProviderService<Queue>;
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Binding,
|
|
5
|
+
declare,
|
|
6
|
+
toEnvKey,
|
|
7
|
+
type Capability,
|
|
8
|
+
type To,
|
|
9
|
+
} from "alchemy-effect";
|
|
10
|
+
import { Function } from "../lambda/index.ts";
|
|
11
|
+
import { QueueClient } from "./queue.client.ts";
|
|
12
|
+
import { Queue } from "./queue.ts";
|
|
13
|
+
|
|
14
|
+
export interface SendMessage<Q = Queue>
|
|
15
|
+
extends Capability<"AWS.SQS.SendMessage", Q> {}
|
|
16
|
+
|
|
17
|
+
export const SendMessage = Binding<
|
|
18
|
+
<Q extends Queue>(queue: Q) => Binding<Function, SendMessage<To<Q>>>
|
|
19
|
+
>(Function, "AWS.SQS.SendMessage");
|
|
20
|
+
|
|
21
|
+
export const sendMessage = <Q extends Queue>(
|
|
22
|
+
queue: Q,
|
|
23
|
+
message: Q["props"]["schema"]["Type"],
|
|
24
|
+
) =>
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
yield* declare<SendMessage<To<Q>>>();
|
|
27
|
+
const sqs = yield* QueueClient;
|
|
28
|
+
const url = process.env[toEnvKey(queue.id, "QUEUE_URL")]!;
|
|
29
|
+
return yield* sqs.sendMessage({
|
|
30
|
+
QueueUrl: url,
|
|
31
|
+
MessageBody: JSON.stringify(message),
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const sendMessageFromLambdaFunction = () =>
|
|
36
|
+
SendMessage.provider.succeed({
|
|
37
|
+
attach: ({ source: queue }) => ({
|
|
38
|
+
env: {
|
|
39
|
+
// ask what attribute is needed to interact? e.g. is it the Queue ARN or the Queue URL?
|
|
40
|
+
[toEnvKey(queue.id, "QUEUE_URL")]: queue.attr.queueUrl,
|
|
41
|
+
},
|
|
42
|
+
policyStatements: [
|
|
43
|
+
{
|
|
44
|
+
Sid: "SendMessage",
|
|
45
|
+
Effect: "Allow",
|
|
46
|
+
Action: ["sqs:SendMessage"], // <- ask LLM how to generate this
|
|
47
|
+
Resource: [queue.attr.queueArn],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type * as S from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
import { Resource } from "alchemy-effect";
|
|
4
|
+
|
|
5
|
+
// required to avoid this error in consumers: "The inferred type of 'Messages' cannot be named without a reference to '../../effect-aws/node_modules/@types/aws-lambda'. This is likely not portable. A type annotation is necessary.ts(2742)"
|
|
6
|
+
export type * as lambda from "aws-lambda";
|
|
7
|
+
|
|
8
|
+
export const Queue = Resource<{
|
|
9
|
+
<const ID extends string, const Props extends QueueProps>(
|
|
10
|
+
id: ID,
|
|
11
|
+
props: Props,
|
|
12
|
+
): Queue<ID, Props>;
|
|
13
|
+
}>("AWS.SQS.Queue");
|
|
14
|
+
|
|
15
|
+
export interface Queue<
|
|
16
|
+
ID extends string = string,
|
|
17
|
+
Props extends QueueProps = QueueProps,
|
|
18
|
+
> extends Resource<"AWS.SQS.Queue", ID, Props, QueueAttrs<Props>> {}
|
|
19
|
+
|
|
20
|
+
export type QueueAttrs<Props extends QueueProps> = {
|
|
21
|
+
queueName: Props["queueName"] extends string ? Props["queueName"] : string;
|
|
22
|
+
queueUrl: Props["fifo"] extends true ? `${string}.fifo` : string;
|
|
23
|
+
queueArn: `arn:aws:sqs:${string}:${string}:${Props["queueName"]}`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type QueueProps<Msg = any> = {
|
|
27
|
+
/**
|
|
28
|
+
* Schema for the message body.
|
|
29
|
+
*/
|
|
30
|
+
schema: S.Schema<Msg>;
|
|
31
|
+
/**
|
|
32
|
+
* Name of the queue.
|
|
33
|
+
* @default ${app}-${stage}-${id}?.fifo
|
|
34
|
+
*/
|
|
35
|
+
queueName?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Delay in seconds for all messages in the queue (`0` - `900`).
|
|
38
|
+
* @default 0
|
|
39
|
+
*/
|
|
40
|
+
delaySeconds?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Maximum message size in bytes (`1,024` - `1,048,576`).
|
|
43
|
+
* @default 1048576
|
|
44
|
+
*/
|
|
45
|
+
maximumMessageSize?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Message retention period in seconds (`60` - `1,209,600`).
|
|
48
|
+
* @default 345600
|
|
49
|
+
*/
|
|
50
|
+
messageRetentionPeriod?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Time in seconds for `ReceiveMessage` to wait for a message (`0` - `20`).
|
|
53
|
+
* @default 0
|
|
54
|
+
*/
|
|
55
|
+
receiveMessageWaitTimeSeconds?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Visibility timeout in seconds (`0` - `43,200`).
|
|
58
|
+
* @default 30
|
|
59
|
+
*/
|
|
60
|
+
visibilityTimeout?: number;
|
|
61
|
+
} & (
|
|
62
|
+
| {
|
|
63
|
+
fifo?: false;
|
|
64
|
+
contentBasedDeduplication?: undefined;
|
|
65
|
+
deduplicationScope?: undefined;
|
|
66
|
+
fifoThroughputLimit?: undefined;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
fifo: true;
|
|
70
|
+
/**
|
|
71
|
+
* Enables content-based deduplication for FIFO queues. Only valid when `fifo` is `true`.
|
|
72
|
+
* @default false
|
|
73
|
+
*/
|
|
74
|
+
contentBasedDeduplication?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Specifies whether message deduplication occurs at the message group or queue level.
|
|
77
|
+
* Valid values are `messageGroup` and `queue`. Only valid when `fifo` is `true`.
|
|
78
|
+
*/
|
|
79
|
+
deduplicationScope?: "messageGroup" | "queue";
|
|
80
|
+
/**
|
|
81
|
+
* Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group.
|
|
82
|
+
* Valid values are `perQueue` and `perMessageGroupId`. Only valid when `fifo` is `true`.
|
|
83
|
+
*/
|
|
84
|
+
fifoThroughputLimit?: "perQueue" | "perMessageGroupId";
|
|
85
|
+
}
|
|
86
|
+
);
|
package/src/aws/sts.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import { STS } from "itty-aws/sts";
|
|
3
|
+
import { createAWSServiceClientLayer } from "./client.ts";
|
|
4
|
+
|
|
5
|
+
export class STSClient extends Context.Tag("AWS::STS::Client")<
|
|
6
|
+
STSClient,
|
|
7
|
+
STS
|
|
8
|
+
>() {}
|
|
9
|
+
|
|
10
|
+
export const client = createAWSServiceClientLayer<typeof STSClient, STS>(
|
|
11
|
+
STSClient,
|
|
12
|
+
STS,
|
|
13
|
+
);
|
package/src/aws/zip.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
|
|
3
|
+
export const zipCode = Effect.fn(function* (
|
|
4
|
+
content: string | Uint8Array<ArrayBufferLike>,
|
|
5
|
+
) {
|
|
6
|
+
// Create a zip buffer in memory
|
|
7
|
+
const zip = new (yield* Effect.promise(() => import("jszip"))).default();
|
|
8
|
+
zip.file("index.mjs", content);
|
|
9
|
+
|
|
10
|
+
return yield* Effect.promise(() =>
|
|
11
|
+
zip.generateAsync({
|
|
12
|
+
type: "nodebuffer",
|
|
13
|
+
compression: "DEFLATE",
|
|
14
|
+
platform: "UNIX",
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// biome-ignore lint/correctness/noUnusedImports: UMD global
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
|
|
7
|
+
import * as Alchemy from "alchemy-effect";
|
|
8
|
+
import { render } from "ink";
|
|
9
|
+
|
|
10
|
+
import { ApprovePlan } from "./components/ApprovePlan.tsx";
|
|
11
|
+
|
|
12
|
+
export const requireApproval = Layer.succeed(
|
|
13
|
+
Alchemy.PlanReviewer,
|
|
14
|
+
Alchemy.PlanReviewer.of({
|
|
15
|
+
approve: <P extends Alchemy.Plan>(plan: P) =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
let approved = false;
|
|
18
|
+
|
|
19
|
+
const { waitUntilExit } = render(
|
|
20
|
+
<ApprovePlan plan={plan} approve={(a) => (approved = a)} />,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
yield* Effect.promise(() => waitUntilExit());
|
|
24
|
+
|
|
25
|
+
if (!approved) {
|
|
26
|
+
yield* Effect.fail(new Alchemy.PlanRejected());
|
|
27
|
+
}
|
|
28
|
+
}),
|
|
29
|
+
}),
|
|
30
|
+
);
|
package/src/cli/clack.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as Clack from "@clack/prompts";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
|
|
4
|
+
const syncFn =
|
|
5
|
+
<F extends (...p: any[]) => any>(f: F) =>
|
|
6
|
+
(...p: Parameters<F>): Effect.Effect<ReturnType<F>> =>
|
|
7
|
+
Effect.sync(() => f(...p));
|
|
8
|
+
|
|
9
|
+
const asyncFn =
|
|
10
|
+
<F extends (...p: any[]) => any>(f: F) =>
|
|
11
|
+
(...p: Parameters<F>): Effect.Effect<Awaited<ReturnType<F>>> =>
|
|
12
|
+
Effect.promise(() => f(...p));
|
|
13
|
+
|
|
14
|
+
export const confirm = asyncFn(Clack.confirm);
|
|
15
|
+
export const tasks = asyncFn(Clack.tasks);
|
|
16
|
+
|
|
17
|
+
export const intro = syncFn(Clack.intro);
|
|
18
|
+
export const outro = syncFn(Clack.outro);
|
|
19
|
+
export const note = syncFn(Clack.note);
|
|
20
|
+
export const spinner = syncFn(Clack.spinner);
|
|
21
|
+
|
|
22
|
+
export const isCancel = Clack.isCancel;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// biome-ignore lint/style/useImportType: UMD global
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import type * as Alchemy from "alchemy-effect";
|
|
5
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
6
|
+
|
|
7
|
+
import { Plan } from "./Plan.tsx";
|
|
8
|
+
|
|
9
|
+
export interface ApprovePlanProps {
|
|
10
|
+
plan: Alchemy.Plan;
|
|
11
|
+
approve: (result: boolean) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ApprovePlan(props: ApprovePlanProps): React.JSX.Element {
|
|
15
|
+
const { plan, approve } = props;
|
|
16
|
+
const [selected, setSelected] = useState(0);
|
|
17
|
+
const { exit } = useApp();
|
|
18
|
+
|
|
19
|
+
useInput((_input, key) => {
|
|
20
|
+
if (key.leftArrow || key.rightArrow) {
|
|
21
|
+
setSelected((prev) => (prev === 0 ? 1 : 0));
|
|
22
|
+
} else if (key.return) {
|
|
23
|
+
approve(selected === 0);
|
|
24
|
+
exit();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Box flexDirection="column">
|
|
30
|
+
<Plan plan={plan} />
|
|
31
|
+
<Box marginTop={1}>
|
|
32
|
+
<Text>Proceed?</Text>
|
|
33
|
+
</Box>
|
|
34
|
+
<Box gap={1}>
|
|
35
|
+
<Text color={selected === 0 ? "green" : "gray"}>
|
|
36
|
+
{selected === 0 ? "◉" : "○"} Yes
|
|
37
|
+
</Text>
|
|
38
|
+
<Text color={selected === 1 ? "red" : "gray"}>
|
|
39
|
+
{selected === 1 ? "◉" : "○"} No
|
|
40
|
+
</Text>
|
|
41
|
+
</Box>
|
|
42
|
+
</Box>
|
|
43
|
+
);
|
|
44
|
+
}
|