@ftschopp/dynatable-core 1.2.0 → 1.2.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/CHANGELOG.md +21 -0
- package/dist/builders/query/create-query-builder.d.ts.map +1 -1
- package/dist/builders/query/create-query-builder.js +25 -7
- package/dist/entity/create-entity-api.d.ts.map +1 -1
- package/dist/entity/create-entity-api.js +7 -1
- package/package.json +1 -1
- package/src/builders/query/create-query-builder.test.ts +70 -0
- package/src/builders/query/create-query-builder.ts +29 -7
- package/src/entity/create-entity-api.ts +9 -1
- package/tests/integration/instagram-clone.integration.test.ts +18 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## @ftschopp/dynatable-core [1.2.2](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.2.1...@ftschopp/dynatable-core@1.2.2) (2026-04-15)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* add filter type scan entity ([dddff15](https://github.com/ftschopp/dynatable/commit/dddff15228f84fd11b0c6d2daa6f5cc8d77c641d))
|
|
7
|
+
|
|
8
|
+
## @ftschopp/dynatable-core [1.2.1](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.2.0...@ftschopp/dynatable-core@1.2.1) (2026-04-15)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* resolve GSI key attributes to KeyConditionExpression when using useIndex ([8b2ee35](https://github.com/ftschopp/dynatable/commit/8b2ee35ca56bae9a32c8f1fec764365e8496674d))
|
|
14
|
+
|
|
15
|
+
## @ftschopp/dynatable-core [1.2.1](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.2.0...@ftschopp/dynatable-core@1.2.1) (2026-04-15)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* resolve GSI key attributes to KeyConditionExpression when using useIndex ([8b2ee35](https://github.com/ftschopp/dynatable/commit/8b2ee35ca56bae9a32c8f1fec764365e8496674d))
|
|
21
|
+
|
|
1
22
|
# @ftschopp/dynatable-core [1.2.0](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.1.0...@ftschopp/dynatable-core@1.2.0) (2026-03-20)
|
|
2
23
|
|
|
3
24
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-query-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/query/create-query-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAK1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"create-query-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/query/create-query-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAK1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAuU7D;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EACtC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,KAAK,CAAC,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,cAAc,GACtB,YAAY,CAAC,KAAK,CAAC,CA4BrB"}
|
|
@@ -6,11 +6,20 @@ const operators_1 = require("../shared/operators");
|
|
|
6
6
|
const conditions_1 = require("../shared/conditions");
|
|
7
7
|
const model_utils_1 = require("../../utils/model-utils");
|
|
8
8
|
/**
|
|
9
|
-
* Determines if a field is a key field (used in pk/sk templates)
|
|
9
|
+
* Determines if a field is a key field (used in pk/sk or index key templates)
|
|
10
10
|
*/
|
|
11
|
-
function isKeyField(fieldName, model) {
|
|
11
|
+
function isKeyField(fieldName, model, indexName) {
|
|
12
12
|
if (!model?.key)
|
|
13
13
|
return false;
|
|
14
|
+
// When an index is specified, also check model.index for key templates
|
|
15
|
+
if (indexName && model.index) {
|
|
16
|
+
for (const [, keyDef] of Object.entries(model.index)) {
|
|
17
|
+
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
18
|
+
if (templateVars.includes(fieldName)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
const keyEntries = Object.entries(model.key);
|
|
15
24
|
for (const [, keyDef] of keyEntries) {
|
|
16
25
|
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
@@ -24,13 +33,22 @@ function isKeyField(fieldName, model) {
|
|
|
24
33
|
* Maps a model attribute name to its corresponding DynamoDB key name (PK/SK)
|
|
25
34
|
* Also returns the key template for value transformation
|
|
26
35
|
*/
|
|
27
|
-
function getKeyNameForAttribute(fieldName, model) {
|
|
36
|
+
function getKeyNameForAttribute(fieldName, model, indexName) {
|
|
28
37
|
if (!model?.key)
|
|
29
38
|
return null;
|
|
39
|
+
// When an index is specified, check model.index first for key templates
|
|
40
|
+
if (indexName && model.index) {
|
|
41
|
+
for (const [keyName, keyDef] of Object.entries(model.index)) {
|
|
42
|
+
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
43
|
+
if (templateVars.includes(fieldName)) {
|
|
44
|
+
return { keyName, template: keyDef.value };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
30
48
|
for (const [keyName, keyDef] of Object.entries(model.key)) {
|
|
31
49
|
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
32
50
|
if (templateVars.includes(fieldName)) {
|
|
33
|
-
return { keyName, template: keyDef.value };
|
|
51
|
+
return { keyName, template: keyDef.value };
|
|
34
52
|
}
|
|
35
53
|
}
|
|
36
54
|
return null;
|
|
@@ -46,7 +64,7 @@ function applyKeyTemplate(template, fieldName, fieldValue) {
|
|
|
46
64
|
* Separates a condition tree into key conditions and filter conditions
|
|
47
65
|
* Key conditions are rewritten to use actual DynamoDB key names (PK/SK)
|
|
48
66
|
*/
|
|
49
|
-
function separateConditions(condition, model) {
|
|
67
|
+
function separateConditions(condition, model, indexName) {
|
|
50
68
|
const keyConditions = [];
|
|
51
69
|
const filterConditions = [];
|
|
52
70
|
function traverse(cond) {
|
|
@@ -76,7 +94,7 @@ function separateConditions(condition, model) {
|
|
|
76
94
|
const fieldMatch = cond.expression.match(/#(\w+)/);
|
|
77
95
|
if (fieldMatch && fieldMatch[1]) {
|
|
78
96
|
const fieldName = fieldMatch[1];
|
|
79
|
-
const keyInfo = getKeyNameForAttribute(fieldName, model);
|
|
97
|
+
const keyInfo = getKeyNameForAttribute(fieldName, model, indexName);
|
|
80
98
|
if (keyInfo) {
|
|
81
99
|
// This is a key field - rewrite the condition to use the actual key name
|
|
82
100
|
// and apply the template to the value
|
|
@@ -178,7 +196,7 @@ function createQueryExecutor(state) {
|
|
|
178
196
|
throw new Error('No where condition specified');
|
|
179
197
|
}
|
|
180
198
|
// Separate key conditions from filter conditions
|
|
181
|
-
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model);
|
|
199
|
+
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model, state.indexName);
|
|
182
200
|
// Build KeyConditionExpression (simple AND)
|
|
183
201
|
const keyExpr = buildSimpleAndExpression(keyConditions);
|
|
184
202
|
// Build FilterExpression (can be complex with OR, NOT, etc.)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-entity-api.d.ts","sourceRoot":"","sources":["../../src/entity/create-entity-api.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AActF,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"create-entity-api.d.ts","sourceRoot":"","sources":["../../src/entity/create-entity-api.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AActF,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAMtD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,eAAe,EAC3D,WAAW,MAAM,EACjB,WAAW,MAAM,EACjB,OAAO,KAAK,EACZ,QAAQ,cAAc,EACtB,UAAS,gBAAqB,KAC7B,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,CAqLtE,CAAC"}
|
|
@@ -54,7 +54,13 @@ const createEntityAPI = (tableName, modelName, model, client, options = {}) => {
|
|
|
54
54
|
return (0, builders_1.createQueryBuilder)(tableName, client, model, logger);
|
|
55
55
|
},
|
|
56
56
|
scan() {
|
|
57
|
-
|
|
57
|
+
// Auto-filter by entity type so scan only returns items for this entity
|
|
58
|
+
const typeFilter = {
|
|
59
|
+
expression: '#_type = :_type',
|
|
60
|
+
names: { '#_type': '_type' },
|
|
61
|
+
values: { ':_type': modelName },
|
|
62
|
+
};
|
|
63
|
+
const builder = (0, builders_1.createScanBuilder)(tableName, client, [typeFilter], [], undefined, false, undefined, undefined, undefined, logger);
|
|
58
64
|
return (0, with_middleware_1.withMiddleware)(builder, (0, factories_1.createCleanKeysMiddleware)(cleanInternalKeys));
|
|
59
65
|
},
|
|
60
66
|
update(key) {
|
package/package.json
CHANGED
|
@@ -228,3 +228,73 @@ describe('QueryBuilder - Pagination', () => {
|
|
|
228
228
|
});
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
|
+
|
|
232
|
+
describe('QueryBuilder - GSI key recognition', () => {
|
|
233
|
+
const client = new DynamoDBClient({});
|
|
234
|
+
const tableName = 'TestTable';
|
|
235
|
+
|
|
236
|
+
interface UserModel {
|
|
237
|
+
id: string;
|
|
238
|
+
email: string;
|
|
239
|
+
name?: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const modelWithIndex: ModelDefinition = {
|
|
243
|
+
key: {
|
|
244
|
+
PK: { type: String, value: 'USER#${id}' },
|
|
245
|
+
SK: { type: String, value: 'USER#${id}' },
|
|
246
|
+
},
|
|
247
|
+
index: {
|
|
248
|
+
GSI1PK: { type: String, value: 'EMAIL#${email}' },
|
|
249
|
+
GSI1SK: { type: String, value: 'EMAIL#${email}' },
|
|
250
|
+
},
|
|
251
|
+
attributes: {
|
|
252
|
+
id: { type: String, required: true },
|
|
253
|
+
email: { type: String, required: true },
|
|
254
|
+
name: { type: String },
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
test('should place GSI index attribute into KeyConditionExpression when useIndex is set', () => {
|
|
259
|
+
const params = createQueryBuilder<UserModel>(tableName, client, modelWithIndex)
|
|
260
|
+
.where((attr, op) => op.eq(attr.email, 'alice@example.com'))
|
|
261
|
+
.useIndex('GSI1')
|
|
262
|
+
.dbParams();
|
|
263
|
+
|
|
264
|
+
// email should be recognized as a key field via model.index and placed in KeyConditionExpression
|
|
265
|
+
expect(params.KeyConditionExpression).toBeDefined();
|
|
266
|
+
expect(params.KeyConditionExpression).toContain('#GSI1PK');
|
|
267
|
+
expect(params.ExpressionAttributeValues).toBeDefined();
|
|
268
|
+
// The value should have the template applied
|
|
269
|
+
expect(Object.values(params.ExpressionAttributeValues!)).toContain('EMAIL#alice@example.com');
|
|
270
|
+
// It should NOT be in FilterExpression
|
|
271
|
+
expect(params.FilterExpression).toBeUndefined();
|
|
272
|
+
expect(params.IndexName).toBe('GSI1');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('should place GSI attribute into FilterExpression when NO index is specified', () => {
|
|
276
|
+
const params = createQueryBuilder<UserModel>(tableName, client, modelWithIndex)
|
|
277
|
+
.where((attr, op) => op.and(op.eq(attr.id, '123'), op.eq(attr.email, 'alice@example.com')))
|
|
278
|
+
.dbParams();
|
|
279
|
+
|
|
280
|
+
// id maps to PK -> KeyConditionExpression
|
|
281
|
+
expect(params.KeyConditionExpression).toBeDefined();
|
|
282
|
+
expect(params.KeyConditionExpression).toContain('#PK');
|
|
283
|
+
// email is NOT a primary key field, so without useIndex it goes to filter
|
|
284
|
+
expect(params.FilterExpression).toBeDefined();
|
|
285
|
+
expect(params.FilterExpression).toContain('#email');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('should handle both primary key and GSI key fields together with useIndex', () => {
|
|
289
|
+
const params = createQueryBuilder<UserModel>(tableName, client, modelWithIndex)
|
|
290
|
+
.where((attr, op) => op.and(op.eq(attr.email, 'alice@example.com'), op.eq(attr.id, '123')))
|
|
291
|
+
.useIndex('GSI1')
|
|
292
|
+
.dbParams();
|
|
293
|
+
|
|
294
|
+
// Both should be key conditions: email via index, id via primary key
|
|
295
|
+
expect(params.KeyConditionExpression).toBeDefined();
|
|
296
|
+
expect(params.KeyConditionExpression).toContain('#GSI1PK');
|
|
297
|
+
expect(params.KeyConditionExpression).toContain('#PK');
|
|
298
|
+
expect(params.FilterExpression).toBeUndefined();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -27,11 +27,21 @@ type QueryState<Model> = {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Determines if a field is a key field (used in pk/sk templates)
|
|
30
|
+
* Determines if a field is a key field (used in pk/sk or index key templates)
|
|
31
31
|
*/
|
|
32
|
-
function isKeyField(fieldName: string, model?: ModelDefinition): boolean {
|
|
32
|
+
function isKeyField(fieldName: string, model?: ModelDefinition, indexName?: string): boolean {
|
|
33
33
|
if (!model?.key) return false;
|
|
34
34
|
|
|
35
|
+
// When an index is specified, also check model.index for key templates
|
|
36
|
+
if (indexName && model.index) {
|
|
37
|
+
for (const [, keyDef] of Object.entries(model.index)) {
|
|
38
|
+
const templateVars = extractTemplateVars(keyDef.value);
|
|
39
|
+
if (templateVars.includes(fieldName)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
const keyEntries = Object.entries(model.key);
|
|
36
46
|
for (const [, keyDef] of keyEntries) {
|
|
37
47
|
const templateVars = extractTemplateVars(keyDef.value);
|
|
@@ -48,14 +58,25 @@ function isKeyField(fieldName: string, model?: ModelDefinition): boolean {
|
|
|
48
58
|
*/
|
|
49
59
|
function getKeyNameForAttribute(
|
|
50
60
|
fieldName: string,
|
|
51
|
-
model?: ModelDefinition
|
|
61
|
+
model?: ModelDefinition,
|
|
62
|
+
indexName?: string
|
|
52
63
|
): { keyName: string; template: string } | null {
|
|
53
64
|
if (!model?.key) return null;
|
|
54
65
|
|
|
66
|
+
// When an index is specified, check model.index first for key templates
|
|
67
|
+
if (indexName && model.index) {
|
|
68
|
+
for (const [keyName, keyDef] of Object.entries(model.index)) {
|
|
69
|
+
const templateVars = extractTemplateVars(keyDef.value);
|
|
70
|
+
if (templateVars.includes(fieldName)) {
|
|
71
|
+
return { keyName, template: keyDef.value };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
for (const [keyName, keyDef] of Object.entries(model.key)) {
|
|
56
77
|
const templateVars = extractTemplateVars(keyDef.value);
|
|
57
78
|
if (templateVars.includes(fieldName)) {
|
|
58
|
-
return { keyName, template: keyDef.value };
|
|
79
|
+
return { keyName, template: keyDef.value };
|
|
59
80
|
}
|
|
60
81
|
}
|
|
61
82
|
return null;
|
|
@@ -75,7 +96,8 @@ function applyKeyTemplate(template: string, fieldName: string, fieldValue: any):
|
|
|
75
96
|
*/
|
|
76
97
|
function separateConditions(
|
|
77
98
|
condition: Condition,
|
|
78
|
-
model?: ModelDefinition
|
|
99
|
+
model?: ModelDefinition,
|
|
100
|
+
indexName?: string
|
|
79
101
|
): { keyConditions: Condition[]; filterConditions: Condition[] } {
|
|
80
102
|
const keyConditions: Condition[] = [];
|
|
81
103
|
const filterConditions: Condition[] = [];
|
|
@@ -106,7 +128,7 @@ function separateConditions(
|
|
|
106
128
|
const fieldMatch = cond.expression.match(/#(\w+)/);
|
|
107
129
|
if (fieldMatch && fieldMatch[1]) {
|
|
108
130
|
const fieldName = fieldMatch[1];
|
|
109
|
-
const keyInfo = getKeyNameForAttribute(fieldName, model);
|
|
131
|
+
const keyInfo = getKeyNameForAttribute(fieldName, model, indexName);
|
|
110
132
|
|
|
111
133
|
if (keyInfo) {
|
|
112
134
|
// This is a key field - rewrite the condition to use the actual key name
|
|
@@ -231,7 +253,7 @@ function createQueryExecutor<Model>(state: QueryState<Model>): QueryExecutor<Mod
|
|
|
231
253
|
}
|
|
232
254
|
|
|
233
255
|
// Separate key conditions from filter conditions
|
|
234
|
-
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model);
|
|
256
|
+
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model, state.indexName);
|
|
235
257
|
|
|
236
258
|
// Build KeyConditionExpression (simple AND)
|
|
237
259
|
const keyExpr = buildSimpleAndExpression(keyConditions);
|
|
@@ -18,6 +18,7 @@ import { EntityAPI, EntityAPIOptions } from './types';
|
|
|
18
18
|
import { withMiddleware } from './middleware/with-middleware';
|
|
19
19
|
import { createCleanKeysMiddleware } from './middleware/factories';
|
|
20
20
|
import { validateKeyFields } from './validation/key-validation';
|
|
21
|
+
import { Condition } from '@/builders/shared/types';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Creates an entity API instance with validation, key resolution, and builder creation.
|
|
@@ -90,10 +91,17 @@ export const createEntityAPI = <Model extends ModelDefinition>(
|
|
|
90
91
|
},
|
|
91
92
|
|
|
92
93
|
scan() {
|
|
94
|
+
// Auto-filter by entity type so scan only returns items for this entity
|
|
95
|
+
const typeFilter: Condition = {
|
|
96
|
+
expression: '#_type = :_type',
|
|
97
|
+
names: { '#_type': '_type' },
|
|
98
|
+
values: { ':_type': modelName },
|
|
99
|
+
};
|
|
100
|
+
|
|
93
101
|
const builder = createScanBuilder<InferModel<Model>>(
|
|
94
102
|
tableName,
|
|
95
103
|
client,
|
|
96
|
-
[],
|
|
104
|
+
[typeFilter],
|
|
97
105
|
[],
|
|
98
106
|
undefined,
|
|
99
107
|
false,
|
|
@@ -726,7 +726,9 @@ describe('Should test dbParams function builder', () => {
|
|
|
726
726
|
const params = await table.entities.User.scan().dbParams();
|
|
727
727
|
|
|
728
728
|
expect(params.TableName).toBe('InstagramClone');
|
|
729
|
-
expect(params.FilterExpression).
|
|
729
|
+
expect(params.FilterExpression).toBe('#_type = :_type');
|
|
730
|
+
expect(params.ExpressionAttributeNames).toMatchObject({ '#_type': '_type' });
|
|
731
|
+
expect(params.ExpressionAttributeValues).toMatchObject({ ':_type': 'User' });
|
|
730
732
|
});
|
|
731
733
|
|
|
732
734
|
test('SCAN Users with filter', async () => {
|
|
@@ -736,10 +738,12 @@ describe('Should test dbParams function builder', () => {
|
|
|
736
738
|
|
|
737
739
|
expect(params.TableName).toBe('InstagramClone');
|
|
738
740
|
expect(params.FilterExpression).toMatch(/#followerCount > :followerCount_\d+/);
|
|
739
|
-
expect(params.ExpressionAttributeNames).
|
|
741
|
+
expect(params.ExpressionAttributeNames).toMatchObject({
|
|
740
742
|
'#followerCount': 'followerCount',
|
|
743
|
+
'#_type': '_type',
|
|
741
744
|
});
|
|
742
745
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain(100);
|
|
746
|
+
expect(Object.values(params.ExpressionAttributeValues || {})).toContain('User');
|
|
743
747
|
});
|
|
744
748
|
|
|
745
749
|
test('SCAN Users with multiple filters', async () => {
|
|
@@ -751,9 +755,10 @@ describe('Should test dbParams function builder', () => {
|
|
|
751
755
|
expect(params.FilterExpression).toMatch(
|
|
752
756
|
/\(#followerCount > :followerCount_\d+\) AND \(#followingCount > :followingCount_\d+\)/
|
|
753
757
|
);
|
|
754
|
-
expect(params.ExpressionAttributeNames).
|
|
758
|
+
expect(params.ExpressionAttributeNames).toMatchObject({
|
|
755
759
|
'#followerCount': 'followerCount',
|
|
756
760
|
'#followingCount': 'followingCount',
|
|
761
|
+
'#_type': '_type',
|
|
757
762
|
});
|
|
758
763
|
});
|
|
759
764
|
|
|
@@ -763,6 +768,8 @@ describe('Should test dbParams function builder', () => {
|
|
|
763
768
|
.dbParams();
|
|
764
769
|
|
|
765
770
|
expect(params.ProjectionExpression).toBe('username, photoId, likesCount');
|
|
771
|
+
expect(params.FilterExpression).toBe('#_type = :_type');
|
|
772
|
+
expect(params.ExpressionAttributeValues).toMatchObject({ ':_type': 'Photo' });
|
|
766
773
|
});
|
|
767
774
|
|
|
768
775
|
test('SCAN Photos with limit', async () => {
|
|
@@ -773,6 +780,8 @@ describe('Should test dbParams function builder', () => {
|
|
|
773
780
|
|
|
774
781
|
expect(params.Limit).toBe(10);
|
|
775
782
|
expect(params.FilterExpression).toMatch(/#likesCount > :likesCount_\d+/);
|
|
783
|
+
expect(params.FilterExpression).toMatch(/#_type = :_type/);
|
|
784
|
+
expect(params.ExpressionAttributeValues).toMatchObject({ ':_type': 'Photo' });
|
|
776
785
|
});
|
|
777
786
|
|
|
778
787
|
test('SCAN with IN operator', async () => {
|
|
@@ -781,9 +790,11 @@ describe('Should test dbParams function builder', () => {
|
|
|
781
790
|
.dbParams();
|
|
782
791
|
|
|
783
792
|
expect(params.FilterExpression).toContain('IN');
|
|
793
|
+
expect(params.FilterExpression).toMatch(/#_type = :_type/);
|
|
784
794
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain('alice');
|
|
785
795
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain('bob');
|
|
786
796
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain('charlie');
|
|
797
|
+
expect(Object.values(params.ExpressionAttributeValues || {})).toContain('User');
|
|
787
798
|
});
|
|
788
799
|
|
|
789
800
|
test('SCAN with size operator', async () => {
|
|
@@ -792,7 +803,9 @@ describe('Should test dbParams function builder', () => {
|
|
|
792
803
|
.dbParams();
|
|
793
804
|
|
|
794
805
|
expect(params.FilterExpression).toMatch(/size\(#name\) > :name_size_\d+/);
|
|
806
|
+
expect(params.FilterExpression).toMatch(/#_type = :_type/);
|
|
795
807
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain(5);
|
|
808
|
+
expect(Object.values(params.ExpressionAttributeValues || {})).toContain('User');
|
|
796
809
|
});
|
|
797
810
|
|
|
798
811
|
test('SCAN Comments with complex filter', async () => {
|
|
@@ -804,6 +817,8 @@ describe('Should test dbParams function builder', () => {
|
|
|
804
817
|
|
|
805
818
|
expect(params.FilterExpression).toMatch(/attribute_exists\(#content\)/);
|
|
806
819
|
expect(params.FilterExpression).toMatch(/size\(#content\) > :content_size_\d+/);
|
|
820
|
+
expect(params.FilterExpression).toMatch(/#_type = :_type/);
|
|
821
|
+
expect(params.ExpressionAttributeValues).toMatchObject({ ':_type': 'Comment' });
|
|
807
822
|
expect(params.ProjectionExpression).toBe('photoId, commentId, content');
|
|
808
823
|
expect(params.Limit).toBe(50);
|
|
809
824
|
});
|