0.7.9 • Published 9 months ago

typesafe-dynamo v0.7.9

Weekly downloads
-
License
MIT
Repository
github
Last release
9 months ago

typesafe-dynamo

Provide type-safe query operations for AWS DynamoDB.

Quick start

npm i --save typesafe-dynamo

Example

import { DynamoDB } from "aws-sdk";
import typesafe from "typesafe-dynamo";

const client = new DynamoDB.DocumentClient({
    region: "ap-northeast-2",
});

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "id">(client, "user");

await table.put(({ values }) => [
    values({
        id: "0000",
        name: "jinsu",
        age: 25,
    }),
]);

const result = await table.get(({ key }) => [
    key({
        id: "0000",
    }),
]);

console.log(result);

Don't worry about features that are not supported yet. You can still use the types from aws-sdk.

const result = await user.scan(() => [{
    FilterExpression: "age >= 25",
}]);

Or you can even use some of the features of this library.

const result = await user.scan(({ select }) => [
    {
        FilterExpression: "age >= 25",
    },
    select("name"),
]);

How to define your own table

function typesafe<Schema, PK, SK, GSI>(...props: OperationProps)

This function defines the type of your table. Schema and PK are required generic type parameters. SK and GSI are optional. Last, you should put your DynamoDB.DocumentClient and your table name to the function parameter.

Schema

type UserSchema = {
    id: string;
    name: string;
    age: int;
    updatedAt: Date;
    createdAt: Date;
}
const table = typesafe<UserSchema, ...>(...)

You can put anything to the Schema type parameter. This type defines the columns and their data types in your table.

This library automatically adds updatedAt to your object putting. Even if your Schema does not have the properties.

PK

type PK = "id"
const table = typesafe<UserSchema, PK, ...>(...)

PK should be one of the keys of the Schema. It defines the partition key of your DynamoDB table.

SK

type SK = "age"
const table = typesafe<UserSchema, PK, SK>(...)

SK should be one of the keys of the Schema. It defines the sort key of your DynamoDB table.

GSI

type GSI = {
    "idNameIndex": ["id", "name"]
}
const table = typesafe<UserSchema, PK, SK, GSI>(...)

GSI should extend GSIList<Schema>. The keys of the GSI type parameter should be each name of your GSI table. And its value should be an array that has a PK element and an SK element of your GSI table.

OperationProps

type OperationProps = [client: DynamoDB.DocumentClient, name: string, option?: OperationOption];

It's the type of the parameters in typesafe function. you should put your DynamoDB.DocumentClient and your table name like below:

const client = new DynamoDB.DocumentClient({
    region: "ap-northeast-2",,
})

const name = "user-table"

const table = typesafe<UserSchema, PK, SK, GSI>(client, name)

You can configure some options too. Check out the OperationOption type.

export type OperationOption = {
  soft?: boolean;
  deleteDateColumn?: string;
  dateFormat?: {
    toDate: (value: Date) => string;
    validateDate: (value: unknown) => boolean;
    fromDate: (value: string) => Date;
  }
}

If soft is true, the delete operation won't delete an item in your DynamoDB table. Of course you can not find the item by other operations though. Note that you should set deleteDateColumn by a colum name so that the operations can judge if it has been deleted by the name.

Also, you can configure how your Date type field is saved in your DynamoDB table by toDate, fromDate and validateDate parameters. By default, it saves a date as a ISO8610 date string.

Check out an example using options:

const table = typesafe<UserSchema, PK, SK, GSI>(client, name, { soft: true, deleteDateColumn: "deletedAt" })

Operations

After a table is defined, you can use operation methods from it.

type Operations<Schema, PK extends keyof Schema, SK extends keyof Schema, GSI extends GSIList<Schema>> = {
  get: GetOperation<Schema, PK, SK>;
  query: QueryOperation<Schema, PK, SK, GSI>;
  scan: ScanOperation<Schema, PK, SK>;
  put: PutOperation<Schema>;
  update: UpdateOperation<Schema, PK, SK>;
  remove: RemoveOperation<Schema, PK, SK>;
}

GetOperation

This is a type-safe version get operation of DynamoDB. You have 2 functions by which you can manipulate parameters: key and select.

key

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "id">(client, name)

const result = await table.get(({ key }) => [
    key({ id: "01" })
])

Like above, every function is given through the parameter of the function parameter of each operation.

select

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "id">(client, name)

