UNPKG

arrow-store

Version:
316 lines (282 loc) 16.4 kB
<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* |