Skip to content

Commit 9119967

Browse files
committed
feat: standardize physical name generation
1 parent ec8120a commit 9119967

File tree

15 files changed

+599
-435
lines changed

15 files changed

+599
-435
lines changed

alchemy-effect/src/apply.ts

Lines changed: 296 additions & 286 deletions
Large diffs are not rendered by default.

alchemy-effect/src/aws/dynamodb/table.provider.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as Schedule from "effect/Schedule";
55
import type { TimeToLiveSpecification } from "itty-aws/dynamodb";
66
import { App } from "../../app.ts";
77
import type { Input } from "../../input.ts";
8-
import type { Provider, ProviderService } from "../../provider.ts";
8+
import type { Provider } from "../../provider.ts";
99
import { createTagger, hasTags } from "../../tags.ts";
1010
import { isScalarAttributeType, toAttributeType } from "./attribute-value.ts";
1111
import { DynamoDBClient } from "./client.ts";
@@ -16,6 +16,7 @@ import {
1616
type TableAttrs,
1717
type TableProps,
1818
} from "./table.ts";
19+
import { createPhysicalName } from "../../physical-name.ts";
1920

2021
// we add an explict type to simplify the Layer type errors because the Table interface has a lot of type args
2122
export const tableProvider = (): Layer.Layer<
@@ -31,7 +32,17 @@ export const tableProvider = (): Layer.Layer<
3132
const createTableName = (
3233
id: string,
3334
props: Input.ResolveProps<TableProps>,
34-
) => props.tableName ?? `${app.name}-${id}-${app.stage}`;
35+
) =>
36+
Effect.gen(function* () {
37+
return (
38+
props.tableName ??
39+
(yield* createPhysicalName({
40+
id,
41+
// see: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TableDescription.html#DDB-Type-TableDescription-TableName
42+
maxLength: 255,
43+
}))
44+
);
45+
});
3546

3647
const tagged = yield* createTagger();
3748

@@ -144,7 +155,7 @@ export const tableProvider = (): Layer.Layer<
144155
}),
145156

146157
create: Effect.fn(function* ({ id, news, session }) {
147-
const tableName = createTableName(id, news);
158+
const tableName = yield* createTableName(id, news);
148159

149160
const response = yield* dynamodb
150161
.createTable({
@@ -272,6 +283,6 @@ export const tableProvider = (): Layer.Layer<
272283
}
273284
}
274285
}),
275-
} satisfies ProviderService<Table<string, TableProps<unknown>>>;
286+
};
276287
}),
277288
);

alchemy-effect/src/aws/ec2/vpc.provider.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import * as Schedule from "effect/Schedule";
33

44
import type { EC2 } from "itty-aws/ec2";
55

6+
import type { VpcId } from "./vpc.ts";
67
import type { ScopedPlanStatusSession } from "../../cli/service.ts";
78
import { somePropsAreDifferent } from "../../diff.ts";
89
import type { ProviderService } from "../../provider.ts";
910
import { createTagger, createTagsList, diffTags } from "../../tags.ts";
10-
import { Account } from "../account.ts";
11-
import { Region } from "../region.ts";
1211
import { EC2Client } from "./client.ts";
13-
import type { VpcId } from "./vpc.ts";
1412
import { Vpc, type VpcAttrs, type VpcProps } from "./vpc.ts";
13+
import { Region } from "../region.ts";
14+
import { Account } from "../account.ts";
1515

