Skip to content

Commit

Permalink
Make sort key optional, support getItem, deleteItem and exists operat…
Browse files Browse the repository at this point in the history
…ions with partition key only (#4)

Description

    Makes sort key name optional in TableIndex when specifying TableConfig. This supports table with only partition key configured.
    getItem, deleteItem and exists now supports operations with only partition key or both keys.

Signature for getItem, deleteItem and exists has changed, now they accepts key: DocumentClient.Key instead of pk and sk as separate arguments.

async getItem<T>( key: DocumentClient.Key,  fields?: Array<keyof T>): Promise<T>

example:

client.getItem({ id: 'book-123' });
client.getItem({ pk: 'library#books', sk: 'book-123' }, ['id', 'isActive']);

Related Issues

Issue: #3
How Has This Been Tested

Unit tests
Types of changes

Bug fix (non-breaking change which fixes an issue)
New feature (non-breaking change which adds functionality)

    Breaking change (fix or feature that would cause existing functionality to change)

Checklist

My code follows the code style of this project.
My change requires a change to the documentation.
I have updated the documentation accordingly.
I have added tests to cover my changes.
All new and existing tests passed.
  • Loading branch information
shidil authored Aug 9, 2020
1 parent 75d8c7c commit c962e1a
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 140 deletions.
49 changes: 31 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ Import DynamoHelper
```typescript
import { DynamoHelper } from '@hitz-group/dynamo-helper';

const { DynamoHelper} = require('@hitz-group/dynamo-helper');

const { DynamoHelper } = require('@hitz-group/dynamo-helper');
```

Use constructor to create the DynamoHelper instance
Expand All @@ -41,34 +40,35 @@ export interface TableConfig {
import { DynamoHelper } from '@hitz-group/dynamo-helper';

const table = {
name: 'my-ddb-table',
indexes: {
default: {
partitionKeyName: 'pk',
sortKeyName: 'sk',
}
}
name: 'my-ddb-table',
indexes: {
default: {
partitionKeyName: 'pk',
sortKeyName: 'sk',
},
},
};
const client = new DynamoHelper(table, 'ap-south-1');

await client.getItem('library#books', 'book-123');
await client.getItem({ id: 'book-123' });
await client.getItem({ pk: 'library#books', sk: 'book-123' });

await client.query({
where: {
pk: 'library#books',
publishedAt: {
between: [15550000, 15800000]
}
}
between: [15550000, 15800000],
},
},
});

```

### buildQueryTableParams

```typescript
import { buildQueryTableParams } from '@hitz-group/dynamo-helper';

const { buildQueryTableParams} = require('@hitz-group/dynamo-helper');
const { buildQueryTableParams } = require('@hitz-group/dynamo-helper');
```

This method generates DynamoDB query input params from given filter object of type `Filter<T>`
Expand Down Expand Up @@ -175,12 +175,25 @@ const products = await dynamoHelper.query<ProductModel>({
### getItem

Fetch an item using pk and sk combination. Returns item if found or returns null
`getItem<T>(key: DocumentClient.Key, fields: Array<keyof T>)`

#### key

Required, at least partition key values must be provided.

#### fields

Optional, specify fields to project

```typescript
import { getItem } from '@hitz-group/dynamo-helper';

// Get a single product matching the key
const product = await dynamoHelper.getItem<ProductModel>('org_uuid', 'product_xxx');
await dynamoHelper.getItem<ProductModel>({ pk: 'org_uuid', sk: 'product_xxx' });
await dynamoHelper.getItem<ProductModel>({ id: 'product_xxx' }, [
'id',
'isActive',
]);
```

### batchGetItems
Expand All @@ -205,7 +218,7 @@ Check if an item exists in the database with the keys provided. Returns a boolea
import { exists } from '@hitz-group/dynamo-helper';

// Check if product already exists
if (await dynamoHelper.exists('x', 1)) {
if (await dynamoHelper.exists({ id: 'x' })) {
console.log('exists');
}
```
Expand All @@ -232,7 +245,7 @@ Remove an item from database matching the key provided if it exists
import { deleteItem } from '@hitz-group/dynamo-helper';

