Skip to content

Commit

Permalink
Merge pull request #33 from blazejkustra/entity-custom-name
Browse files Browse the repository at this point in the history
Add a decorator to set custom names for entities
  • Loading branch information
blazejkustra authored Aug 15, 2024
2 parents 5185f3a + 738fb46 commit 535003a
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 8 deletions.
22 changes: 22 additions & 0 deletions docs/docs/guide/entity/decorators.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ Add following lines in `tsconfig.json` in order to use decorators with Typescrip

To see examples of decorators in use - check out [modeling](/docs/guide/entity/modeling) page.

## entity.customName(entityName)
### Description

This decorator is used to change the name of the entity. By default, the name of the entity is the name of the class. This is especially useful when you use minification or obfuscation tools.

### Arguments

`entityName: string` - String that will be used as the new name for the entity.

### Examples

```ts
import {entity} from 'dynamode/decorators';

@entity.customName("CustomName")
class YourModel extends Entity {
...
}

console.log(YourModel.constructor.name); // CustomName
```

## attribute.partitionKey.string(options?)
### Description

Expand Down
36 changes: 34 additions & 2 deletions docs/docs/guide/entity/modeling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class YourModel extends Entity {

### Decorators

**Only decorated properties will be saved in the database.** Thanks to this you can add undecorated properties to your entity that won't be saved but can be useful for your application logic.
**Only decorated properties will be saved in the database.** Thanks to this you can add undecorated properties to your entity that won't be saved but can be useful for your application logic. Check out the list of decorators [here](/docs/guide/entity/decorators).

:::caution
There are no limits to the number of attributes that you can add, but keep in mind the [DynamoDB size limits](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ServiceQuotas.html).
Expand Down Expand Up @@ -89,12 +89,44 @@ class YourModel extends Entity {
}
```

### dynamodeEntity property

Every entity has a `dynamodeEntity` property that is used to identify the entity in the database. It is a string that represents the class name of the entity.

:::danger
Each of your entities names must be unique within a table. Dynamode uses class names to identify entities.
:::

```ts
import Entity from 'dynamode/entity';

class YourModel extends Entity {
...
}

const instance = YourModelManager.get({ key: '...' });
console.log(instance.dynamodeEntity); // YourModel
```

```ts
import Entity from 'dynamode/entity';
import {entity} from 'dynamode/decorators';

@entity.customName('CustomName')
class YourModel extends Entity {
...
}

const instance = YourModelManager.get({ key: '...' });
console.log(instance.dynamodeEntity); // CustomName
```

### Additional methods

You can add non-static and static methods to your entities that you can call later. You can also add static properties that will be available in your class.

:::danger
Do not override `Entity.dynamodeEntity` static property, unless you know what you are doing.
Do not override `Entity.dynamodeEntity` property, unless you know what you are doing.
:::

```ts
Expand Down
14 changes: 14 additions & 0 deletions lib/decorators/helpers/customName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Dynamode from '@lib/dynamode/index';

function customName(newNameEntity: string): ClassDecorator {
return (entity: any) => {
const oldNameEntity = entity.name;
Object.defineProperty(entity, 'name', {
writable: true,
value: newNameEntity,
});
Dynamode.storage.transferMetadata(oldNameEntity, newNameEntity);
};
}

export default customName;
6 changes: 6 additions & 0 deletions lib/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import customName from '@lib/decorators/helpers/customName';
import { numberDate, stringDate } from '@lib/decorators/helpers/dates';
import {
numberGsiPartitionKey,
Expand Down Expand Up @@ -58,4 +59,9 @@ const attribute = {
suffix,
};

const entity = {
customName,
};

export default attribute;
export { entity };
9 changes: 9 additions & 0 deletions lib/dynamode/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ export default class DynamodeStorage {
return this.tables[tableName].metadata;
}

public transferMetadata(oldNameEntity: string, newNameEntity: string): void {
if (newNameEntity === oldNameEntity) {
return;
}

this.entities[newNameEntity] = this.entities[oldNameEntity];
delete this.entities[oldNameEntity];
}

public validateTableMetadata(entityName: string): void {
const metadata = this.getEntityMetadata(entityName);
const attributes = this.getEntityAttributes(entityName);
Expand Down
3 changes: 2 additions & 1 deletion lib/module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import Condition, { AttributeType } from '@lib/condition';
import attribute from '@lib/decorators';
import attribute, { entity } from '@lib/decorators';
import Dynamode from '@lib/dynamode/index';
import Entity from '@lib/entity';
import Query from '@lib/query';
Expand All @@ -26,6 +26,7 @@ export {

//decorators
attribute,
entity,

//Entity
Entity,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dynamode",
"version": "1.4.0",
"version": "1.5.0",
"description": "Dynamode is a modeling tool for Amazon's DynamoDB",
"main": "index.js",
"types": "index.d.ts",
Expand All @@ -16,7 +16,12 @@
"test:types": "vitest run types --typecheck",
"coverage": "vitest run unit --coverage",
"lint": "eslint lib --ext .ts,.js --max-warnings 0",
"lint:fix": "npm run lint -- --fix"
"lint:fix": "npm run lint -- --fix",
"bump:patch": "npm version patch",
"bump:minor": "npm version minor",
"bump:major": "npm version major",
"bump:next": "npm version prerelease --preid rc",
"publish:next": "npm run build && cd dist && npm publish --tag next"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.474.0",
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/TestIndex.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { vi } from 'vitest';

import attribute from '@lib/decorators';
import attribute, { entity } from '@lib/decorators';
import Dynamode from '@lib/dynamode/index';
import Entity from '@lib/entity';
import TableManager from '@lib/table';
Expand Down Expand Up @@ -28,6 +28,7 @@ export class TestIndex extends Entity {
}
}

@entity.customName('TEST_INDEX_GSI')
export class TestIndexWithGSI extends TestIndex {
// TODO: Make it so that partitionKey decorator is not required for a second time
@attribute.partitionKey.number()
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/decorators/helpers/customName.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';

import customName from '@lib/decorators/helpers/customName';
import Dynamode from '@lib/dynamode/index';

describe('Decorators', () => {
let transferMetadataSpy = vi.spyOn(Dynamode.storage, 'transferMetadata');

beforeEach(() => {
transferMetadataSpy = vi.spyOn(Dynamode.storage, 'transferMetadata');
transferMetadataSpy.mockReturnValue(undefined);
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('customName', async () => {
test('Should call transfer metadata when renaming an entity', async () => {
customName('NEW_NAME')(class OLD_NAME {});
expect(transferMetadataSpy).toHaveBeenNthCalledWith(1, 'OLD_NAME', 'NEW_NAME');
});

test('Should call transfer metadata when renaming an entity with the same name', async () => {
customName('OLD_NAME')(class OLD_NAME {});
expect(transferMetadataSpy).toHaveBeenNthCalledWith(1, 'OLD_NAME', 'OLD_NAME');
});
});
});
41 changes: 41 additions & 0 deletions tests/unit/dynamode/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,47 @@ describe('Dynamode', () => {
});
});

describe('transferMetadata', async () => {
test('Should transfer metadata to a different name', async () => {
storage.registerAttribute('OLD_NAME', 'prop', {
propertyName: 'prop',
type: Number,
role: 'attribute',
});
storage.transferMetadata('OLD_NAME', 'NEW_NAME');

expect(storage.entities['NEW_NAME']).toEqual({
attributes: {
prop: {
propertyName: 'prop',
type: Number,
role: 'attribute',
},
},
});
expect(storage.entities['OLD_NAME']).toEqual(undefined);
});

test('Should preserve metadata when the same name was used', async () => {
storage.registerAttribute('OLD_NAME', 'prop', {
propertyName: 'prop',
type: Number,
role: 'attribute',
});
storage.transferMetadata('OLD_NAME', 'OLD_NAME');

expect(storage.entities['OLD_NAME']).toEqual({
attributes: {
prop: {
propertyName: 'prop',
type: Number,
role: 'attribute',
},
},
});
});
});

describe('validateTableMetadata', async () => {
let validateMetadataAttribute = vi.spyOn(storageHelper, 'validateMetadataAttribute');
let getEntityMetadata = vi.spyOn(storage, 'getEntityMetadata');
Expand Down

0 comments on commit 535003a

Please sign in to comment.