dynamo-query-engine 1.0.0 → 1.0.2

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.
@@ -0,0 +1,279 @@
1
+ # Tests
2
+
3
+ Unit-Tests für DynamoDB Query Engine mit Vitest.
4
+
5
+ ## Test-Struktur
6
+
7
+ ```
8
+ tests/
9
+ ├── unit/
10
+ │ ├── cursor.test.js # Cursor Utils Tests
11
+ │ ├── validation.test.js # Validation Tests
12
+ │ └── gridQueryBuilder.test.js # Query Builder Tests
13
+ └── mocks/
14
+ └── dynamoose.mock.js # Dynamoose Mock Utilities
15
+ ```
16
+
17
+ ## Tests ausführen
18
+
19
+ ### Alle Tests
20
+
21
+ ```bash
22
+ npm test
23
+ ```
24
+
25
+ ### Tests im Watch-Modus
26
+
27
+ ```bash
28
+ npm test -- --watch
29
+ ```
30
+
31
+ ### Tests mit UI
32
+
33
+ ```bash
34
+ npm run test:ui
35
+ ```
36
+
37
+ ### Einzelne Tests
38
+
39
+ ```bash
40
+ npm test cursor
41
+ npm test validation
42
+ npm test gridQueryBuilder
43
+ ```
44
+
45
+ ### Coverage-Report
46
+
47
+ ```bash
48
+ npm run coverage
49
+ ```
50
+
51
+ Der Coverage-Report wird in `coverage/` generiert. Öffnen Sie `coverage/index.html` im Browser für eine detaillierte Ansicht.
52
+
53
+ ## Test-Abdeckung
54
+
55
+ ### Cursor Utils (~80% Coverage)
56
+
57
+ **Getestete Funktionen:**
58
+ - `encodeCursor()` - Base64-Encoding von Cursor-Daten
59
+ - `decodeCursor()` - Base64-Decoding von Cursors
60
+ - `validateCursor()` - Cursor-Hash-Validierung
61
+
62
+ **Test-Cases:**
63
+ - ✅ Erfolgreiche Encodierung
64
+ - ✅ Erfolgreiche Decodierung
65
+ - ✅ Roundtrip encode/decode
66
+ - ✅ Error-Handling für fehlende Parameter
67
+ - ✅ Error-Handling für ungültige Cursors
68
+ - ✅ Cursor-Validierung
69
+ - ✅ Hash-Mismatch-Erkennung
70
+
71
+ ### Validation Utils (~70% Coverage)
72
+
73
+ **Getestete Funktionen:**
74
+ - `validateSingleSort()` - Validierung von Sort-Feldern
75
+ - `validateFilterField()` - Validierung von Filter-Feldern
76
+ - `validateSortField()` - Validierung von Sort-Feldern
77
+ - `validateFilterOperator()` - Validierung von Operatoren
78
+ - `validateExpandField()` - Validierung von Expand-Feldern
79
+ - `validatePaginationModel()` - Validierung von Pagination
80
+
81
+ **Test-Cases:**
82
+ - ✅ Erlaubte Konfigurationen
83
+ - ✅ Error bei nicht-konfigurierten Feldern
84
+ - ✅ Error bei ungültigen Operatoren
85
+ - ✅ Error bei mehrfachen Sort-Feldern
86
+ - ✅ Edge-Cases (leere Configs, null, undefined)
87
+ - ✅ Hilfreiche Fehlermeldungen
88
+
89
+ ### GridQueryBuilder (~50% Coverage)
90
+
91
+ **Getestete Funktionen:**
92
+ - `extractGridConfig()` - Config-Extraktion aus Schema
93
+ - `buildGridQuery()` - Query-Generierung
94
+
95
+ **Test-Cases:**
96
+ - ✅ Basis-Query mit Partition Key
97
+ - ✅ Filter-Anwendung (eq, gte, between)
98
+ - ✅ Sort-Anwendung (asc/desc)
99
+ - ✅ Pagination mit Limit
100
+ - ✅ Cursor-Handling
101
+ - ✅ GSI-Verwendung
102
+ - ✅ Query-Hash-Generierung
103
+ - ✅ Error-Handling
104
+ - ✅ Komplexe Szenarien
105
+
106
+ ## Mocks
107
+
108
+ ### Dynamoose Mock
109
+
110
+ Der `dynamoose.mock.js` bietet Utilities zum Mocken von Dynamoose-Models:
111
+
112
+ ```javascript
113
+ import { createMockSchema, createMockModel } from "../mocks/dynamoose.mock.js";
114
+
115
+ // Schema mocken
116
+ const schema = createMockSchema(attributes);
117
+
118
+ // Model mocken
119
+ const model = createMockModel(schema, queryResult);
120
+ ```
121
+
122
+ **Features:**
123
+ - Chainable Query-Methoden
124
+ - Konfigurierbare Exec-Ergebnisse
125
+ - Schema mit `getAttributes()`
126
+ - Vordefinierte Test-Attribute
127
+
128
+ ## Best Practices
129
+
130
+ ### Test-Struktur
131
+
132
+ ```javascript
133
+ describe("FunctionName", () => {
134
+ describe("Happy Path", () => {
135
+ it("should do X when Y", () => {
136
+ // Arrange
137
+ const input = ...;
138
+
139
+ // Act
140
+ const result = functionName(input);
141
+
142
+ // Assert
143
+ expect(result).toBe(expected);
144
+ });
145
+ });
146
+
147
+ describe("Error Handling", () => {
148
+ it("should throw error when Z", () => {
149
+ expect(() => {
150
+ functionName(invalidInput);
151
+ }).toThrow("Expected error message");
152
+ });
153
+ });
154
+ });
155
+ ```
156
+
157
+ ### beforeEach für Setup
158
+
159
+ ```javascript
160
+ describe("MyTests", () => {
161
+ let mockModel;
162
+ let mockQuery;
163
+
164
+ beforeEach(() => {
165
+ mockQuery = createMockQuery();
166
+ mockModel = createMockModel(schema);
167
+ });
168
+
169
+ it("test case", () => {
170
+ // mockModel ist hier verfügbar
171
+ });
172
+ });
173
+ ```
174
+
175
+ ### Assertions
176
+
177
+ ```javascript
178
+ // Exakte Gleichheit
179
+ expect(result).toBe(expected);
180
+ expect(result).toEqual(expected);
181
+
182
+ // Fehler
183
+ expect(() => func()).toThrow("error message");
184
+ expect(() => func()).toThrow(/regex pattern/);
185
+
186
+ // Mock-Aufrufe
187
+ expect(mockFn).toHaveBeenCalled();
188
+ expect(mockFn).toHaveBeenCalledWith(arg1, arg2);
189
+ expect(mockFn).toHaveBeenCalledTimes(1);
190
+
191
+ // Objekt-Properties
192
+ expect(obj).toBeDefined();
193
+ expect(obj.prop).toBeUndefined();
194
+ expect(array.length).toBeGreaterThan(0);
195
+ ```
196
+
197
+ ## Debugging
198
+
199
+ ### Einzelnen Test ausführen
200
+
201
+ ```javascript
202
+ it.only("should test specific case", () => {
203
+ // Nur dieser Test wird ausgeführt
204
+ });
205
+ ```
206
+
207
+ ### Test überspringen
208
+
209
+ ```javascript
210
+ it.skip("should test later", () => {
211
+ // Wird übersprungen
212
+ });
213
+ ```
214
+
215
+ ### Debug-Output
216
+
217
+ ```javascript
218
+ it("should debug test", () => {
219
+ console.log("Debug info:", someValue);
220
+ expect(someValue).toBeDefined();
221
+ });
222
+ ```
223
+
224
+ ## Coverage-Ziele
225
+
226
+ - **Gesamt**: ~50-60% Coverage
227
+ - **Kritische Utils**: >70% Coverage
228
+ - **Core Logic**: ~50% Coverage
229
+
230
+ ## Continuous Integration
231
+
232
+ Für CI/CD-Pipelines:
233
+
234
+ ```bash
235
+ # Tests ohne Watch-Modus
236
+ npm run test:run
237
+
238
+ # Mit Coverage
239
+ npm run coverage
240
+
241
+ # Exit-Code bei Fehler
242
+ npm run test:run -- --reporter=verbose
243
+ ```
244
+
245
+ ## Erweiterung
246
+
247
+ ### Neue Tests hinzufügen
248
+
249
+ 1. Datei in `tests/unit/` erstellen
250
+ 2. Vitest importieren: `import { describe, it, expect } from "vitest"`
251
+ 3. Tests schreiben
252
+ 4. Ausführen: `npm test`
253
+
254
+ ### Neue Mocks hinzufügen
255
+
256
+ Mocks für zusätzliche Dependencies in `tests/mocks/` ablegen.
257
+
258
+ ## Troubleshooting
259
+
260
+ ### "Cannot find module"
261
+
262
+ ```bash
263
+ # Dependencies installieren
264
+ npm install
265
+ ```
266
+
267
+ ### Tests schlagen fehl nach Code-Änderungen
268
+
269
+ ```bash
270
+ # Cache leeren
271
+ npm test -- --clearCache
272
+ ```
273
+
274
+ ### Coverage zu niedrig
275
+
276
+ Fokus auf kritische Pfade:
277
+ - Happy Path
278
+ - Error-Handling
279
+ - Edge-Cases
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @fileoverview Mock for Dynamoose models and schemas
3
+ */
4
+
5
+ import { vi } from "vitest";
6
+
7
+ /**
8
+ * Creates a mock Dynamoose schema
9
+ * @param {Object} attributes - Schema attributes with query config
10
+ * @returns {Object} Mock schema
11
+ */
12
+ export function createMockSchema(attributes) {
13
+ return {
14
+ getAttributes: vi.fn(() => attributes),
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Creates a chainable mock query object
20
+ * @param {*} execResult - Result to return from exec()
21
+ * @returns {Object} Mock query object
22
+ */
23
+ export function createMockQuery(execResult = []) {
24
+ const mockQuery = {
25
+ eq: vi.fn().mockReturnThis(),
26
+ ne: vi.fn().mockReturnThis(),
27
+ gt: vi.fn().mockReturnThis(),
28
+ ge: vi.fn().mockReturnThis(),
29
+ lt: vi.fn().mockReturnThis(),
30
+ le: vi.fn().mockReturnThis(),
31
+ between: vi.fn().mockReturnThis(),
32
+ beginsWith: vi.fn().mockReturnThis(),
33
+ contains: vi.fn().mockReturnThis(),
34
+ where: vi.fn().mockReturnThis(),
35
+ using: vi.fn().mockReturnThis(),
36
+ limit: vi.fn().mockReturnThis(),
37
+ sort: vi.fn().mockReturnThis(),
38
+ startAt: vi.fn().mockReturnThis(),
39
+ exec: vi.fn().mockResolvedValue(execResult),
40
+ };
41
+
42
+ return mockQuery;
43
+ }
44
+
45
+ /**
46
+ * Creates a mock Dynamoose model
47
+ * @param {Object} schema - Mock schema
48
+ * @param {*} queryResult - Result to return from query execution
49
+ * @returns {Object} Mock model
50
+ */
51
+ export function createMockModel(schema, queryResult = []) {
52
+ const mockQuery = createMockQuery(queryResult);
53
+
54
+ return {
55
+ schema,
56
+ query: vi.fn(() => mockQuery),
57
+ _mockQuery: mockQuery, // Expose for testing
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Common schema attributes for testing
63
+ */
64
+ export const mockAttributes = {
65
+ pk: {
66
+ type: String,
67
+ hashKey: true,
68
+ },
69
+ createdAt: {
70
+ type: String,
71
+ rangeKey: true,
72
+ query: {
73
+ sort: { type: "key" },
74
+ filter: {
75
+ type: "key",
76
+ operators: ["between", "gte", "lte"],
77
+ },
78
+ },
79
+ },
80
+ status: {
81
+ type: String,
82
+ index: {
83
+ name: "status-createdAt-index",
84
+ global: true,
85
+ rangeKey: "createdAt",
86
+ },
87
+ query: {
88
+ filter: {
89
+ type: "key",
90
+ operators: ["eq"],
91
+ index: "status-createdAt-index",
92
+ },
93
+ },
94
+ },
95
+ email: {
96
+ type: String,
97
+ index: {
98
+ name: "email-index",
99
+ global: true,
100
+ },
101
+ query: {
102
+ filter: {
103
+ type: "key",
104
+ operators: ["eq"],
105
+ index: "email-index",
106
+ },
107
+ },
108
+ },
109
+ name: {
110
+ type: String,
111
+ },
112
+ teamIds: {
113
+ type: Array,
114
+ schema: [String],
115
+ query: {
116
+ expand: {
117
+ type: "Team",
118
+ relation: "MANY",
119
+ defaultLimit: 5,
120
+ maxLimit: 20,
121
+ },
122
+ },
123
+ },
124
+ };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @fileoverview Tests for cursor utilities
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+ import {
7
+ encodeCursor,
8
+ decodeCursor,
9
+ validateCursor,
10
+ } from "../../src/utils/cursor.js";
11
+
12
+ describe("Cursor Utils", () => {
13
+ describe("encodeCursor", () => {
14
+ it("should encode cursor with lastKey and queryHash", () => {
15
+ const lastKey = { pk: "ORG#123", sk: "USER#456" };
16
+ const queryHash = "abc123def456";
17
+
18
+ const cursor = encodeCursor(lastKey, queryHash);
19
+
20
+ expect(cursor).toBeDefined();
21
+ expect(typeof cursor).toBe("string");
22
+ expect(cursor.length).toBeGreaterThan(0);
23
+ });
24
+
25
+ it("should create different cursors for different lastKeys", () => {
26
+ const lastKey1 = { pk: "ORG#123", sk: "USER#456" };
27
+ const lastKey2 = { pk: "ORG#456", sk: "USER#789" };
28
+ const queryHash = "abc123";
29
+
30
+ const cursor1 = encodeCursor(lastKey1, queryHash);
31
+ const cursor2 = encodeCursor(lastKey2, queryHash);
32
+
33
+ expect(cursor1).not.toBe(cursor2);
34
+ });
35
+
36
+ it("should throw error when lastKey is missing", () => {
37
+ expect(() => {
38
+ encodeCursor(null, "queryHash");
39
+ }).toThrow("lastKey is required");
40
+ });
41
+
42
+ it("should throw error when queryHash is missing", () => {
43
+ expect(() => {
44
+ encodeCursor({ pk: "ORG#123" }, null);
45
+ }).toThrow("queryHash is required");
46
+ });
47
+ });
48
+
49
+ describe("decodeCursor", () => {
50
+ it("should decode a valid cursor", () => {
51
+ const lastKey = { pk: "ORG#123", sk: "USER#456" };
52
+ const queryHash = "abc123def456";
53
+ const cursor = encodeCursor(lastKey, queryHash);
54
+
55
+ const decoded = decodeCursor(cursor);
56
+
57
+ expect(decoded).toBeDefined();
58
+ expect(decoded.lastKey).toEqual(lastKey);
59
+ expect(decoded.queryHash).toBe(queryHash);
60
+ });
61
+
62
+ it("should roundtrip encode and decode", () => {
63
+ const originalData = {
64
+ lastKey: { pk: "ORG#999", createdAt: "2024-01-01" },
65
+ queryHash: "hash999",
66
+ };
67
+
68
+ const cursor = encodeCursor(originalData.lastKey, originalData.queryHash);
69
+ const decoded = decodeCursor(cursor);
70
+
71
+ expect(decoded).toEqual(originalData);
72
+ });
73
+
74
+ it("should throw error for invalid cursor string", () => {
75
+ expect(() => {
76
+ decodeCursor("invalid-cursor-string");
77
+ }).toThrow("Failed to decode cursor");
78
+ });
79
+
80
+ it("should throw error for empty cursor", () => {
81
+ expect(() => {
82
+ decodeCursor("");
83
+ }).toThrow("Invalid cursor: must be a non-empty string");
84
+ });
85
+
86
+ it("should throw error for null cursor", () => {
87
+ expect(() => {
88
+ decodeCursor(null);
89
+ }).toThrow("Invalid cursor: must be a non-empty string");
90
+ });
91
+
92
+ it("should throw error for malformed cursor structure", () => {
93
+ // Create a cursor without required fields
94
+ const invalidData = { someField: "value" };
95
+ const cursor = Buffer.from(JSON.stringify(invalidData)).toString(
96
+ "base64"
97
+ );
98
+
99
+ expect(() => {
100
+ decodeCursor(cursor);
101
+ }).toThrow("Invalid cursor structure");
102
+ });
103
+ });
104
+
105
+ describe("validateCursor", () => {
106
+ it("should not throw when query hashes match", () => {
107
+ const queryHash = "abc123def456";
108
+
109
+ expect(() => {
110
+ validateCursor(queryHash, queryHash);
111
+ }).not.toThrow();
112
+ });
113
+
114
+ it("should throw when query hashes do not match", () => {
115
+ const decodedHash = "abc123";
116
+ const currentHash = "def456";
117
+
118
+ expect(() => {
119
+ validateCursor(decodedHash, currentHash);
120
+ }).toThrow("Cursor does not match current query");
121
+ });
122
+
123
+ it("should provide helpful error message", () => {
124
+ expect(() => {
125
+ validateCursor("oldHash", "newHash");
126
+ }).toThrow(/Filters, sort, or page size have changed/);
127
+ });
128
+ });
129
+
130
+ describe("Integration: encode, decode, validate", () => {
131
+ it("should handle complete cursor workflow", () => {
132
+ const lastKey = { pk: "ORG#123", sk: "USER#789" };
133
+ const queryHash = "query-hash-123";
134
+
135
+ // Encode
136
+ const cursor = encodeCursor(lastKey, queryHash);
137
+ expect(cursor).toBeDefined();
138
+
139
+ // Decode
140
+ const decoded = decodeCursor(cursor);
141
+ expect(decoded.lastKey).toEqual(lastKey);
142
+ expect(decoded.queryHash).toBe(queryHash);
143
+
144
+ // Validate
145
+ expect(() => {
146
+ validateCursor(decoded.queryHash, queryHash);
147
+ }).not.toThrow();
148
+ });
149
+
150
+ it("should detect query changes between pagination requests", () => {
151
+ const lastKey = { pk: "ORG#123" };
152
+ const originalQueryHash = "original-hash";
153
+ const modifiedQueryHash = "modified-hash";
154
+
155
+ // Encode with original query
156
+ const cursor = encodeCursor(lastKey, originalQueryHash);
157
+
158
+ // Decode
159
+ const decoded = decodeCursor(cursor);
160
+
161
+ // Validate with modified query - should fail
162
+ expect(() => {
163
+ validateCursor(decoded.queryHash, modifiedQueryHash);
164
+ }).toThrow("Cursor does not match current query");
165
+ });
166
+ });
167
+ });