From 6d3f9856ced84ee935e133c98266ad6a67a47769 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sat, 6 Jul 2024 16:59:36 +0200 Subject: [PATCH 1/8] Fix: Cannot add multiple attribute decorators for primary key --- lib/decorators/helpers/decorateAttribute.ts | 2 +- lib/dynamode/storage/helpers/validator.ts | 85 ++++++++------------- lib/dynamode/storage/index.ts | 68 ++++++++++++----- lib/dynamode/storage/types.ts | 16 ++-- lib/query/index.ts | 15 ++-- 5 files changed, 99 insertions(+), 87 deletions(-) diff --git a/lib/decorators/helpers/decorateAttribute.ts b/lib/decorators/helpers/decorateAttribute.ts index 5f97ea5..46961a2 100644 --- a/lib/decorators/helpers/decorateAttribute.ts +++ b/lib/decorators/helpers/decorateAttribute.ts @@ -19,7 +19,7 @@ export function decorateAttribute( throw new Error(`Index name is required for ${role} attribute`); } - return Dynamode.storage.registerAttribute(entityName, propertyName, { + return Dynamode.storage.registerIndex(entityName, propertyName, { propertyName, type, role: 'index', diff --git a/lib/dynamode/storage/helpers/validator.ts b/lib/dynamode/storage/helpers/validator.ts index 5a2a678..20ea6dd 100644 --- a/lib/dynamode/storage/helpers/validator.ts +++ b/lib/dynamode/storage/helpers/validator.ts @@ -1,13 +1,10 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import type { AttributeRole, ValidateDecoratedAttribute, ValidateMetadataAttribute } from '@lib/dynamode/storage/types'; -import Entity from '@lib/entity'; -import { Metadata } from '@lib/table/types'; +import type { ValidateDecoratedAttribute, ValidateMetadataAttribute } from '@lib/dynamode/storage/types'; import { DYNAMODE_ALLOWED_KEY_TYPES, ValidationError } from '@lib/utils'; export function validateMetadataAttribute({ attributes, name, - role, + validRoles, indexName, entityName, }: ValidateMetadataAttribute): void { @@ -16,21 +13,17 @@ export function validateMetadataAttribute({ throw new ValidationError(`Attribute "${name}" should be decorated in "${entityName}" Entity.`); } - if (attribute.role !== role) { + if (!validRoles.includes(attribute.role)) { throw new ValidationError(`Attribute "${name}" is decorated with a wrong role in "${entityName}" Entity.`); } - if (!indexName && attribute.role === 'index') { - throw new ValidationError(`Index for attribute "${name}" should be added to "${entityName}" Entity metadata.`); - } - - if (indexName && attribute.role !== 'index') { + if (indexName && !attribute.indexes) { throw new ValidationError( `Attribute "${name}" should be decorated with index "${indexName}" in "${entityName}" Entity.`, ); } - if (indexName && attribute.role === 'index' && !attribute.indexes.some((index) => index.name === indexName)) { + if (indexName && !attribute.indexes?.some((index) => index.name === indexName)) { throw new ValidationError( `Attribute "${name}" is not decorated with index "${indexName}" in "${entityName}" Entity.`, ); @@ -47,51 +40,37 @@ export function validateDecoratedAttribute({ metadata, entityName, }: ValidateDecoratedAttribute): void { - const roleValidationMap: Record boolean> = { - partitionKey: ({ name, metadata }) => metadata.partitionKey !== name, - sortKey: ({ name, metadata }) => metadata.sortKey !== name, - index: ({ attribute, name, metadata }) => { - if (!('indexes' in attribute) || !attribute.indexes.length) { - return true; - } - - return attribute.indexes.some((index) => { - switch (index.role) { - case 'gsiPartitionKey': - return metadata.indexes?.[index.name]?.partitionKey !== name; - case 'gsiSortKey': - case 'lsiSortKey': - return metadata.indexes?.[index.name]?.sortKey !== name; - default: - return true; - } - }); - }, - date: () => false, - attribute: () => false, - dynamodeEntity: () => false, - }; - - const validateAttributeRole = roleValidationMap[attribute.role]; - if (validateAttributeRole({ attribute, name, metadata, entityName })) { + if (attribute.role === 'partitionKey' && metadata.partitionKey !== name) { throw new ValidationError( - `Attribute "${name}" is decorated with a wrong role in "${entityName}" Entity. This could mean two things:\n1. The attribute is not defined in the table metadata.\n2. The attribute is defined in the table metadata but wrong decorator was used.\n`, + `Attribute "${name}" is not defined as a partition key in "${entityName}" Entity's metadata.`, ); } -} -export function validateMetadataUniqueness(entityName: string, metadata: Metadata): void { - const allIndexes = Object.values(metadata.indexes ?? {}).flatMap((index) => [index.partitionKey, index.sortKey]); - - const metadataKeys = [ - metadata.partitionKey, - metadata.sortKey, - metadata.createdAt, - metadata.updatedAt, - ...new Set(allIndexes), - ].filter((attribute) => !!attribute); + if (attribute.role === 'sortKey' && metadata.sortKey !== name) { + throw new ValidationError(`Attribute "${name}" is not defined as a sort key in "${entityName}" Entity's metadata.`); + } - if (metadataKeys.length !== new Set(metadataKeys).size) { - throw new ValidationError(`Duplicated metadata keys passed to "${entityName}" TableManager.`); + if (!attribute.indexes) { + return; } + + attribute.indexes.forEach((index) => { + if (index.role === 'gsiPartitionKey' && metadata.indexes?.[index.name]?.partitionKey !== name) { + throw new ValidationError( + `Attribute "${name}" is not defined as a GSI partition key in "${entityName}" Entity's metadata for index named "${index.name}".`, + ); + } + + if (index.role === 'gsiSortKey' && metadata.indexes?.[index.name]?.sortKey !== name) { + throw new ValidationError( + `Attribute "${name}" is not defined as a GSI sort key in "${entityName}" Entity's metadata for index named "${index.name}".`, + ); + } + + if (index.role === 'lsiSortKey' && metadata.indexes?.[index.name]?.sortKey !== name) { + throw new ValidationError( + `Attribute "${name}" is not defined as a LSI sort key in "${entityName}" Entity's metadata for index named "${index.name}".`, + ); + } + }); } diff --git a/lib/dynamode/storage/index.ts b/lib/dynamode/storage/index.ts index e1c0ab8..bc34379 100644 --- a/lib/dynamode/storage/index.ts +++ b/lib/dynamode/storage/index.ts @@ -1,9 +1,8 @@ -import { - validateDecoratedAttribute, - validateMetadataAttribute, - validateMetadataUniqueness, -} from '@lib/dynamode/storage/helpers/validator'; +import { Except } from 'type-fest'; + +import { validateDecoratedAttribute, validateMetadataAttribute } from '@lib/dynamode/storage/helpers/validator'; import type { + AttributeIndexMetadata, AttributeMetadata, AttributesMetadata, EntitiesMetadata, @@ -51,21 +50,43 @@ export default class DynamodeStorage { this.entities[entity.name] = entityMetadata; } - public registerAttribute(entityName: string, propertyName: string, value: AttributeMetadata): void { + public registerAttribute( + entityName: string, + propertyName: string, + value: Except, + ): void { if (!this.entities[entityName]) { this.entities[entityName] = { attributes: {} } as EntityMetadata; } const attributeMetadata = this.entities[entityName].attributes[propertyName]; - if (attributeMetadata && (attributeMetadata.role !== 'index' || value.role !== 'index')) { + // Throw error if attribute was already decorated and it's not an index + if (attributeMetadata && attributeMetadata.role !== 'index') { throw new DynamodeStorageError(`Attribute "${propertyName}" was already decorated in entity "${entityName}"`); } - if (attributeMetadata && attributeMetadata.role === 'index' && value.role === 'index') { - return void attributeMetadata.indexes.push(...value.indexes); + // Otherwise, register attribute and copy over indexes + this.entities[entityName].attributes[propertyName] = { + ...value, + indexes: attributeMetadata?.indexes, + }; + } + + public registerIndex(entityName: string, propertyName: string, value: AttributeIndexMetadata): void { + if (!this.entities[entityName]) { + this.entities[entityName] = { attributes: {} } as EntityMetadata; + } + + const attributeMetadata = this.entities[entityName].attributes[propertyName]; + + // Merge indexes if attribute was already decorated + if (attributeMetadata) { + attributeMetadata.indexes = [...(attributeMetadata.indexes ?? []), ...value.indexes]; + return; } + // Register attribute with indexes this.entities[entityName].attributes[propertyName] = value; } @@ -138,28 +159,25 @@ export default class DynamodeStorage { const metadata = this.getEntityMetadata(entityName); const attributes = this.getEntityAttributes(entityName); - // Validate metadata - validateMetadataUniqueness(entityName, metadata); - // Validate decorated attributes Object.entries(attributes).forEach(([name, attribute]) => validateDecoratedAttribute({ name, attribute, metadata, entityName }), ); // Validate table partition key - validateMetadataAttribute({ name: metadata.partitionKey, attributes, role: 'partitionKey', entityName }); + validateMetadataAttribute({ name: metadata.partitionKey, attributes, validRoles: ['partitionKey'], entityName }); // Validate table sort key if (metadata.sortKey) { - validateMetadataAttribute({ name: metadata.sortKey, attributes, role: 'sortKey', entityName }); + validateMetadataAttribute({ name: metadata.sortKey, attributes, validRoles: ['sortKey'], entityName }); } // Validate table timestamps if (metadata.createdAt) { - validateMetadataAttribute({ name: metadata.createdAt, attributes, role: 'date', entityName }); + validateMetadataAttribute({ name: metadata.createdAt, attributes, validRoles: ['date'], entityName }); } if (metadata.updatedAt) { - validateMetadataAttribute({ name: metadata.updatedAt, attributes, role: 'date', entityName }); + validateMetadataAttribute({ name: metadata.updatedAt, attributes, validRoles: ['date'], entityName }); } // Validate table indexes @@ -175,19 +193,31 @@ export default class DynamodeStorage { validateMetadataAttribute({ name: index.partitionKey, attributes, - role: 'index', indexName, entityName, + validRoles: ['partitionKey', 'index'], }); if (index.sortKey) { - validateMetadataAttribute({ name: index.sortKey, attributes, role: 'index', indexName, entityName }); + validateMetadataAttribute({ + name: index.sortKey, + attributes, + indexName, + entityName, + validRoles: ['sortKey', 'index'], + }); } } // Validate LSI if (index.sortKey && !index.partitionKey) { - validateMetadataAttribute({ name: index.sortKey, attributes, role: 'index', indexName, entityName }); + validateMetadataAttribute({ + name: index.sortKey, + attributes, + indexName, + entityName, + validRoles: ['sortKey', 'index'], + }); } }); } diff --git a/lib/dynamode/storage/types.ts b/lib/dynamode/storage/types.ts index 6ca001b..40e35b5 100644 --- a/lib/dynamode/storage/types.ts +++ b/lib/dynamode/storage/types.ts @@ -24,20 +24,18 @@ type BaseAttributeMetadata = { suffix?: string; }; -export type NonIndexAttributeMetadata = BaseAttributeMetadata & { - role: Exclude; - indexName?: never; -}; - export type IndexMetadata = { name: string; role: AttributeIndexRole }; -export type IndexAttributeMetadata = BaseAttributeMetadata & { +export type AttributeMetadata = BaseAttributeMetadata & { + role: AttributeRole; + indexes?: IndexMetadata[]; +}; + +export type AttributeIndexMetadata = BaseAttributeMetadata & { role: 'index'; indexes: IndexMetadata[]; }; -export type AttributeMetadata = NonIndexAttributeMetadata | IndexAttributeMetadata; - export type AttributesMetadata = { [attributeName: string]: AttributeMetadata; }; @@ -67,7 +65,7 @@ export type ValidateMetadataAttribute = { entityName: string; name: string; attributes: AttributesMetadata; - role: AttributeRole; + validRoles: AttributeRole[]; indexName?: string; }; diff --git a/lib/query/index.ts b/lib/query/index.ts index 8f4563f..9631d07 100644 --- a/lib/query/index.ts +++ b/lib/query/index.ts @@ -125,13 +125,18 @@ export default class Query, E extends typeof Entity> exten throw new ValidationError('You need to use ".partitionKey()" method before calling ".run()"'); } - // Primary key - if (partitionKeyMetadata.role === 'partitionKey' && sortKeyMetadata?.role !== 'index') { + // Primary key without sort key + if (partitionKeyMetadata.role === 'partitionKey' && !sortKeyMetadata) { + return; + } + + // Primary key with sort key + if (partitionKeyMetadata.role === 'partitionKey' && sortKeyMetadata?.role === 'sortKey') { return; } // GSI with sort key - if (partitionKeyMetadata.role === 'index' && sortKeyMetadata?.role === 'index') { + if (partitionKeyMetadata.indexes && sortKeyMetadata?.indexes) { const pkIndexes: IndexMetadata[] = partitionKeyMetadata.indexes; const skIndexes: IndexMetadata[] = sortKeyMetadata.indexes; @@ -153,7 +158,7 @@ export default class Query, E extends typeof Entity> exten } // GSI without sort key - if (partitionKeyMetadata.role === 'index' && !sortKeyMetadata) { + if (partitionKeyMetadata.indexes && !sortKeyMetadata) { const possibleIndexes = partitionKeyMetadata.indexes; if (possibleIndexes.length > 1) { @@ -169,7 +174,7 @@ export default class Query, E extends typeof Entity> exten } // LSI with sort key - if (partitionKeyMetadata.role === 'partitionKey' && sortKeyMetadata?.role === 'index') { + if (partitionKeyMetadata.role === 'partitionKey' && sortKeyMetadata?.indexes) { const possibleIndexes = sortKeyMetadata.indexes; if (possibleIndexes.length > 1) { From 7e5eccffcddfb4be17889ee09490c367580669c7 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sat, 6 Jul 2024 17:38:35 +0200 Subject: [PATCH 2/8] Fix tests after changes --- examples/test.ts | 51 +++++++++ .../helpers/decorateAttribute.test.ts | 6 +- tests/unit/dynamode/helpers.test.ts | 102 +++++------------- tests/unit/dynamode/index.test.ts | 23 ++-- 4 files changed, 91 insertions(+), 91 deletions(-) create mode 100644 examples/test.ts diff --git a/examples/test.ts b/examples/test.ts new file mode 100644 index 0000000..24a20ee --- /dev/null +++ b/examples/test.ts @@ -0,0 +1,51 @@ +import attribute from '../dist/decorators'; +import Entity from '../dist/entity'; +import TableManager from '../dist/table'; + +export class TableUser extends Entity { + @attribute.gsi.partitionKey.string({ indexName: 'index-1' }) + @attribute.partitionKey.string() + domain: string; + + @attribute.sortKey.string() + @attribute.gsi.sortKey.string({ indexName: 'index-1' }) + email: string; + + constructor(data: { domain: string; email: string }) { + super(data); + this.domain = data.domain; + this.email = data.email; + } +} + +export const UserdataTableManager = new TableManager(TableUser, { + tableName: 'USERS_TABLE_NAME', + partitionKey: 'domain', + sortKey: 'email', + indexes: { + 'index-1': { + partitionKey: 'domain', + sortKey: 'email', + }, + }, +}); + +const EntityManager = UserdataTableManager.entityManager(); + +async function test() { + const entity = await EntityManager.put( + new TableUser({ + domain: 'test', + email: 'not_empty', + }), + ); + console.log(entity); +} + +async function createTable() { + const table = await UserdataTableManager.createTable(); + console.log(table); +} + +// createTable(); +test(); diff --git a/tests/unit/decorators/helpers/decorateAttribute.test.ts b/tests/unit/decorators/helpers/decorateAttribute.test.ts index 742f128..8b5b5b4 100644 --- a/tests/unit/decorators/helpers/decorateAttribute.test.ts +++ b/tests/unit/decorators/helpers/decorateAttribute.test.ts @@ -7,10 +7,14 @@ import { MockEntity, mockInstance } from '../../../fixtures'; describe('Decorators', () => { let registerAttributeSpy = vi.spyOn(Dynamode.storage, 'registerAttribute'); + let registerIndexSpy = vi.spyOn(Dynamode.storage, 'registerIndex'); beforeEach(() => { registerAttributeSpy = vi.spyOn(Dynamode.storage, 'registerAttribute'); registerAttributeSpy.mockReturnValue(undefined); + + registerIndexSpy = vi.spyOn(Dynamode.storage, 'registerIndex'); + registerIndexSpy.mockReturnValue(undefined); }); afterEach(() => { @@ -35,7 +39,7 @@ describe('Decorators', () => { indexName: 'GSI', })(mockInstance, 'gsiKey'); - expect(registerAttributeSpy).toBeCalledWith(MockEntity.name, 'gsiKey', { + expect(registerIndexSpy).toBeCalledWith(MockEntity.name, 'gsiKey', { propertyName: 'gsiKey', type: String, role: 'index', diff --git a/tests/unit/dynamode/helpers.test.ts b/tests/unit/dynamode/helpers.test.ts index 54e1205..43f8a79 100644 --- a/tests/unit/dynamode/helpers.test.ts +++ b/tests/unit/dynamode/helpers.test.ts @@ -1,11 +1,7 @@ import { describe, expect, test } from 'vitest'; -import { - validateDecoratedAttribute, - validateMetadataAttribute, - validateMetadataUniqueness, -} from '@lib/dynamode/storage/helpers/validator'; -import { AttributesMetadata, IndexAttributeMetadata } from '@lib/dynamode/storage/types'; +import { validateDecoratedAttribute, validateMetadataAttribute } from '@lib/dynamode/storage/helpers/validator'; +import { AttributesMetadata } from '@lib/dynamode/storage/types'; import { Metadata } from '@lib/table/types'; import { MockEntity, TEST_TABLE_NAME } from '../../fixtures'; @@ -16,7 +12,7 @@ const attributes: AttributesMetadata = { propertyName: 'partitionKey', type: String, role: 'partitionKey', - indexName: undefined, + indexes: undefined, prefix: 'prefix', suffix: undefined, }, @@ -24,7 +20,7 @@ const attributes: AttributesMetadata = { propertyName: 'sortKey', type: String, role: 'sortKey', - indexName: undefined, + indexes: undefined, prefix: undefined, suffix: undefined, }, @@ -59,7 +55,7 @@ const attributes: AttributesMetadata = { propertyName: 'LSI_1_SK', type: Number, role: 'index', - indexes: [{ name: 'LSI_1_NAME', role: 'gsiSortKey' }], + indexes: [{ name: 'LSI_1_NAME', role: 'lsiSortKey' }], prefix: undefined, suffix: undefined, }, @@ -67,7 +63,7 @@ const attributes: AttributesMetadata = { propertyName: 'strDate', type: String, role: 'date', - indexName: undefined, + indexes: undefined, prefix: undefined, suffix: undefined, }, @@ -75,7 +71,7 @@ const attributes: AttributesMetadata = { propertyName: 'numDate', type: Number, role: 'date', - indexName: undefined, + indexes: undefined, prefix: undefined, suffix: undefined, }, @@ -83,7 +79,7 @@ const attributes: AttributesMetadata = { propertyName: 'attr', type: String, role: 'attribute', - indexName: undefined, + indexes: undefined, prefix: undefined, suffix: undefined, }, @@ -110,30 +106,13 @@ const metadata: Metadata = { updatedAt: 'numDate', }; -const metadataInvalid: Metadata = { - tableName: TEST_TABLE_NAME, - partitionKey: 'partitionKey', - sortKey: 'sortKey', - indexes: { - GSI_1_NAME: { - partitionKey: 'partitionKey', - sortKey: 'GSI_SK', - }, - LSI_1_NAME: { - sortKey: 'LSI_1_SK', - }, - }, - createdAt: 'strDate', - updatedAt: 'numDate', -}; - describe('Dynamode helpers', () => { describe('validateMetadataAttribute', async () => { test('Should successfully validate attributes', async () => { expect( validateMetadataAttribute({ name: 'partitionKey', - role: 'partitionKey', + validRoles: ['partitionKey'], attributes: { partitionKey: attributes.partitionKey }, entityName, }), @@ -141,7 +120,7 @@ describe('Dynamode helpers', () => { expect( validateMetadataAttribute({ name: 'sortKey', - role: 'sortKey', + validRoles: ['sortKey'], attributes: { sortKey: attributes.sortKey }, entityName, }), @@ -149,7 +128,7 @@ describe('Dynamode helpers', () => { expect( validateMetadataAttribute({ name: 'GSI_1_PK', - role: 'index', + validRoles: ['partitionKey', 'index'], attributes: { GSI_1_PK: attributes.GSI_1_PK }, indexName: 'GSI_1_NAME', entityName, @@ -158,7 +137,7 @@ describe('Dynamode helpers', () => { expect( validateMetadataAttribute({ name: 'GSI_SK', - role: 'index', + validRoles: ['sortKey', 'index'], attributes: { GSI_SK: attributes.GSI_SK }, indexName: 'GSI_1_NAME', entityName, @@ -167,7 +146,7 @@ describe('Dynamode helpers', () => { expect( validateMetadataAttribute({ name: 'LSI_1_SK', - role: 'index', + validRoles: ['sortKey', 'index'], attributes: { LSI_1_SK: attributes.LSI_1_SK }, indexName: 'LSI_1_NAME', entityName, @@ -177,7 +156,7 @@ describe('Dynamode helpers', () => { test("Should throw an error if attribute isn't decorated", async () => { expect(() => - validateMetadataAttribute({ name: 'name', role: 'partitionKey', attributes: {}, entityName }), + validateMetadataAttribute({ name: 'name', validRoles: ['partitionKey'], attributes: {}, entityName }), ).toThrowError(/^Attribute ".*" should be decorated in "EntityName" Entity.$/); }); @@ -185,7 +164,7 @@ describe('Dynamode helpers', () => { expect(() => validateMetadataAttribute({ name: 'partitionKey', - role: 'sortKey', + validRoles: ['sortKey'], attributes: { partitionKey: attributes.partitionKey }, entityName, }), @@ -196,7 +175,7 @@ describe('Dynamode helpers', () => { expect(() => validateMetadataAttribute({ name: 'sortKey', - role: 'sortKey', + validRoles: ['sortKey'], attributes: { sortKey: attributes.sortKey }, indexName: 'indexName', entityName, @@ -205,27 +184,19 @@ describe('Dynamode helpers', () => { expect(() => validateMetadataAttribute({ name: 'GSI_SK', - role: 'index', + validRoles: ['sortKey', 'index'], attributes: { GSI_SK: attributes.GSI_SK }, indexName: 'indexName', entityName, }), ).toThrowError(/^Attribute ".*" is not decorated with index "indexName" in "EntityName" Entity.$/); - expect(() => - validateMetadataAttribute({ - name: 'GSI_SK', - role: 'index', - attributes: { GSI_SK: attributes.GSI_SK }, - entityName, - }), - ).toThrowError(/^Index for attribute ".*" should be added to "EntityName" Entity metadata.$/); }); test("Should throw an error if attribute roles doesn't match", async () => { expect(() => validateMetadataAttribute({ name: 'partitionKey', - role: 'partitionKey', + validRoles: ['partitionKey'], attributes: { partitionKey: { ...attributes.partitionKey, type: Date as any } }, entityName, }), @@ -271,56 +242,35 @@ describe('Dynamode helpers', () => { test('Should throw an error if decorated attribute is different than this in metadata', async () => { expect(() => validateDecoratedAttribute({ name: 'name', attribute: attributes.partitionKey, entityName, metadata }), - ).toThrowError(/^Attribute ".*" is decorated with a wrong role in "EntityName" Entity.*/); + ).toThrowError(/^Attribute ".*" is not defined as a partition key in ".*" Entity's metadata.*/); expect(() => validateDecoratedAttribute({ name: 'name', attribute: attributes.sortKey, entityName, metadata }), - ).toThrowError(/^Attribute ".*" is decorated with a wrong role in "EntityName" Entity.*/); + ).toThrowError(/^Attribute ".*" is not defined as a sort key in ".*" Entity's metadata.*/); expect(() => validateDecoratedAttribute({ name: 'name', attribute: attributes.GSI_1_PK, entityName, metadata }), - ).toThrowError(/^Attribute ".*" is decorated with a wrong role in "EntityName" Entity.*/); + ).toThrowError(/^Attribute ".*" is not defined as a GSI partition key in ".*" Entity's metadata.*/); expect(() => validateDecoratedAttribute({ name: 'name', attribute: attributes.GSI_SK, entityName, metadata }), - ).toThrowError(/^Attribute ".*" is decorated with a wrong role in "EntityName" Entity.*/); + ).toThrowError(/^Attribute ".*" is not defined as a GSI sort key in ".*" Entity's metadata.*/); expect(() => validateDecoratedAttribute({ name: 'name', attribute: attributes.LSI_1_SK, entityName, metadata }), - ).toThrowError(/^Attribute ".*" is decorated with a wrong role in "EntityName" Entity.*/); - - expect(() => - validateDecoratedAttribute({ - name: 'LSI_1_SK', - attribute: { ...attributes.LSI_1_SK, indexes: [] } as IndexAttributeMetadata, - entityName, - metadata, - }), - ).toThrowError(/^Attribute ".*" is decorated with a wrong role in "EntityName" Entity.*/); + ).toThrowError(/^Attribute ".*" is not defined as a LSI sort key in ".*" Entity's metadata.*/); expect(() => validateDecoratedAttribute({ name: 'LSI_1_SK', attribute: { ...attributes.LSI_1_SK, - indexes: [{ name: 'LSI_1_NAME', role: 'invalid' as any }], - } as IndexAttributeMetadata, + indexes: [{ name: 'LSI_1_NAME', role: 'gsiPartitionKey' as any }], + }, entityName, metadata, }), - ).toThrowError(/^Attribute ".*" is decorated with a wrong role in "EntityName" Entity.*/); - }); - }); - - describe('validateMetadataUniqueness', async () => { - test('Should successfully validate that all keys are unique', async () => { - expect(validateMetadataUniqueness(entityName, metadata)).toBeUndefined(); - }); - - test('Should throw an error if decorated attribute is different than this in metadata', async () => { - expect(() => validateMetadataUniqueness(entityName, metadataInvalid)).toThrowError( - /^Duplicated metadata keys passed to "EntityName" TableManager.$/, - ); + ).toThrowError(/^Attribute ".*" is not defined as a GSI partition key in ".*" Entity's metadata.*/); }); }); }); diff --git a/tests/unit/dynamode/index.test.ts b/tests/unit/dynamode/index.test.ts index f5d1fc7..3c97f79 100644 --- a/tests/unit/dynamode/index.test.ts +++ b/tests/unit/dynamode/index.test.ts @@ -4,7 +4,6 @@ import { convertToAttr, convertToNative, marshall, unmarshall } from '@aws-sdk/u import Dynamode from '@lib/dynamode/index'; import DynamodeStorage from '@lib/dynamode/storage'; import * as storageHelper from '@lib/dynamode/storage/helpers/validator'; -import { IndexAttributeMetadata } from '@lib/dynamode/storage/types'; import { Metadata } from '@lib/table/types'; import { DynamodeStorageError, ValidationError } from '@lib/utils/errors'; @@ -102,17 +101,14 @@ describe('Dynamode', () => { prefix: 'prefix', suffix: 'suffix', type: String, - role: 'index', - indexes: [{ name: 'indexName', role: 'gsiPartitionKey' }], + role: 'partitionKey', }); expect(storage.entities[MockEntity.name].attributes['propertyName'].propertyName).toEqual('propertyName'); - expect( - (storage.entities[MockEntity.name].attributes['propertyName'] as IndexAttributeMetadata).indexes, - ).toEqual([{ name: 'indexName', role: 'gsiPartitionKey' }]); + expect(storage.entities[MockEntity.name].attributes['propertyName'].indexes).toEqual(undefined); expect(storage.entities[MockEntity.name].attributes['propertyName'].prefix).toEqual('prefix'); expect(storage.entities[MockEntity.name].attributes['propertyName'].suffix).toEqual('suffix'); expect(storage.entities[MockEntity.name].attributes['propertyName'].type).toEqual(String); - expect(storage.entities[MockEntity.name].attributes['propertyName'].role).toEqual('index'); + expect(storage.entities[MockEntity.name].attributes['propertyName'].role).toEqual('partitionKey'); }); test('Should successfully register parent class property', async () => { @@ -164,10 +160,9 @@ describe('Dynamode', () => { type: Number, }, propertyName: { - indexes: [{ name: 'indexName', role: 'gsiPartitionKey' }], prefix: 'PREFIX', propertyName: 'propertyName', - role: 'index', + role: 'partitionKey', suffix: 'SUFFIX', type: String, }, @@ -250,13 +245,13 @@ describe('Dynamode', () => { expect(validateMetadataAttribute).toHaveBeenNthCalledWith(1, { name: 'partitionKey', entityName: 'TestTable', - role: 'partitionKey', + validRoles: ['partitionKey'], attributes: {}, }); expect(validateMetadataAttribute).toHaveBeenNthCalledWith(2, { name: 'sortKey', entityName: 'TestTable', - role: 'sortKey', + validRoles: ['sortKey'], attributes: {}, }); }); @@ -274,13 +269,13 @@ describe('Dynamode', () => { expect(validateMetadataAttribute).toHaveBeenNthCalledWith(1, { name: 'pk', entityName: 'TestTable', - role: 'partitionKey', + validRoles: ['partitionKey'], attributes: {}, }); expect(validateMetadataAttribute).toHaveBeenNthCalledWith(2, { name: 'sk', entityName: 'TestTable', - role: 'sortKey', + validRoles: ['sortKey'], attributes: {}, }); }); @@ -300,7 +295,7 @@ describe('Dynamode', () => { expect(validateMetadataAttribute).toHaveBeenNthCalledWith(1, { name: 'pk', entityName: 'TestTable', - role: 'partitionKey', + validRoles: ['partitionKey'], attributes: {}, }); }); From 179f6fc9dbc875891c122f51bd1fc6168183583a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sat, 6 Jul 2024 18:06:28 +0200 Subject: [PATCH 3/8] Fix tests and coverage --- tests/unit/dynamode/index.test.ts | 159 ++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/tests/unit/dynamode/index.test.ts b/tests/unit/dynamode/index.test.ts index 3c97f79..89d9581 100644 --- a/tests/unit/dynamode/index.test.ts +++ b/tests/unit/dynamode/index.test.ts @@ -135,6 +135,124 @@ describe('Dynamode', () => { }); }); + describe('registerIndex', () => { + test('Should successfully register index', async () => { + storage.registerIndex(MockEntity.name, 'propertyNameIndex', { + propertyName: 'propertyNameIndex', + prefix: 'prefix', + suffix: 'suffix', + type: String, + role: 'index', + indexes: [{ name: 'index', role: 'gsiPartitionKey' }], + }); + + expect(storage.entities[MockEntity.name].attributes['propertyNameIndex'].propertyName).toEqual( + 'propertyNameIndex', + ); + expect(storage.entities[MockEntity.name].attributes['propertyNameIndex'].indexes).toEqual([ + { name: 'index', role: 'gsiPartitionKey' }, + ]); + expect(storage.entities[MockEntity.name].attributes['propertyNameIndex'].prefix).toEqual('prefix'); + expect(storage.entities[MockEntity.name].attributes['propertyNameIndex'].suffix).toEqual('suffix'); + expect(storage.entities[MockEntity.name].attributes['propertyNameIndex'].type).toEqual(String); + expect(storage.entities[MockEntity.name].attributes['propertyNameIndex'].role).toEqual('index'); + }); + + test('Should successfully register parent class property', async () => { + storage.registerIndex(TestTable.name, 'parentPropertyNameIndex', { + propertyName: 'parentPropertyNameIndex', + type: Number, + role: 'index', + indexes: [{ name: 'index', role: 'lsiSortKey' }], + }); + + expect(storage.entities[TestTable.name].attributes['parentPropertyNameIndex'].propertyName).toEqual( + 'parentPropertyNameIndex', + ); + expect(storage.entities[TestTable.name].attributes['parentPropertyNameIndex'].indexes).toEqual([ + { name: 'index', role: 'lsiSortKey' }, + ]); + expect(storage.entities[TestTable.name].attributes['parentPropertyNameIndex'].type).toEqual(Number); + expect(storage.entities[TestTable.name].attributes['parentPropertyNameIndex'].role).toEqual('index'); + }); + + test('Should successfully register indexes multiple times', async () => { + storage.registerIndex(TestTable.name, 'propertyNameIndex2', { + propertyName: 'propertyNameIndex2', + type: Number, + role: 'index', + indexes: [{ name: 'index1', role: 'lsiSortKey' }], + }); + + storage.registerIndex(TestTable.name, 'propertyNameIndex2', { + propertyName: 'propertyNameIndex2', + type: String, + role: 'index', + indexes: [{ name: 'index2', role: 'gsiSortKey' }], + }); + + expect(storage.entities[TestTable.name].attributes['propertyNameIndex2'].propertyName).toEqual( + 'propertyNameIndex2', + ); + expect(storage.entities[TestTable.name].attributes['propertyNameIndex2'].indexes).toEqual([ + { name: 'index1', role: 'lsiSortKey' }, + { name: 'index2', role: 'gsiSortKey' }, + ]); + expect(storage.entities[TestTable.name].attributes['propertyNameIndex2'].type).toEqual(Number); + expect(storage.entities[TestTable.name].attributes['propertyNameIndex2'].role).toEqual('index'); + }); + }); + + describe('registerAttribute and registerIndex', () => { + test('Should successfully register attribute first then index', async () => { + const emptyStorage = new DynamodeStorage(); + + emptyStorage.registerAttribute(MockEntity.name, 'propertyName', { + propertyName: 'propertyName', + type: String, + role: 'partitionKey', + }); + + emptyStorage.registerIndex(MockEntity.name, 'propertyName', { + propertyName: 'propertyName', + type: String, + role: 'index', + indexes: [{ name: 'index', role: 'gsiPartitionKey' }], + }); + + expect(emptyStorage.entities[MockEntity.name].attributes['propertyName'].propertyName).toEqual('propertyName'); + expect(emptyStorage.entities[MockEntity.name].attributes['propertyName'].indexes).toEqual([ + { name: 'index', role: 'gsiPartitionKey' }, + ]); + expect(emptyStorage.entities[MockEntity.name].attributes['propertyName'].type).toEqual(String); + expect(emptyStorage.entities[MockEntity.name].attributes['propertyName'].role).toEqual('partitionKey'); + }); + + test('Should successfully register index first then attribute', async () => { + const emptyStorage = new DynamodeStorage(); + + emptyStorage.registerIndex(MockEntity.name, 'propertyName', { + propertyName: 'propertyName', + type: String, + role: 'index', + indexes: [{ name: 'index', role: 'gsiPartitionKey' }], + }); + + emptyStorage.registerAttribute(MockEntity.name, 'propertyName', { + propertyName: 'propertyName', + type: String, + role: 'partitionKey', + }); + + expect(emptyStorage.entities[MockEntity.name].attributes['propertyName'].propertyName).toEqual('propertyName'); + expect(emptyStorage.entities[MockEntity.name].attributes['propertyName'].indexes).toEqual([ + { name: 'index', role: 'gsiPartitionKey' }, + ]); + expect(emptyStorage.entities[MockEntity.name].attributes['propertyName'].type).toEqual(String); + expect(emptyStorage.entities[MockEntity.name].attributes['propertyName'].role).toEqual('partitionKey'); + }); + }); + describe('updateAttributePrefix', () => { test('Should successfully update class property with a prefix', async () => { expect(storage.entities[MockEntity.name].attributes['propertyName'].prefix).toEqual('prefix'); @@ -158,6 +276,7 @@ describe('Dynamode', () => { propertyName: 'parentPropertyName', role: 'attribute', type: Number, + indexes: undefined, }, propertyName: { prefix: 'PREFIX', @@ -165,6 +284,46 @@ describe('Dynamode', () => { role: 'partitionKey', suffix: 'SUFFIX', type: String, + indexes: undefined, + }, + propertyNameIndex: { + indexes: [ + { + name: 'index', + role: 'gsiPartitionKey', + }, + ], + prefix: 'prefix', + propertyName: 'propertyNameIndex', + role: 'index', + suffix: 'suffix', + type: String, + }, + propertyNameIndex2: { + indexes: [ + { + name: 'index1', + role: 'lsiSortKey', + }, + { + name: 'index2', + role: 'gsiSortKey', + }, + ], + propertyName: 'propertyNameIndex2', + role: 'index', + type: Number, + }, + parentPropertyNameIndex: { + indexes: [ + { + name: 'index', + role: 'lsiSortKey', + }, + ], + propertyName: 'parentPropertyNameIndex', + role: 'index', + type: Number, }, }); }); From 606af2d0b8c673ec0e50fcb6f7603b5eb1260f7c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sat, 6 Jul 2024 18:06:57 +0200 Subject: [PATCH 4/8] Rename to playground --- examples/{test.ts => playground.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{test.ts => playground.ts} (100%) diff --git a/examples/test.ts b/examples/playground.ts similarity index 100% rename from examples/test.ts rename to examples/playground.ts From da22e2d9483225c1f93efbeaad8f7081e2b94152 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sat, 6 Jul 2024 18:08:28 +0200 Subject: [PATCH 5/8] Fix typecheck --- tests/unit/query/index.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/query/index.test.ts b/tests/unit/query/index.test.ts index 1a61b19..4b171a9 100644 --- a/tests/unit/query/index.test.ts +++ b/tests/unit/query/index.test.ts @@ -16,7 +16,7 @@ const attributes = { propertyName: 'partitionKey', type: String, role: 'partitionKey', - indexName: undefined, + indexes: undefined, prefix: 'prefix', suffix: undefined, }, @@ -24,7 +24,7 @@ const attributes = { propertyName: 'sortKey', type: String, role: 'sortKey', - indexName: undefined, + indexes: undefined, prefix: undefined, suffix: undefined, }, @@ -82,7 +82,7 @@ const attributes = { propertyName: 'attr', type: String, role: 'attribute', - indexName: undefined, + indexes: undefined, prefix: undefined, suffix: undefined, }, From 3edd9548bf2be21d1be998793c3d2ba874e80c69 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sun, 7 Jul 2024 21:55:08 +0200 Subject: [PATCH 6/8] Allow GSI decorators to decorate both partition and sort keys interchangeably --- lib/dynamode/storage/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dynamode/storage/index.ts b/lib/dynamode/storage/index.ts index bc34379..ba32298 100644 --- a/lib/dynamode/storage/index.ts +++ b/lib/dynamode/storage/index.ts @@ -195,7 +195,7 @@ export default class DynamodeStorage { attributes, indexName, entityName, - validRoles: ['partitionKey', 'index'], + validRoles: ['partitionKey', 'sortKey', 'index'], }); if (index.sortKey) { @@ -204,7 +204,7 @@ export default class DynamodeStorage { attributes, indexName, entityName, - validRoles: ['sortKey', 'index'], + validRoles: ['partitionKey', 'sortKey', 'index'], }); } } From 71501022d57821fc1876392a7004f85dbae2e1c3 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sat, 20 Jul 2024 13:26:09 +0200 Subject: [PATCH 7/8] Move fixtures to a catalog --- tests/e2e/condition.test.ts | 2 +- tests/e2e/entity/batchDelete.test.ts | 2 +- tests/e2e/entity/batchGet.test.ts | 2 +- tests/e2e/entity/batchPut.test.ts | 2 +- tests/e2e/entity/create.test.ts | 2 +- tests/e2e/entity/delete.test.ts | 2 +- tests/e2e/entity/get.test.ts | 2 +- tests/e2e/entity/put.test.ts | 2 +- tests/e2e/entity/query.test.ts | 2 +- tests/e2e/entity/update.test.ts | 2 +- tests/e2e/mockEntityFactory.ts | 2 +- tests/{fixtures.ts => fixtures/TestTable.ts} | 0 tests/types/EntityKey.test.ts | 2 +- tests/types/EntityValue.test.ts | 2 +- tests/unit/condition/index.test.ts | 2 +- tests/unit/decorators/helpers/decorateAttribute.test.ts | 2 +- tests/unit/decorators/helpers/prefixSuffix.test.ts | 2 +- tests/unit/dynamode/helpers.test.ts | 2 +- tests/unit/dynamode/index.test.ts | 2 +- tests/unit/entity/helpers/buildExpressions.test.ts | 2 +- tests/unit/entity/helpers/buildOperators.test.ts | 2 +- tests/unit/entity/helpers/converters.test.ts | 2 +- tests/unit/entity/helpers/transformValues.test.ts | 2 +- tests/unit/entity/index.test.ts | 8 +++++++- tests/unit/query/index.test.ts | 2 +- tests/unit/retriever/index.test.ts | 2 +- tests/unit/scan/index.test.ts | 2 +- tests/unit/stream/index.test.ts | 2 +- tests/unit/table/helpers/definitions.test.ts | 2 +- tests/unit/table/helpers/indexes.test.ts | 2 +- tests/unit/table/index.test.ts | 2 +- tests/unit/transactionGet/index.test.ts | 2 +- tests/unit/transactionWrite/index.test.ts | 2 +- 33 files changed, 38 insertions(+), 32 deletions(-) rename tests/{fixtures.ts => fixtures/TestTable.ts} (100%) diff --git a/tests/e2e/condition.test.ts b/tests/e2e/condition.test.ts index 0398052..0993343 100644 --- a/tests/e2e/condition.test.ts +++ b/tests/e2e/condition.test.ts @@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; import { AttributeType } from '@lib/condition'; -import { mockDate, MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../fixtures'; +import { mockDate, MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../fixtures/TestTable'; import { mockEntityFactory } from './mockEntityFactory'; diff --git a/tests/e2e/entity/batchDelete.test.ts b/tests/e2e/entity/batchDelete.test.ts index 4a82e92..9c31f39 100644 --- a/tests/e2e/entity/batchDelete.test.ts +++ b/tests/e2e/entity/batchDelete.test.ts @@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; import { NotFoundError } from '@lib/utils'; -import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures'; +import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; import { mockEntityFactory } from '../mockEntityFactory'; describe('EntityManager.batchDelete', () => { diff --git a/tests/e2e/entity/batchGet.test.ts b/tests/e2e/entity/batchGet.test.ts index b138a94..ff2d472 100644 --- a/tests/e2e/entity/batchGet.test.ts +++ b/tests/e2e/entity/batchGet.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; -import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures'; +import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; import { mockEntityFactory } from '../mockEntityFactory'; describe('EntityManager.batchGet', () => { diff --git a/tests/e2e/entity/batchPut.test.ts b/tests/e2e/entity/batchPut.test.ts index c1879ca..5080883 100644 --- a/tests/e2e/entity/batchPut.test.ts +++ b/tests/e2e/entity/batchPut.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; -import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures'; +import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; import { mockEntityFactory } from '../mockEntityFactory'; describe('EntityManager.batchPut', () => { diff --git a/tests/e2e/entity/create.test.ts b/tests/e2e/entity/create.test.ts index 83a6012..c5a5fdc 100644 --- a/tests/e2e/entity/create.test.ts +++ b/tests/e2e/entity/create.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; -import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures'; +import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; import { mockEntityFactory } from '../mockEntityFactory'; describe.sequential('EntityManager.create', () => { diff --git a/tests/e2e/entity/delete.test.ts b/tests/e2e/entity/delete.test.ts index 8f3d4e7..bef7fb1 100644 --- a/tests/e2e/entity/delete.test.ts +++ b/tests/e2e/entity/delete.test.ts @@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; import { NotFoundError } from '@lib/utils'; -import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures'; +import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; import { mockEntityFactory } from '../mockEntityFactory'; describe('EntityManager.delete', () => { diff --git a/tests/e2e/entity/get.test.ts b/tests/e2e/entity/get.test.ts index c0f0765..5d1537c 100644 --- a/tests/e2e/entity/get.test.ts +++ b/tests/e2e/entity/get.test.ts @@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; import { NotFoundError } from '@lib/utils'; -import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures'; +import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; import { mockEntityFactory } from '../mockEntityFactory'; describe('EntityManager.get', () => { diff --git a/tests/e2e/entity/put.test.ts b/tests/e2e/entity/put.test.ts index 10fdd67..f348e69 100644 --- a/tests/e2e/entity/put.test.ts +++ b/tests/e2e/entity/put.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; -import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures'; +import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; import { mockEntityFactory } from '../mockEntityFactory'; describe.sequential('EntityManager.put', () => { diff --git a/tests/e2e/entity/query.test.ts b/tests/e2e/entity/query.test.ts index 6185334..367d099 100644 --- a/tests/e2e/entity/query.test.ts +++ b/tests/e2e/entity/query.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; -import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures'; +import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; import { mockEntityFactory } from '../mockEntityFactory'; describe('EntityManager.query', () => { diff --git a/tests/e2e/entity/update.test.ts b/tests/e2e/entity/update.test.ts index f73e106..5d2a24d 100644 --- a/tests/e2e/entity/update.test.ts +++ b/tests/e2e/entity/update.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; -import { mockDate, MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures'; +import { mockDate, MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable'; import { mockEntityFactory } from '../mockEntityFactory'; describe.sequential('EntityManager.update', () => { diff --git a/tests/e2e/mockEntityFactory.ts b/tests/e2e/mockEntityFactory.ts index e37396c..a26ffaf 100644 --- a/tests/e2e/mockEntityFactory.ts +++ b/tests/e2e/mockEntityFactory.ts @@ -1,4 +1,4 @@ -import { MockEntity, MockEntityProps } from '../fixtures'; +import { MockEntity, MockEntityProps } from '../fixtures/TestTable'; export function mockEntityFactory(props?: Partial): MockEntity { return new MockEntity({ diff --git a/tests/fixtures.ts b/tests/fixtures/TestTable.ts similarity index 100% rename from tests/fixtures.ts rename to tests/fixtures/TestTable.ts diff --git a/tests/types/EntityKey.test.ts b/tests/types/EntityKey.test.ts index 85b1574..c3fc524 100644 --- a/tests/types/EntityKey.test.ts +++ b/tests/types/EntityKey.test.ts @@ -3,7 +3,7 @@ import { describe, expectTypeOf, test } from 'vitest'; import Entity from '@lib/entity'; import { EntityKey } from '@lib/entity/types'; -import { MockEntity } from '../fixtures'; +import { MockEntity } from '../fixtures/TestTable'; type BaseKeys = 'dynamodeEntity'; diff --git a/tests/types/EntityValue.test.ts b/tests/types/EntityValue.test.ts index 2c5e4ee..7b9033f 100644 --- a/tests/types/EntityValue.test.ts +++ b/tests/types/EntityValue.test.ts @@ -2,7 +2,7 @@ import { describe, expectTypeOf, test } from 'vitest'; import { EntityValue } from '@lib/entity/types'; -import { MockEntity, Property } from '../fixtures'; +import { MockEntity, Property } from '../fixtures/TestTable'; type DynamodeEntityValue = EntityValue; type StringValue = EntityValue; diff --git a/tests/unit/condition/index.test.ts b/tests/unit/condition/index.test.ts index d3370f1..a97f1e2 100644 --- a/tests/unit/condition/index.test.ts +++ b/tests/unit/condition/index.test.ts @@ -4,7 +4,7 @@ import Condition from '@lib/condition'; import { AttributeType } from '@lib/condition/types'; import { BASE_OPERATOR, OPERATORS, ValidationError } from '@lib/utils'; -import { MockEntity, MockEntityManager } from '../../fixtures'; +import { MockEntity, MockEntityManager } from '../../fixtures/TestTable'; describe('Condition', () => { let condition = MockEntityManager.condition(); diff --git a/tests/unit/decorators/helpers/decorateAttribute.test.ts b/tests/unit/decorators/helpers/decorateAttribute.test.ts index 8b5b5b4..bef9aab 100644 --- a/tests/unit/decorators/helpers/decorateAttribute.test.ts +++ b/tests/unit/decorators/helpers/decorateAttribute.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { decorateAttribute } from '@lib/decorators/helpers/decorateAttribute'; import Dynamode from '@lib/dynamode/index'; -import { MockEntity, mockInstance } from '../../../fixtures'; +import { MockEntity, mockInstance } from '../../../fixtures/TestTable'; describe('Decorators', () => { let registerAttributeSpy = vi.spyOn(Dynamode.storage, 'registerAttribute'); diff --git a/tests/unit/decorators/helpers/prefixSuffix.test.ts b/tests/unit/decorators/helpers/prefixSuffix.test.ts index f0efe10..b522bc4 100644 --- a/tests/unit/decorators/helpers/prefixSuffix.test.ts +++ b/tests/unit/decorators/helpers/prefixSuffix.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test, vi } from 'vitest'; import { prefix, suffix } from '@lib/decorators/helpers/prefixSuffix'; import Dynamode from '@lib/dynamode/index'; -import { MockEntity, mockInstance } from '../../../fixtures'; +import { MockEntity, mockInstance } from '../../../fixtures/TestTable'; describe('Decorators', () => { describe('prefix', async () => { diff --git a/tests/unit/dynamode/helpers.test.ts b/tests/unit/dynamode/helpers.test.ts index 43f8a79..be92c3a 100644 --- a/tests/unit/dynamode/helpers.test.ts +++ b/tests/unit/dynamode/helpers.test.ts @@ -4,7 +4,7 @@ import { validateDecoratedAttribute, validateMetadataAttribute } from '@lib/dyna import { AttributesMetadata } from '@lib/dynamode/storage/types'; import { Metadata } from '@lib/table/types'; -import { MockEntity, TEST_TABLE_NAME } from '../../fixtures'; +import { MockEntity, TEST_TABLE_NAME } from '../../fixtures/TestTable'; const entityName = 'EntityName'; const attributes: AttributesMetadata = { diff --git a/tests/unit/dynamode/index.test.ts b/tests/unit/dynamode/index.test.ts index 89d9581..00131e2 100644 --- a/tests/unit/dynamode/index.test.ts +++ b/tests/unit/dynamode/index.test.ts @@ -7,7 +7,7 @@ import * as storageHelper from '@lib/dynamode/storage/helpers/validator'; import { Metadata } from '@lib/table/types'; import { DynamodeStorageError, ValidationError } from '@lib/utils/errors'; -import { MockEntity, TEST_TABLE_NAME, TestTable } from '../../fixtures'; +import { MockEntity, TEST_TABLE_NAME, TestTable } from '../../fixtures/TestTable'; const metadata: Metadata = { tableName: TEST_TABLE_NAME, diff --git a/tests/unit/entity/helpers/buildExpressions.test.ts b/tests/unit/entity/helpers/buildExpressions.test.ts index a7beeb8..b8274f2 100644 --- a/tests/unit/entity/helpers/buildExpressions.test.ts +++ b/tests/unit/entity/helpers/buildExpressions.test.ts @@ -10,7 +10,7 @@ import { import * as buildOperators from '@lib/entity/helpers/buildOperators'; import { BASE_OPERATOR, DYNAMODE_ENTITY, OPERATORS, UPDATE_OPERATORS } from '@lib/utils'; -import { MockEntity } from '../../../fixtures'; +import { MockEntity } from '../../../fixtures/TestTable'; const expressionBuilderRunSpy = vi.fn(); diff --git a/tests/unit/entity/helpers/buildOperators.test.ts b/tests/unit/entity/helpers/buildOperators.test.ts index f64ba71..ec0e20c 100644 --- a/tests/unit/entity/helpers/buildOperators.test.ts +++ b/tests/unit/entity/helpers/buildOperators.test.ts @@ -4,7 +4,7 @@ import { buildProjectionOperators, buildUpdateOperators } from '@lib/entity/help import { Dynamode } from '@lib/module'; import { BASE_OPERATOR, DYNAMODE_ENTITY, InvalidParameter, UPDATE_OPERATORS } from '@lib/utils'; -import { MockEntity } from '../../../fixtures'; +import { MockEntity } from '../../../fixtures/TestTable'; describe('Build operators entity helpers', () => { describe('buildProjectionOperators', async () => { diff --git a/tests/unit/entity/helpers/converters.test.ts b/tests/unit/entity/helpers/converters.test.ts index a9bc432..5e9f31c 100644 --- a/tests/unit/entity/helpers/converters.test.ts +++ b/tests/unit/entity/helpers/converters.test.ts @@ -11,7 +11,7 @@ import { } from '@lib/entity/helpers/converters'; import * as transformValuesHelpers from '@lib/entity/helpers/transformValues'; -import { mockDate, MockEntity, mockInstance, TestTableMetadata } from '../../../fixtures'; +import { mockDate, MockEntity, mockInstance, TestTableMetadata } from '../../../fixtures/TestTable'; const metadata = { tableName: 'test-table', diff --git a/tests/unit/entity/helpers/transformValues.test.ts b/tests/unit/entity/helpers/transformValues.test.ts index 1e4cead..4bc4b18 100644 --- a/tests/unit/entity/helpers/transformValues.test.ts +++ b/tests/unit/entity/helpers/transformValues.test.ts @@ -4,7 +4,7 @@ import Dynamode from '@lib/dynamode/index'; import { prefixSuffixValue, transformDateValue, truncateValue } from '@lib/entity/helpers/transformValues'; import { InvalidParameter } from '@lib/utils'; -import { MockEntity } from '../../../fixtures'; +import { MockEntity } from '../../../fixtures/TestTable'; describe('Prefix and suffix entity helpers', () => { let getEntityAttributesSpy = vi.spyOn(Dynamode.storage, 'getEntityAttributes'); diff --git a/tests/unit/entity/index.test.ts b/tests/unit/entity/index.test.ts index a14769e..f825520 100644 --- a/tests/unit/entity/index.test.ts +++ b/tests/unit/entity/index.test.ts @@ -17,7 +17,13 @@ import { AttributeValues, NotFoundError } from '@lib/utils'; import { OPERATORS } from '@lib/utils/constants'; import * as converterUtils from '@lib/utils/converter'; -import { MockEntity, MockEntityManager, mockInstance, TEST_TABLE_NAME, testTableInstance } from '../../fixtures'; +import { + MockEntity, + MockEntityManager, + mockInstance, + TEST_TABLE_NAME, + testTableInstance, +} from '../../fixtures/TestTable'; const expressionBuilderRunSpy = vi.fn(); diff --git a/tests/unit/query/index.test.ts b/tests/unit/query/index.test.ts index 4b171a9..927f1bd 100644 --- a/tests/unit/query/index.test.ts +++ b/tests/unit/query/index.test.ts @@ -9,7 +9,7 @@ import { Metadata } from '@lib/table/types'; import { BASE_OPERATOR } from '@lib/utils'; import * as utils from '@lib/utils/helpers'; -import { MockEntity, MockEntityManager, mockInstance, TEST_TABLE_NAME } from '../../fixtures'; +import { MockEntity, MockEntityManager, mockInstance, TEST_TABLE_NAME } from '../../fixtures/TestTable'; const attributes = { partitionKey: { diff --git a/tests/unit/retriever/index.test.ts b/tests/unit/retriever/index.test.ts index 529fcd8..3a72c7a 100644 --- a/tests/unit/retriever/index.test.ts +++ b/tests/unit/retriever/index.test.ts @@ -5,7 +5,7 @@ import * as entityConvertHelpers from '@lib/entity/helpers/converters'; import RetrieverBase from '@lib/retriever'; import { BASE_OPERATOR } from '@lib/utils'; -import { MockEntity, TEST_TABLE_NAME, TestTableMetadata } from '../../fixtures'; +import { MockEntity, TEST_TABLE_NAME, TestTableMetadata } from '../../fixtures/TestTable'; describe('RetrieverBase', () => { let retriever = new RetrieverBase(MockEntity); diff --git a/tests/unit/scan/index.test.ts b/tests/unit/scan/index.test.ts index 55556c2..d0e20db 100644 --- a/tests/unit/scan/index.test.ts +++ b/tests/unit/scan/index.test.ts @@ -7,7 +7,7 @@ import Scan from '@lib/scan'; import { Metadata } from '@lib/table/types'; import { BASE_OPERATOR } from '@lib/utils'; -import { MockEntity, MockEntityManager, mockInstance, TEST_TABLE_NAME } from '../../fixtures'; +import { MockEntity, MockEntityManager, mockInstance, TEST_TABLE_NAME } from '../../fixtures/TestTable'; vi.mock('@lib/utils/ExpressionBuilder', () => { const ExpressionBuilder = vi.fn(() => ({ diff --git a/tests/unit/stream/index.test.ts b/tests/unit/stream/index.test.ts index f669a8d..11537c8 100644 --- a/tests/unit/stream/index.test.ts +++ b/tests/unit/stream/index.test.ts @@ -5,7 +5,7 @@ import Stream from '@lib/stream'; import { DynamoDBRecord } from '@lib/stream/types'; import { DynamodeStreamError } from '@lib/utils'; -import { mockDate, MockEntity, mockInstance, TestTable } from '../../fixtures'; +import { mockDate, MockEntity, mockInstance, TestTable } from '../../fixtures/TestTable'; const validMockEntityImage = { dynamodeEntity: { S: 'MockEntity' }, diff --git a/tests/unit/table/helpers/definitions.test.ts b/tests/unit/table/helpers/definitions.test.ts index 45d22a5..3bf691d 100644 --- a/tests/unit/table/helpers/definitions.test.ts +++ b/tests/unit/table/helpers/definitions.test.ts @@ -4,7 +4,7 @@ import { Dynamode } from '@lib/module'; import { getTableAttributeDefinitions } from '@lib/table/helpers/definitions'; import * as attributeHelper from '@lib/table/helpers/utils'; -import { TestTable, TestTableManager, TestTableMetadata } from '../../../fixtures'; +import { TestTable, TestTableManager, TestTableMetadata } from '../../../fixtures/TestTable'; describe('getTableAttributeDefinitions', () => { let getAttributeTypeSpy = vi.spyOn(attributeHelper, 'getAttributeType'); diff --git a/tests/unit/table/helpers/indexes.test.ts b/tests/unit/table/helpers/indexes.test.ts index 542aba4..2a6192a 100644 --- a/tests/unit/table/helpers/indexes.test.ts +++ b/tests/unit/table/helpers/indexes.test.ts @@ -4,7 +4,7 @@ import { GlobalSecondaryIndex, LocalSecondaryIndex } from '@aws-sdk/client-dynam import { getTableGlobalSecondaryIndexes, getTableLocalSecondaryIndexes } from '@lib/table/helpers/indexes'; import * as schemaHelper from '@lib/table/helpers/schema'; -import { TestTable, TestTableManager, TestTableMetadata } from '../../../fixtures'; +import { TestTable, TestTableManager, TestTableMetadata } from '../../../fixtures/TestTable'; describe('getTableLocalSecondaryIndexes', () => { let getKeySchemaSpy = vi.spyOn(schemaHelper, 'getKeySchema'); diff --git a/tests/unit/table/index.test.ts b/tests/unit/table/index.test.ts index 9a5b57d..9608c28 100644 --- a/tests/unit/table/index.test.ts +++ b/tests/unit/table/index.test.ts @@ -19,7 +19,7 @@ import * as schemaHelper from '@lib/table/helpers/schema'; import * as validatorHelper from '@lib/table/helpers/validator'; import { ValidationError } from '@lib/utils'; -import { MockEntity, MockEntityManager, TEST_TABLE_NAME, TestTable, TestTableManager } from '../../fixtures'; +import { MockEntity, MockEntityManager, TEST_TABLE_NAME, TestTable, TestTableManager } from '../../fixtures/TestTable'; const metadata = { tableName: TEST_TABLE_NAME, diff --git a/tests/unit/transactionGet/index.test.ts b/tests/unit/transactionGet/index.test.ts index fa97158..6509c24 100644 --- a/tests/unit/transactionGet/index.test.ts +++ b/tests/unit/transactionGet/index.test.ts @@ -7,7 +7,7 @@ import transactionGet from '@lib/transactionGet'; import { TransactionGet } from '@lib/transactionGet/types'; import { NotFoundError } from '@lib/utils'; -import { MockEntity, mockInstance, TEST_TABLE_NAME, TestTable, testTableInstance } from '../../fixtures'; +import { MockEntity, mockInstance, TEST_TABLE_NAME, TestTable, testTableInstance } from '../../fixtures/TestTable'; const transactionGetInputMockEntity: TransactionGet = { get: { diff --git a/tests/unit/transactionWrite/index.test.ts b/tests/unit/transactionWrite/index.test.ts index 15e8d28..d100b4d 100644 --- a/tests/unit/transactionWrite/index.test.ts +++ b/tests/unit/transactionWrite/index.test.ts @@ -12,7 +12,7 @@ import { TransactionUpdate, } from '@lib/transactionWrite/types'; -import { MockEntity, TEST_TABLE_NAME } from '../../fixtures'; +import { MockEntity, TEST_TABLE_NAME } from '../../fixtures/TestTable'; const transactionUpdate: TransactionUpdate = { update: { From 307c83e9fd98ce3a8cfa20fefa3f98df8dc8c4a2 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sun, 21 Jul 2024 20:42:04 +0200 Subject: [PATCH 8/8] Add remaining tests --- tests/e2e/indexes/query.test.ts | 425 ++++++++++++++++++++++++++++++++ tests/fixtures/TestIndex.ts | 175 +++++++++++++ 2 files changed, 600 insertions(+) create mode 100644 tests/e2e/indexes/query.test.ts create mode 100644 tests/fixtures/TestIndex.ts diff --git a/tests/e2e/indexes/query.test.ts b/tests/e2e/indexes/query.test.ts new file mode 100644 index 0000000..5669c76 --- /dev/null +++ b/tests/e2e/indexes/query.test.ts @@ -0,0 +1,425 @@ +import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'; + +import { + TestIndexInverse, + TestIndexInverseEntityManager, + TestIndexInverseManager, + TestIndexLSI, + TestIndexLSIEntityManager, + TestIndexLSIManager, + TestIndexMultipleGSI, + TestIndexMultipleGSIEntityManager, + TestIndexMultipleGSIManager, + TestIndexWithGSI, + TestIndexWithGSIEntityManager, + TestIndexWithGSIManager, +} from '../../fixtures/TestIndex'; + +describe('Indexes query tests', () => { + beforeAll(async () => { + vi.useFakeTimers(); + await TestIndexWithGSIManager.createTable(); + await TestIndexInverseManager.createTable(); + await TestIndexLSIManager.createTable(); + await TestIndexMultipleGSIManager.createTable(); + }); + + afterAll(async () => { + await TestIndexWithGSIManager.deleteTable('TestIndexWithGSI'); + await TestIndexInverseManager.deleteTable('TestIndexInverse'); + await TestIndexLSIManager.deleteTable('TestIndexLSI'); + await TestIndexMultipleGSIManager.deleteTable('TestIndexMultipleGSI'); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + describe.sequential('TestIndexWithGSIManager', () => { + test('Should be able to retrieve an item with primary key', async () => { + // Arrange + const mock = new TestIndexWithGSI({ partitionKey: 111, sortKey: 222 }); + await TestIndexWithGSIEntityManager.put(mock); + + // Act + const mockEntityRetrieved = await TestIndexWithGSIEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .sortKey('sortKey') + .eq(222) + .run(); + const mockEntityRetrieved2 = await TestIndexWithGSIEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .sortKey('sortKey') + .gt(200) + .run(); + const mockEntityRetrieved3 = await TestIndexWithGSIEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .run(); + + // Assert + expect(mockEntityRetrieved.items[0]).toEqual(mock); + expect(mockEntityRetrieved2.items[0]).toEqual(mock); + expect(mockEntityRetrieved3.items[0]).toEqual(mock); + + expect(mockEntityRetrieved.count).toEqual(1); + expect(mockEntityRetrieved2.count).toEqual(1); + expect(mockEntityRetrieved3.count).toEqual(1); + }); + + test('Should be able to retrieve an item with GSI', async () => { + // Arrange + const mock = new TestIndexWithGSI({ partitionKey: 111, sortKey: 222 }); + await TestIndexWithGSIEntityManager.put(mock); + + // Act + const mockEntityRetrieved = await TestIndexWithGSIEntityManager.query() + .indexName('index-normal') + .partitionKey('partitionKey') + .eq(111) + .sortKey('sortKey') + .eq(222) + .run(); + const mockEntityRetrieved2 = await TestIndexWithGSIEntityManager.query() + .indexName('index-normal') + .partitionKey('partitionKey') + .eq(111) + .sortKey('sortKey') + .gt(200) + .run(); + const mockEntityRetrieved3 = await TestIndexWithGSIEntityManager.query() + .indexName('index-normal') + .partitionKey('partitionKey') + .eq(111) + .run(); + + // Assert + expect(mockEntityRetrieved.items[0]).toEqual(mock); + expect(mockEntityRetrieved2.items[0]).toEqual(mock); + expect(mockEntityRetrieved3.items[0]).toEqual(mock); + + expect(mockEntityRetrieved.count).toEqual(1); + expect(mockEntityRetrieved2.count).toEqual(1); + expect(mockEntityRetrieved3.count).toEqual(1); + }); + }); + + describe.sequential('TestIndexInverseManager', () => { + test('Should be able to retrieve an item with primary key', async () => { + // Arrange + const mock = new TestIndexInverse({ partitionKey: 111, sortKey: 222 }); + await TestIndexInverseEntityManager.put(mock); + + // Act + const mockEntityRetrieved = await TestIndexInverseEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .sortKey('sortKey') + .eq(222) + .run(); + const mockEntityRetrieved2 = await TestIndexInverseEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .sortKey('sortKey') + .gt(200) + .run(); + const mockEntityRetrieved3 = await TestIndexInverseEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .run(); + + // Assert + expect(mockEntityRetrieved.items[0]).toEqual(mock); + expect(mockEntityRetrieved2.items[0]).toEqual(mock); + expect(mockEntityRetrieved3.items[0]).toEqual(mock); + + expect(mockEntityRetrieved.count).toEqual(1); + expect(mockEntityRetrieved2.count).toEqual(1); + expect(mockEntityRetrieved3.count).toEqual(1); + }); + + test('Should be able to retrieve an item with GSI', async () => { + // Arrange + const mock = new TestIndexInverse({ partitionKey: 111, sortKey: 222 }); + await TestIndexInverseEntityManager.put(mock); + + // Act + const mockEntityRetrieved = await TestIndexInverseEntityManager.query() + .partitionKey('sortKey') + .eq(222) + .sortKey('partitionKey') + .eq(111) + .run(); + const mockEntityRetrieved2 = await TestIndexInverseEntityManager.query() + .partitionKey('sortKey') + .eq(222) + .sortKey('partitionKey') + .lt(200) + .run(); + const mockEntityRetrieved3 = await TestIndexInverseEntityManager.query().partitionKey('sortKey').eq(222).run(); + + // Assert + expect(mockEntityRetrieved.items[0]).toEqual(mock); + expect(mockEntityRetrieved2.items[0]).toEqual(mock); + expect(mockEntityRetrieved3.items[0]).toEqual(mock); + + expect(mockEntityRetrieved.count).toEqual(1); + expect(mockEntityRetrieved2.count).toEqual(1); + expect(mockEntityRetrieved3.count).toEqual(1); + }); + }); + + describe.sequential('TestIndexLSIManager', () => { + test('Should be able to retrieve an item with primary key', async () => { + // Arrange + const mock = new TestIndexLSI({ partitionKey: 111, sortKey: 222, otherKey: '333' }); + await TestIndexLSIEntityManager.put(mock); + + // Act + const mockEntityRetrieved = await TestIndexLSIEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .sortKey('sortKey') + .eq(222) + .run(); + const mockEntityRetrieved2 = await TestIndexLSIEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .sortKey('sortKey') + .gt(200) + .run(); + const mockEntityRetrieved3 = await TestIndexLSIEntityManager.query().partitionKey('partitionKey').eq(111).run(); + + // Assert + expect(mockEntityRetrieved.items[0]).toEqual(mock); + expect(mockEntityRetrieved2.items[0]).toEqual(mock); + expect(mockEntityRetrieved3.items[0]).toEqual(mock); + + expect(mockEntityRetrieved.count).toEqual(1); + expect(mockEntityRetrieved2.count).toEqual(1); + expect(mockEntityRetrieved3.count).toEqual(1); + }); + + test('Should be able to retrieve an item with LSI', async () => { + // Arrange + const mock = new TestIndexLSI({ partitionKey: 111, sortKey: 222, otherKey: '333' }); + await TestIndexLSIEntityManager.put(mock); + + // Act + const mockEntityRetrieved = await TestIndexLSIEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .sortKey('otherKey') + .eq('333') + .run(); + const mockEntityRetrieved2 = await TestIndexLSIEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .sortKey('otherKey') + .gt('300') + .run(); + const mockEntityRetrieved3 = await TestIndexLSIEntityManager.query() + .indexName('index-lsi') + .partitionKey('partitionKey') + .eq(111) + .run(); + + // Assert + expect(mockEntityRetrieved.items[0]).toEqual(mock); + expect(mockEntityRetrieved2.items[0]).toEqual(mock); + expect(mockEntityRetrieved3.items[0]).toEqual(mock); + + expect(mockEntityRetrieved.count).toEqual(1); + expect(mockEntityRetrieved2.count).toEqual(1); + expect(mockEntityRetrieved3.count).toEqual(1); + }); + }); + + describe.sequential('TestIndexMultipleGSIManager', () => { + test('Should be able to retrieve an item with primary key', async () => { + // Arrange + const mock = new TestIndexMultipleGSI({ + partitionKey: 111, + sortKey: 222, + gsi_1_pk: '333', + gsi_2_3_4_pk: 444, + gsi_1_2_sk: 555, + gsi_3_sk: '666', + }); + await TestIndexMultipleGSIEntityManager.put(mock); + + // Act + const mockEntityRetrieved = await TestIndexMultipleGSIEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .sortKey('sortKey') + .eq(222) + .run(); + const mockEntityRetrieved2 = await TestIndexMultipleGSIEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .sortKey('sortKey') + .gt(200) + .run(); + const mockEntityRetrieved3 = await TestIndexMultipleGSIEntityManager.query() + .partitionKey('partitionKey') + .eq(111) + .run(); + + // Assert + expect(mockEntityRetrieved.items[0]).toEqual(mock); + expect(mockEntityRetrieved2.items[0]).toEqual(mock); + expect(mockEntityRetrieved3.items[0]).toEqual(mock); + + expect(mockEntityRetrieved.count).toEqual(1); + expect(mockEntityRetrieved2.count).toEqual(1); + expect(mockEntityRetrieved3.count).toEqual(1); + }); + + test('Should be able to retrieve an item with GSI number 1', async () => { + // Arrange + const mock = new TestIndexMultipleGSI({ + partitionKey: 111, + sortKey: 222, + gsi_1_pk: '333', + gsi_2_3_4_pk: 444, + gsi_1_2_sk: 555, + gsi_3_sk: '666', + }); + await TestIndexMultipleGSIEntityManager.put(mock); + + // Act + const mockEntityRetrieved = await TestIndexMultipleGSIEntityManager.query() + .partitionKey('gsi_1_pk') + .eq('333') + .sortKey('gsi_1_2_sk') + .eq(555) + .run(); + const mockEntityRetrieved2 = await TestIndexMultipleGSIEntityManager.query() + .partitionKey('gsi_1_pk') + .eq('333') + .sortKey('gsi_1_2_sk') + .gt(500) + .run(); + const mockEntityRetrieved3 = await TestIndexMultipleGSIEntityManager.query() + .partitionKey('gsi_1_pk') + .eq('333') + .run(); + + // Assert + expect(mockEntityRetrieved.items[0]).toEqual(mock); + expect(mockEntityRetrieved2.items[0]).toEqual(mock); + expect(mockEntityRetrieved3.items[0]).toEqual(mock); + + expect(mockEntityRetrieved.count).toEqual(1); + expect(mockEntityRetrieved2.count).toEqual(1); + expect(mockEntityRetrieved3.count).toEqual(1); + }); + + test('Should be able to retrieve an item with GSI number 2', async () => { + // Arrange + const mock = new TestIndexMultipleGSI({ + partitionKey: 111, + sortKey: 222, + gsi_1_pk: '333', + gsi_2_3_4_pk: 444, + gsi_1_2_sk: 555, + gsi_3_sk: '666', + }); + await TestIndexMultipleGSIEntityManager.put(mock); + + // Act + const mockEntityRetrieved = await TestIndexMultipleGSIEntityManager.query() + .partitionKey('gsi_2_3_4_pk') + .eq(444) + .sortKey('gsi_1_2_sk') + .eq(555) + .run(); + const mockEntityRetrieved2 = await TestIndexMultipleGSIEntityManager.query() + .partitionKey('gsi_2_3_4_pk') + .eq(444) + .sortKey('gsi_1_2_sk') + .gt(500) + .run(); + const mockEntityRetrieved3 = await TestIndexMultipleGSIEntityManager.query() + .indexName('index-2') + .partitionKey('gsi_2_3_4_pk') + .eq(444) + .run(); + + // Assert + expect(mockEntityRetrieved.items[0]).toEqual(mock); + expect(mockEntityRetrieved2.items[0]).toEqual(mock); + expect(mockEntityRetrieved3.items[0]).toEqual(mock); + + expect(mockEntityRetrieved.count).toEqual(1); + expect(mockEntityRetrieved2.count).toEqual(1); + expect(mockEntityRetrieved3.count).toEqual(1); + }); + + test('Should be able to retrieve an item with GSI number 3', async () => { + // Arrange + const mock = new TestIndexMultipleGSI({ + partitionKey: 111, + sortKey: 222, + gsi_1_pk: '333', + gsi_2_3_4_pk: 444, + gsi_1_2_sk: 555, + gsi_3_sk: '666', + }); + await TestIndexMultipleGSIEntityManager.put(mock); + + // Act + const mockEntityRetrieved = await TestIndexMultipleGSIEntityManager.query() + .partitionKey('gsi_2_3_4_pk') + .eq(444) + .sortKey('gsi_3_sk') + .eq('666') + .run(); + const mockEntityRetrieved2 = await TestIndexMultipleGSIEntityManager.query() + .partitionKey('gsi_2_3_4_pk') + .eq(444) + .sortKey('gsi_3_sk') + .gt('600') + .run(); + const mockEntityRetrieved3 = await TestIndexMultipleGSIEntityManager.query() + .indexName('index-3') + .partitionKey('gsi_2_3_4_pk') + .eq(444) + .run(); + + // Assert + expect(mockEntityRetrieved.items[0]).toEqual(mock); + expect(mockEntityRetrieved2.items[0]).toEqual(mock); + expect(mockEntityRetrieved3.items[0]).toEqual(mock); + + expect(mockEntityRetrieved.count).toEqual(1); + expect(mockEntityRetrieved2.count).toEqual(1); + expect(mockEntityRetrieved3.count).toEqual(1); + }); + + test('Should be able to retrieve an item with GSI number 4', async () => { + // Arrange + const mock = new TestIndexMultipleGSI({ + partitionKey: 111, + sortKey: 222, + gsi_1_pk: '333', + gsi_2_3_4_pk: 444, + gsi_1_2_sk: 555, + gsi_3_sk: '666', + }); + await TestIndexMultipleGSIEntityManager.put(mock); + + // Act + const mockEntityRetrieved = await TestIndexMultipleGSIEntityManager.query() + .indexName('index-4') + .partitionKey('gsi_2_3_4_pk') + .eq(444) + .run(); + + // Assert + expect(mockEntityRetrieved.items[0]).toEqual(mock); + expect(mockEntityRetrieved.count).toEqual(1); + }); + }); +}); diff --git a/tests/fixtures/TestIndex.ts b/tests/fixtures/TestIndex.ts new file mode 100644 index 0000000..e5092c0 --- /dev/null +++ b/tests/fixtures/TestIndex.ts @@ -0,0 +1,175 @@ +import { vi } from 'vitest'; + +import attribute from '@lib/decorators'; +import Dynamode from '@lib/dynamode/index'; +import Entity from '@lib/entity'; +import TableManager from '@lib/table'; + +vi.useFakeTimers(); + +export const mockDate = new Date(1000000000000); +vi.setSystemTime(mockDate); + +Dynamode.ddb.local(); +export const ddb = Dynamode.ddb.get(); + +export class TestIndex extends Entity { + @attribute.partitionKey.number() + partitionKey: number; + + @attribute.sortKey.number() + sortKey: number; + + constructor(props: { partitionKey: number; sortKey: number }) { + super(); + + this.partitionKey = props.partitionKey; + this.sortKey = props.sortKey; + } +} + +export class TestIndexWithGSI extends TestIndex { + // TODO: Make it so that partitionKey decorator is not required for a second time + @attribute.partitionKey.number() + @attribute.gsi.partitionKey.number({ indexName: 'index-normal' }) + partitionKey!: number; + + @attribute.sortKey.number() + @attribute.gsi.sortKey.number({ indexName: 'index-normal' }) + sortKey!: number; + + constructor(props: { partitionKey: number; sortKey: number }) { + super(props); + } +} + +export const TestIndexWithGSIManager = new TableManager(TestIndexWithGSI, { + tableName: 'TestIndexWithGSI', + partitionKey: 'partitionKey', + sortKey: 'sortKey', + indexes: { + 'index-normal': { + partitionKey: 'partitionKey', + sortKey: 'sortKey', + }, + }, +}); + +export const TestIndexWithGSIEntityManager = TestIndexWithGSIManager.entityManager(); + +export class TestIndexInverse extends TestIndex { + @attribute.partitionKey.number() + @attribute.gsi.sortKey.number({ indexName: 'index-inverse' }) + partitionKey!: number; + + @attribute.sortKey.number() + @attribute.gsi.partitionKey.number({ indexName: 'index-inverse' }) + sortKey!: number; + + constructor(props: { partitionKey: number; sortKey: number }) { + super(props); + } +} + +export const TestIndexInverseManager = new TableManager(TestIndexInverse, { + tableName: 'TestIndexInverse', + partitionKey: 'partitionKey', + sortKey: 'sortKey', + indexes: { + 'index-inverse': { + partitionKey: 'sortKey', + sortKey: 'partitionKey', + }, + }, +}); + +export const TestIndexInverseEntityManager = TestIndexInverseManager.entityManager(); + +export class TestIndexLSI extends TestIndex { + @attribute.sortKey.number() + @attribute.lsi.sortKey.number({ indexName: 'index-lsi' }) + sortKey!: number; + + @attribute.lsi.sortKey.string({ indexName: 'index-lsi2' }) + otherKey: string; + + constructor(props: { partitionKey: number; sortKey: number; otherKey: string }) { + super(props); + + this.otherKey = props.otherKey; + } +} + +export const TestIndexLSIManager = new TableManager(TestIndexLSI, { + tableName: 'TestIndexLSI', + partitionKey: 'partitionKey', + sortKey: 'sortKey', + indexes: { + 'index-lsi': { + sortKey: 'sortKey', + }, + 'index-lsi2': { + sortKey: 'otherKey', + }, + }, +}); + +export const TestIndexLSIEntityManager = TestIndexLSIManager.entityManager(); + +export class TestIndexMultipleGSI extends TestIndex { + @attribute.gsi.partitionKey.string({ indexName: 'index-1' }) + gsi_1_pk?: string; + + @attribute.gsi.partitionKey.number({ indexName: 'index-2' }) + @attribute.gsi.partitionKey.number({ indexName: 'index-3' }) + @attribute.gsi.partitionKey.number({ indexName: 'index-4' }) + gsi_2_3_4_pk?: number; + + @attribute.gsi.sortKey.number({ indexName: 'index-1' }) + @attribute.gsi.sortKey.number({ indexName: 'index-2' }) + gsi_1_2_sk?: number; + + @attribute.gsi.sortKey.string({ indexName: 'index-3' }) + gsi_3_sk?: string; + + constructor(props: { + partitionKey: number; + sortKey: number; + gsi_1_pk?: string; + gsi_2_3_4_pk?: number; + gsi_1_2_sk?: number; + gsi_3_sk?: string; + }) { + super(props); + + this.gsi_1_pk = props.gsi_1_pk; + this.gsi_2_3_4_pk = props.gsi_2_3_4_pk; + this.gsi_1_2_sk = props.gsi_1_2_sk; + this.gsi_3_sk = props.gsi_3_sk; + } +} + +export const TestIndexMultipleGSIManager = new TableManager(TestIndexMultipleGSI, { + tableName: 'TestIndexMultipleGSI', + partitionKey: 'partitionKey', + sortKey: 'sortKey', + indexes: { + 'index-1': { + partitionKey: 'gsi_1_pk', + sortKey: 'gsi_1_2_sk', + }, + 'index-2': { + partitionKey: 'gsi_2_3_4_pk', + sortKey: 'gsi_1_2_sk', + }, + 'index-3': { + partitionKey: 'gsi_2_3_4_pk', + sortKey: 'gsi_3_sk', + }, + 'index-4': { + partitionKey: 'gsi_2_3_4_pk', + }, + }, +}); + +export const TestIndexMultipleGSIEntityManager = TestIndexMultipleGSIManager.entityManager();