@squiz/db-lib 1.71.2 → 1.72.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/lib/AbstractRepository.d.ts +2 -0
  3. package/lib/AbstractRepository.d.ts.map +1 -0
  4. package/lib/AbstractRepository.integration.spec.d.ts +1 -0
  5. package/lib/AbstractRepository.integration.spec.d.ts.map +1 -0
  6. package/lib/AbstractRepository.integration.spec.js +118 -0
  7. package/lib/AbstractRepository.integration.spec.js.map +1 -0
  8. package/lib/AbstractRepository.js +187 -0
  9. package/lib/AbstractRepository.js.map +1 -0
  10. package/lib/ConnectionManager.d.ts +1 -0
  11. package/lib/ConnectionManager.d.ts.map +1 -0
  12. package/lib/ConnectionManager.js +58 -0
  13. package/lib/ConnectionManager.js.map +1 -0
  14. package/lib/Migrator.d.ts +1 -0
  15. package/lib/Migrator.d.ts.map +1 -0
  16. package/lib/Migrator.js +160 -0
  17. package/lib/Migrator.js.map +1 -0
  18. package/lib/PostgresErrorCodes.d.ts +1 -0
  19. package/lib/PostgresErrorCodes.d.ts.map +1 -0
  20. package/lib/PostgresErrorCodes.js +274 -0
  21. package/lib/PostgresErrorCodes.js.map +1 -0
  22. package/lib/Repositories.d.ts +1 -0
  23. package/lib/Repositories.d.ts.map +1 -0
  24. package/lib/Repositories.js +3 -0
  25. package/lib/Repositories.js.map +1 -0
  26. package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +50 -9
  27. package/lib/dynamodb/AbstractDynamoDbRepository.d.ts.map +1 -0
  28. package/lib/dynamodb/AbstractDynamoDbRepository.js +456 -0
  29. package/lib/dynamodb/AbstractDynamoDbRepository.js.map +1 -0
  30. package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts +1 -0
  31. package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts.map +1 -0
  32. package/lib/dynamodb/AbstractDynamoDbRepository.spec.js +924 -0
  33. package/lib/dynamodb/AbstractDynamoDbRepository.spec.js.map +1 -0
  34. package/lib/dynamodb/DynamoDbManager.d.ts +1 -0
  35. package/lib/dynamodb/DynamoDbManager.d.ts.map +1 -0
  36. package/lib/dynamodb/DynamoDbManager.js +66 -0
  37. package/lib/dynamodb/DynamoDbManager.js.map +1 -0
  38. package/lib/dynamodb/getDynamoDbOptions.d.ts +1 -0
  39. package/lib/dynamodb/getDynamoDbOptions.d.ts.map +1 -0
  40. package/lib/dynamodb/getDynamoDbOptions.js +15 -0
  41. package/lib/dynamodb/getDynamoDbOptions.js.map +1 -0
  42. package/lib/error/DuplicateItemError.d.ts +1 -0
  43. package/lib/error/DuplicateItemError.d.ts.map +1 -0
  44. package/lib/error/DuplicateItemError.js +12 -0
  45. package/lib/error/DuplicateItemError.js.map +1 -0
  46. package/lib/error/InvalidDataFormatError.d.ts +1 -0
  47. package/lib/error/InvalidDataFormatError.d.ts.map +1 -0
  48. package/lib/error/InvalidDataFormatError.js +12 -0
  49. package/lib/error/InvalidDataFormatError.js.map +1 -0
  50. package/lib/error/InvalidDbSchemaError.d.ts +1 -0
  51. package/lib/error/InvalidDbSchemaError.d.ts.map +1 -0
  52. package/lib/error/InvalidDbSchemaError.js +12 -0
  53. package/lib/error/InvalidDbSchemaError.js.map +1 -0
  54. package/lib/error/MissingKeyValuesError.d.ts +1 -0
  55. package/lib/error/MissingKeyValuesError.d.ts.map +1 -0
  56. package/lib/error/MissingKeyValuesError.js +12 -0
  57. package/lib/error/MissingKeyValuesError.js.map +1 -0
  58. package/lib/error/TransactionError.d.ts +1 -0
  59. package/lib/error/TransactionError.d.ts.map +1 -0
  60. package/lib/error/TransactionError.js +12 -0
  61. package/lib/error/TransactionError.js.map +1 -0
  62. package/lib/getConnectionInfo.d.ts +1 -0
  63. package/lib/getConnectionInfo.d.ts.map +1 -0
  64. package/lib/getConnectionInfo.js +30 -0
  65. package/lib/getConnectionInfo.js.map +1 -0
  66. package/lib/index.d.ts +1 -0
  67. package/lib/index.d.ts.map +1 -0
  68. package/lib/index.js +33 -70416
  69. package/lib/index.js.map +1 -7
  70. package/package.json +5 -5
  71. package/src/AbstractRepository.ts +26 -20
  72. package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +289 -37
  73. package/src/dynamodb/AbstractDynamoDbRepository.ts +140 -31
  74. package/src/dynamodb/getDynamoDbOptions.ts +1 -1
  75. package/tsconfig.json +5 -2
  76. package/tsconfig.tsbuildinfo +1 -1
  77. package/build.js +0 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/db-lib",
