Skip to content

Commit

Permalink
feat: drop aws-sdk dep, accept it as a parameter when working with Sets
Browse files Browse the repository at this point in the history
  • Loading branch information
ruicsh committed Mar 4, 2023
1 parent 39b00d5 commit 635c290
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 221 deletions.
8 changes: 2 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,13 @@
"test:ci": "vitest run",
"test": "vitest --watch"
},
"dependencies": {},
"devDependencies": {
"@types/node": "18.14.6",
"@typescript-eslint/eslint-plugin": "5.54.0",
"@typescript-eslint/parser": "5.54.0",
"@vitest/coverage-c8": "0.29.2",
"aws-sdk": "^2.1328.0",
"esbuild": "0.17.10",
"aws-sdk": "^2.2.0",
"esbuild": "0.17.11",
"eslint": "8.35.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.0.0",
Expand All @@ -64,8 +63,5 @@
"typescript": "4.9.5",
"vitest": "0.29.2",
"zx": "7.2.0"
},
"peerDependencies": {
"aws-sdk": "^2.2.0"
}
}
17 changes: 12 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
<a href="https://codeclimate.com/github/tuplo/dynoexpr/test_coverage">
<img src="https://api.codeclimate.com/v1/badges/3564497cf991d094e2eb/test_coverage" />
</a>
<img src="https://github.com/tuplo/dynoexpr/workflows/build/badge.svg">
</p>
</p>

</div>

Expand Down Expand Up @@ -171,13 +170,16 @@ const params = dynoexpr({

### Working with Sets

If a value is provided as a Set, it will be converted to `DocumentClient.DynamoDbSet`.
If a value is provided as a Set, it will be converted to `DocumentClient.DynamoDbSet`. But `dynoexpr` doesn't include `DocumentClient` so you need to provide it.

```typescript
import { DocumentClient } from "aws-sdk/clients/dynamodb";

const params = dynoexpr({
DocumentClient,
Update: {
Color: new Set(['Orange', 'Purple'])
}
},
})

/*
Expand All @@ -193,10 +195,13 @@ const params = dynoexpr({
*/
```

### When using UpdateAdd or UpdateDelete, arrays are converted to DynamoDbSet
#### When using UpdateAdd or UpdateDelete, arrays are converted to DynamoDbSet

```typescript
import { DocumentClient } from "aws-sdk/clients/dynamodb";

const params = dynoexpr({
DocumentClient,
UpdateAdd: {
Color: ['Orange', 'Purple']
}
Expand Down Expand Up @@ -382,6 +387,8 @@ type DynamoDbValue =
UpdateAdd: { [key: string]: DynamoDbValue },
UpdateDelete: { [key: string]: DynamoDbValue },
UpdateRemove: { [key: string]: DynamoDbValue },

DocumentClient: AWS.DynamoDB.DocumentClient
}
```

Expand Down
88 changes: 88 additions & 0 deletions src/document-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { DocumentClient as DocClientV2 } from "aws-sdk/clients/dynamodb";

import { AwsSdkDocumentClient } from "./document-client";

describe("aws sdk document client", () => {
afterEach(() => {
AwsSdkDocumentClient.setDocumentClient(null);
});

it("throws an error when there's no AWS SKD provided", () => {
const docClient = new AwsSdkDocumentClient();
const fn = () => docClient.createSet([1, 2, 3]);

const expected =
"dynoexpr: When working with Sets, please provide the AWS DocumentClient (v2).";
expect(fn).toThrowError(expected);
});

it("creates a AWS Set using AWS SDK DocumentClient v2", () => {
AwsSdkDocumentClient.setDocumentClient(DocClientV2);
const docClient = new AwsSdkDocumentClient();
const actual = docClient.createSet([1, 2, 3]);

const awsDocClient = new DocClientV2();
const expected = awsDocClient.createSet([1, 2, 3]);
expect(actual).toStrictEqual(expected);
});

describe("creates sets", () => {
const docClient = new AwsSdkDocumentClient();

beforeEach(() => {
AwsSdkDocumentClient.setDocumentClient(DocClientV2);
});

it("creates DynamoDBSet instances for strings", () => {
const args = ["hello", "world"];
const actual = docClient.createSet(args);

expect(actual.type).toBe("String");
expect(actual.values).toHaveLength(args.length);
expect(actual.values).toContain("hello");
expect(actual.values).toContain("world");
});

it("creates DynamoDBSet instances for numbers", () => {
const args = [42, 1, 2];
const actual = docClient.createSet(args);

expect(actual.type).toBe("Number");
expect(actual.values).toHaveLength(args.length);
expect(actual.values).toContain(42);
expect(actual.values).toContain(1);
expect(actual.values).toContain(2);
});

it("creates DynamoDBSet instances for binary types", () => {
const args = [
Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]),
Buffer.from([0x61, 0x62, 0x63]),
];
const actual = docClient.createSet(args);

expect(actual.type).toBe("Binary");
expect(actual.values).toHaveLength(args.length);
expect(actual.values).toContainEqual(args[0]);
expect(actual.values).toContain(args[1]);
});

it("does not throw an error with mixed set types if validation is not explicitly enabled", () => {
const args = ["hello", 42];
const actual = docClient.createSet(args);

expect(actual.type).toBe("String");
expect(actual.values).toHaveLength(args.length);
expect(actual.values).toContain("hello");
expect(actual.values).toContain(42);
});

it("throws an error with mixed set types if validation is enabled", () => {
const params = ["hello", 42];
const expression = () => docClient.createSet(params, { validate: true });

const expected = "String Set contains Number value";
expect(expression).toThrow(expected);
});
});
});
22 changes: 22 additions & 0 deletions src/document-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* eslint-disable class-methods-use-this */
let AwsSdk: unknown = null;