// delete product
await dynamoHelper.deleteItem('x', '1'});
await dynamoHelper.deleteItem({ id: '1' });
```

### transactPutItems
Expand Down
14 changes: 6 additions & 8 deletions src/DynamoHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,10 @@ export class DynamoHelper {
}

async getItem<T extends AnyObject>(
pk: string,
sk: string,
key: DocumentClient.Key,
fields?: Array<keyof T>,
): Promise<T> {
return getItem(this.dbClient, this.table, pk, sk, fields);
return getItem(this.dbClient, this.table, key, fields);
}

async batchGetItems(
Expand All @@ -60,8 +59,8 @@ export class DynamoHelper {
return batchGetItems(this.dbClient, this.table, keys, fields);
}

async exists(pk: string, sk: string): Promise<boolean> {
return exists(this.dbClient, this.table, pk, sk);
async exists(key: DocumentClient.Key): Promise<boolean> {
return exists(this.dbClient, this.table, key);
}

async batchExists(
Expand All @@ -71,10 +70,9 @@ export class DynamoHelper {
}

async deleteItem(
pk: string,
sk: string,
key: DocumentClient.Key,
): Promise<PromiseResult<DeleteItemOutput, AWSError>> {
return deleteItem(this.dbClient, this.table, pk, sk);
return deleteItem(this.dbClient, this.table, key);
}

async batchDeleteItems(
Expand Down
64 changes: 34 additions & 30 deletions src/mutation/deleteItem.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,48 @@ describe('deleteItem', () => {
expect(typeof deleteItem).toBe('function');
});

test('argument validation', async () => {
await expect(deleteItem(undefined)).rejects.toThrowError(
'Expected key to be of type object and not empty',
);
await expect(deleteItem(null)).rejects.toThrowError(
'Expected key to be of type object and not empty',
);
await expect(deleteItem('null')).rejects.toThrowError(
'Expected key to be of type object and not empty',
);
await expect(deleteItem(2 as never, '')).rejects.toThrowError(
'Expected key to be of type object and not empty',
);
});

test('key validation', async () => {
await expect(deleteItem({ id: 'string' })).rejects.toThrowError(
'Invalid key: expected key to contain at least partition key',
);
await expect(deleteItem({ pk: 'string' })).resolves.not.toThrow();
// Custom partition key name in table config
await expect(
deleteItemMethod(
testClient,
{ ...testTableConf, indexes: { default: { partitionKeyName: 'id' } } },
{ id: 'string' },
),
).resolves.not.toThrow();
});

test('promise rejection', async () => {
spy.mockReturnValue({
promise: jest.fn().mockRejectedValue([]),
});

await expect(deleteItem('xxxx', 'yyyy')).rejects.toStrictEqual([]);
await expect(deleteItem({ pk: 'xxxx', sk: 'yyyy' })).rejects.toStrictEqual(
[],
);
});

test('uses delete correctly', async () => {
await deleteItem('xxxx', 'yyyy');
await deleteItem({ pk: 'xxxx', sk: 'yyyy' });
expect(spy).toHaveBeenCalledWith({
TableName: testTableConf.name,
Key: {
Expand All @@ -40,32 +72,4 @@ describe('deleteItem', () => {
},
} as DeleteItemInput);
});

test('uses key names from table index configuration', async () => {
spy.mockReturnValue({
promise: jest.fn().mockResolvedValue({ Item: { id: 'xxxx' } }),
});

await deleteItemMethod(
testClient,
{
...testTableConf,
indexes: {
default: {
partitionKeyName: 'key1',
sortKeyName: 'key2',
},
},
},
'xxxx',
'yyyy',
);
expect(testClient.delete).toHaveBeenCalledWith({
TableName: testTableConf.name,
Key: {
key1: 'xxxx',
key2: 'yyyy',
},
});
});
});
18 changes: 12 additions & 6 deletions src/mutation/deleteItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,24 @@ import { TableConfig } from '../types';
export async function deleteItem(
dbClient: DocumentClient,
table: TableConfig,
pk: string,
sk: string,
key: DocumentClient.Key,
): Promise<PromiseResult<DocumentClient.DeleteItemOutput, AWSError>> {
const index = table.indexes.default;

if (!key || typeof key !== 'object' || Object.keys(key).length === 0) {
throw new Error('Expected key to be of type object and not empty');
}

if (!key[index.partitionKeyName]) {
throw new Error(
'Invalid key: expected key to contain at least partition key',
);
}

return dbClient
.delete({
TableName: table.name,
Key: {
[index.partitionKeyName]: pk,
[index.sortKeyName]: sk,
},
Key: key,
})
.promise();
}
24 changes: 9 additions & 15 deletions src/query/exists.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,29 @@ describe('exists', () => {
});

test('validates arguments', async () => {
await expect(exists(undefined, undefined)).rejects.toThrowError(
'Expected two arguments of type string, string received undefined, undefined',
await expect(exists(undefined)).rejects.toThrowError(
'Expected key to be of type object and not empty',
);
await expect(exists(null, null)).rejects.toThrowError(
'Expected two arguments of type string, string received object, object',
await expect(exists(null)).rejects.toThrowError(
'Expected key to be of type object and not empty',
);
await expect(exists('null', null)).rejects.toThrowError(
'Expected two arguments of type string, string received string, object',
);
await expect(exists(undefined, '')).rejects.toThrowError(
'Expected two arguments of type string, string received undefined, string',
await expect(exists('null')).rejects.toThrowError(
'Expected key to be of type object and not empty',
);
await expect(exists(2 as never, '')).rejects.toThrowError(
'Expected two arguments of type string, string received number, string',
);
await expect(exists('', '')).rejects.toThrowError(
'Expected both arguments to have length greater than 0',
'Expected key to be of type object and not empty',
);
});

test('returns boolean value', async () => {
// No results found, hence empty list.
// getItem will return null in this case
await expect(exists('xxxx', 'yyyy')).resolves.toBe(false);
await expect(exists({ pk: 'xxxx', sk: 'yyyy' })).resolves.toBe(false);

spy.mockReturnValue({
promise: jest.fn().mockResolvedValue({ Item: { id: 'xxxx' } }),
});

await expect(exists('xxxx', 'yyyy')).resolves.toBe(true);
await expect(exists({ pk: 'xxxx', sk: 'yyyy' })).resolves.toBe(true);
});
});
5 changes: 2 additions & 3 deletions src/query/exists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import { TableConfig } from '../types';
export async function exists(
dbClient: DocumentClient,
table: TableConfig,
pk: string,
sk: string
key: DocumentClient.Key,
): Promise<boolean> {
const item = await getItem(dbClient, table, pk, sk, ['id']);
const item = await getItem(dbClient, table, key, ['id']);
return item ? true : false;
}
Loading

0 comments on commit c962e1a

Please sign in to comment.