@aurios/mizzle 1.1.2 → 1.1.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.
Files changed (116) hide show
  1. package/.turbo/turbo-build.log +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -57
  4. package/dist/chunk-AQVECMXP.js +1 -0
  5. package/dist/chunk-DU7UPWBW.js +1 -0
  6. package/dist/chunk-GPYZK4WY.js +1 -0
  7. package/dist/chunk-NPPZW6VT.js +1 -0
  8. package/dist/chunk-TOYV2M4M.js +1 -0
  9. package/dist/chunk-UM3YF5EC.js +1 -0
  10. package/dist/columns.d.ts +1 -0
  11. package/dist/columns.js +1 -0
  12. package/dist/db-zHIHBm1E.d.ts +815 -0
  13. package/dist/db.d.ts +3 -0
  14. package/dist/db.js +1 -0
  15. package/dist/diff.d.ts +18 -0
  16. package/dist/diff.js +1 -0
  17. package/dist/index.d.ts +42 -0
  18. package/dist/index.js +1 -0
  19. package/dist/introspection.d.ts +7 -0
  20. package/dist/introspection.js +1 -0
  21. package/dist/operators-BVreW0ky.d.ts +719 -0
  22. package/dist/snapshot.d.ts +24 -0
  23. package/dist/snapshot.js +1 -0
  24. package/dist/table.d.ts +1 -0
  25. package/dist/table.js +1 -0
  26. package/dist/transaction-RE7LXTGV.js +1 -0
  27. package/package.json +82 -66
  28. package/src/builders/base.ts +53 -56
  29. package/src/builders/batch-get.ts +63 -58
  30. package/src/builders/batch-write.ts +81 -78
  31. package/src/builders/delete.ts +46 -53
  32. package/src/builders/insert.ts +158 -150
  33. package/src/builders/query-promise.ts +26 -35
  34. package/src/builders/relational-builder.ts +214 -191
  35. package/src/builders/select.ts +250 -237
  36. package/src/builders/transaction.ts +170 -152
  37. package/src/builders/update.ts +197 -192
  38. package/src/columns/binary-set.ts +29 -38
  39. package/src/columns/binary.ts +25 -35
  40. package/src/columns/boolean.ts +25 -30
  41. package/src/columns/date.ts +57 -64
  42. package/src/columns/index.ts +15 -15
  43. package/src/columns/json.ts +39 -48
  44. package/src/columns/list.ts +26 -36
  45. package/src/columns/map.ts +26 -34
  46. package/src/columns/number-set.ts +29 -38
  47. package/src/columns/number.ts +33 -40
  48. package/src/columns/string-set.ts +38 -47
  49. package/src/columns/string.ts +37 -49
  50. package/src/columns/uuid.ts +26 -33
  51. package/src/core/client.ts +9 -9
  52. package/src/core/column-builder.ts +194 -220
  53. package/src/core/column.ts +127 -135
  54. package/src/core/diff.ts +40 -34
  55. package/src/core/errors.ts +20 -17
  56. package/src/core/introspection.ts +62 -58
  57. package/src/core/operations.ts +17 -23
  58. package/src/core/parser.ts +82 -89
  59. package/src/core/relations.ts +164 -154
  60. package/src/core/retry.ts +52 -52
  61. package/src/core/snapshot.ts +131 -130
  62. package/src/core/strategies.ts +222 -218
  63. package/src/core/table.ts +189 -202
  64. package/src/core/validation.ts +52 -52
  65. package/src/db.ts +211 -209
  66. package/src/expressions/actions.ts +26 -26
  67. package/src/expressions/builder.ts +62 -54
  68. package/src/expressions/operators.ts +48 -48
  69. package/src/expressions/update-builder.ts +78 -76
  70. package/src/index.ts +1 -1
  71. package/src/indexes.ts +8 -8
  72. package/test/batch-resilience.test.ts +138 -0
  73. package/test/builders/delete.test.ts +100 -0
  74. package/test/builders/insert.test.ts +216 -0
  75. package/test/builders/relational-types.test.ts +55 -0
  76. package/test/builders/relational.integration.test.ts +291 -0
  77. package/test/builders/relational.test.ts +66 -0
  78. package/test/builders/select.test.ts +411 -0
  79. package/test/builders/transaction-errors.test.ts +46 -0
  80. package/test/builders/transaction-execution.test.ts +99 -0
  81. package/test/builders/transaction-proxy.test.ts +41 -0
  82. package/test/builders/update-expression.test.ts +106 -0
  83. package/test/builders/update.test.ts +179 -0
  84. package/test/core/diff.test.ts +152 -0
  85. package/test/core/expressions.test.ts +64 -0
  86. package/test/core/introspection.test.ts +47 -0
  87. package/test/core/parser.test.ts +69 -0
  88. package/test/core/snapshot-gen.test.ts +155 -0
  89. package/test/core/snapshot.test.ts +52 -0
  90. package/test/date-column.test.ts +159 -0
  91. package/test/fluent-writes.integration.test.ts +148 -0
  92. package/test/integration-retry.test.ts +77 -0
  93. package/test/integration.test.ts +105 -0
  94. package/test/item-size-error.test.ts +16 -0
  95. package/test/item-size-validation.test.ts +82 -0
  96. package/test/item-size.test.ts +47 -0
  97. package/test/iterator-pagination.integration.test.ts +132 -0
  98. package/test/jsdoc-builders.test.ts +55 -0
  99. package/test/jsdoc-schema.test.ts +107 -0
  100. package/test/json-column.test.ts +51 -0
  101. package/test/metadata.test.ts +54 -0
  102. package/test/mizzle-package.test.ts +20 -0
  103. package/test/relational-centralized.test.ts +83 -0
  104. package/test/relational-definition.test.ts +75 -0
  105. package/test/relational-init.test.ts +30 -0
  106. package/test/relational-proxy.test.ts +52 -0
  107. package/test/relations.test.ts +45 -0
  108. package/test/resilience-config.test.ts +34 -0
  109. package/test/retry-handler.test.ts +63 -0
  110. package/test/transaction.integration.test.ts +153 -0
  111. package/test/unified-select.integration.test.ts +153 -0
  112. package/test/unified-update.integration.test.ts +139 -0
  113. package/test/update.integration.test.ts +132 -0
  114. package/tsconfig.json +12 -8
  115. package/tsup.config.ts +11 -11
  116. package/vitest.config.ts +8 -0
