@ftschopp/dynatable-core 1.2.5 → 1.3.0
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 +7 -0
- package/dist/builders/query/create-query-builder.d.ts +1 -1
- package/dist/builders/query/create-query-builder.d.ts.map +1 -1
- package/dist/builders/query/create-query-builder.js +35 -10
- package/dist/builders/shared/operators.d.ts.map +1 -1
- package/dist/builders/shared/operators.js +8 -0
- package/dist/builders/shared/types.d.ts +1 -0
- package/dist/builders/shared/types.d.ts.map +1 -1
- package/dist/builders/update/create-update-builder.d.ts +8 -2
- package/dist/builders/update/create-update-builder.d.ts.map +1 -1
- package/dist/builders/update/create-update-builder.js +45 -16
- package/dist/builders/update/types.d.ts +13 -0
- package/dist/builders/update/types.d.ts.map +1 -1
- package/dist/entity/create-entity-api.d.ts.map +1 -1
- package/dist/entity/create-entity-api.js +8 -4
- package/dist/utils/model-utils.d.ts +28 -0
- package/dist/utils/model-utils.d.ts.map +1 -1
- package/dist/utils/model-utils.js +34 -1
- package/package.json +1 -1
- package/src/builders/query/create-query-builder.test.ts +204 -0
- package/src/builders/query/create-query-builder.ts +51 -10
- package/src/builders/shared/operators.ts +8 -0
- package/src/builders/shared/types.ts +1 -0
- package/src/builders/update/create-update-builder.test.ts +197 -0
- package/src/builders/update/create-update-builder.ts +72 -19
- package/src/builders/update/types.ts +14 -0
- package/src/entity/create-entity-api.ts +8 -4
- package/src/utils/model-utils.test.ts +91 -1
- package/src/utils/model-utils.ts +54 -0
- package/tests/integration/instagram-clone.integration.test.ts +23 -3
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
3
|
import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
|
|
4
4
|
import { buildExpression, AttrBuilder, Condition, createOpBuilder, AttrRef } from '../shared';
|
|
5
|
-
import { UpdateBuilder, UpdateAction } from './types';
|
|
5
|
+
import { UpdateBuilder, UpdateAction, IndexContext } from './types';
|
|
6
6
|
import { DynamoDBLogger } from '../../utils/dynamodb-logger';
|
|
7
|
+
import { computeIndexUpdates } from '../../utils/model-utils';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Creates an UpdateBuilder for an item key and table.
|
|
11
|
+
*
|
|
12
|
+
* When `indexContext` is provided, fields written via `.set()` that participate
|
|
13
|
+
* in any secondary-index template are detected and the affected index keys are
|
|
14
|
+
* recomputed automatically and included in the SET expression. If a template
|
|
15
|
+
* cannot be fully resolved from the primary-key vars plus the updates, building
|
|
16
|
+
* the params throws — the caller must include the missing fields in `.set()`.
|
|
10
17
|
*/
|
|
11
18
|
export function createUpdateBuilder<Model>(
|
|
12
19
|
tableName: string,
|
|
@@ -22,7 +29,9 @@ export function createUpdateBuilder<Model>(
|
|
|
22
29
|
returnMode: 'NONE' | 'ALL_OLD' | 'ALL_NEW' | 'UPDATED_OLD' | 'UPDATED_NEW' = 'NONE',
|
|
23
30
|
valueCounter = 0,
|
|
24
31
|
enableTimestamps = false,
|
|
25
|
-
logger?: DynamoDBLogger
|
|
32
|
+
logger?: DynamoDBLogger,
|
|
33
|
+
indexContext?: IndexContext,
|
|
34
|
+
setInputs: Record<string, any> = {}
|
|
26
35
|
): UpdateBuilder<Model> {
|
|
27
36
|
const conditions = [...prevConditions];
|
|
28
37
|
|
|
@@ -55,27 +64,25 @@ export function createUpdateBuilder<Model>(
|
|
|
55
64
|
returnMode,
|
|
56
65
|
valueCounter,
|
|
57
66
|
enableTimestamps,
|
|
58
|
-
logger
|
|
67
|
+
logger,
|
|
68
|
+
indexContext,
|
|
69
|
+
setInputs
|
|
59
70
|
);
|
|
60
71
|
},
|
|
61
72
|
|
|
62
73
|
set(attrOrUpdates: keyof Model | AttrRef | Partial<Model>, value?: any) {
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
74
|
+
// When no value is provided and the first arg is a plain object,
|
|
75
|
+
// treat it as a Partial<Model>. The AttrRef overload always passes
|
|
76
|
+
// a value, so it's handled by the single-update path below.
|
|
66
77
|
if (
|
|
67
78
|
value === undefined &&
|
|
68
79
|
typeof attrOrUpdates === 'object' &&
|
|
69
|
-
attrOrUpdates !== null
|
|
70
|
-
!(
|
|
71
|
-
Object.keys(attrOrUpdates).length === 1 &&
|
|
72
|
-
'name' in attrOrUpdates &&
|
|
73
|
-
typeof (attrOrUpdates as any).name === 'string'
|
|
74
|
-
)
|
|
80
|
+
attrOrUpdates !== null
|
|
75
81
|
) {
|
|
76
82
|
// Multiple updates case
|
|
77
83
|
const updates = attrOrUpdates as Partial<Model>;
|
|
78
84
|
const newActions: UpdateAction[] = [];
|
|
85
|
+
const newSetInputs = { ...setInputs };
|
|
79
86
|
|
|
80
87
|
for (const [attr, val] of Object.entries(updates)) {
|
|
81
88
|
const attrName = attr;
|
|
@@ -85,6 +92,7 @@ export function createUpdateBuilder<Model>(
|
|
|
85
92
|
names: { [`#${attrName}`]: attrName },
|
|
86
93
|
values: { [`:${valueName}`]: val },
|
|
87
94
|
});
|
|
95
|
+
newSetInputs[attrName] = val;
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
return createUpdateBuilder(
|
|
@@ -96,7 +104,9 @@ export function createUpdateBuilder<Model>(
|
|
|
96
104
|
returnMode,
|
|
97
105
|
valueCounter,
|
|
98
106
|
enableTimestamps,
|
|
99
|
-
logger
|
|
107
|
+
logger,
|
|
108
|
+
indexContext,
|
|
109
|
+
newSetInputs
|
|
100
110
|
);
|
|
101
111
|
}
|
|
102
112
|
|
|
@@ -117,7 +127,9 @@ export function createUpdateBuilder<Model>(
|
|
|
117
127
|
returnMode,
|
|
118
128
|
valueCounter,
|
|
119
129
|
enableTimestamps,
|
|
120
|
-
logger
|
|
130
|
+
logger,
|
|
131
|
+
indexContext,
|
|
132
|
+
{ ...setInputs, [attrName]: value }
|
|
121
133
|
);
|
|
122
134
|
},
|
|
123
135
|
|
|
@@ -136,7 +148,9 @@ export function createUpdateBuilder<Model>(
|
|
|
136
148
|
returnMode,
|
|
137
149
|
valueCounter,
|
|
138
150
|
enableTimestamps,
|
|
139
|
-
logger
|
|
151
|
+
logger,
|
|
152
|
+
indexContext,
|
|
153
|
+
setInputs
|
|
140
154
|
);
|
|
141
155
|
},
|
|
142
156
|
|
|
@@ -157,7 +171,9 @@ export function createUpdateBuilder<Model>(
|
|
|
157
171
|
returnMode,
|
|
158
172
|
valueCounter,
|
|
159
173
|
enableTimestamps,
|
|
160
|
-
logger
|
|
174
|
+
logger,
|
|
175
|
+
indexContext,
|
|
176
|
+
setInputs
|
|
161
177
|
);
|
|
162
178
|
},
|
|
163
179
|
|
|
@@ -178,7 +194,9 @@ export function createUpdateBuilder<Model>(
|
|
|
178
194
|
returnMode,
|
|
179
195
|
valueCounter,
|
|
180
196
|
enableTimestamps,
|
|
181
|
-
logger
|
|
197
|
+
logger,
|
|
198
|
+
indexContext,
|
|
199
|
+
setInputs
|
|
182
200
|
);
|
|
183
201
|
},
|
|
184
202
|
|
|
@@ -192,7 +210,9 @@ export function createUpdateBuilder<Model>(
|
|
|
192
210
|
mode,
|
|
193
211
|
valueCounter,
|
|
194
212
|
enableTimestamps,
|
|
195
|
-
logger
|
|
213
|
+
logger,
|
|
214
|
+
indexContext,
|
|
215
|
+
setInputs
|
|
196
216
|
);
|
|
197
217
|
},
|
|
198
218
|
|
|
@@ -203,7 +223,40 @@ export function createUpdateBuilder<Model>(
|
|
|
203
223
|
const allValues: Record<string, any> = {};
|
|
204
224
|
|
|
205
225
|
// Clone updateActions to avoid mutation
|
|
206
|
-
const actionsToProcess = { ...updateActions };
|
|
226
|
+
const actionsToProcess = { ...updateActions, set: [...updateActions.set] };
|
|
227
|
+
|
|
228
|
+
// Auto-recompute secondary-index keys whose templates depend on any
|
|
229
|
+
// updated field. If a template can't be fully resolved from the primary
|
|
230
|
+
// key vars + the .set() payload, we throw — recomputing from the
|
|
231
|
+
// existing item would require an extra read the builder won't do.
|
|
232
|
+
if (indexContext) {
|
|
233
|
+
const { actions: idxActions, missing } = computeIndexUpdates(
|
|
234
|
+
indexContext.model,
|
|
235
|
+
indexContext.keyVars,
|
|
236
|
+
setInputs
|
|
237
|
+
);
|
|
238
|
+
if (missing.length > 0) {
|
|
239
|
+
const details = missing
|
|
240
|
+
.map(
|
|
241
|
+
(m) =>
|
|
242
|
+
` - ${m.index} ("${m.template}"): missing ${m.missing.join(', ')}`
|
|
243
|
+
)
|
|
244
|
+
.join('\n');
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Update touches fields that participate in secondary index templates, ` +
|
|
247
|
+
`but the templates cannot be fully resolved from the update payload. ` +
|
|
248
|
+
`Include the missing fields in .set():\n${details}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
for (const [indexName, resolved] of Object.entries(idxActions)) {
|
|
252
|
+
const valueName = getUniqueValueName(indexName);
|
|
253
|
+
actionsToProcess.set.push({
|
|
254
|
+
expression: `#${indexName} = :${valueName}`,
|
|
255
|
+
names: { [`#${indexName}`]: indexName },
|
|
256
|
+
values: { [`:${valueName}`]: resolved },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
207
260
|
|
|
208
261
|
// Add updatedAt timestamp if enabled
|
|
209
262
|
if (enableTimestamps) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UpdateCommandInput } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
import { ModelDefinition } from '@/core/types';
|
|
2
3
|
import { OperationBuilder, AttrRef } from '../shared';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -10,6 +11,19 @@ export type UpdateAction = {
|
|
|
10
11
|
values?: Record<string, any>;
|
|
11
12
|
};
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Context that lets the update builder auto-recompute secondary-index keys
|
|
16
|
+
* when a `.set()` touches a field that participates in their templates.
|
|
17
|
+
*
|
|
18
|
+
* `keyVars` carries the original template-variable values used to build the
|
|
19
|
+
* primary key (e.g. `{ id: '123' }` from `update({ id: '123' })`) so they can
|
|
20
|
+
* be combined with the user's `.set()` payload when resolving index templates.
|
|
21
|
+
*/
|
|
22
|
+
export type IndexContext = {
|
|
23
|
+
model: ModelDefinition;
|
|
24
|
+
keyVars: Record<string, any>;
|
|
25
|
+
};
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* Builder interface for DynamoDB UpdateItem operations
|
|
15
29
|
*/
|
|
@@ -85,9 +85,10 @@ export const createEntityAPI = <Model extends ModelDefinition>(
|
|
|
85
85
|
},
|
|
86
86
|
|
|
87
87
|
query() {
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
88
|
+
// Auto-filter by entity type so query() only returns items for this
|
|
89
|
+
// entity. Without this, queries on shared GSIs return items from other
|
|
90
|
+
// entities that share the same partition key.
|
|
91
|
+
return createQueryBuilder<InferModel<Model>>(tableName, client, model, logger, modelName);
|
|
91
92
|
},
|
|
92
93
|
|
|
93
94
|
scan() {
|
|
@@ -130,7 +131,10 @@ export const createEntityAPI = <Model extends ModelDefinition>(
|
|
|
130
131
|
'NONE',
|
|
131
132
|
0,
|
|
132
133
|
timestamps,
|
|
133
|
-
logger
|
|
134
|
+
logger,
|
|
135
|
+
// Carry the original template-vars-bearing key (e.g. { username: 'jane' })
|
|
136
|
+
// so the builder can recompute index keys when .set() touches their template fields.
|
|
137
|
+
{ model, keyVars: key as Record<string, unknown> }
|
|
134
138
|
);
|
|
135
139
|
|
|
136
140
|
return withMiddleware(builder, createCleanKeysMiddleware(cleanInternalKeys));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { applyPostDefaults, stripInternalKeys } from './model-utils';
|
|
2
|
+
import { applyPostDefaults, computeIndexUpdates, stripInternalKeys } from './model-utils';
|
|
3
3
|
import { ModelDefinition } from '../core/types';
|
|
4
4
|
|
|
5
5
|
describe('applyPostDefaults - Timestamps', () => {
|
|
@@ -360,3 +360,93 @@ describe('stripInternalKeys', () => {
|
|
|
360
360
|
});
|
|
361
361
|
});
|
|
362
362
|
});
|
|
363
|
+
|
|
364
|
+
describe('computeIndexUpdates', () => {
|
|
365
|
+
const personnelModel: ModelDefinition = {
|
|
366
|
+
key: {
|
|
367
|
+
PK: { type: String, value: 'PERSON#${id}' },
|
|
368
|
+
SK: { type: String, value: 'PROFILE' },
|
|
369
|
+
},
|
|
370
|
+
index: {
|
|
371
|
+
GSI1PK: { type: String, value: 'AIRPORT#${airportId}' },
|
|
372
|
+
GSI1SK: { type: String, value: 'PERSON#${lastName}#${firstName}' },
|
|
373
|
+
},
|
|
374
|
+
attributes: {
|
|
375
|
+
id: { type: String, required: true },
|
|
376
|
+
airportId: { type: String, required: true },
|
|
377
|
+
firstName: { type: String, required: true },
|
|
378
|
+
lastName: { type: String, required: true },
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
test('returns empty result when model has no indexes', () => {
|
|
383
|
+
const noIndexModel: ModelDefinition = {
|
|
384
|
+
key: {
|
|
385
|
+
PK: { type: String, value: 'USER#${id}' },
|
|
386
|
+
SK: { type: String, value: 'USER#${id}' },
|
|
387
|
+
},
|
|
388
|
+
attributes: { id: { type: String, required: true } },
|
|
389
|
+
};
|
|
390
|
+
const result = computeIndexUpdates(noIndexModel, { id: '1' }, { name: 'X' });
|
|
391
|
+
expect(result).toEqual({ actions: {}, missing: [] });
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('skips indexes whose template references no updated field', () => {
|
|
395
|
+
// Updating only "role" — none of the index templates mention role.
|
|
396
|
+
const result = computeIndexUpdates(
|
|
397
|
+
personnelModel,
|
|
398
|
+
{ id: '1' },
|
|
399
|
+
{ role: 'pilot' }
|
|
400
|
+
);
|
|
401
|
+
expect(result).toEqual({ actions: {}, missing: [] });
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('recomputes an affected index when all template vars resolve', () => {
|
|
405
|
+
const result = computeIndexUpdates(
|
|
406
|
+
personnelModel,
|
|
407
|
+
{ id: '1' },
|
|
408
|
+
{ firstName: 'Ada', lastName: 'Lovelace' }
|
|
409
|
+
);
|
|
410
|
+
expect(result.actions).toEqual({ GSI1SK: 'PERSON#Lovelace#Ada' });
|
|
411
|
+
expect(result.missing).toEqual([]);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('reports missing template vars instead of throwing', () => {
|
|
415
|
+
const result = computeIndexUpdates(
|
|
416
|
+
personnelModel,
|
|
417
|
+
{ id: '1' },
|
|
418
|
+
{ lastName: 'Lovelace' }
|
|
419
|
+
);
|
|
420
|
+
expect(result.actions).toEqual({});
|
|
421
|
+
expect(result.missing).toEqual([
|
|
422
|
+
{
|
|
423
|
+
index: 'GSI1SK',
|
|
424
|
+
template: 'PERSON#${lastName}#${firstName}',
|
|
425
|
+
missing: ['firstName'],
|
|
426
|
+
},
|
|
427
|
+
]);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test('uses keyVars to resolve fields not present in updates', () => {
|
|
431
|
+
// airportId is not in updates but in keyVars — GSI1PK should still resolve.
|
|
432
|
+
const result = computeIndexUpdates(
|
|
433
|
+
personnelModel,
|
|
434
|
+
{ id: '1', airportId: 'EZE' },
|
|
435
|
+
{ airportId: 'AEP' } // changing it
|
|
436
|
+
);
|
|
437
|
+
expect(result.actions).toEqual({ GSI1PK: 'AIRPORT#AEP' });
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test('handles multiple affected indexes independently', () => {
|
|
441
|
+
const result = computeIndexUpdates(
|
|
442
|
+
personnelModel,
|
|
443
|
+
{ id: '1' },
|
|
444
|
+
{ airportId: 'AEP', firstName: 'Ada', lastName: 'Lovelace' }
|
|
445
|
+
);
|
|
446
|
+
expect(result.actions).toEqual({
|
|
447
|
+
GSI1PK: 'AIRPORT#AEP',
|
|
448
|
+
GSI1SK: 'PERSON#Lovelace#Ada',
|
|
449
|
+
});
|
|
450
|
+
expect(result.missing).toEqual([]);
|
|
451
|
+
});
|
|
452
|
+
});
|
package/src/utils/model-utils.ts
CHANGED
|
@@ -59,6 +59,60 @@ export const resolveKeys = <M extends ModelDefinition>(
|
|
|
59
59
|
return out;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Result of analyzing how a partial update affects a model's secondary index keys.
|
|
64
|
+
*
|
|
65
|
+
* `actions` maps index-attribute name → freshly resolved template value, ready to SET.
|
|
66
|
+
* `missing` lists indexes whose template references at least one updated field but
|
|
67
|
+
* cannot be fully resolved from `keyVars + updates`; those entries identify the
|
|
68
|
+
* variables the caller must additionally provide.
|
|
69
|
+
*/
|
|
70
|
+
export type IndexUpdateAnalysis = {
|
|
71
|
+
actions: Record<string, string>;
|
|
72
|
+
missing: { index: string; template: string; missing: string[] }[];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Determines which secondary-index keys must be recomputed given a partial update.
|
|
77
|
+
*
|
|
78
|
+
* An index is "affected" iff at least one of its template variables appears in
|
|
79
|
+
* `updates`. For each affected index we attempt to resolve the template from the
|
|
80
|
+
* union of `keyVars` (template vars carried in the primary key, e.g. `id`) and
|
|
81
|
+
* `updates` (the user's `.set()` payload). If any template variable is missing
|
|
82
|
+
* from that union, the index is reported in `missing` instead of `actions` —
|
|
83
|
+
* recomputing it would require reading the existing item from DynamoDB, which
|
|
84
|
+
* the builder intentionally does not do.
|
|
85
|
+
*/
|
|
86
|
+
export const computeIndexUpdates = <M extends ModelDefinition>(
|
|
87
|
+
model: M,
|
|
88
|
+
keyVars: Record<string, any>,
|
|
89
|
+
updates: Record<string, any>
|
|
90
|
+
): IndexUpdateAnalysis => {
|
|
91
|
+
const actions: Record<string, string> = {};
|
|
92
|
+
const missing: { index: string; template: string; missing: string[] }[] = [];
|
|
93
|
+
|
|
94
|
+
if (!model.index) return { actions, missing };
|
|
95
|
+
|
|
96
|
+
const updateKeys = new Set(Object.keys(updates));
|
|
97
|
+
const combined: Record<string, any> = { ...keyVars, ...updates };
|
|
98
|
+
|
|
99
|
+
for (const [indexName, indexDef] of Object.entries(model.index)) {
|
|
100
|
+
const templateVars = extractTemplateVars(indexDef.value);
|
|
101
|
+
const isAffected = templateVars.some((v) => updateKeys.has(v));
|
|
102
|
+
if (!isAffected) continue;
|
|
103
|
+
|
|
104
|
+
const missingVars = templateVars.filter((v) => combined[v] === undefined);
|
|
105
|
+
if (missingVars.length > 0) {
|
|
106
|
+
missing.push({ index: indexName, template: indexDef.value, missing: missingVars });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
actions[indexName] = resolveTemplate(indexDef.value, combined);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { actions, missing };
|
|
114
|
+
};
|
|
115
|
+
|
|
62
116
|
/**
|
|
63
117
|
* Applies default and generated values to validated input
|
|
64
118
|
*/
|
|
@@ -250,15 +250,20 @@ describe('Should test dbParams function builder', () => {
|
|
|
250
250
|
.where((attr, op) => op.eq(attr.username, 'juanca'))
|
|
251
251
|
.dbParams();
|
|
252
252
|
|
|
253
|
-
// After query builder fix: username -> pk, and value gets template applied
|
|
253
|
+
// After query builder fix: username -> pk, and value gets template applied.
|
|
254
|
+
// Entity API auto-injects a `_type = <modelName>` filter so that queries on
|
|
255
|
+
// shared partition keys don't return items of other entities.
|
|
254
256
|
expect(params).toEqual({
|
|
255
257
|
TableName: 'InstagramClone',
|
|
256
258
|
KeyConditionExpression: '#PK = :username_0',
|
|
259
|
+
FilterExpression: '#_type = :_type',
|
|
257
260
|
ExpressionAttributeNames: {
|
|
258
261
|
'#PK': 'PK',
|
|
262
|
+
'#_type': '_type',
|
|
259
263
|
},
|
|
260
264
|
ExpressionAttributeValues: {
|
|
261
265
|
':username_0': 'UP#juanca', // Template applied: UP#${username}
|
|
266
|
+
':_type': 'Photo',
|
|
262
267
|
},
|
|
263
268
|
});
|
|
264
269
|
|
|
@@ -266,6 +271,7 @@ describe('Should test dbParams function builder', () => {
|
|
|
266
271
|
expect(params.KeyConditionExpression).toMatch(/#PK = :username_\d+/);
|
|
267
272
|
expect(params.ExpressionAttributeNames).toEqual({
|
|
268
273
|
'#PK': 'PK',
|
|
274
|
+
'#_type': '_type',
|
|
269
275
|
});
|
|
270
276
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain('UP#juanca');
|
|
271
277
|
});
|
|
@@ -278,23 +284,27 @@ describe('Should test dbParams function builder', () => {
|
|
|
278
284
|
expect(params).toEqual({
|
|
279
285
|
TableName: 'InstagramClone',
|
|
280
286
|
KeyConditionExpression: '#PK = :username_0',
|
|
281
|
-
FilterExpression: '#likesCount > :likesCount_1',
|
|
287
|
+
FilterExpression: '(#likesCount > :likesCount_1) AND (#_type = :_type)',
|
|
282
288
|
ExpressionAttributeNames: {
|
|
283
289
|
'#likesCount': 'likesCount',
|
|
284
290
|
'#PK': 'PK',
|
|
291
|
+
'#_type': '_type',
|
|
285
292
|
},
|
|
286
293
|
ExpressionAttributeValues: {
|
|
287
294
|
':likesCount_1': 0,
|
|
288
295
|
':username_0': 'UP#juanca',
|
|
296
|
+
':_type': 'Photo',
|
|
289
297
|
},
|
|
290
298
|
});
|
|
291
299
|
|
|
292
300
|
expect(params.TableName).toBe('InstagramClone');
|
|
293
301
|
expect(params.KeyConditionExpression).toMatch(/#PK = :username_\d+/);
|
|
294
302
|
expect(params.FilterExpression).toMatch(/#likesCount > :likesCount_\d+/);
|
|
303
|
+
expect(params.FilterExpression).toMatch(/#_type = :_type/);
|
|
295
304
|
expect(params.ExpressionAttributeNames).toEqual({
|
|
296
305
|
'#PK': 'PK',
|
|
297
306
|
'#likesCount': 'likesCount',
|
|
307
|
+
'#_type': '_type',
|
|
298
308
|
});
|
|
299
309
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain('UP#juanca');
|
|
300
310
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain(0);
|
|
@@ -310,11 +320,14 @@ describe('Should test dbParams function builder', () => {
|
|
|
310
320
|
expect(params).toEqual({
|
|
311
321
|
TableName: 'InstagramClone',
|
|
312
322
|
KeyConditionExpression: '#PK = :username_0',
|
|
323
|
+
FilterExpression: '#_type = :_type',
|
|
313
324
|
ExpressionAttributeNames: {
|
|
314
325
|
'#PK': 'PK',
|
|
326
|
+
'#_type': '_type',
|
|
315
327
|
},
|
|
316
328
|
ExpressionAttributeValues: {
|
|
317
329
|
':username_0': 'UP#juanca',
|
|
330
|
+
':_type': 'Photo',
|
|
318
331
|
},
|
|
319
332
|
Limit: 10,
|
|
320
333
|
ScanIndexForward: false,
|
|
@@ -344,16 +357,19 @@ describe('Should test dbParams function builder', () => {
|
|
|
344
357
|
expect(params).toEqual({
|
|
345
358
|
TableName: 'InstagramClone',
|
|
346
359
|
KeyConditionExpression: '#PK = :photoId_0',
|
|
360
|
+
FilterExpression: '#_type = :_type',
|
|
347
361
|
ExpressionAttributeNames: {
|
|
348
362
|
'#PK': 'PK',
|
|
363
|
+
'#_type': '_type',
|
|
349
364
|
},
|
|
350
365
|
ExpressionAttributeValues: {
|
|
351
366
|
':photoId_0': 'PL#photo123', // Template applied: PL#${photoId}
|
|
367
|
+
':_type': 'Like',
|
|
352
368
|
},
|
|
353
369
|
});
|
|
354
370
|
expect(params.TableName).toBe('InstagramClone');
|
|
355
371
|
expect(params.KeyConditionExpression).toMatch(/#PK = :photoId_\d+/);
|
|
356
|
-
expect(params.ExpressionAttributeNames).toEqual({ '#PK': 'PK' });
|
|
372
|
+
expect(params.ExpressionAttributeNames).toEqual({ '#PK': 'PK', '#_type': '_type' });
|
|
357
373
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain('PL#photo123');
|
|
358
374
|
});
|
|
359
375
|
|
|
@@ -372,10 +388,12 @@ describe('Should test dbParams function builder', () => {
|
|
|
372
388
|
expect(params.FilterExpression).toMatch(
|
|
373
389
|
/\(#likesCount > :likesCount_\d+\) OR \(#commentCount > :commentCount_\d+\)/
|
|
374
390
|
);
|
|
391
|
+
expect(params.FilterExpression).toMatch(/#_type = :_type/);
|
|
375
392
|
expect(params.ExpressionAttributeNames).toEqual({
|
|
376
393
|
'#PK': 'PK',
|
|
377
394
|
'#likesCount': 'likesCount',
|
|
378
395
|
'#commentCount': 'commentCount',
|
|
396
|
+
'#_type': '_type',
|
|
379
397
|
});
|
|
380
398
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain('UP#juanca');
|
|
381
399
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain(100);
|
|
@@ -401,10 +419,12 @@ describe('Should test dbParams function builder', () => {
|
|
|
401
419
|
/\(#likesCount > :likesCount_\d+\) AND \(#commentCount < :commentCount_\d+\)/
|
|
402
420
|
);
|
|
403
421
|
expect(params.FilterExpression).toMatch(/OR/);
|
|
422
|
+
expect(params.FilterExpression).toMatch(/#_type = :_type/);
|
|
404
423
|
expect(params.ExpressionAttributeNames).toEqual({
|
|
405
424
|
'#PK': 'PK',
|
|
406
425
|
'#likesCount': 'likesCount',
|
|
407
426
|
'#commentCount': 'commentCount',
|
|
427
|
+
'#_type': '_type',
|
|
408
428
|
});
|
|
409
429
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain('UP#juanca');
|
|
410
430
|
expect(Object.values(params.ExpressionAttributeValues || {})).toContain(100);
|