3
- "version": "1.71.2",
3
+ "version": "1.72.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "private": false,
@@ -8,7 +8,7 @@
8
8
  "access": "public"
9
9
  },
10
10
  "scripts": {
11
- "compile": "node build.js && tsc",
11
+ "compile": "tsc -b",
12
12
  "test": "jest -c jest.config.ts --testMatch=\"**/*.spec.ts\" --testMatch=\"!**/*.integration.spec.ts\" --passWithNoTests",
13
13
  "test:integration": "jest --testMatch=\"**/*.integration.spec.ts\"",
14
14
  "test:update-snapshots": "jest -c jest.config.ts --updateSnapshot",
@@ -19,7 +19,7 @@
19
19
  "devDependencies": {
20
20
  "@types/jest": "28.1.8",
21
21
  "@types/node": "20.12.4",
22
- "@types/pg": "8.6.6",
22
+ "@types/pg": "^8.11.8",
23
23
  "aws-sdk-client-mock": "^4.0.0",
24
24
  "aws-sdk-client-mock-jest": "4.0.0",
25
25
  "eslint": "8.33.0",
@@ -36,9 +36,9 @@
36
36
  "@aws-sdk/client-secrets-manager": "3.651.1",
37
37
  "@aws-sdk/lib-dynamodb": "^3.651.1",
38
38
  "@opentelemetry/api": "^1.6.0",
39
- "@squiz/dx-common-lib": "^1.66.4",
39
+ "@squiz/dx-common-lib": "^1.69.0",
40
40
  "@squiz/dx-logger-lib": "^1.64.0",
41
41
  "dotenv": "16.0.3",
42
- "pg": "8.9.0"
42
+ "pg": "^8.12.0"
43
43
  }
44
44
  }
@@ -1,4 +1,4 @@
1
- import { PoolClient, Pool } from 'pg';
1
+ import { PoolClient, Pool, escapeIdentifier } from 'pg';
2
2
  import { Repositories } from './Repositories';
3
3
  import { InvalidUpdateValueError } from '@squiz/dx-common-lib';
4
4
 
@@ -56,19 +56,29 @@ export abstract class AbstractRepository<SHAPE extends object, DATA_CLASS extend
56
56
  return await this.pool.connect();
57
57
  }
58
58
 