1616
export const vpcProvider = () =>
1717
Vpc.provider.effect(
@@ -45,9 +45,8 @@ export const vpcProvider = () =>
4545
return { action: "replace" };
4646
}
4747
}),
48-
create: Effect.fn(function* ({ id, news, session }) {
49-
const tags = createTags(id, news.tags);
5048

49+
create: Effect.fn(function* ({ id, news, session }) {
5150
// 1. Call CreateVpc
5251
const createResult = yield* ec2.createVpc({
5352
// TODO(sam): add all properties
@@ -65,7 +64,7 @@ export const vpcProvider = () =>
6564
TagSpecifications: [
6665
{
6766
ResourceType: "vpc",
68-
Tags: createTagsList(tags),
67+
Tags: createTagsList(createTags(id, news.tags)),
6968
},
7069
],
7170
DryRun: false,
@@ -74,7 +73,7 @@ export const vpcProvider = () =>
7473
const vpcId = createResult.Vpc!.VpcId! as VpcId;
7574
yield* session.note(`VPC created: ${vpcId}`);
7675

77-
// 2. Modify DNS attributes if specified (separate API calls)
76+
// 3. Modify DNS attributes if specified (separate API calls)
7877
yield* ec2.modifyVpcAttribute({
7978
VpcId: vpcId,
8079
EnableDnsSupport: { Value: news.enableDnsSupport ?? true },
@@ -122,7 +121,6 @@ export const vpcProvider = () =>
122121
ipv6Pool: assoc.Ipv6Pool,
123122
}),
124123
),
125-
tags,
126124
} satisfies VpcAttrs<VpcProps>;
127125
}),
128126

@@ -192,7 +190,10 @@ export const vpcProvider = () =>
192190
while: (e) => {
193191
// DependencyViolation means there are still dependent resources
194192
// This can happen if subnets/IGW are being deleted concurrently
195-
return e._tag === "DependencyViolation";
193+
return (
194+
e._tag === "ValidationError" &&
195+
e.message?.includes("DependencyViolation")
196+
);
196197
},
197198
schedule: Schedule.exponential(1000, 1.5).pipe(
198199
Schedule.intersect(Schedule.recurs(10)), // Try up to 10 times

alchemy-effect/src/aws/lambda/function.provider.ts

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ import type {
1212
} from "itty-aws/lambda";
1313
import { App } from "../../app.ts";
1414
import { DotAlchemy } from "../../dot-alchemy.ts";
15-
import type { ProviderService } from "../../provider.ts";
1615
import { createTagger, createTagsList, hasTags } from "../../tags.ts";
1716
import { Account } from "../account.ts";
1817
import * as IAM from "../iam.ts";
1918
import { Region } from "../region.ts";
2019
import { zipCode } from "../zip.ts";
20+
import { createPhysicalName } from "../../physical-name.ts";
2121
import { LambdaClient } from "./client.ts";
2222
import { Function, type FunctionAttr, type FunctionProps } from "./function.ts";
2323

@@ -34,25 +34,35 @@ export const functionProvider = () =>
3434

3535
// const assets = yield* Assets;
3636

37-
const createFunctionName = (id: string) =>
38-
`${app.name}-${app.stage}-${id}-${region}`;
37+
const createFunctionName = (
38+
id: string,
39+
functionName: string | undefined,
40+
) =>
41+
Effect.gen(function* () {
42+
return (
43+
functionName ?? (yield* createPhysicalName({ id, maxLength: 64 }))
44+
);
45+
});
46+
3947
const createRoleName = (id: string) =>
40-
`${app.name}-${app.stage}-${id}-${region}`;
48+
createPhysicalName({ id, maxLength: 64 });
49+
4150
const createPolicyName = (id: string) =>
42-
`${app.name}-${app.stage}-${id}-${region}`;
51+
createPhysicalName({ id, maxLength: 128 });
4352

44-
const createPhysicalNames = (id: string) => {
45-
const roleName = createRoleName(id);
46-
const policyName = createPolicyName(id);
47-
const functionName = createFunctionName(id);
48-
return {
49-
roleName,
50-
policyName,
51-
functionName,
52-
roleArn: `arn:aws:iam::${accountId}:role/${roleName}`,
53-
functionArn: `arn:aws:lambda:${region}:${accountId}:function:${functionName}`,
54-
};
55-
};
53+
const createNames = (id: string, functionName: string | undefined) =>
54+
Effect.gen(function* () {
55+
const roleName = yield* createRoleName(id);
56+
const policyName = yield* createPolicyName(id);
57+
const fn = yield* createFunctionName(id, functionName);
58+
return {
59+
roleName,
60+
policyName,
61+
functionName: fn,
62+
roleArn: `arn:aws:iam::${accountId}:role/${roleName}`,
63+
functionArn: `arn:aws:lambda:${region}:${accountId}:function:${fn}`,
64+
};
65+
});
5666

5767
const attachBindings = Effect.fn(function* ({
5868
roleName,
@@ -431,7 +441,7 @@ export const functionProvider = () =>
431441
if (
432442
// function name changed
433443
output.functionName !==
434-
(news.functionName ?? createFunctionName(id)) ||
444+
(yield* createFunctionName(id, news.functionName)) ||
435445
// url changed
436446
olds.url !== news.url
437447
) {
@@ -456,7 +466,10 @@ export const functionProvider = () =>
456466
...output,
457467
functionUrl: (yield* lambda
458468
.getFunctionUrlConfig({
459-
FunctionName: createFunctionName(id),
469+
FunctionName: yield* createFunctionName(
470+
id,
471+
output.functionName,
472+
),
460473
})
461474
.pipe(
462475
Effect.map((f) => f.FunctionUrl),
@@ -476,7 +489,10 @@ export const functionProvider = () =>
476489
}),
477490

478491
precreate: Effect.fn(function* ({ id, news }) {
479-
const { roleName, functionName, roleArn } = createPhysicalNames(id);
492+
const { roleName, functionName, roleArn } = yield* createNames(
493+
id,
494+
news.functionName,
495+
);
480496

481497
const role = yield* createRoleIfNotExists({ id, roleName });
482498

@@ -495,19 +511,16 @@ export const functionProvider = () =>
495511
functionArn: `arn:aws:lambda:${region}:${accountId}:function:${functionName}`,
496512
functionName,
497513
functionUrl: undefined,
498-
roleName: createRoleName(id),
514+
roleName,
499515
code: {
500516
hash: yield* hashCode(code),
501517
},
502518
roleArn,
503519
} satisfies FunctionAttr<FunctionProps>;
504520
}),
505521
create: Effect.fn(function* ({ id, news, bindings, session }) {
506-
const roleName = createRoleName(id);
507-
const policyName = createPolicyName(id);
508-
// const policyArn = `arn:aws:iam::${accountId}:policy/${policyName}`;
509-
const functionName = news.functionName ?? createFunctionName(id);
510-
const functionArn = `arn:aws:lambda:${region}:${accountId}:function:${functionName}`;
522+
const { roleName, policyName, functionName, functionArn } =
523+
yield* createNames(id, news.functionName);
511524

512525
const role = yield* createRoleIfNotExists({ id, roleName });
513526

@@ -557,10 +570,8 @@ export const functionProvider = () =>
557570
output,
558571
session,
559572
}) {
560-
const roleName = createRoleName(id);
561-
const policyName = createPolicyName(id);
562-
const functionName = news.functionName ?? createFunctionName(id);
563-
const functionArn = `arn:aws:lambda:${region}:${accountId}:function:${functionName}`;
573+
const { roleName, policyName, functionName, functionArn } =
574+
yield* createNames(id, news.functionName);
564575

565576
const env = yield* attachBindings({
566577
roleName,
@@ -659,6 +670,6 @@ export const functionProvider = () =>
659670
.pipe(Effect.catchTag("NoSuchEntityException", () => Effect.void));
660671
return null as any;
661672
}),
662-
} satisfies ProviderService<Function>;
673+
};
663674
}),
664675
);

alchemy-effect/src/aws/sqs/queue.provider.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as Effect from "effect/Effect";
22
import * as Schedule from "effect/Schedule";
33

4-
import { App } from "../../app.ts";
5-
import type { ProviderService } from "../../provider.ts";
4+
import { createPhysicalName } from "../../physical-name.ts";
65
import { Account } from "../account.ts";
76
import { Region } from "../region.ts";
87
import { SQSClient } from "./client.ts";
@@ -12,7 +11,6 @@ export const queueProvider = () =>
1211
Queue.provider.effect(
1312
Effect.gen(function* () {
1413
const sqs = yield* SQSClient;
15-
const app = yield* App;
1614
const region = yield* Region;
1715
const accountId = yield* Account;
1816
const createQueueName = (
@@ -22,8 +20,16 @@ export const queueProvider = () =>
2220
fifo?: boolean;
2321
},
2422
) =>
25-
props.queueName ??
26-
`${app.name}-${id}-${app.stage}${props.fifo ? ".fifo" : ""}`;
23+
Effect.gen(function* () {
24+
if (props.queueName) {
25+
return props.queueName;
26+
}
27+
const baseName = yield* createPhysicalName({
28+
id,
29+
maxLength: props.fifo ? 80 - ".fifo".length : 80,
30+
});
31+
return props.fifo ? `${baseName}.fifo` : baseName;
32+
});
2733
const createAttributes = (props: QueueProps) => ({
2834
FifoQueue: props.fifo ? "true" : "false",
2935
FifoThroughputLimit: props.fifoThroughputLimit,
@@ -46,15 +52,15 @@ export const queueProvider = () =>
4652
if (oldFifo !== newFifo) {
4753
return { action: "replace" } as const;
4854
}
49-
const oldQueueName = createQueueName(id, olds);
50-
const newQueueName = createQueueName(id, news);
55+
const oldQueueName = yield* createQueueName(id, olds);
56+
const newQueueName = yield* createQueueName(id, news);
5157
if (oldQueueName !== newQueueName) {
5258
return { action: "replace" } as const;
5359
}
5460
return { action: "noop" } as const;
5561
}),
5662
create: Effect.fn(function* ({ id, news, session }) {
57-
const queueName = createQueueName(id, news);
63+
const queueName = yield* createQueueName(id, news);
5864
const response = yield* sqs
5965
.createQueue({
6066
QueueName: queueName,
@@ -97,6 +103,6 @@ export const queueProvider = () =>
97103
})
98104
.pipe(Effect.catchTag("QueueDoesNotExist", () => Effect.void));
99105
}),
100-
} satisfies ProviderService<Queue>;
106+
};
101107
}),
102108
);

