Skip to content

Commit

Permalink
Merge pull request #26 from blazejkustra/feature/support-binary
Browse files Browse the repository at this point in the history
[Feature] Support binary!
  • Loading branch information
blazejkustra committed Apr 17, 2024
2 parents 8cc755a + 67125e8 commit 6d10b4a
Show file tree
Hide file tree
Showing 23 changed files with 90 additions and 18 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies ⏳
run: npm ci
- name: Build 🔧
run: npm run build
- name: Typecheck 🏷️
run: npm run typecheck
- name: Lint 🧹
Expand All @@ -47,5 +49,3 @@ jobs:
with:
path-to-lcov: ./coverage/lcov.info
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build 🔧
run: npm run build
2 changes: 0 additions & 2 deletions docs/docs/getting_started/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ Dynamode is highly influenced by other ORMs/ODMs, such as [TypeORM](https://www.
- Migrations and automatic migrations generation.
- PartiQL support
- Capture DynamoDB errors and make it easier to work with
- Support binary data type

### Road map

* [ ] Query that supports querying different types of entities at once with TS in mind.
* [ ] Support binary types [link](https://www.github.com/aws/aws-sdk-js-v3/blob/06417909a3/packages/util-dynamodb/src/convertToAttr.ts#L166) and [link](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_util_dynamodb.html)
* [ ] Possibility to have more than one suffix/prefix
* [ ] PartiQL support
* [ ] Add dependsOn decorator to throw/warn when updating
Expand Down
17 changes: 17 additions & 0 deletions docs/docs/guide/entity/decorators.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,23 @@ class YourModel extends Entity {
}
```

## attribute.binary()

### Description

This decorator is used to tag an attribute of type `UInt8Array` (binary data).

### Examples

```ts
class YourModel extends Entity {
...
@attribute.binary()
key: UInt8Array;
...
}
```

## attribute.object()

### Description
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/guide/entity/modeling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Supported DynamoDB data types and its Dynamode equivalents:
| `Null` | `null` | Null represents an attribute with an unknown or `undefined` state. |
| `String` | `string` | Partition and sort keys can't be empty strings. |
| `Number` | `number` | DynamoDB does not support `Infinite` and `NaN` values. |
| `Binary` | `N/A` | Not yet supported by Dynamode. |
| `Binary` | `Uint8Array` | Binary data is represented using the `Uint8Array` type. DynamoDB does not natively support other binary types such as `File`/`Buffer` |
| `Boolean` | `boolean` | `true` or `false`. |
| `List` | `Array<unknown>` | There are no restrictions on the data types that can be stored in an `Array`. Elements in an array do not have to be of the same type. |
| `Map` | `Map<string, unknown>` / `Record<string, unknown>` / `{ [key: string]: unknown }` / `{ [key]: unknown, ... }` | There are no restrictions on the data types that can be stored in a `Map`/`object`. Elements in a map do not have to be of the same type. |
Expand Down
6 changes: 6 additions & 0 deletions examples/AllPossibleProperties/methods/batchPut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ async function batchPut() {
number: 10,
map: new Map<string, string>([['1', 'test']]),
boolean: true,
binary: new Uint8Array([1, 2, 3]),
GSI_1_PK: 'test',
GSI_1_SK: 1,
}),
new AllPossibleProperties({
partitionKey: 'pk2',
Expand All @@ -23,6 +26,9 @@ async function batchPut() {
number: 10,
map: new Map<string, string>([['1', 'test']]),
boolean: true,
binary: new Uint8Array([1, 2, 3]),
GSI_1_PK: 'test',
GSI_1_SK: 2,
}),
]);

Expand Down
1 change: 1 addition & 0 deletions examples/AllPossibleProperties/methods/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ async function create() {
number: 10,
map: new Map<string, string>([['1', 'test']]),
boolean: true,
binary: new Uint8Array([1, 2, 3]),
}),
);

Expand Down
1 change: 1 addition & 0 deletions examples/AllPossibleProperties/methods/put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ async function put() {
number: 10,
map: new Map<string, string>([['1', 'test']]),
boolean: true,
binary: new Uint8Array([1, 2, 3]),
}),
);

Expand Down
14 changes: 11 additions & 3 deletions examples/AllPossibleProperties/methods/scan.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { AllPossiblePropertiesManager } from '../model';

async function scan() {
const userScan = await AllPossiblePropertiesManager.scan()
const scan1 = await AllPossiblePropertiesManager.scan()
.attribute('string')
.beginsWith('k')
.startAt({ sortKey: 'user', partitionKey: 'pk3' })
.indexName('GSI_1_NAME')
.limit(1)
.run();

const scan2 = await AllPossiblePropertiesManager.scan()
.attribute('string')
.beginsWith('k')
.startAt(scan1.lastKey)
.indexName('GSI_1_NAME')
.run();

console.log();
console.log('OUTPUT:');
console.log(userScan);
console.log(scan1);
console.log(scan2);
}

scan();
4 changes: 2 additions & 2 deletions examples/AllPossibleProperties/methods/transactionGet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ async function transaction() {
sortKey: 'sk1',
}),
AllPossiblePropertiesManager.transaction.get({
partitionKey: 'pk1',
sortKey: 'sk1',
partitionKey: 'pk2',
sortKey: 'sk2',
}),
]);

Expand Down
2 changes: 2 additions & 0 deletions examples/AllPossibleProperties/methods/transactionWrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ async function transaction() {
number: 10,
map: new Map<string, string>([['1', 'test']]),
boolean: true,
binary: new Uint8Array([1, 2, 3]),
}),
),
AllPossiblePropertiesManager.transaction.create(
Expand All @@ -41,6 +42,7 @@ async function transaction() {
number: 10,
map: new Map<string, string>([['1', 'test']]),
boolean: true,
binary: new Uint8Array([1, 2, 3]),
}),
),
AllPossiblePropertiesManager.transaction.delete({
Expand Down
6 changes: 6 additions & 0 deletions examples/AllPossibleProperties/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type AllPossiblePropertiesProps = {
set: Set<string>;
number?: number;
boolean: boolean;
binary: Uint8Array;
};

const TABLE_NAME = 'all-possible-properties';
Expand Down Expand Up @@ -79,6 +80,9 @@ export class AllPossibleProperties extends Entity {
@attribute.boolean()
boolean: boolean;

@attribute.binary()
binary: Uint8Array;

unsaved: string;

constructor(props: AllPossiblePropertiesProps) {
Expand All @@ -104,6 +108,8 @@ export class AllPossibleProperties extends Entity {
this.set = props.set;
this.number = props.number;
this.boolean = props.boolean;
this.binary = props.binary;

this.unsaved = 'unsaved';
}

Expand Down
7 changes: 7 additions & 0 deletions lib/decorators/helpers/other.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ export function number(): <T extends Partial<Record<K, number>>, K extends strin
return decorateAttribute(Number, 'attribute');
}

export function binary(): <T extends Partial<Record<K, Uint8Array>>, K extends string>(
Entity: T,
propertyName: K,
) => void {
return decorateAttribute(Uint8Array, 'attribute');
}

export function boolean(): <T extends Partial<Record<K, boolean>>, K extends string>(
Entity: T,
propertyName: K,
Expand Down
3 changes: 2 additions & 1 deletion lib/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
stringGsiSortKey,
} from '@lib/decorators/helpers/gsi';
import { numberLsiSortKey, stringLsiSortKey } from '@lib/decorators/helpers/lsi';
import { array, boolean, map, number, object, set, string } from '@lib/decorators/helpers/other';
import { array, binary, boolean, map, number, object, set, string } from '@lib/decorators/helpers/other';
import { prefix, suffix } from '@lib/decorators/helpers/prefixSuffix';
import {
numberPartitionKey,
Expand All @@ -23,6 +23,7 @@ const attribute = {
array,
set,
map,
binary,

date: {
string: stringDate,
Expand Down
3 changes: 2 additions & 1 deletion lib/dynamode/storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export type AttributeType =
| ObjectConstructor
| ArrayConstructor
| SetConstructor
| MapConstructor;
| MapConstructor
| Uint8ArrayConstructor;

export type AttributeRole =
| 'partitionKey'
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type FlattenObject<TValue> = CollapseEntries<CreateObjectEntries<TValue,

type Entry = { key: string; value: unknown };
type EmptyEntry<TValue> = { key: ''; value: TValue };
type ExcludedTypes = Date | Set<unknown> | Map<unknown, unknown>;
type ExcludedTypes = Date | Set<unknown> | Map<unknown, unknown> | Uint8Array;
type ArrayEncoder = `[${bigint}]`;

type EscapeArrayKey<TKey extends string> = TKey extends `${infer TKeyBefore}.${ArrayEncoder}${infer TKeyAfter}`
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/mockEntityFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function mockEntityFactory(props?: Partial<MockEntityProps>): MockEntity
set: new Set(['1', '2', '3']),
array: ['1', '2'],
boolean: true,
binary: new Uint8Array([1, 2, 3]),
...props,
});
}
6 changes: 6 additions & 0 deletions tests/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export type MockEntityProps = TestTableProps & {
set: Set<string>;
number?: number;
boolean: boolean;
binary: Uint8Array;
};

export class MockEntity extends TestTable {
Expand Down Expand Up @@ -118,6 +119,9 @@ export class MockEntity extends TestTable {
@attribute.date.number()
numDate: Date;

@attribute.binary()
binary: Uint8Array;

unsaved: string;

constructor(props: MockEntityProps) {
Expand All @@ -132,6 +136,7 @@ export class MockEntity extends TestTable {
this.boolean = props.boolean;
this.strDate = new Date();
this.numDate = new Date();
this.binary = props.binary;
this.unsaved = 'unsaved';
}

Expand Down Expand Up @@ -181,6 +186,7 @@ export const mockInstance = new MockEntity({
set: new Set(['1', '2', '3']),
array: ['1', '2'],
boolean: true,
binary: new Uint8Array([1, 2, 3]),
});

vi.useRealTimers();
1 change: 1 addition & 0 deletions tests/types/EntityKey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type MockEntityDeepKeys =
| 'boolean'
| 'strDate'
| 'numDate'
| 'binary'
| 'unsaved'
| 'partitionKey'
| 'sortKey'
Expand Down
2 changes: 2 additions & 0 deletions tests/types/EntityValue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type NumberValue = EntityValue<typeof MockEntity, 'number'>;
type BooleanValue = EntityValue<typeof MockEntity, 'boolean'>;
type StrDateValue = EntityValue<typeof MockEntity, 'strDate'>;
type NumDateValue = EntityValue<typeof MockEntity, 'numDate'>;
type BinaryValue = EntityValue<typeof MockEntity, 'binary'>;
type UnsavedValue = EntityValue<typeof MockEntity, 'unsaved'>;
type PartitionKeyValue = EntityValue<typeof MockEntity, 'partitionKey'>;
type SortKeyValue = EntityValue<typeof MockEntity, 'sortKey'>;
Expand Down Expand Up @@ -51,6 +52,7 @@ describe('EntityValue type tests', () => {
expectTypeOf<BooleanValue>().toEqualTypeOf<boolean>();
expectTypeOf<StrDateValue>().toEqualTypeOf<Date>();
expectTypeOf<NumDateValue>().toEqualTypeOf<Date>();
expectTypeOf<BinaryValue>().toEqualTypeOf<Uint8Array>();
expectTypeOf<UnsavedValue>().toEqualTypeOf<string>();
expectTypeOf<PartitionKeyValue>().toEqualTypeOf<string>();
expectTypeOf<SortKeyValue>().toEqualTypeOf<string>();
Expand Down
9 changes: 8 additions & 1 deletion tests/unit/decorators/helpers/other.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';

import * as decorateAttribute from '@lib/decorators/helpers/decorateAttribute';
import { array, boolean, map, number, object, set, string } from '@lib/decorators/helpers/other';
import { array, binary, boolean, map, number, object, set, string } from '@lib/decorators/helpers/other';

describe('Decorators', () => {
let decorateAttributeSpy = vi.spyOn(decorateAttribute, 'decorateAttribute');
Expand Down Expand Up @@ -68,4 +68,11 @@ describe('Decorators', () => {
expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Map, 'attribute');
});
});

describe('binary', async () => {
test('Should call decorateAttribute with Uint8Array attribute type', async () => {
binary();
expect(decorateAttributeSpy).toHaveBeenNthCalledWith(1, Uint8Array, 'attribute');
});
});
});
3 changes: 2 additions & 1 deletion tests/unit/decorators/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
stringGsiSortKey,
} from '@lib/decorators/helpers/gsi';
import { numberLsiSortKey, stringLsiSortKey } from '@lib/decorators/helpers/lsi';
import { array, boolean, number, object, set, string } from '@lib/decorators/helpers/other';
import { array, binary, boolean, number, object, set, string } from '@lib/decorators/helpers/other';
import { prefix, suffix } from '@lib/decorators/helpers/prefixSuffix';
import {
numberPartitionKey,
Expand All @@ -27,6 +27,7 @@ describe('Decorators', () => {
expect(attribute.object).toEqual(object);
expect(attribute.array).toEqual(array);
expect(attribute.set).toEqual(set);
expect(attribute.binary).toEqual(binary);
});

test('Should return proper date attribute decorators', async () => {
Expand Down
11 changes: 8 additions & 3 deletions tests/unit/entity/helpers/converters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ const mockEntityAttributes = {
propertyName: 'boolean',
type: Boolean,
},
binary: {
propertyName: 'binary',
type: Uint8Array,
},
} as any as AttributesMetadata;

const dynamoObject = {
Expand All @@ -102,6 +106,7 @@ const dynamoObject = {
set: { SS: ['1', '2', '3'] },
array: { L: [{ S: '1' }, { S: '2' }] },
boolean: { BOOL: true },
binary: { B: new Uint8Array([1, 2, 3]) },
};

describe('Converters entity helpers', () => {
Expand Down Expand Up @@ -136,15 +141,15 @@ describe('Converters entity helpers', () => {
getEntityMetadataSpy.mockReturnValue(metadata as any);

expect(convertAttributeValuesToEntity(MockEntity, dynamoObject)).toEqual(mockInstance);
expect(truncateValueSpy).toBeCalledTimes(15);
expect(truncateValueSpy).toBeCalledTimes(16);
});

test('Should return object in dynamode format', async () => {
getEntityAttributesSpy.mockReturnValue(mockEntityAttributes);
getEntityMetadataSpy.mockReturnValue(metadata as any);

expect(convertAttributeValuesToEntity(MockEntity, dynamoObject)).toEqual(mockInstance);
expect(truncateValueSpy).toBeCalledTimes(15);
expect(truncateValueSpy).toBeCalledTimes(16);
});
});

Expand All @@ -162,7 +167,7 @@ describe('Converters entity helpers', () => {
getEntityAttributesSpy.mockReturnValue(mockEntityAttributes);

expect(convertEntityToAttributeValues(MockEntity, mockInstance)).toEqual(dynamoObject);
expect(transformValueSpy).toBeCalledTimes(15);
expect(transformValueSpy).toBeCalledTimes(16);
});
});

Expand Down
1 change: 1 addition & 0 deletions tests/unit/stream/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const validMockEntityImage = {
boolean: { BOOL: true },
strDate: { S: '2001-09-09T01:46:40.000Z' },
numDate: { N: '1000000000000' },
binary: { B: new Uint8Array([1, 2, 3]) },
};

const validNewImageStream: DynamoDBRecord = {
Expand Down

0 comments on commit 6d10b4a

Please sign in to comment.