arrow-store
Version:
TypeScript DynamoDB ORM
316 lines (282 loc) • 16.4 kB
Markdown
<p align="center">
<a href="https://www.npmjs.com/package/arrow-store/"><img src="arrow-store.png" width="150" /></a>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/arrow-store">
<img src="https://img.shields.io/npm/v/arrow-store.svg" alt="npm version" >
</a>
<a href="LICENSE">
<img src="https://img.shields.io/npm/l/arrow-store.svg" alt="license">
</a>
</p>
<h1 align="center">ArrowStore</h1>
# ArrowStore
```shell
npm i arrow-store
```
ArrowStore is an extensible TypeScript object-relational mapper for AWS DynamoDB that simplifies the DynamoDB API usage for developers.
See the working version of [AWS Lambda Sample project](/samples) code to play and deploy in your environment.
ArrowStore allows developers to:
* Map a received set of DynamoDB AttributeValues to an object
* Map an object to a set of DynamoDB AttributeValues
* Query a table's partition with an optional sort key attribute and comparison operator and filter expressions
* Put, Delete and Update a record with an optional condition expression (e.g. atomic operation)
* Batch Put, Batch Delete and Batch Get records
* Transactional Put, Delete, Update records with optional condition expressions and condition expression for the whole transaction
## ArrowStore ORM Considerations
The library was created to make it easy for new developers to start working on your project without going to deep into AWS DynamoDB API.
* A basic understanding of the AWS DynamoDB is highly recommended to avoid common pitfalls.
* The ArrowStore library leverages AWS DynamoDB Low-Level API
* The ArrowStore's parser reads the ES6 arrow function - it stringifies the arrow function and builds an AST tree. Avoid using syntactic sugar, such as a question mark (?) when checking for an empty value or build an if-else statement. A JS transpiler will expand it to a function with a body and the ArrowStore's engine will not be able to build an AST Tree.
* No scopes are supported when passing an object's accessor value from a local scope. In the arrow function you must specify an object accessor and pass this object as an argument or use constant values without accessors
* Each requested object must have a mapping schema - from and to DynamoDB's AttributeValue. No raw-requests are supported
* Projections are not supported yet and, currently, there are no plans to implement it yet
* List (L), Binary (B) and Binary Set (BS) attribute values are not supported
## DynamoDB AttributeValue Mappings
Consider a JSON object example:
```
{
"clockType": "Hybrid",
"clockModel": "DW8F1",
"brand": "Fossil",
"regulatory": {
"availableInCountries": ["USA", "CAN", "CHN"],
"madeUtc": "2021-01-30T18:05:56.001Z",
"partNumber": 106956,
"isDemoVersion": false
}
}
```
# DynamoDB Requests
## GetItem
With the object defined above, we'll show you how to send a GetItem-request with the ArrowStore DynamoDB Client:
```typescript
import {DefaultDynamoDBClient, DynamoDBClientResolver} from "arrow-store";
class AppDynamoDBClientResolver implements DynamoDBClientResolver {
resolve(): DynamoDB {
config.update({region: 'us-west-2'});
const credentials = new SharedIniFileCredentials({profile: 'arrow-store'});
config.credentials = credentials;
const client = new DynamoDB();
return client;
}
}
export async function getClockRecordAsync(clockModel: string): Promise<ClockRecord | null> {
const client = new DefaultDynamoDBClient(new AppDynamoDBClientResolver(), schemaProvider, new DefaultDynamoDBRecordMapper(schemaProvider));
const record = await client.getAsync(new ClockRecordId(clockModel));
return record;
}
```
This operation will result in the following request:
```shell
aws dynamodb get-item \
--table-name MyDynamoDBTable \
--key '{"PartitionKey": {"S": "ClockRecord"}, "SecondaryKey": {"S": "DW8F1"}}'
```
## PutItem
```typescript
export async function putClockRecordAsync(clockRecord: ClockRecord): Promise {
const client = new DefaultDynamoDBClient(new AppDynamoDBClientResolver(), schemaProvider, new DefaultDynamoDBRecordMapper(schemaProvider));
const isSaved = await dynamoService
.put(clockRecord)
.when(x => !x.clockModel)
.executeAsync();
}
```
The putClockRecordAsync-method call will result in:
```shell
aws dynamodb put-item \
--table-name MyDynamoDBTable \
--condition-expression "attribute_not_exists(SecondaryKey)" \
--item file://item.json
```
## UpdateItem
```typescript
export async function updateClockRecordAsync(clockRecordId: ClockRecordId): Promise {
const client = new DefaultDynamoDBClient(new AppDynamoDBClientResolver(), schemaProvider, new DefaultDynamoDBRecordMapper(schemaProvider));
const params = {countries: ["ITL"]};
const updated = await dynamoService
.update(clockRecordId)
.when(x => !!x.regulatory.madeUtc)
.set((x, ctx) => x.regulatory.availableInCountries.push(...ctx.countries), params)
.setWhenNotExists(x => x.regulatory.isDemoVersion, x => x.regulatory.isDemoVersion = true)
.set(x => x.clockType = "Analog")
.set(x => x.partNumber += 5)
.destroy(x => x.regulatory.madeUtc)
.executeAsync();
}
```
The updateClockRecordAsync-method call will result in:
```shell
aws dynamodb update-item \
--table-name MyDynamoDBTable \
--key '{"PartitionKey": {"S": "ClockRecord"}, "SecondaryKey": {"S": "DW8F1"}}'
--condition-expression "attribute_exists(#attr_0.#attr_1.#attr_2)"
--update-expression "ADD #attr_0.#attr_1.#attr_3 :attr_val_0,
#attr_0.#attr_1.#attr_5 :attr_val_3
SET #attr_0.#attr_1.#attr_6 = if_not_exists(#attr_0.#attr_1.#attr_6, :attr_val_1)",
#attr_0.#attr_4 = :attr_val_2
REMOVE #attr_0.#attr_1.#attr_2
--expression-attribute-names file://attr-names.json
--expression-attribute-values file://attr-values.json
```
./attr-names.json:
```json
{
"#attr_0": "RECORD_DATA",
"#attr_1": "REGULATORY",
"#attr_2": "MADE_DATE_UTC",
"#attr_3": "AVAILABLE_IN_COUNTRIES",
"#attr_4": "CLOCK_TYPE",
"#attr_5": "PART_NUMBER",
"#attr_6": "IS_DEMO"
}
```
./attr-values.json
```json
{
":attr_val_0": {
"SS": ["ITL"]
},
":attr_val_1": {
"BOOL": true
},
":attr_val_2": {
"S": "Analog"
},
":attr_val_3": {
"N": "5"
}
}
```
The same call outcome but with expanded attribute names and values for a better readability:
```shell
aws dynamodb update-item \
--table-name MyDynamoDBTable \
--key '{"PartitionKey": {"S": "ClockRecord"}, "SecondaryKey": {"S": "DW8F1"}}'
--condition-expression "attribute_exists(RECORD_DATA.REGULATORY.MADE_DATE_UTC)"
--update-expression 'ADD RECORD_DATA.REGULATORY.AVAILABLE_IN_COUNTRIES {"SS": ["ITL"]},
RECORD_DATA.REGULATORY.PART_NUMBER {"N": "5"}
SET RECORD_DATA.REGULATORY.IS_DEMO = if_not_exists(RECORD_DATA.REGULATORY.IS_DEMO, {"BOOL": true}),
RECORD_DATA.CLOCK_TYPE = {"S": "Analog"}
REMOVE RECORD_DATA.REGULATORY.MADE_DATE_UTC'
```
## DeleteItem
```typescript
export async function deleteItemAsync(clockRecordId: ClockRecordId): Promise<void> {
const client = new DefaultDynamoDBClient(new AppDynamoDBClientResolver(), schemaProvider, new DefaultDynamoDBRecordMapper(schemaProvider));
const removed = await dynamoService
.delete(clockRecordId)
.when(x => !!x.regulatory.isDemoVersion || x.clockType === "Analog")
.executeAsync();
}
```
The outcome:
```shell
aws dynamodb delete-item \
--table-name MyDynamoDBTable \
--key '{"PartitionKey": {"S": "ClockRecord"}, "SecondaryKey": {"S": "DW8F1"}}'
--condition-expression 'attribute_exists(RECORD_DATA.REGULATORY.IS_DEMO) OR RECORD_DATA.CLOCK_TYPE = {'S": "Analog'}'
```
## Query
```typescript
export class ClockRecordsQuery implements ArrowStoreTypeRecordId<ClockRecord> {
getPrimaryKeys(): ReadonlyArray<PrimaryAttributeValue> {
return [new PartitionKey('ClockRecord')];
}
getRecordTypeId(): string {
return RECORD_TYPES.ClockRecord;
}
getCtor(): ArrowStoreRecordCtor<ClockRecord> {
return ClockRecord;
}
getIndexName(): string | undefined {
return undefined;
}
isConsistentRead(): boolean {
return false;
}
getTableName(): string {
return "MyDynamoDBTable";
}
}
```
```typescript
import {ClockRecordsQuery} from "./models";
export async function queryClockRecordsAsync(): Promise<ClockRecord[]> {
const client = new DefaultDynamoDBClient(new AppDynamoDBClientResolver(), schemaProvider, new DefaultDynamoDBRecordMapper(schemaProvider));
const queryResult = await dynamoService
.query(new ClockRecordsQuery())
.where(x => x.brand.startsWith("F") && x.regulatory.isDemoVersion && x.regulatory.availableInCountries.includes("USA"))
.executeAsync();
return queryResult.records;
}
```
Will result in:
```shell
aws dynamodb query \
--table-name MyDynamoDBTable
--key-condition-expression 'PartitionKey = :attr_val_0' \
--filter-expression 'begins_with(RECORD_DATA.BRAND, :attr_val_1 AND RECORD_DATA.REGULATORY.IS_DEMO = :attr_val_2 AND contains(RECORD_DATA.REGULATORY.AVAILABLE_IN_COUNTRIES, :attr_val_3))'
--expression-attribute-values '{":attr_val_0":{"S":"ClockRecord"}, ":attr_val_1": {"S": "F"}, ":attr_val_2": {"BOOL": true}, ":attr_val_3": {"S": "USA"}}'
```
## BatchGetItem
```typescript
export async function batchGetAsync(recordIds: ArrowStoreRecordId[]): Promise<DynamoDBRecord[]> {
const client = new DefaultDynamoDBClient(new AppDynamoDBClientResolver(), schemaProvider, new DefaultDynamoDBRecordMapper(schemaProvider));
return await client.batchGetAsync(recordIds);
}
```
In this BatchGetItems example, the DynamoDBService call of batchGetAsync returns the requested records, and also populate the array of GetRecordInBatchRequest with the result per requested ID for convenience.
## BatchWriteItem
```typescript
import {DynamoDBRecordIndex} from "./record";
export async function batchWriteAsync(putRecord: ArrowStoreRecord, deleteRecordId: ArrowStoreRecordId): Promise<void> {
const client = new DefaultDynamoDBClient(new AppDynamoDBClientResolver(), schemaProvider, new DefaultDynamoDBRecordMapper(schemaProvider));
await client.batchWriteAsync(writer => writer.put(record).delete(deleteRecordId));
}
```
## TransactWriteItem
```typescript
export async function transactWriteAsync(putRecord: ArrowStoreRecord, deleteRecordId: ArrowStoreRecordId): Promise<void> {
const client = new DefaultDynamoDBClient(new AppDynamoDBClientResolver(), schemaProvider, new DefaultDynamoDBRecordMapper(schemaProvider));
await client
.transactWriteItems("some-idempotency-key")
.when(new ClockRecordId("DW"), x => x.clockType === "Digital")
.delete(new ClockRecordId("CAS123"), deleteCondition => deleteCondition.when(x => !!x.clockType))
.put(clockRecord, putCondition => putCondition.when(x => !!x.clockType))
.update(new ClockRecordId("UNKNOWN"), updater => {
updater
.set(x => x.clockType = "Analog")
.destroy(x => x.isDemoVersion)
.when(x => x.clockType === "Digital");
})
.executeAsync();
}
```
## TransactGetItem
```typescript
export async function transactGetAsync(recordIds: ArrowStoreRecordId[]): Promise<any[]> {
const client = new DefaultDynamoDBClient(new AppDynamoDBClientResolver(), schemaProvider, new DefaultDynamoDBRecordMapper(schemaProvider));
return await client.transactGetItemsAsync(recordIds);
}
```
## Function Expressions
| AWS DynamoDB Expression | Arrow Function |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| attribute_exists(_path_) | query => !!query.member<br/>query => !!query.booleanMember |
| attribute_not_exists(_path_) | query => !query.member<br/>query => !!!query.booleanMember |
| begins_with(_path_, _substr_) | query => query.stringMember.startsWith("_substr_") |
| not begins_with(_path_, _substr_) | query => query.stringMember.startsWith("_substr_") |
| contains(#string_set_attr, :v_colors)<br/>attributeNames: {<br/>#string_set_attr: "COLORS"<br/>}<br/>attributeValues: {<br/>":v_colors": {"S": "Red"}<br/>} | query => query.colorsSet.contains("Red") |
| contains(#string_attr, :v_sub)<br/>attributeNames: {<br/>#string_attr: "NAME"<br/>}<br/>attributeValues: {<br/>":v_sub": {"S": "the"}<br/>} | query => query.stringMember.contains("the") |
| size(_path_) = :v_num | query => Checks the string length: ```query.stringMember.length === 10```<br/>Checks the string set size: ```query => query.colorsSet.length === 3``` |
## Update Expressions
| AWS DynamoDB Expression | Arrow Function |
|--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| SET Price = Price - :p<br/>where {":p": {"N": "5"}} | updater => updater.set(x => x.price = x.price - 5) |
| SET Colors = list_append(Colors, :v_colors)<br/>where {":v_colors": {"L": [{"S": "Red"}]}} | updater => updater.set(x => x.colors = x.colors.concat('Red')) |
| SET Colors = list_append(:v_colors, Colors)<br/>where {":v_colors": {"L": [{"S": "Red"}]}} | updater => updater.set((x, ctx) => x.colors = ctx.additionalColors.concat(x.colors)) |
| ADD Colors :v_colors<br/>where {":v_colors": {"S": "Red"}} | updater => updater.set(x => x.colors.push("Red") |
| REMOVE Colors[0], Colors[1] | updater => updater.set(x => x.colors.splice(0, 1) |
| DELETE Color :v_colors | *IN PROGRESS* |