1import { describe, expect, test } from "alchemy-test";
2
3import {
4 fromAttributeValue,
5 isMapSchemaType,
6 isNumberSetSchema,
7 isStringSetSchema,
8 toAttributeType,
9 toAttributeValue,
10} from "@/AWS/DynamoDB/AttributeValue";
11import type { AttributeValue } from "@distilled.cloud/aws/dynamodb";
12import * as Effect from "effect/Effect";
13import * as S from "effect/Schema";
14
15describe(
16 "toAttributeValue",
17 { tags: ["unit", "provider:aws", "provider:aws:dynamodb", "local"] },
18 () => {
19 test("undefined -> NULL false", async () => {
20 const result = await Effect.runPromise(toAttributeValue(undefined));
21 expect(result).toEqual({ NULL: false });
22 });
23
24 test("null -> NULL true", async () => {
25 const result = await Effect.runPromise(toAttributeValue(null));
26 expect(result).toEqual({ NULL: true });
27 });
28
29 test("boolean true -> BOOL", async () => {
30 const result = await Effect.runPromise(toAttributeValue(true));
31 expect(result).toEqual({ BOOL: true });
32 });
33
34 test("boolean false -> BOOL", async () => {
35 const result = await Effect.runPromise(toAttributeValue(false));
36 expect(result).toEqual({ BOOL: false });
37 });
38
39 test("string -> S", async () => {
40 const result = await Effect.runPromise(toAttributeValue("hello"));
41 expect(result).toEqual({ S: "hello" });
42 });
43
44 test("number -> N", async () => {
45 const result = await Effect.runPromise(toAttributeValue(42));
46 expect(result).toEqual({ N: "42" });
47 });
48
49 test("float number -> N", async () => {
50 const result = await Effect.runPromise(toAttributeValue(3.14));
51 expect(result).toEqual({ N: "3.14" });
52 });
53
54 test("array -> L", async () => {
55 const result = await Effect.runPromise(toAttributeValue([1, 2, 3]));
56 expect(result).toEqual({
57 L: [{ N: "1" }, { N: "2" }, { N: "3" }],
58 });
59 });
60
61 test("mixed array -> L", async () => {
62 const result = await Effect.runPromise(
63 toAttributeValue(["hello", 42, true]),
64 );
65 expect(result).toEqual({
66 L: [{ S: "hello" }, { N: "42" }, { BOOL: true }],
67 });
68 });
69
70 test("empty array -> L", async () => {
71 const result = await Effect.runPromise(toAttributeValue([]));
72 expect(result).toEqual({ L: [] });
73 });
74
75 test("object -> M", async () => {
76 const result = await Effect.runPromise(
77 toAttributeValue({ name: "Alice", age: 30 }),
78 );
79 expect(result).toEqual({
80 M: {
81 name: { S: "Alice" },
82 age: { N: "30" },
83 },
84 });
85 });
86
87 test("nested object -> M", async () => {
88 const result = await Effect.runPromise(
89 toAttributeValue({
90 user: {
91 name: "Alice",
92 details: {
93 age: 30,
94 },
95 },
96 }),
97 );
98 expect(result).toEqual({
99 M: {
100 user: {
101 M: {
102 name: { S: "Alice" },
103 details: {
104 M: {
105 age: { N: "30" },
106 },
107 },
108 },
109 },
110 },
111 });
112 });
113
114 test("empty object -> M", async () => {
115 const result = await Effect.runPromise(toAttributeValue({}));
116 expect(result).toEqual({ M: {} });
117 });
118
119 test("Set of strings -> SS", async () => {
120 const result = await Effect.runPromise(
121 toAttributeValue(new Set(["a", "b", "c"])),
122 );
123 expect(result).toEqual({ SS: ["a", "b", "c"] });
124 });
125
126 test("Set of numbers -> NS", async () => {
127 const result = await Effect.runPromise(
128 toAttributeValue(new Set([1, 2, 3])),
129 );
130 // NS values are returned as-is (numbers in this case)
131 expect(result.NS).toEqual(["1", "2", "3"]);
132 });
133
134 test("empty Set -> SS (empty)", async () => {
135 const result = await Effect.runPromise(toAttributeValue(new Set()));
136 expect(result).toEqual({ SS: [] });
137 });
138
139 test("Set of mixed types -> L", async () => {
140 const result = await Effect.runPromise(
141 toAttributeValue(new Set([1, "two", true])),
142 );
143 expect(result).toEqual({
144 L: [{ N: "1" }, { S: "two" }, { BOOL: true }],
145 });
146 });
147
148 test("Uint8Array -> B", async () => {
149 const uint8 = new Uint8Array([1, 2, 3, 4]);
150 const result = await Effect.runPromise(toAttributeValue(uint8));
151 expect(result).toEqual({ B: uint8 });
152 });
153
154 test("Buffer -> B", async () => {
155 const buffer = Buffer.from([1, 2, 3, 4]);
156 const result = await Effect.runPromise(toAttributeValue(buffer));
157 expect(result.B).toBeInstanceOf(Uint8Array);
158 });
159
160 test("File -> B", async () => {
161 const file = new File(["hello"], "test.txt");
162 const result = await Effect.runPromise(toAttributeValue(file));
163 expect(result.B).toBeInstanceOf(Uint8Array);
164 });
165
166 test("ArrayBuffer -> B", async () => {
167 const arrayBuffer = new ArrayBuffer(4);
168 const result = await Effect.runPromise(toAttributeValue(arrayBuffer));
169 expect(result.B).toBeInstanceOf(Uint8Array);
170 });
171
172 test("complex nested structure", async () => {
173 const result = await Effect.runPromise(
174 toAttributeValue({
175 id: "123",
176 name: "Alice",
177 age: 30,
178 active: true,
179 tags: ["developer", "typescript"],
180 metadata: {
181 created: "2023-01-01",
182 count: 42,
183 },
184 scores: new Set([100, 95, 88]),
185 }),
186 );
187 expect(result).toEqual({
188 M: {
189 id: { S: "123" },
190 name: { S: "Alice" },
191 age: { N: "30" },
192 active: { BOOL: true },
193 tags: {
194 L: [{ S: "developer" }, { S: "typescript" }],
195 },
196 metadata: {
197 M: {
198 created: { S: "2023-01-01" },
199 count: { N: "42" },
200 },
201 },
202 scores: { NS: expect.any(Array) },
203 },
204 });
205 });
206
207 test("invalid type throws error", async () => {
208 const symbol = Symbol("test");
209 const result = Effect.runPromise(
210 toAttributeValue(symbol).pipe(
211 Effect.map(() => false),
212 Effect.catchTag("InvalidAttributeValue", (_error) =>
213 Effect.succeed(true),
214 ),
215 ),
216 );
217 await expect(result).resolves.toBe(true);
218 });
219 },
220);
221
222describe(
223 "fromAttributeValue",
224 { tags: ["unit", "provider:aws", "provider:aws:dynamodb", "local"] },
225 () => {
226 test("NULL true -> null", () => {
227 expect(fromAttributeValue({ NULL: true })).toBe(null);
228 });
229
230 test("BOOL -> boolean", () => {
231 expect(fromAttributeValue({ BOOL: true })).toBe(true);
232 expect(fromAttributeValue({ BOOL: false })).toBe(false);
233 });
234
235 test("S -> string", () => {
236 expect(fromAttributeValue({ S: "hello" })).toBe("hello");
237 });
238
239 test("N -> number", () => {
240 expect(fromAttributeValue({ N: "42" })).toBe(42);
241 expect(fromAttributeValue({ N: "3.14" })).toBe(3.14);
242 });
243
244 test("L -> array", () => {
245 const result = fromAttributeValue({
246 L: [{ N: "1" }, { N: "2" }, { N: "3" }],
247 });
248 expect(result).toEqual([1, 2, 3]);
249 });
250
251 test("M -> object", () => {
252 const result = fromAttributeValue({
253 M: {
254 name: { S: "Alice" },
255 age: { N: "30" },
256 },
257 });
258 expect(result).toEqual({ name: "Alice", age: 30 });
259 });
260
261 test("SS -> Set of strings", () => {
262 const result = fromAttributeValue({ SS: ["a", "b", "c"] });
263 expect(result).toEqual(new Set(["a", "b", "c"]));
264 });
265
266 test("NS -> Set of numbers", () => {
267 const result = fromAttributeValue({ NS: ["1", "2", "3"] });
268 expect(result).toEqual(new Set(["1", "2", "3"]));
269 });
270
271 test("BS -> Set of binary", () => {
272 const binary = [new Uint8Array([1, 2]), new Uint8Array([3, 4])];
273 const result = fromAttributeValue({ BS: binary });
274 expect(result).toEqual(new Set(binary));
275 });
276
277 test("nested structure", () => {
278 const result = fromAttributeValue({
279 M: {
280 id: { S: "123" },
281 name: { S: "Alice" },
282 tags: {
283 L: [{ S: "developer" }, { S: "typescript" }],
284 },
285 metadata: {
286 M: {
287 count: { N: "42" },
288 },
289 },
290 },
291 });
292 expect(result).toEqual({
293 id: "123",
294 name: "Alice",
295 tags: ["developer", "typescript"],
296 metadata: {
297 count: 42,
298 },
299 });
300 });
301
302 test("unknown attribute value throws error", () => {
303 expect(() => fromAttributeValue({} as AttributeValue)).toThrow();
304 });
305 },
306);
307
308describe(
309 "toAttributeType",
310 { tags: ["unit", "provider:aws", "provider:aws:dynamodb", "local"] },
311 () => {
312 test("String schema -> S", () => {
313 expect(toAttributeType(S.String)).toBe("S");
314 });
315
316 test("Number schema -> N", () => {
317 expect(toAttributeType(S.Number)).toBe("N");
318 });
319
320 test("Struct schema -> M", () => {
321 expect(
322 toAttributeType(
323 S.Struct({
324 name: S.String,
325 age: S.Number,
326 }),
327 ),
328 ).toBe("M");
329 });
330
331 test("Record schema -> M", () => {
332 expect(toAttributeType(S.Record(S.String, S.String))).toBe("M");
333 });
334
335 test("Map schema -> M", () => {
336 expect(toAttributeType(S.ReadonlyMap(S.String, S.String))).toBe("M");
337 });
338
339 test("Array/List schema -> L", () => {
340 expect(toAttributeType(S.Array(S.String))).toBe("L");
341 });
342
343 test("Set of strings -> SS", () => {
344 expect(toAttributeType(S.ReadonlySet(S.String))).toBe("SS");
345 });
346
347 test("Set of numbers -> NS", () => {
348 expect(toAttributeType(S.ReadonlySet(S.Number))).toBe("NS");
349 });
350
351 test("Class schema -> M", () => {
352 class User extends S.Class<User>("User")({
353 name: S.String,
354 age: S.Number,
355 }) {}
356 expect(toAttributeType(User)).toBe("M");
357 });
358
359 test("default fallback -> S", () => {
360 expect(toAttributeType(S.Boolean)).toBe("S");
361 });
362 },
363);
364
365describe(
366 "isMapSchemaType",
367 { tags: ["unit", "provider:aws", "provider:aws:dynamodb", "local"] },
368 () => {
369 test("Map schema", () => {
370 expect(isMapSchemaType(S.ReadonlyMap(S.String, S.String))).toBe(true);
371 });
372
373 test("Record schema", () => {
374 expect(isMapSchemaType(S.Record(S.String, S.String))).toBe(true);
375 });
376
377 test("Struct schema", () => {
378 expect(
379 isMapSchemaType(
380 S.Struct({
381 name: S.String,
382 age: S.Number,
383 }),
384 ),
385 ).toBe(true);
386 });
387
388 test("Class schema", () => {
389 class User extends S.Class<User>("User")({
390 name: S.String,
391 age: S.Number,
392 }) {}
393 expect(isMapSchemaType(User)).toBe(true);
394 });
395
396 test("not a map schema", () => {
397 expect(isMapSchemaType(S.String)).toBe(false);
398 expect(isMapSchemaType(S.Number)).toBe(false);
399 expect(isMapSchemaType(S.Array(S.String))).toBe(false);
400 });
401 },
402);
403
404describe(
405 "isStringSetSchema",
406 { tags: ["unit", "provider:aws", "provider:aws:dynamodb", "local"] },
407 () => {
408 test("Set of strings", () => {
409 expect(isStringSetSchema(S.ReadonlySet(S.String))).toBe(true);
410 });
411
412 test("not a string set", () => {
413 expect(isStringSetSchema(S.ReadonlySet(S.Number))).toBe(false);
414 expect(isStringSetSchema(S.String)).toBe(false);
415 expect(isStringSetSchema(S.Array(S.String))).toBe(false);
416 });
417 },
418);
419
420describe(
421 "isNumberSetSchema",
422 { tags: ["unit", "provider:aws", "provider:aws:dynamodb", "local"] },
423 () => {
424 test("Set of numbers", () => {
425 expect(isNumberSetSchema(S.ReadonlySet(S.Number))).toBe(true);
426 });
427
428 test("not a number set", () => {
429 expect(isNumberSetSchema(S.ReadonlySet(S.String))).toBe(false);
430 expect(isNumberSetSchema(S.Number)).toBe(false);
431 expect(isNumberSetSchema(S.Array(S.Number))).toBe(false);
432 });
433 },
434);
435
436describe(
437 "round-trip conversions",
438 { tags: ["unit", "provider:aws", "provider:aws:dynamodb", "local"] },
439 () => {
440 test("string", async () => {
441 const original = "hello";
442 const attr = await Effect.runPromise(toAttributeValue(original));
443 const result = fromAttributeValue(attr);
444 expect(result).toBe(original);
445 });
446
447 test("number", async () => {
448 const original = 42;
449 const attr = await Effect.runPromise(toAttributeValue(original));
450 const result = fromAttributeValue(attr);
451 expect(result).toBe(original);
452 });
453
454 test("boolean", async () => {
455 const original = true;
456 const attr = await Effect.runPromise(toAttributeValue(original));
457 const result = fromAttributeValue(attr);
458 expect(result).toBe(original);
459 });
460
461 test("null", async () => {
462 const original = null;
463 const attr = await Effect.runPromise(toAttributeValue(original));
464 const result = fromAttributeValue(attr);
465 expect(result).toBe(original);
466 });
467
468 test("array", async () => {
469 const original = [1, "two", true];
470 const attr = await Effect.runPromise(toAttributeValue(original));
471 const result = fromAttributeValue(attr);
472 expect(result).toEqual(original);
473 });
474
475 test("object", async () => {
476 const original = { name: "Alice", age: 30, active: true };
477 const attr = await Effect.runPromise(toAttributeValue(original));
478 const result = fromAttributeValue(attr);
479 expect(result).toEqual(original);
480 });
481
482 test("Set of strings", async () => {
483 const original = new Set(["a", "b", "c"]);
484 const attr = await Effect.runPromise(toAttributeValue(original));
485 const result = fromAttributeValue(attr);
486 expect(result).toEqual(original);
487 });
488
489 test("complex nested structure", async () => {
490 const original = {
491 id: "123",
492 name: "Alice",
493 age: 30,
494 active: true,
495 tags: ["developer", "typescript"],
496 metadata: {
497 created: "2023-01-01",
498 count: 42,
499 },
500 };
501 const attr = await Effect.runPromise(toAttributeValue(original));
502 const result = fromAttributeValue(attr);
503 expect(result).toEqual(original);
504 });
505 },
506);