npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

arrow-store

v1.0.2

Published

TypeScript DynamoDB ORM

Downloads

20

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 === 10Checks 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 |