dynamo-query-engine 1.0.1 → 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.
package/README.md CHANGED
@@ -39,7 +39,7 @@ const UserSchema = new dynamoose.Schema({
39
39
  createdAt: {
40
40
  type: String,
41
41
  rangeKey: true,
42
- grid: {
42
+ query: {
43
43
  sort: { type: "key" },
44
44
  filter: {
45
45
  type: "key",
@@ -54,7 +54,7 @@ const UserSchema = new dynamoose.Schema({
54
54
  global: true,
55
55
  rangeKey: "createdAt",
56
56
  },
57
- grid: {
57
+ query: {
58
58
  filter: {
59
59
  type: "key",
60
60
  operators: ["eq"],
@@ -65,7 +65,7 @@ const UserSchema = new dynamoose.Schema({
65
65
  teamIds: {
66
66
  type: Array,
67
67
  schema: [String],
68
- grid: {
68
+ query: {
69
69
  expand: {
70
70
  type: "Team",
71
71
  relation: "MANY",
@@ -176,7 +176,7 @@ query UsersGrid(
176
176
  ### Filter Configuration
177
177
 
178
178
  ```javascript
179
- grid: {
179
+ query: {
180
180
  filter: {
181
181
  type: "key", // Only "key" supported
182
182
  operators: ["eq", "gte", "lte"], // Allowed operators
@@ -190,7 +190,7 @@ grid: {
190
190
  ### Sort Configuration
191
191
 
192
192
  ```javascript
193
- grid: {
193
+ query: {
194
194
  sort: { type: "key" } // Only range keys can be sorted
195
195
  }
196
196
  ```
@@ -200,7 +200,7 @@ grid: {
200
200
  ### Expand Configuration
201
201
 
202
202
  ```javascript
203
- grid: {
203
+ query: {
204
204
  expand: {
205
205
  type: "Team", // Model name (must be registered)
206
206
  relation: "MANY", // "ONE" or "MANY"
@@ -316,7 +316,7 @@ status: {
316
316
  global: true,
317
317
  rangeKey: "createdAt",
318
318
  },
319
- grid: {
319
+ query: {
320
320
  filter: {
321
321
  type: "key",
322
322
  operators: ["eq"],
@@ -376,10 +376,52 @@ The library provides descriptive errors:
376
376
  - `"Cursor does not match query"` - Query params changed between pages
377
377
  - `"Model 'X' not found in registry"` - Model not registered
378
378
 
379
+ ## Testing
380
+
381
+ The package includes comprehensive unit tests with Vitest.
382
+
383
+ ### Run Tests
384
+
385
+ ```bash
386
+ # Run all tests
387
+ npm test
388
+
389
+ # Run tests with UI
390
+ npm run test:ui
391
+
392
+ # Run tests once (CI mode)
393
+ npm run test:run
394
+
395
+ # Generate coverage report
396
+ npm run coverage
397
+ ```
398
+
399
+ ### Test Coverage
400
+
401
+ - **Cursor Utils**: ~80% coverage
402
+ - **Validation**: ~70% coverage
403
+ - **GridQueryBuilder**: ~50% coverage
404
+ - **Overall**: ~50-60% coverage
405
+
406
+ See [tests/README.md](./tests/README.md) for detailed testing documentation.
407
+
379
408
  ## Contributing
380
409
 
381
410
  Contributions are welcome! Please open an issue or pull request.
382
411
 
412
+ ### Development Setup
413
+
414
+ ```bash
415
+ # Install dependencies
416
+ npm install
417
+
418
+ # Run tests
419
+ npm test
420
+
421
+ # Check coverage
422
+ npm run coverage
423
+ ```
424
+
383
425
  ## License
384
426
 
385
427
  MIT © Sascha Eckstein
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dynamo-query-engine",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Type-safe DynamoDB query builder for GraphQL with support for filtering, sorting, pagination, and relation expansion",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -10,7 +10,10 @@
10
10
  "./utils": "./src/utils/index.js"
11
11
  },
12
12
  "scripts": {
13
- "test": "echo \"Error: no test specified\" && exit 1"
13
+ "test": "vitest",
14
+ "test:ui": "vitest --ui",
15
+ "test:run": "vitest run",
16
+ "coverage": "vitest --coverage"
14
17
  },
15
18
  "keywords": [
16
19
  "dynamodb",
@@ -39,6 +42,10 @@
39
42
  "dependencies": {
40
43
  "graphql-parse-resolve-info": "^4.13.0"
41
44
  },
45
+ "devDependencies": {
46
+ "@vitest/ui": "^2.1.8",
47
+ "vitest": "^2.1.8"
48
+ },
42
49
  "engines": {
43
50
  "node": ">=18.0.0"
44
51
  }
@@ -11,7 +11,7 @@ import { extractGridConfig } from "./gridQueryBuilder.js";
11
11
  /**
12
12
  * Extract expand policy from schema
13
13
  * @param {*} schema - Dynamoose schema
14
- * @returns {Object.<string, ExpandConfig>} Expand policies by field name
14
+ * @returns {Object.<string, import("../types/jsdoc.js").ExpandConfig>} Expand policies by field name
15
15
  */
16
16
  export function extractExpandPolicy(schema) {
17
17
  const gridConfig = extractGridConfig(schema);
@@ -31,15 +31,15 @@ function hashQuery({ filterModel, sortModel, pageSize }) {
31
31
  /**
32
32
  * Extract grid configuration from Dynamoose schema
33
33
  * @param {*} schema - Dynamoose schema
34
- * @returns {ExtractedGridConfig} Extracted grid configuration
34
+ * @returns {Object.<string, import("../types/jsdoc.js").GridConfig>} Extracted grid configuration
35
35
  */
36
36
  export function extractGridConfig(schema) {
37
37
  const attributes = schema.getAttributes();
38
38
  const gridConfig = {};
39
39
 
40
40
  for (const [field, definition] of Object.entries(attributes)) {
41
- if (definition.grid) {
42
- gridConfig[field] = definition.grid;
41
+ if (definition.query) {
42
+ gridConfig[field] = definition.query;
43
43
  }
44
44
  }
45
45
 
@@ -20,7 +20,7 @@ export function validateSingleSort(sortModel) {
20
20
  /**
21
21
  * Validates that a field can be filtered based on grid config
22
22
  * @param {string} field - Field name to filter
23
- * @param {ExtractedGridConfig} gridConfig - Extracted grid configuration
23
+ * @param {Object.<string, import("../types/jsdoc.js").GridConfig>} gridConfig - Extracted grid configuration
24
24
  * @throws {Error} If filtering is not allowed on this field
25
25
  */
26
26
  export function validateFilterField(field, gridConfig) {
@@ -35,7 +35,7 @@ export function validateFilterField(field, gridConfig) {
35
35
  /**
36
36
  * Validates that a field can be sorted based on grid config
37
37
  * @param {string} field - Field name to sort
38
- * @param {ExtractedGridConfig} gridConfig - Extracted grid configuration
38
+ * @param {Object.<string, import("../types/jsdoc.js").GridConfig>} gridConfig - Extracted grid configuration
39
39
  * @throws {Error} If sorting is not allowed on this field
40
40
  */
41
41
  export function validateSortField(field, gridConfig) {
@@ -51,7 +51,7 @@ export function validateSortField(field, gridConfig) {
51
51
  * Validates that operator is allowed for a filter field
52
52
  * @param {string} field - Field name
53
53
  * @param {string} operator - Operator to validate
54
- * @param {ExtractedGridConfig} gridConfig - Extracted grid configuration
54
+ * @param {Object.<string, import("../types/jsdoc.js").GridConfig>} gridConfig - Extracted grid configuration
55
55
  * @throws {Error} If operator is not allowed for this field
56
56
  */
57
57
  export function validateFilterOperator(field, operator, gridConfig) {
@@ -70,7 +70,7 @@ export function validateFilterOperator(field, operator, gridConfig) {
70
70
  /**
71
71
  * Validates that a field can be expanded based on grid config
72
72
  * @param {string} field - Field name to expand
73
- * @param {ExtractedGridConfig} gridConfig - Extracted grid configuration
73
+ * @param {Object.<string, import("../types/jsdoc.js").GridConfig>} gridConfig - Extracted grid configuration
74
74
  * @throws {Error} If expand is not allowed on this field
75
75
  */
76
76
  export function validateExpandField(field, gridConfig) {
@@ -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
+ };