Skip to content

Commit

Permalink
Merge branch 'master' of github.com:hitz-group/dynamo-helper
Browse files Browse the repository at this point in the history
  • Loading branch information
shidil committed Aug 10, 2020
2 parents e1b199d + c962e1a commit 1837bc6
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 1837bc6

Please sign in to comment.