59
+ private sanitiseValue(value: Partial<SHAPE>) {
60
+ const sanitisedValue = Object.entries(value)
61
+ .map(([key, value]) => [this.modelPropertyToSqlColumn[key as keyof SHAPE], value] as const)
62
+ .filter(([column, _v]) => !!column);
63
+
64
+ const columns = sanitisedValue.map(([column]) => escapeIdentifier(column));
65
+ const bindingParams = sanitisedValue.map((_, index) => `$${index + 1}`);
66
+ const values = sanitisedValue.map(([_column, value]) => value);
67
+
68
+ return {
69
+ columns,
70
+ bindingParams,
71
+ values,
72
+ };
73
+ }
74
+
59
75
  async create(value: SHAPE, transactionClient: PoolClient | null = null): Promise<SHAPE> {
60
76
  const valueAsClass = new this.classRef(value as Record<string, unknown>);
61
- const columns = Object.keys(valueAsClass)
62
- .map((a) => `"${this.modelPropertyToSqlColumn[a as keyof SHAPE]}"`)
63
- .join(', ');
64
-
65
- const values = Object.values(valueAsClass)
66
- .map((a, index) => `$${index + 1}`)
67
- .join(', ');
77
+ const { columns, bindingParams, values } = this.sanitiseValue(valueAsClass);
68
78
 
69
79
  const result = await this.executeQuery(
70
- `INSERT INTO ${this.tableName} (${columns}) VALUES (${values}) RETURNING *`,
71
- Object.values(valueAsClass),
80
+ `INSERT INTO ${this.tableName} (${columns.join(', ')}) VALUES (${bindingParams.join(', ')}) RETURNING *`,
81
+ values,
72
82
  transactionClient,
73
83
  );
74
84
 
@@ -84,20 +94,16 @@ export abstract class AbstractRepository<SHAPE extends object, DATA_CLASS extend
84
94
  throw new InvalidUpdateValueError('Failed updating the repository, update values cannot be empty');
85
95
  }
86
96
  const whereValues = Object.values(where);
87
- const newValues = Object.values(newValue);
88
-
89
- const setString = Object.keys(newValue)
90
- .map((a, index) => `"${this.modelPropertyToSqlColumn[a as keyof SHAPE]}" = $${index + 1}`)
91
- .join(', ');
92
-
93
- const whereString = this.createWhereStringFromPartialModel(where, newValues.length);
97
+ const newValues = this.sanitiseValue(newValue);
98
+ const setValues = newValues.columns.map((c, i) => `${c} = ${newValues.bindingParams[i]}`);
99
+ const whereString = this.createWhereStringFromPartialModel(where, setValues.length);
94
100
 
95
101
  const result = await this.executeQuery(
96
102
  `UPDATE ${this.tableName}
97
- SET ${setString}
103
+ SET ${setValues.join(', ')}
98
104
  WHERE ${whereString}
99
105
  RETURNING *`,
100
- [...Object.values(newValues), ...Object.values(whereValues)],
106
+ [...newValues.values, ...whereValues],
101
107
  transactionClient,
102
108
  );
103
109
 
@@ -112,7 +118,7 @@ export abstract class AbstractRepository<SHAPE extends object, DATA_CLASS extend
112
118
 
113
119
  const result = await client.query(`DELETE FROM ${this.tableName} WHERE ${whereString}`, Object.values(where));
114
120
 
