Skip to content

Commit 877ba8f

Browse files
authored
feat(aws): DynamoDB Table and getItem (#10)
1 parent 082bdfa commit 877ba8f

34 files changed

+2163
-77
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"typescript.experimental.useTsgo": false,
33
"typescript.tsdk": "node_modules/typescript/lib",
4+
"typescript.enablePromptUseWorkspaceTsdk": true,
45
"oxc.fmt.experimental": true,
56
"oxc.enable": true,
67
"editor.defaultFormatter": "oxc.oxc-vscode",

alchemy-effect/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
"import": "./lib/aws/index.js",
3737
"types": "./lib/aws/index.d.ts"
3838
},
39+
"./aws/dynamodb": {
40+
"bun": "./src/aws/dynamodb/index.ts",
41+
"import": "./lib/aws/dynamodb/index.js",
42+
"types": "./lib/aws/dynamodb/index.d.ts"
43+
},
3944
"./aws/lambda": {
4045
"bun": "./src/aws/lambda/index.ts",
4146
"import": "./lib/aws/lambda/index.js",
@@ -92,6 +97,7 @@
9297
"effect": "catalog:",
9398
"ink": "^6.3.1",
9499
"react": "^19.2.0",
100+
"react-devtools-core": "^7.0.1",
95101
"tsdown": "^0.15.4"
96102
},
97103
"peerDependencies": {

alchemy-effect/src/app.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ import * as Data from "effect/Data";
33
import * as Effect from "effect/Effect";
44
import * as Layer from "effect/Layer";
55

6-
export class App extends Context.Tag("App")<
7-
App,
8-
{
9-
name: string;
10-
stage: string;
11-
}
12-
>() {}
6+
export interface AppProps {
7+
name: string;
8+
stage: string;
9+
}
10+
11+
export class App extends Context.Tag("App")<App, AppProps>() {}
1312

1413
export class FailedToParseArg extends Data.TaggedError("FailedToParseArg")<{
1514
message: string;

alchemy-effect/src/aws/account.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ export class FailedToGetAccount extends Data.TaggedError(
1111
cause: Error;
1212
}> {}
1313

14-
export class AccountID extends Context.Tag("AWS::AccountID")<
15-
AccountID,
16-
string
14+
export type AccountID = string;
15+
16+
export class Account extends Context.Tag("AWS::AccountID")<
17+
Account,
18+
AccountID
1719
>() {}
1820

1921
export const fromIdentity = () =>
2022
Layer.effect(
21-
AccountID,
23+
Account,
2224
Effect.gen(function* () {
2325
const sts = yield* STS.STSClient;
2426
const identity = yield* sts.getCallerIdentity({}).pipe(

alchemy-effect/src/aws/arn.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
1-
import type * as Context from "effect/Context";
2-
3-
export interface Arn<Self> {
4-
arn: Self;
5-
}
6-
7-
export type Tag<Self, A extends string> = Context.Tag<Arn<Self>, A>;
1+
export type Arn = string;
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import * as Data from "effect/Data";
2+
import * as Effect from "effect/Effect";
3+
import * as S from "effect/Schema";
4+
import type { AttributeValue, ScalarAttributeType } from "itty-aws/dynamodb";
5+
import {
6+
getSetValueAST,
7+
isClassSchema,
8+
isListSchema,
9+
isMapSchema,
10+
isNumberSchema,
11+
isRecordLikeSchema,
12+
isRecordSchema,
13+
isSetSchema,
14+
isStringSchema,
15+
isStructSchema,
16+
} from "../../schema.ts";
17+
18+
// this seems important for handling S.Struct.Fields https://effect.website/docs/schema/classes/#recursive-types-with-different-encoded-and-type
19+
// interface CategoryEncoded extends Schema.Struct.Encoded<typeof fields> { .. }
20+
21+
export class InvalidAttributeValue extends Data.TaggedError(
22+
"InvalidAttributeValue",
23+
)<{
24+
message: string;
25+
value: any;
26+
}> {}
27+
28+
export const toAttributeValue: (
29+
value: any,
30+
) => Effect.Effect<AttributeValue, InvalidAttributeValue, never> = Effect.fn(
31+
function* (value: any) {
32+
if (value === undefined) {
33+
return {
34+
NULL: false,
35+
};
36+
} else if (value === null) {
37+
return {
38+
NULL: true,
39+
};
40+
} else if (typeof value === "boolean") {
41+
return {
42+
BOOL: value,
43+
};
44+
} else if (typeof value === "string") {
45+
return {
46+
S: value,
47+
};
48+
} else if (typeof value === "number") {
49+
return {
50+
N: value.toString(10),
51+
};
52+
} else if (Array.isArray(value)) {
53+
return {
54+
L: yield* Effect.all(value.map(toAttributeValue)),
55+
};
56+
} else if (value instanceof Set) {
57+
const setType = getType(value);
58+
if (setType === "EMPTY_SET") {
59+
return {
60+
SS: [],
61+
};
62+
} else if (Array.isArray(setType)) {
63+
return {
64+
L: yield* Effect.all(setType.map(toAttributeValue)),
65+
};
66+
} else if (setType === "SS") {
67+
return {
68+
SS: Array.from(value.values()),
69+
};
70+
} else if (setType === "NS") {
71+
return {
72+
NS: Array.from(value.values()).map((value) => value.toString(10)),
73+
};
74+
} else if (setType === "BS") {
75+
return {
76+
BS: Array.from(value.values()),
77+
};
78+
} else {
79+
return {
80+
L: yield* Effect.all(
81+
Array.from(value.values()).map(toAttributeValue),
82+
),
83+
};
84+
}
85+
} else if (Buffer.isBuffer(value)) {
86+
return {
87+
B: new Uint8Array(value),
88+
};
89+
} else if (value instanceof File) {
90+
return {
91+
B: new Uint8Array(yield* Effect.promise(() => value.arrayBuffer())),
92+
};
93+
} else if (value instanceof Uint8Array) {
94+
return {
95+
B: value,
96+
};
97+
} else if (value instanceof ArrayBuffer) {
98+
return {
99+
B: new Uint8Array(value),
100+
};
101+
} else if (typeof value === "object") {
102+
return {
103+
M: Object.fromEntries(
104+
yield* Effect.all(
105+
Object.entries(value).map(([key, value]) =>
106+
toAttributeValue(value).pipe(Effect.map((value) => [key, value])),
107+
),
108+
),
109+
),
110+
};
111+
}
112+
113+
return yield* Effect.fail(
114+
new InvalidAttributeValue({
115+
message: `Unknown value type: ${typeof value}`,
116+
value,
117+
}),
118+
);
119+
},
120+
);
121+
122+
export const fromAttributeValue = (value: AttributeValue): any => {
123+
if (value.NULL) {
124+
return null;
125+
} else if (typeof value.BOOL === "boolean") {
126+
return value.BOOL;
127+
} else if (value.L) {
128+
return value.L.map(fromAttributeValue);
129+
} else if (value.M) {
130+
return Object.fromEntries(
131+
Object.entries(value.M).map(([key, value]) => [
132+
key,
133+
fromAttributeValue(value),
134+
]),
135+
);
136+
} else if (value.N) {
137+
return parseFloat(value.N);
138+
} else if (value.S) {
139+
// how do we know if this is a date?
140+
return value.S;
141+
} else if (value.SS) {
142+
return new Set(value.SS);
143+
} else if (value.NS) {
144+
return new Set(value.NS);
145+
} else if (value.BS) {
146+
return new Set(value.BS);
147+
} else {
148+
throw new Error(`Unknown attribute value: ${JSON.stringify(value)}`);
149+
}
150+
};
151+
152+
type ValueType =
153+
| "L"
154+
| "BOOL"
155+
| "EMPTY_SET"
156+
| "M"
157+
| "NULL"
158+
| "N"
159+
| "M"
160+
| "S"
161+
| "SS"
162+
| "BS"
163+
| "NS"
164+
| "undefined";
165+
166+
const getType = (value: any): ValueType | ValueType[] => {
167+
if (value === undefined) {
168+
return "undefined";
169+
} else if (value === null) {
170+
return "NULL";
171+
} else if (typeof value === "boolean") {
172+
return "BOOL";
173+
} else if (typeof value === "string") {
174+
return "S";
175+
} else if (typeof value === "number") {
176+
return "N";
177+
} else if (Array.isArray(value)) {
178+
return "L";
179+
} else if (value instanceof Set) {
180+
return value.size === 0
181+
? "EMPTY_SET"
182+
: (() => {
183+
const types = Array.from(value.values())
184+
.flatMap(getType)
185+
.filter((type, i, arr) => arr.indexOf(type) === i);
186+
187+
return types.length === 1
188+
? types[0] === "S"
189+
? "SS"
190+
: types[0] === "N"
191+
? "NS"
192+
: types[0] === "BOOL"
193+
? "BS"
194+
: types[0]
195+
: "L";
196+
})();
197+
} else if (value instanceof Map) {
198+
return "M";
199+
} else if (typeof value === "object") {
200+
return "M";
201+
} else {
202+
throw new Error(`Unknown value type: ${typeof value}`);
203+
}
204+
};
205+
206+
export const isScalarAttributeType = (
207+
type: string,
208+
): type is ScalarAttributeType => {
209+
return type === "S" || type === "N" || type === "B";
210+
};
211+
212+
export const toAttributeType = (schema: S.Schema<any>) => {
213+
if (isStringSchema(schema)) {
214+
return "S";
215+
} else if (isNumberSchema(schema)) {
216+
return "N";
217+
} else if (isRecordLikeSchema(schema)) {
218+
return "M";
219+
} else if (isStringSetSchema(schema)) {
220+
return "SS";
221+
} else if (isNumberSetSchema(schema)) {
222+
return "NS";
223+
} else if (isListSchema(schema)) {
224+
return "L";
225+
}
226+
return "S";
227+
};
228+
229+
export const isMapSchemaType = (schema: S.Schema<any>) =>
230+
isMapSchema(schema) ||
231+
isRecordSchema(schema) ||
232+
isStructSchema(schema) ||
233+
isClassSchema(schema) ||
234+
false;
235+
236+
export const isStringSetSchema = (schema: S.Schema<any>) =>
237+
isSetSchema(schema) && isStringSchema(getSetValueAST(schema));
238+
239+
export const isNumberSetSchema = (schema: S.Schema<any>) =>
240+
isSetSchema(schema) && isNumberSchema(getSetValueAST(schema));
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as Context from "effect/Context";
2+
import * as Layer from "effect/Layer";
3+
4+
import { DynamoDB } from "itty-aws/dynamodb";
5+
import { createAWSServiceClientLayer } from "../client.ts";
6+
import * as Credentials from "../credentials.ts";
7+
import * as Region from "../region.ts";
8+
9+
export class DynamoDBClient extends Context.Tag("AWS::DynamoDB::Client")<
10+
DynamoDBClient,
11+
DynamoDB
12+
>() {}
13+
14+
export const client = createAWSServiceClientLayer<
15+
typeof DynamoDBClient,
16+
DynamoDB
17+
>(DynamoDBClient, DynamoDB);
18+
19+
export const clientFromEnv = () =>
20+
Layer.provide(client(), Layer.merge(Credentials.fromEnv(), Region.fromEnv()));

0 commit comments

Comments
 (0)