@@ -0,0 +1,155 @@
1
+ import { expect, test, describe, beforeEach, afterEach } from "vitest";
2
+ import {
3
+ generateSnapshot,
4
+ getNextMigrationVersion,
5
+ saveSnapshot,
6
+ loadSnapshot,
7
+ type MizzleSnapshot,
8
+ } from "@aurios/mizzle/snapshot";
9
+ import { PhysicalTable } from "@aurios/mizzle/table";
10
+ import type { ColumnBuider } from "@aurios/mizzle/core/column-builder";
11
+ import { TABLE_SYMBOLS, ENTITY_SYMBOLS } from "@repo/shared";
12
+ import { mkdirSync, rmSync, writeFileSync } from "fs";
13
+ import { join } from "path";
14
+ import { tmpdir } from "os";
15
+
16
+ // Mock helpers
17
+ const mockColumn = (name: string, type: string) => ({
18
+ name,
19
+ getDynamoType: () => type,
20
+ _: { name, type },
21
+ });
22
+
23
+ const mockTable = (name: string, pkName: string, pkType: string) => {
24
+ const table = new PhysicalTable(name, {
25
+ pk: { build: () => mockColumn(pkName, pkType) } as unknown as ColumnBuider,
26
+ });
27
+ table[TABLE_SYMBOLS.TABLE_NAME] = name;
28
+ table[TABLE_SYMBOLS.PARTITION_KEY] = mockColumn(pkName, pkType);
29
+ return table;
30
+ };
31
+
32
+ const mockEntity = (table: PhysicalTable, columns: Record<string, string>) => {
33
+ const colBuilders = Object.fromEntries(
34
+ Object.entries(columns).map(([name, type]) => [name, mockColumn(name, type)]),
35
+ );
36
+ return {
37
+ [ENTITY_SYMBOLS.PHYSICAL_TABLE]: table,
38
+ [ENTITY_SYMBOLS.COLUMNS]: colBuilders,
39
+ } as any;
40
+ };
41
+
42
+ const TEMP_DIR = join(tmpdir(), "mizzle-snapshot-test-" + Date.now());
43
+
44
+ describe("Snapshot Persistence", () => {
45
+ beforeEach(() => {
46
+ mkdirSync(TEMP_DIR, { recursive: true });
47
+ });
48
+
49
+ afterEach(() => {
50
+ rmSync(TEMP_DIR, { recursive: true, force: true });
51
+ });
52
+
53
+ test("should save and load snapshot", async () => {
54
+ const snapshot: MizzleSnapshot = {
55
+ version: "1",
56
+ tables: {
57
+ test: {
58
+ TableName: "test",
59
+ AttributeDefinitions: [],
60
+ KeySchema: [],
61
+ },
62
+ },
63
+ };
64
+
65
+ await saveSnapshot(TEMP_DIR, snapshot);
66
+ const loaded = await loadSnapshot(TEMP_DIR);
67
+
68
+ expect(loaded).toEqual(snapshot);
69
+ });
70
+
71
+ test("should return null if snapshot does not exist", async () => {
72
+ const loaded = await loadSnapshot(TEMP_DIR);
73
+ expect(loaded).toBeNull();
74
+ });
75
+ });
76
+
77
+ describe("Snapshot Generation", () => {
78
+ test("should generate MizzleSnapshot from tables and entities", () => {
79
+ const table = mockTable("users", "id", "S");
80
+ const schema = { tables: [table], entities: [] };
81
+
82
+ const snapshot = generateSnapshot(schema);
83
+
84
+ expect(snapshot.version).toBe("1");
85
+ expect(snapshot.tables["users"]).toBeDefined();
86
+ expect(snapshot.tables["users"]!.TableName).toBe("users");
87
+ expect(snapshot.tables["users"]!.AttributeDefinitions).toEqual([
88
+ { AttributeName: "id", AttributeType: "S" },
89
+ ]);
90
+ });
91
+
92
+ test("should include indexes in snapshot", () => {
93
+ const table = mockTable("users", "id", "S");
94
+ table[TABLE_SYMBOLS.INDEXES] = {
95
+ byEmail: { type: "gsi", config: { pk: "email" } },
96
+ byDate: { type: "lsi", config: { sk: "date" } },
97
+ };
98
+
99
+ const entity = mockEntity(table, {
100
+ id: "S",
101
+ email: "S",
102
+ date: "N",
103
+ });
104
+
105
+ const snapshot = generateSnapshot({
106
+ tables: [table],
107
+ entities: [entity],
108
+ });
109
+ const userTable = snapshot.tables["users"]!;
110
+
111
+ expect(userTable.GlobalSecondaryIndexes).toHaveLength(1);
112
+ expect(userTable.GlobalSecondaryIndexes![0]!.IndexName).toBe("byEmail");
113
+ expect(userTable.LocalSecondaryIndexes).toHaveLength(1);
114
+ expect(userTable.LocalSecondaryIndexes![0]!.IndexName).toBe("byDate");
115
+
116
+ // Verify Attributes
117
+ const attrs = userTable.AttributeDefinitions;
118
+ expect(attrs).toContainEqual({
119
+ AttributeName: "email",
120
+ AttributeType: "S",
121
+ });
122
+ expect(attrs).toContainEqual({
123
+ AttributeName: "date",
124
+ AttributeType: "N",
125
+ });
126
+ });
127
+ });
128
+
129
+ describe("Migration Versioning", () => {
130
+ beforeEach(() => {
131
+ mkdirSync(TEMP_DIR, { recursive: true });
132
+ });
133
+
134
+ afterEach(() => {
135
+ rmSync(TEMP_DIR, { recursive: true, force: true });
136
+ });
137
+
138
+ test("should return '0000' if no migrations exist", async () => {
139
+ const version = await getNextMigrationVersion(TEMP_DIR);
140
+ expect(version).toBe("0000");
141
+ });
142
+
143
+ test("should return '0001' if '0000_init.ts' exists", async () => {
144
+ writeFileSync(join(TEMP_DIR, "0000_init.ts"), "");
145
+ const version = await getNextMigrationVersion(TEMP_DIR);
146
+ expect(version).toBe("0001");
147
+ });
148
+
149
+ test("should handle gaps and return next sequential", async () => {
150
+ writeFileSync(join(TEMP_DIR, "0000_init.ts"), "");
151
+ writeFileSync(join(TEMP_DIR, "0002_foo.ts"), ""); // Gap
152
+ const version = await getNextMigrationVersion(TEMP_DIR);
153
+ expect(version).toBe("0003"); // Should be max + 1
154
+ });
155
+ });
@@ -0,0 +1,52 @@
1
+ import { expect, test, describe, beforeEach, afterEach } from "vitest";
2
+ import { saveSnapshot, loadSnapshot, type MizzleSnapshot } from "@aurios/mizzle/snapshot";
3
+ import { mkdirSync, rmSync, existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { tmpdir } from "os";
6
+
7
+ const TEMP_DIR = join(tmpdir(), "mizzle-snapshot-test-" + Date.now());
8
+ const MIGRATIONS_DIR = join(TEMP_DIR, "migrations");
9
+
10
+ describe("Snapshot Storage", () => {
11
+ beforeEach(() => {
12
+ mkdirSync(MIGRATIONS_DIR, { recursive: true });
13
+ });
14
+
15
+ afterEach(() => {
16
+ rmSync(TEMP_DIR, { recursive: true, force: true });
17
+ });
18
+
19
+ test("should save and load a snapshot", async () => {
20
+ const snapshot: MizzleSnapshot = {
21
+ version: "1",
22
+ tables: {
23
+ users: {
24
+ TableName: "users",
25
+ AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
26
+ KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
27
+ },
28
+ },
29
+ };
30
+
31
+ const snapshotPath = join(MIGRATIONS_DIR, "snapshot.json");
32
+ await saveSnapshot(MIGRATIONS_DIR, snapshot);
33
+
34
+ expect(existsSync(snapshotPath)).toBe(true);
35
+
36
+ const loaded = await loadSnapshot(MIGRATIONS_DIR);
37
+ expect(loaded).toEqual(snapshot);
38
+ });
39
+
40
+ test("should create directory if it does not exist", async () => {
41
+ const newDir = join(TEMP_DIR, "new-migrations");
42
+ const snapshot: MizzleSnapshot = { version: "1", tables: {} };
43
+
44
+ await saveSnapshot(newDir, snapshot);
45
+ expect(existsSync(join(newDir, "snapshot.json"))).toBe(true);
46
+ });
47
+
48
+ test("should return null if no snapshot exists", async () => {
49
+ const loaded = await loadSnapshot(join(TEMP_DIR, "empty"));
50
+ expect(loaded).toBeNull();
51
+ });
52
+ });
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { date } from "@aurios/mizzle/columns";
3
+ import { dynamoTable, dynamoEntity } from "@aurios/mizzle/table";
4
+ import { InsertBase, UpdateBuilder, eq, buildExpression } from "@aurios/mizzle";
5
+ import type { IMizzleClient } from "@aurios/mizzle/core/client";
6
+
7
+ describe("date column", () => {
8
+ it("should define a date column", () => {
9
+ const col = date("created_at");
10
+ const c = col as unknown as { config: { name: string; dataType: string; columnType: string } };
11
+ expect(c.config.name).toBe("created_at");
12
+ expect(c.config.dataType).toBe("date");
13
+ expect(c.config.columnType).toBe("S");
14
+ });
15
+
16
+ const table = dynamoTable("test", {
17
+ pk: date("pk").partitionKey(),
18
+ });
19
+ const entity = dynamoEntity(table, "test", {
20
+ pk: date("pk"),
21
+ });
22
+ const col = entity.pk;
23
+
24
+ describe("serialization (mapToDynamoValue)", () => {
25
+ it("should serialize Date objects to ISO strings", () => {
26
+ const d = new Date("2023-01-01T10:00:00.000Z");
27
+ expect(col.mapToDynamoValue(d)).toBe("2023-01-01T10:00:00.000Z");
28
+ });
29
+
30
+ it("should pass through valid ISO strings", () => {
31
+ const iso = "2023-01-01T10:00:00.000Z";
32
+ expect(col.mapToDynamoValue(iso)).toBe(iso);
33
+ });
34
+
35
+ it("should convert numeric timestamps to ISO strings", () => {
36
+ const d = new Date("2023-01-01T10:00:00.000Z");
37
+ const ts = d.getTime();
38
+ expect(col.mapToDynamoValue(ts)).toBe("2023-01-01T10:00:00.000Z");
39
+ });
40
+
41
+ it("should throw error for invalid dates", () => {
42
+ expect(() => col.mapToDynamoValue("invalid-date")).toThrow();
43
+ expect(() => col.mapToDynamoValue(NaN)).toThrow();
44
+ });
45
+ });
46
+
47
+ describe("deserialization (mapFromDynamoValue)", () => {
48
+ it("should deserialize ISO strings to Date objects", () => {
49
+ const iso = "2023-01-01T10:00:00.000Z";
50
+ const result = col.mapFromDynamoValue(iso);
51
+ expect(result).toBeInstanceOf(Date);
52
+ expect((result as Date).toISOString()).toBe(iso);
53
+ });
54
+
55
+ it("should handle null/undefined gracefully (pass through)", () => {
56
+ expect(col.mapFromDynamoValue(null)).toBe(null);
57
+ expect(col.mapFromDynamoValue(undefined)).toBe(undefined);
58
+ });
59
+ });
60
+
61
+ describe("InsertBase integration", () => {
62
+ it("should serialize date in buildItem()", () => {
63
+ const mockClient = {} as unknown;
64
+ const d = new Date("2023-10-27T10:00:00.000Z");
65
+
66
+ const insert = new InsertBase(entity, mockClient as unknown as any, {
67
+ pk: d,
68
+ });
69
+
70
+ const item = (insert as unknown as any).buildItem();
71
+ expect(item.pk).toBe("2023-10-27T10:00:00.000Z");
72
+ });
73
+
74
+ it("should apply defaultNow()", () => {
75
+ const t = dynamoTable("test", { pk: date("pk").partitionKey() });
76
+ const e = dynamoEntity(t, "test", {
77
+ pk: date("pk"),
78
+ createdAt: date("createdAt").defaultNow(),
79
+ });
80
+ const insert = new InsertBase(e, {} as unknown as any, { pk: new Date() });
81
+ const item = (insert as unknown as { buildItem: () => Record<string, unknown> }).buildItem();
82
+ expect(item.createdAt).toBeDefined();
83
+ expect(typeof item.createdAt).toBe("string");
84
+ expect(new Date(item.createdAt as string).getTime()).toBeLessThanOrEqual(Date.now());
85
+ });
86
+ });
87
+
88
+ describe("UpdateBuilder integration", () => {
89
+ it("should serialize date in set()", () => {
90
+ const mockClient = {} as unknown;
91
+ const update = new UpdateBuilder(entity, mockClient as unknown as any);
92
+ const d = new Date("2023-10-27T10:00:00.000Z");
93
+
94
+ update.set({ pk: d });
95
+
96
+ expect(
97
+ (update as unknown as { _state: { set: Record<string, { value: unknown }> } })._state.set.pk
98
+ ?.value,
99
+ ).toBe("2023-10-27T10:00:00.000Z");
100
+ });
101
+
102
+ it("should apply onUpdateNow()", async () => {
103
+ const t = dynamoTable("test", { pk: date("pk").partitionKey() });
104
+ const e = dynamoEntity(t, "test", {
105
+ pk: date("pk"),
106
+ updatedAt: date("updatedAt").onUpdateNow(),
107
+ });
108
+ const mockClient = { send: vi.fn().mockResolvedValue({ Attributes: {} }) } as unknown;
109
+ const update = new UpdateBuilder(e, mockClient as unknown as any);
110
+ update.set({ pk: new Date() });
111
+
112
+ await (update as unknown as any).execute();
113
+
114
+ const state = (update as unknown as any)._state;
115
+ expect(state.set.updatedAt).toBeDefined();
116
+ expect(typeof state.set.updatedAt!.value).toBe("string");
117
+ });
118
+ });
119
+
120
+ describe("Integration & Sorting", () => {
121
+ it("should sort ISO strings chronologically", () => {
122
+ const d1 = new Date("2023-01-01T10:00:00.000Z").toISOString();
123
+ const d2 = new Date("2023-01-02T10:00:00.000Z").toISOString();
124
+ const d3 = new Date("2024-01-01T10:00:00.000Z").toISOString();
125
+
126
+ const dates = [d3, d1, d2];
127
+ dates.sort();
128
+
129
+ expect(dates).toEqual([d1, d2, d3]);
130
+ });
131
+
132
+ it("should handle reserved words as column names", () => {
133
+ const t = dynamoTable("test", { pk: date("date").partitionKey() });
134
+ const e = dynamoEntity(t, "test", {
135
+ pk: date("date"),
136
+ });
137
+
138
+ const expression = eq(e.pk, new Date("2023-10-27T10:00:00.000Z"));
139
+
140
+ const names: Record<string, string> = {};
141
+ const values: Record<string, unknown> = {};
142
+ const addName = (n: string) => {
143
+ const k = `#n${Object.keys(names).length}`;
144
+ names[k] = n;
145
+ return k;
146
+ };
147
+ const addValue = (v: unknown) => {
148
+ const k = `:v${Object.keys(values).length}`;
149
+ values[k] = v;
150
+ return k;
151
+ };
152
+
153
+ const result = buildExpression(expression, addName, addValue);
154
+
155
+ expect(result).toBe("#n0 = :v0");
156
+ expect(names["#n0"]).toBe("date");
157
+ });
158
+ });
159
+ });
@@ -0,0 +1,148 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { dynamoTable, dynamoEntity } from "@aurios/mizzle/table";
3
+ import { string, uuid, number, list } from "@aurios/mizzle/columns";
4
+ import { prefixKey, staticKey, add, append, remove, ifNotExists } from "@aurios/mizzle";
5
+ import { DynamoDBClient, CreateTableCommand, DeleteTableCommand, DescribeTableCommand } from "@aws-sdk/client-dynamodb";
6
+ import { mizzle } from "@aurios/mizzle/db";
7
+ import { eq } from "@aurios/mizzle/expressions/operators";
8
+
9
+ const client = new DynamoDBClient({
10
+ endpoint: "http://localhost:8000",
11
+ region: "us-east-1",
12
+ credentials: {
13
+ accessKeyId: "local",
14
+ secretAccessKey: "local",
15
+ },
16
+ });
17
+
18
+ describe("Fluent Writes Integration", () => {
19
+ const tableName = "FluentWritesTable";
20
+ const table = dynamoTable(tableName, {
21
+ pk: string("pk"),
22
+ sk: string("sk"),
23
+ });
24
+
25
+ const user = dynamoEntity(
26
+ table,
27
+ "User",
28
+ {
29
+ id: uuid(),
30
+ name: string(), // Reserved Word
31
+ status: string(), // Reserved Word
32
+ order: number(), // Reserved Word
33
+ loginCount: number(),
34
+ tags: list(),
35
+ bio: string(),
36
+ },
37
+ (cols) => ({
38
+ pk: prefixKey("USER#", cols.id),
39
+ sk: staticKey("METADATA"),
40
+ }),
41
+ );
42
+
43
+ beforeAll(async () => {
44
+ try {
45
+ await client.send(
46
+ new CreateTableCommand({
47
+ TableName: tableName,
48
+ KeySchema: [
49
+ { AttributeName: "pk", KeyType: "HASH" },
50
+ { AttributeName: "sk", KeyType: "RANGE" },
51
+ ],
52
+ AttributeDefinitions: [
53
+ { AttributeName: "pk", AttributeType: "S" },
54
+ { AttributeName: "sk", AttributeType: "S" },
55
+ ],
56
+ ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 },
57
+ }),
58
+ );
59
+
60
+ // Wait for table to be active
61
+ let active = false;
62
+ while (!active) {
63
+ const { Table } = await client.send(new DescribeTableCommand({ TableName: tableName }));
64
+ if (Table?.TableStatus === "ACTIVE") active = true;
65
+ else await new Promise((r) => setTimeout(r, 100));
66
+ }
67
+ } catch {
68
+ /* ignore */
69
+ }
70
+ });
71
+
72
+ afterAll(async () => {
73
+ try {
74
+ await client.send(new DeleteTableCommand({ TableName: tableName }));
75
+ } catch {
76
+ /* ignore */
77
+ }
78
+ });
79
+
80
+ it("should handle reserved words and multiple actions in one set()", async () => {
81
+ const db = mizzle(client);
82
+
83
+ // 1. Insert
84
+ const newUser = await db
85
+ .insert(user)
86
+ .values({
87
+ name: "Alice",
88
+ status: "pending",
89
+ order: 1,
90
+ loginCount: 0,
91
+ tags: ["initial"],
92
+ bio: "Old bio",
93
+ })
94
+ .returning()
95
+ .execute();
96
+
97
+ // 2. Multi-Action Update using .set() with helpers
98
+ // We test SET (name, status), ADD (loginCount), REMOVE (bio), SET list_append (tags)
99
+ const updated = await db
100
+ .update(user)
101
+ .set({
102
+ name: "Alice Smith",
103
+ status: "active",
104
+ order: add(1),
105
+ loginCount: add(5),
106
+ tags: append(["pro"]),
107
+ bio: remove(),
108
+ })
109
+ .where(eq(user.id, newUser.id))
110
+ .returning("ALL_NEW")
111
+ .execute();
112
+
113
+ const res = updated as Record<string, unknown>;
114
+ expect(res.name).toBe("Alice Smith");
115
+ expect(res.status).toBe("active");
116
+ expect(res.order).toBe(2);
117
+ expect(res.loginCount).toBe(5);
118
+ expect(res.tags).toEqual(["initial", "pro"]);
119
+ expect(res.bio).toBeUndefined();
120
+ });
121
+
122
+ it("should handle ifNotExists helper", async () => {
123
+ const db = mizzle(client);
124
+
125
+ const newUser = await db
126
+ .insert(user)
127
+ .values({
128
+ name: "Bob",
129
+ loginCount: 0,
130
+ })
131
+ .returning()
132
+ .execute();
133
+
134
+ // Use ifNotExists on existing field and non-existing field
135
+ await db
136
+ .update(user)
137
+ .set({
138
+ name: ifNotExists("ShouldNotChange"),
139
+ status: ifNotExists("initialized"),
140
+ })
141
+ .where(eq(user.id, newUser.id))
142
+ .execute();
143
+
144
+ const final = await db.select().from(user).where(eq(user.id, newUser.id));
145
+ expect(final[0]!.name).toBe("Bob"); // Stayed "Bob" because it existed
146
+ expect(final[0]!.status).toBe("initialized"); // Set because it was missing
147
+ });
148
+ });
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { mizzle } from "@aurios/mizzle";
3
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
4
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
5
+ import { ENTITY_SYMBOLS, TABLE_SYMBOLS } from "@repo/shared";
6
+
7
+ // Mock send method
8
+ const mockSend = vi.fn();
9
+ const mockDocClient = {
10
+ send: mockSend,
11
+ middlewareStack: {
12
+ add: vi.fn(),
13
+ remove: vi.fn(),
14
+ },
15
+ };
16
+
17
+ describe("Integration Retry", () => {
18
+ beforeEach(() => {
19
+ vi.spyOn(DynamoDBDocumentClient, "from").mockReturnValue(mockDocClient as any);
20
+ });
21
+
22
+ afterEach(() => {
23
+ vi.restoreAllMocks();
24
+ });
25
+ const mockTable = {
26
+ [ENTITY_SYMBOLS.PHYSICAL_TABLE]: {
27
+ [TABLE_SYMBOLS.TABLE_NAME]: "test-table",
28
+ [TABLE_SYMBOLS.PARTITION_KEY]: { name: "pk" },
29
+ [TABLE_SYMBOLS.SORT_KEY]: undefined,
30
+ [TABLE_SYMBOLS.INDEXES]: {},
31
+ },
32
+ [ENTITY_SYMBOLS.ENTITY_STRATEGY]: { pk: { type: "static", segments: ["test"] } },
33
+ } as any;
34
+
35
+ it("should retry operations via mizzle client", async () => {
36
+ const client = new DynamoDBClient({});
37
+
38
+ // Mock error
39
+ const error = new Error("ThrottlingException");
40
+ error.name = "ThrottlingException";
41
+
42
+ mockSend.mockReset();
43
+ mockSend.mockRejectedValueOnce(error).mockResolvedValue({ Items: [] }); // Success on 2nd try
44
+
45
+ const db = mizzle({
46
+ client: client,
47
+ retry: { maxAttempts: 3, baseDelay: 10 },
48
+ });
49
+
50
+ // Use select builder which calls client.send
51
+ // We use .executeScan() path if no keys provided, but resolveKeys is called first.
52
+ // resolveKeys might complain if strategies/values don't match.
53
+ // Let's force a scan or just rely on the fact that resolveKeys returns something.
54
+
55
+ await db.select().from(mockTable).execute();
56
+
57
+ expect(mockSend).toHaveBeenCalledTimes(2);
58
+ });
59
+
60
+ it("should throw after max attempts", async () => {
61
+ const client = new DynamoDBClient({});
62
+
63
+ const error = new Error("ThrottlingException");
64
+ error.name = "ThrottlingException";
65
+
66
+ mockSend.mockReset();
67
+ mockSend.mockRejectedValue(error);
68
+
69
+ const db = mizzle({
70
+ client: client,
71
+ retry: { maxAttempts: 3, baseDelay: 10 },
72
+ });
73
+
74
+ await expect(db.select().from(mockTable).execute()).rejects.toThrow("ThrottlingException");
75
+ expect(mockSend).toHaveBeenCalledTimes(3);
76
+ });
77
+ });
@@ -0,0 +1,105 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { dynamoTable, dynamoEntity } from "@aurios/mizzle/table";
3
+ import { string, uuid, number, boolean, list, map } from "@aurios/mizzle/columns";
4
+ import { prefixKey, staticKey } from "@aurios/mizzle";
5
+ import { DynamoDBClient, CreateTableCommand, DeleteTableCommand, DescribeTableCommand } from "@aws-sdk/client-dynamodb";
6
+ import { mizzle } from "@aurios/mizzle/db";
7
+ import { eq } from "@aurios/mizzle/expressions/operators";
8
+
9
+ const client = new DynamoDBClient({
10
+ endpoint: "http://localhost:8000",
11
+ region: "us-east-1",
12
+ credentials: {
13
+ accessKeyId: "local",
14
+ secretAccessKey: "local",
15
+ },
16
+ });
17
+
18
+ describe("End-to-End Integration", () => {
19
+ const tableName = "IntegrationTestTable";
20
+ const table = dynamoTable(tableName, {
21
+ pk: string("pk"),
22
+ sk: string("sk"),
23
+ });
24
+
25
+ const project = dynamoEntity(
26
+ table,
27
+ "Project",
28
+ {
29
+ id: uuid(),
30
+ name: string(),
31
+ description: string(),
32
+ stars: number(),
33
+ isPublic: boolean(),
34
+ tags: list(),
35
+ config: map(),
36
+ },
37
+ (cols) => ({
38
+ pk: prefixKey("PROJ#", cols.id),
39
+ sk: staticKey("INFO"),
40
+ }),
41
+ );
42
+
43
+ beforeAll(async () => {
44
+ try {
45
+ await client.send(
46
+ new CreateTableCommand({
47
+ TableName: tableName,
48
+ KeySchema: [
49
+ { AttributeName: "pk", KeyType: "HASH" },
50
+ { AttributeName: "sk", KeyType: "RANGE" },
51
+ ],
52
+ AttributeDefinitions: [
53
+ { AttributeName: "pk", AttributeType: "S" },
54
+ { AttributeName: "sk", AttributeType: "S" },
55
+ ],
56
+ ProvisionedThroughput: {
57
+ ReadCapacityUnits: 5,
58
+ WriteCapacityUnits: 5,
59
+ },
60
+ }),
61
+ );
62
+ } catch {
63
+ /* ignore */
64
+ }
65
+ });
66
+
67
+ afterAll(async () => {
68
+ try {
69
+ await client.send(new DeleteTableCommand({ TableName: tableName }));
70
+ } catch {
71
+ /* ignore */
72
+ }
73
+ });
74
+
75
+ it("should perform a full Create -> Read lifecycle", async () => {
76
+ const db = mizzle(client);
77
+
78
+ // 1. Insert
79
+ const newProject = {
80
+ name: "Mizzle",
81
+ description: "Lightweight DynamoDB ORM",
82
+ stars: 100,
83
+ isPublic: true,
84
+ tags: ["typescript", "orm"],
85
+ config: { version: "1.0.0" },
86
+ } as unknown as any; // Cast to unknown because 'id' is generated but usually required by type
87
+
88
+ const inserted = (await db.insert(project).values(newProject).returning().execute()) as any;
89
+
90
+ expect(inserted.id).toBeDefined();
91
+ expect(inserted.pk).toBe(`PROJ#${inserted.id}`);
92
+ expect(inserted.sk).toBe("INFO");
93
+
94
+ // 2. Select
95
+ const results = await db.select().from(project).where(eq(project.id, inserted.id));
96
+
97
+ expect(results).toHaveLength(1);
98
+ expect(results[0]).toMatchObject({
99
+ id: inserted.id,
100
+ name: "Mizzle",
101
+ stars: 100,
102
+ config: { version: "1.0.0" },
103
+ });
104
+ });
105
+ });