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.
- package/README.md +434 -0
- package/index.js +31 -0
- package/package.json +44 -4
- package/src/core/expandResolver.js +154 -0
- package/src/core/gridQueryBuilder.js +219 -0
- package/src/core/index.js +11 -0
- package/src/core/modelRegistry.js +87 -0
- package/src/types/jsdoc.js +125 -0
- package/src/utils/cursor.js +65 -0
- package/src/utils/index.js +13 -0
- package/src/utils/validation.js +104 -0
- package/tests/README.md +279 -0
- package/tests/mocks/dynamoose.mock.js +124 -0
- package/tests/unit/cursor.test.js +167 -0
- package/tests/unit/gridQueryBuilder.test.js +474 -0
- package/tests/unit/validation.test.js +350 -0
- package/vitest.config.js +20 -0
package/tests/README.md
ADDED
|
@@ -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
|
+
});
|