115
- return result.rowCount;
121
+ return result.rowCount ?? 0;
116
122
  } finally {
117
123
  if (client && !transactionClient) {
118
124
  client.release();
@@ -1,4 +1,4 @@
1
- import { AbstractDynamoDbRepository } from './AbstractDynamoDbRepository';
1
+ import { AbstractDynamoDbRepository, QueryOptions } from './AbstractDynamoDbRepository';
2
2
  import {
3
3
  DeleteCommand,
4
4
  DeleteCommandInput,
@@ -14,6 +14,8 @@ import {
14
14
  UpdateCommandInput,
15
15
  TransactWriteCommand,
16
16
  TransactWriteCommandInput,
17
+ BatchGetCommand,
18
+ BatchGetCommandInput,
17
19
  } from '@aws-sdk/lib-dynamodb';
18
20
  import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
19
21
  import { DynamoDbManager, Transaction } from './DynamoDbManager';
@@ -245,13 +247,11 @@ describe('AbstractRepository', () => {
245
247
  ConditionExpression: `attribute_exists(pk)`,
246
248
  };
247
249
 
248
- const partialItemWithKeyFields = {
249
- name: 'foo',
250
- };
251
250
  const updateItem = {
251
+ name: 'foo',
252
252
  country: 'au-updated',
253
253
  };
254
- const result = await repository.updateItem(partialItemWithKeyFields, updateItem);
254
+ const result = await repository.updateItem(updateItem);
255
255
  expect(ddbClientMock).toHaveReceivedNthCommandWith(2, UpdateCommand, input);
256
256
  expect(result).toEqual(
257
257
  new TestItem({
@@ -263,20 +263,49 @@ describe('AbstractRepository', () => {
263
263
  );
264
264
  });
265
265
 
266
- it('should undefined if item does does not exist', async () => {
266
+ it('should not trigger update request if the input attributes are same as in the existing item', async () => {
267
267
  ddbClientMock.on(GetCommand).resolves({
268
268
  $metadata: {
269
269
  httpStatusCode: 200,
270
270
  },
271
+ Item: {
272
+ name: 'foo',
273
+ age: 99,
274
+ country: 'au',
275
+ data: {},
276
+ },
271
277
  });
278
+ ddbClientMock.on(UpdateCommand).rejects(new Error('updateItem() called when not expected'));
272
279
 
273
- const partialItemWithKeyFields = {
280
+ // update input attributes are same as in the existing item
281
+ const updateItem = {
274
282
  name: 'foo',
283
+ country: 'au',
275
284
  };
285
+
286
+ const result = await repository.updateItem(updateItem);
287
+ expect(result).toEqual(
288
+ new TestItem({
289
+ name: 'foo',
290
+ age: 99,
291
+ country: 'au',
292
+ data: {},
293
+ }),
294
+ );
295
+ });
296
+
297
+ it('should return undefined if item does does not exist', async () => {
298
+ ddbClientMock.on(GetCommand).resolves({
299
+ $metadata: {
300
+ httpStatusCode: 200,
301
+ },
302
+ });
303
+
276
304
  const updateItem = {
305
+ name: 'foo',
277
306
  country: 'au-updated',
278
307
  };
279
- const result = await repository.updateItem(partialItemWithKeyFields, updateItem);
308
+ const result = await repository.updateItem(updateItem);
280
309
  expect(result).toEqual(undefined);
281
310
  });
282
311
 
@@ -300,13 +329,11 @@ describe('AbstractRepository', () => {
300
329
  }),
301
330
  );
302
331
 
303
- const partialItemWithKeyFields = {
304
- name: 'foo',
305
- };
306
332
  const updateItem = {
333
+ name: 'foo',
307
334
  country: 'au-updated',
308
335
  };
309
- const result = await repository.updateItem(partialItemWithKeyFields, updateItem);
336
+ const result = await repository.updateItem(updateItem);
310
337
  expect(result).toEqual(undefined);
311
338
  });
312
339
 
@@ -323,15 +350,13 @@ describe('AbstractRepository', () => {
323
350
  },
324
351
  });
325
352
 
326
- const partialItemWithKeyFields = {
327
- name: 'foo',
328
- };
329
353
  const updateItem = {
354
+ name: 'foo',
330
355
  country: 61, // should be "string" type
331
356
  };
332
- await expect(
333
- repository.updateItem(partialItemWithKeyFields, updateItem as unknown as Partial<TestItem>),
334
- ).rejects.toEqual(new Error('Invalid "country"'));
357
+ await expect(repository.updateItem(updateItem as unknown as Partial<TestItem>)).rejects.toEqual(
358
+ new Error('Invalid "country"'),
359
+ );
335
360
  });
336
361
 
337
362
  it('should throw error if excess column in input', async () => {
@@ -347,26 +372,22 @@ describe('AbstractRepository', () => {
347
372
  },
348
373
  });
349
374
 
350
- const partialItemWithKeyFields = {
351
- name: 'foo',
352
- };
353
375
  const updateItem = {
376
+ name: 'foo',
354
377
  country: 'au-updated',
355
378
  extra: '',
356
379
  };
357
- await expect(
358
- repository.updateItem(partialItemWithKeyFields, updateItem as unknown as Partial<TestItem>),
359
- ).rejects.toEqual(new InvalidDbSchemaError('Excess properties in entity test-item-entity: extra'));
380
+ await expect(repository.updateItem(updateItem as unknown as Partial<TestItem>)).rejects.toEqual(
381
+ new InvalidDbSchemaError('Excess properties in entity test-item-entity: extra'),
382
+ );
360
383
  });
361
384
 
362
385
  it('should throw error if input does not includes key field(s)', async () => {
363
- const partialItemWithKeyFields = {
364
- age: 99,
365
- };
366
386
  const updateItem = {
387
+ age: 99,
367
388
  country: 'au-updated', // should be "string" type
368
389
  };
369
- await expect(repository.updateItem(partialItemWithKeyFields, updateItem)).rejects.toEqual(
390
+ await expect(repository.updateItem(updateItem)).rejects.toEqual(
370
391
  new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
371
392
  );
372
393
  });
@@ -473,6 +494,120 @@ describe('AbstractRepository', () => {
473
494
  });
474
495
  });
475
496
 
497
+ describe('getItems()', () => {
498
+ it('should use BatchGetItem to get result', async () => {
499
+ ddbClientMock.on(BatchGetCommand).resolves({
500
+ $metadata: {
501
+ httpStatusCode: 200,
502
+ },
503
+ Responses: {
504
+ [TABLE_NAME]: [
505
+ {
506
+ name: 'foo',
507
+ age: 99,
508
+ country: 'au',
509
+ data: {},
510
+ data2: '{"foo":"bar","num":123}',
511
+ },
512
+ {
513
+ name: 'foo2',
514
+ age: 999,
515
+ country: 'au',
516
+ data: {},
517
+ data2: '{"foo":"bar","num":123}',
518
+ },
519
+ ],
520
+ },
521
+ });
522
+ const input: BatchGetCommandInput = {
523
+ RequestItems: {
524
+ [TABLE_NAME]: {
525
+ Keys: [
526
+ { pk: 'test_item#foo', sk: '#meta' },
527
+ { pk: 'test_item#foo2', sk: '#meta' },
528
+ ],
529
+ },
530
+ },
531
+ };
532
+
533
+ const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
534
+ const result = await repository.getItems(requestItems);
535
+ expect(ddbClientMock).toHaveReceivedCommandWith(BatchGetCommand, input);
536
+ expect(result).toEqual([
537
+ new TestItem({
538
+ name: 'foo',
539
+ age: 99,
540
+ country: 'au',
541
+ data: {},
542
+ data2: {
543
+ foo: 'bar',
544
+ num: 123,
545
+ },
546
+ }),
547
+ new TestItem({
548
+ name: 'foo2',
549
+ age: 999,
550
+ country: 'au',
551
+ data: {},
552
+ data2: {
553
+ foo: 'bar',
554
+ num: 123,
555
+ },
556
+ }),
557
+ ]);
558
+ });
559
+
560
+ it('should request BatchGetItem in batch of 100 items to get result', async () => {
561
+ ddbClientMock.on(BatchGetCommand).resolves({
562
+ $metadata: {
563
+ httpStatusCode: 200,
564
+ },
565
+ });
566
+
567
+ const requestItems = [];
568
+ for (let i = 0; i < 120; i++) {
569
+ requestItems.push({ name: `foo${i}` });
570
+ }
571
+ // keys for first batch request
572
+ const keys1 = [];
573
+ for (let i = 0; i < 100; i++) {
574
+ keys1.push({ pk: `test_item#foo${i}`, sk: '#meta' });
575
+ }
576
+ // keys for second batch request
577
+ const keys2 = [];
578
+ for (let i = 100; i < 120; i++) {
579
+ keys2.push({ pk: `test_item#foo${i}`, sk: '#meta' });
580
+ }
581
+
582
+ const input1: BatchGetCommandInput = {
583
+ RequestItems: {
584
+ [TABLE_NAME]: {
585
+ Keys: keys1,
586
+ },
587
+ },
588
+ };
589
+ const input2: BatchGetCommandInput = {
590
+ RequestItems: {
591
+ [TABLE_NAME]: {
592
+ Keys: keys2,
593
+ },
594
+ },
595
+ };
596
+
597
+ await repository.getItems(requestItems);
598
+ expect(ddbClientMock).toHaveReceivedCommandTimes(BatchGetCommand, 2);
599
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchGetCommand, input1);
600
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchGetCommand, input2);
601
+ });
602
+
603
+ it('should throw error if any input item does not includes key field(s)', async () => {
604
+ const requestItems = [{ name: 'foo' }, { age: 22 }];
605
+ await expect(repository.getItems(requestItems)).rejects.toEqual(
606
+ new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
607
+ );
608
+ });
609
+ });
610
+
476
611
  describe('queryItems()', () => {
477
612
  it('should return the items if found', async () => {
478
613
  ddbClientMock.on(QueryCommand).resolves({
@@ -555,8 +690,7 @@ describe('AbstractRepository', () => {
555
690
  };
556
691
 
557
692
  const partialItem = { name: 'foo' };
558
- const useSortKey = true;
559
- const _result = await repository.queryItems(partialItem, useSortKey);
693
+ const _result = await repository.queryItems(partialItem, { useSortKey: true });
560
694
  expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
561
695
  });
562
696
 
@@ -595,7 +729,7 @@ describe('AbstractRepository', () => {
595
729
 
596
730
  const partialItem = { country: 'au' };
597
731
  const useSortKey = false;
598
- const result = await repository.queryItems(partialItem, useSortKey, index);
732
+ const result = await repository.queryItems(partialItem, { useSortKey, index });
599
733
  expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
600
734
  expect(result).toEqual([
601
735
  new TestItem({
@@ -636,14 +770,134 @@ describe('AbstractRepository', () => {
636
770
 
637
771
  const partialItem = { age: 99, country: 'au' };
638
772
  const useSortKey = true;
639
- const _result = await repository.queryItems(partialItem, useSortKey, index);
773
+ const _result = await repository.queryItems(partialItem, { useSortKey, index });
774
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
775
+ });
776
+
777
+ it('should set input query correctly when "filter - begins_with" query option is set', async () => {
778
+ ddbClientMock.on(QueryCommand).resolves({
779
+ $metadata: {
780
+ httpStatusCode: 200,
781
+ },
782
+ });
783
+ const input: QueryCommandInput = {
784
+ TableName: TABLE_NAME,
785
+ KeyConditionExpression: '#pkName = :pkValue AND begins_with(#skName, :skValue)',
786
+ ExpressionAttributeNames: {
787
+ '#pkName': 'pk',
788
+ '#skName': 'sk',
789
+ },
790
+ ExpressionAttributeValues: {
791
+ ':pkValue': 'test_item#foo',
792
+ ':skValue': 'keyword-x',
793
+ },
794
+ };
795
+
796
+ const partialItem = { name: 'foo' };
797
+ const queryOptions: QueryOptions = {
798
+ filter: {
799
+ type: 'begins_with',
800
+ keyword: 'keyword-x',
801
+ },
802
+ };
803
+ await repository.queryItems(partialItem, queryOptions);
804
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
805
+ });
806
+
807
+ it('should throw error invalid "filter" query option is set', async () => {
808
+ ddbClientMock.on(QueryCommand).resolves({
809
+ $metadata: {
810
+ httpStatusCode: 200,
811
+ },
812
+ });
813
+ const partialItem = { name: 'foo' };
814
+ const queryOptions = {
815
+ filter: {
816
+ type: 'invalid-type',
817
+ keyword: 'keyword-x',
818
+ },
819
+ } as unknown as QueryOptions;
820
+ await expect(repository.queryItems(partialItem, queryOptions)).rejects.toEqual(
821
+ new Error(`Invalid query filter type: invalid-type`),
822
+ );
823
+ });
824
+
825
+ it('should set input query correctly when "limit" query option is set', async () => {
826
+ ddbClientMock.on(QueryCommand).resolves({
827
+ $metadata: {
828
+ httpStatusCode: 200,
829
+ },
830
+ });
831
+ const input: QueryCommandInput = {
832
+ TableName: TABLE_NAME,
833
+ KeyConditionExpression: '#pkName = :pkValue',
834
+ ExpressionAttributeNames: {
835
+ '#pkName': 'pk',
836
+ },
837
+ ExpressionAttributeValues: {
838
+ ':pkValue': 'test_item#foo',
839
+ },
840
+ Limit: 33,
841
+ };
842
+
843
+ const partialItem = { name: 'foo' };
844
+ const queryOptions = { limit: 33 };
845
+ await repository.queryItems(partialItem, queryOptions);
846
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
847
+ });
848
+
849
+ it('should set input query correctly when "order asc" query option is set', async () => {
850
+ ddbClientMock.on(QueryCommand).resolves({
851
+ $metadata: {
852
+ httpStatusCode: 200,
853
+ },
854
+ });
855
+ const input: QueryCommandInput = {
856
+ TableName: TABLE_NAME,
857
+ KeyConditionExpression: '#pkName = :pkValue',
858
+ ExpressionAttributeNames: {
859
+ '#pkName': 'pk',
860
+ },
861
+ ExpressionAttributeValues: {
862
+ ':pkValue': 'test_item#foo',
863
+ },
864
+ ScanIndexForward: true,
865
+ };
866
+
867
+ const partialItem = { name: 'foo' };
868
+ const queryOptions = { order: 'asc' } as unknown as QueryOptions;
869
+ await repository.queryItems(partialItem, queryOptions);
870
+ expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
871
+ });
872
+
873
+ it('should set input query correctly when "order desc" query option is set', async () => {
874
+ ddbClientMock.on(QueryCommand).resolves({
875
+ $metadata: {
876
+ httpStatusCode: 200,
877
+ },
878
+ });
879
+ const input: QueryCommandInput = {
880
+ TableName: TABLE_NAME,
881
+ KeyConditionExpression: '#pkName = :pkValue',
882
+ ExpressionAttributeNames: {
883
+ '#pkName': 'pk',
884
+ },
885
+ ExpressionAttributeValues: {
886
+ ':pkValue': 'test_item#foo',
887
+ },
888
+ ScanIndexForward: false,
889
+ };
890
+
891
+ const partialItem = { name: 'foo' };
892
+ const queryOptions = { order: 'desc' } as unknown as QueryOptions;
893
+ await repository.queryItems(partialItem, queryOptions);
640
894
  expect(ddbClientMock).toHaveReceivedCommandWith(QueryCommand, input);
641
895
  });
642
896
 
643
897
  it('should throw error for invalid index query', async () => {
644
898
  const index = 'undefined-index';
645
899
  const partialItem = { age: 99, country: 'au' };
646
- await expect(repository.queryItems(partialItem, false, index)).rejects.toEqual(
900
+ await expect(repository.queryItems(partialItem, { index })).rejects.toEqual(
647
901
  new MissingKeyValuesError(`Table index '${index}' not defined on entity test-item-entity`),
648
902
  );
649
903
  });
@@ -659,7 +913,7 @@ describe('AbstractRepository', () => {
659
913
  const partialItem = { name: 'foo' };
660
914
  const useSortKey = false;
661
915
  const index = 'gsi1_pk-gsi1_sk-index';
662
- await expect(repository.queryItems(partialItem, useSortKey, index)).rejects.toEqual(
916
+ await expect(repository.queryItems(partialItem, { useSortKey, index })).rejects.toEqual(
663
917
  new MissingKeyValuesError(`Key field "country" must be specified in the input item in entity test-item-entity`),
664
918
  );
665
919
  });
@@ -668,7 +922,7 @@ describe('AbstractRepository', () => {
668
922
  const partialItem = { country: 'au' };
669
923
  const useSortKey = true;
670
924
  const index = 'gsi1_pk-gsi1_sk-index';
671
- await expect(repository.queryItems(partialItem, useSortKey, index)).rejects.toEqual(
925
+ await expect(repository.queryItems(partialItem, { useSortKey, index })).rejects.toEqual(
672
926
  new MissingKeyValuesError(`Key field "age" must be specified in the input item in entity test-item-entity`),
673
927
  );
674
928
  });
@@ -752,8 +1006,6 @@ describe('AbstractRepository', () => {
752
1006
  await repository.updateItem(
753
1007
  {
754
1008
  name: 'foo2',
755
- },
756
- {
757
1009
  age: 55,
758
1010
  },
759
1011
  transaction,