alchemy-effect/src/cloudflare/kv/namespace.provider.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { KV } from "cloudflare/resources";
22
import * as Effect from "effect/Effect";
33
import { App } from "../../app.ts";
4+
import { createPhysicalName } from "../../physical-name.ts";
45
import { Account } from "../account.ts";
56
import { CloudflareApi } from "../api.ts";
67
import {
@@ -17,7 +18,9 @@ export const namespaceProvider = () =>
1718
const accountId = yield* Account;
1819

1920
const createTitle = (id: string, title: string | undefined) =>
20-
title ?? `${app.name}-${id}-${app.stage}`;
21+
Effect.gen(function* () {
22+
return title ?? (yield* createPhysicalName({ id }));
23+
});
2124

2225
const mapResult = <Props extends NamespaceProps>(
2326
result: KV.Namespace,
@@ -30,29 +33,30 @@ export const namespaceProvider = () =>
3033

3134
return {
3235
stables: ["namespaceId", "accountId"],
33-
diff: ({ id, news, output }) =>
34-
Effect.sync(() => {
35-
if (output.accountId !== accountId) {
36-
return { action: "replace" };
37-
}
38-
const title = createTitle(id, news.title);
39-
if (title !== output.title) {
40-
return { action: "update" };
41-
}
42-
}),
36+
diff: Effect.fn(function* ({ id, news, output }) {
37+
if (output.accountId !== accountId) {
38+
return { action: "replace" };
39+
}
40+
const title = yield* createTitle(id, news.title);
41+
if (title !== output.title) {
42+
return { action: "update" };
43+
}
44+
}),
4345
create: Effect.fn(function* ({ id, news }) {
46+
const title = yield* createTitle(id, news.title);
4447
return yield* api.kv.namespaces
4548
.create({
4649
account_id: accountId,
47-
title: createTitle(id, news.title),
50+
title,
4851
})
4952
.pipe(Effect.map(mapResult<NamespaceProps>));
5053
}),
5154
update: Effect.fn(function* ({ id, news, output }) {
55+
const title = yield* createTitle(id, news.title);
5256
return yield* api.kv.namespaces
5357
.update(output.namespaceId, {
5458
account_id: accountId,
55-
title: createTitle(id, news.title),
59+
title,
5660
})
5761
.pipe(Effect.map(mapResult<NamespaceProps>));
5862
}),
@@ -74,7 +78,7 @@ export const namespaceProvider = () =>
7478
Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
7579
);
7680
}
77-
const title = createTitle(id, olds?.title); // why is olds optional? because read can be called before the resource exists (sync)
81+
const title = yield* createTitle(id, olds?.title); // why is olds optional? because read can be called before the resource exists (sync)
7882
let page = 1;
7983
while (true) {
8084
// todo: abstract pagination

0 commit comments

Comments
 (0)