export class AwsSdkDocumentClient {
static setDocumentClient(clientAwsSdk: unknown) {
AwsSdk = clientAwsSdk;
}

createSet(
list: unknown[] | Record<string, unknown>,
options?: Record<string, unknown>
) {
if (!AwsSdk) {
throw Error(
"dynoexpr: When working with Sets, please provide the AWS DocumentClient (v2)."
);
}

// @ts-expect-error Property 'prototype' does not exist on type '{}'.
return AwsSdk.prototype.createSet(list, options);
}
}
71 changes: 62 additions & 9 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,88 @@ import { DocumentClient } from "aws-sdk/clients/dynamodb";

import type { IDynoexprOutput } from "src/dynoexpr.d";

import dynoexpr from ".";
import dynoexpr from "./index";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function assertType<T, U extends T>(): void {
expect.anything();
}

describe("high level API", () => {
it("creates DynamoDb parameters", () => {
const actual = dynoexpr({
KeyCondition: { id: "567" },
Condition: { rating: "> 4.5" },
Filter: { color: "blue" },
Projection: ["weight", "size"],
});

const expected = {
ConditionExpression: "(#n8793843d > :va7bbf170)",
ExpressionAttributeNames: {
"#n395c1a24": "size",
"#n4bce9bfd": "color",
"#n75a60c8f": "id",
"#n8793843d": "rating",
"#nf337db8f": "weight",
},
ExpressionAttributeValues: {
":v64c5aa3d": "567",
":v91380c8f": "blue",
":va7bbf170": 4.5,
},
FilterExpression: "(#n4bce9bfd = :v91380c8f)",
KeyConditionExpression: "(#n75a60c8f = :v64c5aa3d)",
ProjectionExpression: "#nf337db8f,#n395c1a24",
};
expect(actual).toStrictEqual(expected);
});

it("doesn't require a type to be provided", () => {
expect.assertions(1);
const params = dynoexpr({
const args = dynoexpr({
TableName: "Table",
Key: 1,
UpdateSet: { color: "pink" },
});

assertType<IDynoexprOutput, typeof params>();
expect(params.TableName).toBe("Table");
assertType<IDynoexprOutput, typeof args>();
expect(args.TableName).toBe("Table");
});

it("accepts a type to be applied to the output", () => {
expect.assertions(1);
const params = dynoexpr<DocumentClient.UpdateItemInput>({
const args = dynoexpr<DocumentClient.UpdateItemInput>({
TableName: "Table",
Key: 123,
UpdateSet: { color: "pink" },
});

assertType<DocumentClient.ScanInput, typeof params>();
expect(params.Key).toBe(123);
assertType<DocumentClient.ScanInput, typeof args>();
expect(args.Key).toBe(123);
});

it("throws an error if it's working with Sets but doesn't have DocumentClient", () => {
const fn = () => dynoexpr({ Update: { color: new Set(["blue"]) } });

const expected =
"dynoexpr: When working with Sets, please provide the AWS DocumentClient (v2).";
expect(fn).toThrowError(expected);
});

it("accepts a provided DocumentClient (v2) for working with Sets", () => {
const docClient = new DocumentClient();
const color = new Set(["blue", "yellow"]);
const actual = dynoexpr({
UpdateSet: { color },
DocumentClient,
});

const expected = {
ExpressionAttributeNames: { "#n4bce9bfd": "color" },
ExpressionAttributeValues: {
":ve325d039": docClient.createSet(["blue", "yellow"]),
},
UpdateExpression: "SET #n4bce9bfd = :ve325d039",
};
expect(actual).toStrictEqual(expected);
});
});
40 changes: 30 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,41 @@ import {
getTransactExpressions,
isTransactRequest,
} from "./operations/transact";
import { AwsSdkDocumentClient } from "./document-client";

type IDynoexprParams =
| IDynoexprInput
| IBatchRequestInput
| ITransactRequestInput;
interface IDynoexprArgs
extends IDynoexprInput,
IBatchRequestInput,
ITransactRequestInput {
DocumentClient: unknown;
}

function cleanOutput<T>(output: unknown) {
const { DocumentClient, ...restOfOutput } = (output || {}) as {
[key: string]: unknown;
};

function dynoexpr<T = IDynoexprOutput>(params: IDynoexprParams): T {
if (isBatchRequest(params)) {
return getBatchExpressions(params) as IDynoexprOutput as T;
return restOfOutput as T;
}

function dynoexpr<T = IDynoexprOutput>(args: Partial<IDynoexprArgs>): T {
if (args.DocumentClient) {
AwsSdkDocumentClient.setDocumentClient(args.DocumentClient);
}
if (isTransactRequest(params)) {
return getTransactExpressions(params) as IDynoexprOutput as T;

let returns: unknown;

if (isBatchRequest(args)) {
returns = getBatchExpressions(args) as IDynoexprOutput;
}

if (isTransactRequest(args)) {
returns = getTransactExpressions(args) as IDynoexprOutput;
}

return getSingleTableExpressions(params) as T;
returns = getSingleTableExpressions(args);

return cleanOutput<T>(returns);
}

export default dynoexpr;

0 comments on commit 635c290

Please sign in to comment.