const result = await table.get(({ key, select }) => [
    key({ id: "01" }),
    select("age", "id")
])

Sometimes you may want to select only some of the properties from the Schema.

QueryOperation

This is a type-safe version query operation of DynamoDB. You have 7 functions by which you can manipulate parameters: condition, filter, nextOf, indexName, select, limit, and direction. But I'll omit the select function here because it's just the same as the example in GetOperation.

condition

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "age", "id">(client, name)

const result = await table.query(({ condition }) => [
    condition({ age: 30 }),
])

It's going to find every user who is 30 years old.

filter

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "age", "id">(client, name)

const result = await table.query(({ condition, filter }) => [
    condition({ age: 30 }),
    filter({ name: "John" })
])

It's going to find every John who is 30 years old.

nextOf

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "age", "id">(client, name)

const result = await table.query(({ condition, nextOf }) => [
    condition({ age: 30 }),
    nextOf({ name: "John", age: 30 })
])

It's going to find every john after the 30 yo john.

indexName

type User = {
    id: string;
    name: string;
    age: number;
}

type GSI = {
    "nameAgeIndex": ["name", "age"]
}

const table = typesafe<User, "age", "id", GSI>(client, name)

const result = await table.query(({ indexName }) => [
    indexName("nameAgeIndex").condition({
        name: "john",
    })
])

It enables you to use the Global Secondary Index of your DynamoDB table.

limit

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "age", "id">(client, name)

const result = await table.query(({ condition, limit }) => [
    condition({ age: 30 }),
    limit(10)
])

It's going to find at most 10 users who are 30 years old.

direction

type User = {
    id: string;
    name: string;
    age: number;
}

type GSI = {
    "nameAgeIndex": ["name", "Age"]
}

const table = typesafe<User, "age", "id", GSI>(client, name)

const result = await table.query(({ indexName }) => [
    indexName("nameAgeIndex").condition({
        name: "john",
    }),
    indexName("nameAgeIndex").direction("BACKWORD")
])

It's going to find every John but from the oldest.

ScanOperation

This is a type-safe version scan operation of DynamoDB. You have 4 functions by which you can manipulate parameters: filter, nextOf, select, and limit. But everything is just the same as with QueryOperation.

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "age", "id">(client, name)

const result = await table.scan(({ filter }) => [
    filter({ name: "John" })
])

PutOperation

This is a type-safe version scan operation of DynamoDB. You have only one function by which you can manipulate parameters: values.

values

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "age", "id">(client, name)

const result = await table.put(({ values }) => [
    values({
        "id": "01",
        "name": "John",
        "age": 30,
    })
])

UpdateOperation

This is a type-safe version update operation of DynamoDB. You have 2 functions by which you can manipulate parameters: key and replace. key is just the same as the one in the GetOperation. It picks an item with the PK and SK if it exists.

replace

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "id", "name">(client, name)

const result = await table.update(({ key, replace }) => [
    key({
        "id": "01",
        "name": "John",
    })
    replace({
        "age": 29,
    })
])

RemoveOperation

This is a type-safe version of the delete operation of DynamoDB. You have only one function by which you can manipulate parameters: key. key is just the same as the one in the GetOperation.

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "id", "name">(client, name)

const result = await table.remove(({ key }) => [
    key({
        "id": "01",
        "name": "John",
    })
])

Errors

Operations throw AWSError if an internal error occurs. Use try-catch

type User = {
    id: string;
    name: string;
    age: number;
}

const table = typesafe<User, "id">(client, name)

try {
    const result = await table.get(({ key }) => [
        key({ id: "01" })
    ])
} catch (e: any) {
    if ("statusCode" in e) {
        //TODO: Error handling
    }
}
0.7.9

9 months ago

0.7.6

10 months ago

0.7.5

10 months ago

0.7.8

9 months ago

0.7.7

10 months ago

0.7.2

1 year ago

0.7.1

1 year ago

0.7.4

1 year ago

0.7.3

1 year ago

0.7.0

1 year ago

0.6.3

1 year ago

0.6.2

1 year ago

0.6.1

1 year ago

0.6.0

1 year ago

0.5.2

1 year ago

0.5.1

1 year ago

0.5.0

1 year ago

0.4.3

1 year ago

0.4.2

1 year ago

0.4.1

1 year ago

0.4.0

1 year ago

0.3.2

2 years ago

0.3.1

2 years ago

0.3.0

2 years ago

0.2.0

2 years ago

0.1.2

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago