Skip to content

Commit

Permalink
feat: parse ProjectionExpression and filter returned Item type (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
sam authored Feb 6, 2022
1 parent ba5848e commit 16cd9a9
Show file tree
Hide file tree
Showing 14 changed files with 408 additions and 20 deletions.
1 change: 1 addition & 0 deletions .gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions .projenrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const project = new typescript.TypeScriptProject({
eslintOptions: {
ignorePatterns: ["**"],
},
gitignore: [".DS_Store"],
releaseToNpm: true,
});

project.synth();
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# typesafe-dynamodb

This is a type-only library that can be used to augment a standard AWS SDK v2 client library for AWS DynamoDB.
[![npm version](https://badge.fury.io/js/typesafe-dynamodb.svg)](https://badge.fury.io/js/typesafe-dynamodb)

`typesafe-dynamodb` is a type-only library which replaces the type signatures of the AWS SDK's DynamoDB client. It substitutes `getItem`, `putItem`, `deleteItem` and `query` API methods with type-safe alternatives that are aware of the data in your tables and also adaptive to the semantics of the API request, e.g. by validating `ExpressionAttributeNames` and `ExpressionAttributeValues` contain all the values used in a `ConditionExpression` string, or by understanding the effect of a `ProjectionExpression` on the returned data type.

The end goal is to provide types that have total understanding of the AWS DynamoDB API and enable full utilization of the TypeScript type system for modeling complex DynmaoDB tables, such as the application of union types and template string literals for single-table designs.

## Installation

Expand All @@ -10,7 +14,15 @@ npm install --save-dev typesafe-dynamodb

## Usage

Declare standard TypeScript types to represent the data in your table:
To use `typesafe-dynamodb`, there is no need to change anything about your existing runtime code. It is purely type definitions, so you only need to cast an instance of `AWS.DynamoDB` to the `TypeSafeDynamoDB<T, HashKey, RangeKey>` interface and use the client as normal, except now you can enjoy a dynamic, type-safe experience in your IDE instead.

```ts
import { DynamoDB } from "aws-sdk";

const client = new DynamoDB();
```

Start by declaring a standard TypeScript interface which describes the structure of data in your DynamoDB Table:

```ts
interface Record {
Expand All @@ -22,15 +34,15 @@ interface Record {
}
```

Then, cast your `DynamoDB` table instance to `TypeSafeDynamoDB`;
Then, cast the `DynamoDB` client instance to `TypeSafeDynamoDB`;

```ts
import { DynamoDB } from "aws-sdk";

const dynamodb = new DynamoDB() as TypeSafeDynamoDB<Record, "key", "sort">;
const typesafeClient: TypeSafeDynamoDB<Record, "key", "sort"> = client;
```

The `TypeSafeDynamoDB` type replaces the `getItem`, `putItem`, `deleteItem` and `query` API calls with an implementation that understands the structure of data in the table.
`"key"` is the name of the Hash Key attribute, and `"sort"` is the name of the Range Key attribute.

Finally, use the client as you normally would, except now with intelligent type hints and validations.

## Features

Expand All @@ -44,7 +56,13 @@ Same for the `Item` in the response:

![typesafe GetItemOutput Item](img/get-item-response.gif)

### Filter of AttributesToGet
### Filter result with ProjectionExpression

The `ProjectionExpression` field is parsed and applied to filter the returned type of `getItem` and `query`.

![typesafe ProjectionExpression](img/get-item-projection.gif)

### Filter with AttributesToGet

If you specify `AttributesToGet`, then the returned type only contains those properties.

Expand Down
Binary file added img/get-item-projection.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 19 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,25 @@ export interface TypeSafeDynamoDB<
PartitionKey extends keyof Item,
RangeKey extends keyof Item | undefined = undefined
> extends Omit<DynamoDB, "getItem" | "deleteItem" | "putItem" | "query"> {
getItem<AttributesToGet extends keyof Item | undefined>(
params: GetItemInput<Item, PartitionKey, RangeKey, AttributesToGet>,
callback?: Callback<GetItemOutput<Item, AttributesToGet>, AWSError>
): Request<GetItemOutput<Item, AttributesToGet>, AWSError>;
getItem<
AttributesToGet extends keyof Item | undefined = undefined,
ProjectionExpression extends string | undefined = undefined
>(
params: GetItemInput<
Item,
PartitionKey,
RangeKey,
AttributesToGet,
ProjectionExpression
>,
callback?: Callback<
GetItemOutput<Item, AttributesToGet, ProjectionExpression>,
AWSError
>
): Request<
GetItemOutput<Item, AttributesToGet, ProjectionExpression>,
AWSError
>;

deleteItem<
ConditionExpression extends string | undefined,
Expand Down
2 changes: 1 addition & 1 deletion src/delete-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ToAttributeMap } from "./attribute-value";
import {
ExpressionAttributeNames,
ExpressionAttributeValues,
} from "./expression";
} from "./expression-attributes";

export type DeleteItemInput<
Item extends object,
Expand Down
File renamed without changes.
20 changes: 16 additions & 4 deletions src/get-item.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import type { DynamoDB } from "aws-sdk";
import { ToAttributeMap } from "./attribute-value";
import { KeyAttribute } from "./key";
import { ApplyProjection } from "./projection-expression";

export interface GetItemInput<
Item extends object,
PartitionKey extends keyof Item,
RangeKey extends keyof Item | undefined,
AttributesToGet extends keyof Item | undefined
> extends Omit<DynamoDB.GetItemInput, "Key" | "AttributesToGet"> {
AttributesToGet extends keyof Item | undefined,
ProjectionExpression extends string | undefined
> extends Omit<
DynamoDB.GetItemInput,
"Key" | "AttributesToGet" | "ProjectionExpression"
> {
Key: KeyAttribute<Item, PartitionKey, RangeKey>;
readonly AttributesToGet?: readonly AttributesToGet[];
readonly ProjectionExpression?: ProjectionExpression;
}
export interface GetItemOutput<
Item extends object,
AttributesToGet extends keyof Item | undefined = undefined
AttributesToGet extends keyof Item | undefined,
ProjectionExpression extends string | undefined
> extends Omit<DynamoDB.GetItemOutput, "Item"> {
Item?: ToAttributeMap<
undefined extends AttributesToGet
? Item
? undefined extends ProjectionExpression
? Item
: Extract<
ApplyProjection<Item, Extract<ProjectionExpression, string>>,
object
>
: Pick<Item, Extract<AttributesToGet, keyof Item>>
>;
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export * from "./attribute-value";
export * from "./callback";
export * from "./client";
export * from "./expression";
export * from "./expression-attributes";
export * from "./get-item";
export * from "./key";
export * from "./projection-expression";
export * from "./put-item";
export * from "./query";
203 changes: 203 additions & 0 deletions src/projection-expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { AlphaNumeric } from "./letter";

export type ProjectionExpr =
| ArrayIndex
| Identifier
| NumberLiteral
| NameRef
| PropRef
| ValueRef;
//

export interface NameRef<Name extends string = string> {
kind: "name-ref";
name: Name;
}
export interface ValueRef<Name extends string = string> {
kind: "value-ref";
name: Name;
}
export interface Identifier<I extends string = string> {
kind: "identifier";
name: I;
}

export interface NumberLiteral<N extends string = string> {
kind: "index";
number: N;
}
export interface PropRef<
Expr extends ProjectionExpr = any,
Id extends Identifier = any
> {
kind: "prop-ref";
expr: Expr;
name: Id;
}
export interface ArrayIndex<
List extends ProjectionExpr = any,
Number extends NumberLiteral | ValueRef = NumberLiteral | ValueRef
> {
kind: "array-index";
list: List;
number: Number;
}

export type ParseProjectionExpression<Text extends string> = Parse<
Text,
[],
undefined
>;

type AsExpr<T> = Extract<T, ProjectionExpr>;

type Parse<
Text extends string,
Expressions extends ProjectionExpr[],
Expr extends ProjectionExpr | undefined
> = Text extends `.${infer Rest}`
? Expr extends ProjectionExpr
? Parse<Rest, Expressions, PropRef<Expr, Identifier<"">>>
: never
: Text extends `[:${infer Rest}`
? Parse<Rest, Expressions, ArrayIndex<AsExpr<Expr>, ValueRef<"">>>
: Text extends `[${infer Rest}`
? Parse<Rest, Expressions, ArrayIndex<AsExpr<Expr>, NumberLiteral<"">>>
: Text extends `]${infer Rest}`
? Parse<Rest, Expressions, Expr>
: Text extends `]`
? Concat<Expressions, Expr>
: Text extends `${"," | " "}${infer Rest}`
? Parse<Rest, Concat<Expressions, Expr>, undefined>
: Text extends `#${infer Rest}`
? Parse<Rest, Concat<Expressions, Expr>, NameRef<Rest>>
: Text extends `:${infer Rest}`
? Parse<Rest, Concat<Expressions, Expr>, ValueRef<Rest>>
: Text extends `${AlphaNumeric}${string}`
? Text extends `${infer char}${infer Rest}`
? Parse<Rest, Expressions, Append<Expr, char>>
: never
: Text extends `${AlphaNumeric}${string}`
? Text extends `${infer char}${infer Rest}`
? Parse<Rest, Expressions, Append<Expr, char>>
: never
: Concat<Expressions, Expr>;

type Concat<
Expressions extends ProjectionExpr[],
CurrentExpr extends ProjectionExpr | undefined
> = undefined extends CurrentExpr
? Expressions
: [...Expressions, Extract<CurrentExpr, ProjectionExpr>];

type Append<
Expr extends ProjectionExpr | undefined,
char extends string
> = Expr extends undefined
? Identifier<char>
: Expr extends Identifier<infer Name>
? Identifier<`${Name}${char}`>
: Expr extends NumberLiteral<infer Name>
? NumberLiteral<`${Name}${char}`>
: Expr extends NameRef<infer Name>
? NameRef<`${Name}${char}`>
: Expr extends ValueRef<infer Name>
? ValueRef<`${Name}${char}`>
: Expr extends PropRef<infer expr, infer name>
? PropRef<expr, Extract<Append<name, char>, Identifier>>
: Expr extends ArrayIndex<infer expr, infer idx>
? ArrayIndex<expr, Extract<Append<idx, char>, NumberLiteral>>
: never;

export type ApplyProjection<T, Expr extends string> = Flatten<
UnionToIntersection<
ApplyProjectionExpr<T, ParseProjectionExpression<Expr>[number]>
>
>;

type ApplyProjectionExpr<T, Expr extends ProjectionExpr> = T extends undefined
? never
: Expr extends PropRef<infer expr, infer i>
? {
[p in keyof ApplyProjectionExpr<T, expr>]: ApplyProjectionExpr<
ApplyProjectionExpr<T, expr>[p],
i
>;
}
: Expr extends ArrayIndex<infer expr, infer i>
? {
[p in keyof ApplyProjectionExpr<T, expr>]: ApplyProjectionExpr<
ApplyProjectionExpr<T, expr>[keyof ApplyProjectionExpr<T, expr>],
i
>;
}
: Expr extends Identifier<infer I>
? I extends keyof T
? Pick<T, I>
: never
: Expr extends NumberLiteral<infer I>
? ParseInt<I> extends keyof T
? Pick<T, ParseInt<I>>
: never
: never;

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;

type Flatten<T> = T extends object
? keyof T extends number
? FlattenArray<Extract<T, Record<number, any>>>
: {
[k in keyof T]: Flatten<T[k]>;
}
: T;

type FlattenArray<
T extends { [i in number]: any },
i extends number = 0
> = i extends keyof T
? [T[i], ...FlattenArray<T, Inc<i>>]
: number extends i
? []
: FlattenArray<T, Inc<i>>;

type Inc<N extends number> = N extends 0
? 1
: N extends 1
? 2
: N extends 2
? 3
: N extends 3
? 4
: N extends 4
? 5
: N extends 5
? 6
: N extends 6
? 7
: number;

type ParseInt<N extends string> = N extends "0"
? 0
: N extends "1"
? 1
: N extends "2"
? 2
: N extends "3"
? 3
: N extends "4"
? 4
: N extends "5"
? 5
: N extends "6"
? 6
: N extends "7"
? 7
: N extends "8"
? 8
: N extends "9"
? 9
: number;
2 changes: 1 addition & 1 deletion src/put-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ToAttributeMap } from "./attribute-value";
import {
ExpressionAttributeNames,
ExpressionAttributeValues,
} from "./expression";
} from "./expression-attributes";

export type PutItemInput<
Item extends object,
Expand Down
2 changes: 1 addition & 1 deletion src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ToAttributeMap } from "./attribute-value";
import {
ExpressionAttributeNames,
ExpressionAttributeValues,
} from "./expression";
} from "./expression-attributes";

export type QueryInput<
Item extends object,
Expand Down
Loading

0 comments on commit 16cd9a9

Please sign in to comment.