arrow-store
v1.0.2
Published
TypeScript DynamoDB ORM
Downloads
20
Maintainers
Readme
ArrowStore
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 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:
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:
aws dynamodb get-item \
--table-name MyDynamoDBTable \
--key '{"PartitionKey": {"S": "ClockRecord"}, "SecondaryKey": {"S": "DW8F1"}}'
PutItem
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:
aws dynamodb put-item \
--table-name MyDynamoDBTable \
--condition-expression "attribute_not_exists(SecondaryKey)" \
--item file://item.json
UpdateItem
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:
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:
{
"#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
{
":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:
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
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:
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
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";
}
}
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:
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
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
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
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
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.memberquery => !!query.booleanMember |
| attribute_not_exists(path) | query => !query.memberquery => !!!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)attributeNames: {#string_set_attr: "COLORS"}attributeValues: {":v_colors": {"S": "Red"}} | query => query.colorsSet.contains("Red") |
| contains(#string_attr, :v_sub)attributeNames: {#string_attr: "NAME"}attributeValues: {":v_sub": {"S": "the"}} | query => query.stringMember.contains("the") |
| size(path) = :v_num | query => Checks the string length: query.stringMember.length === 10
Checks the string set size: query => query.colorsSet.length === 3
|
Update Expressions
| AWS DynamoDB Expression | Arrow Function | |--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------| | SET Price = Price - :pwhere {":p": {"N": "5"}} | updater => updater.set(x => x.price = x.price - 5) | | SET Colors = list_append(Colors, :v_colors)where {":v_colors": {"L": [{"S": "Red"}]}} | updater => updater.set(x => x.colors = x.colors.concat('Red')) | | SET Colors = list_append(:v_colors, Colors)where {":v_colors": {"L": [{"S": "Red"}]}} | updater => updater.set((x, ctx) => x.colors = ctx.additionalColors.concat(x.colors)) | | ADD Colors :v_colorswhere {":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 |