Skip to content

Commit

Permalink
Merge pull request #251 from shiftcode/#250-model-metadata
Browse files Browse the repository at this point in the history
#250 model metadata
  • Loading branch information
michaelwittwer authored Jan 11, 2020
2 parents 9d8acc4 + d827d77 commit ab7175c
Show file tree
Hide file tree
Showing 35 changed files with 859 additions and 246 deletions.
115 changes: 107 additions & 8 deletions src/decorator/decorators.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
SimpleModel,
} from '../../test/models'
import { Form } from '../../test/models/real-world'
import { updateDynamoEasyConfig } from '../config/update-config.function'
import { LogLevel } from '../logger/log-level.type'
import { CollectionProperty } from './impl/collection/collection-property.decorator'
import { GSIPartitionKey } from './impl/index/gsi-partition-key.decorator'
import { GSISortKey } from './impl/index/gsi-sort-key.decorator'
Expand Down Expand Up @@ -65,7 +67,7 @@ describe('Decorators should add correct metadata', () => {
})

it('with no properties', () => {
expect(modelOptions.properties).toBeUndefined()
expect(modelOptions.properties).toEqual([])
})
})

Expand Down Expand Up @@ -317,6 +319,77 @@ describe('Decorators should add correct metadata', () => {
})
})

describe('multiple property decorators', () => {
const REVERSE_INDEX = 'reverse-index'
const OTHER_INDEX = 'other-index'
const LSI_1 = 'lsi-1'
const LSI_2 = 'lsi-2'

@Model()
class ABC {
@PartitionKey()
@Property({ name: 'pk' })
@GSISortKey(REVERSE_INDEX)
id: string

@SortKey()
@Property({ name: 'sk' })
@GSIPartitionKey(REVERSE_INDEX)
@GSISortKey(OTHER_INDEX)
timestamp: number

@GSIPartitionKey(OTHER_INDEX)
@LSISortKey(LSI_1)
@LSISortKey(LSI_2)
otherId: string
}

let metaData: Metadata<ABC>

beforeEach(() => (metaData = metadataForModel(ABC)))

it('PartitionKey & Property & GSISortKey should combine the data', () => {
const propData = metaData.forProperty('id')
expect(propData).toEqual({
key: { type: 'HASH' },
name: 'id',
nameDb: 'pk',
typeInfo: { type: String },
keyForGSI: { [REVERSE_INDEX]: 'RANGE' },
})
})
it('SortKey & Property & GSIPartitionKey & GSISortKey should combine the data', () => {
const propData = metaData.forProperty('timestamp')
expect(propData).toEqual({
key: { type: 'RANGE' },
name: 'timestamp',
nameDb: 'sk',
typeInfo: { type: Number },
keyForGSI: { [REVERSE_INDEX]: 'HASH', [OTHER_INDEX]: 'RANGE' },
})
})
it('GSIPartitionKey & multiple LSISortkey should combine the data', () => {
const propData = metaData.forProperty('otherId')
expect(propData).toBeDefined()
expect(propData!.name).toEqual('otherId')
expect(propData!.nameDb).toEqual('otherId')
expect(propData!.typeInfo).toEqual({ type: String })
expect(propData!.keyForGSI).toEqual({ [OTHER_INDEX]: 'HASH' })
expect(propData!.sortKeyForLSI).toContain(LSI_1)
expect(propData!.sortKeyForLSI).toContain(LSI_2)
})
it('correctly defines the indexes', () => {
const reverseIndex = metaData.getIndex(REVERSE_INDEX)
const otherIndex = metaData.getIndex(OTHER_INDEX)
const lsi1 = metaData.getIndex(LSI_1)
const lsi2 = metaData.getIndex(LSI_2)
expect(reverseIndex).toEqual({ partitionKey: 'sk', sortKey: 'pk' })
expect(otherIndex).toEqual({ partitionKey: 'otherId', sortKey: 'sk' })
expect(lsi1).toEqual({ partitionKey: 'pk', sortKey: 'otherId' })
expect(lsi2).toEqual({ partitionKey: 'pk', sortKey: 'otherId' })
})
})

describe('enum (no Enum decorator)', () => {
let metadata: Metadata<ModelWithEnum>

Expand Down Expand Up @@ -544,17 +617,43 @@ describe('Decorators should add correct metadata', () => {
})

describe('should throw when more than one partitionKey was defined in a model', () => {
expect(() => {
it('does so', () => {
expect(() => {
@Model()
class InvalidModel {
@PartitionKey()
partKeyA: string

@PartitionKey()
partKeyB: string
}

return new InvalidModel()
}).toThrow()
})
})

describe('decorate property multiple times identically', () => {
let logReceiver: jest.Mock

beforeEach(() => {
logReceiver = jest.fn()
updateDynamoEasyConfig({ logReceiver })
})

it('should not throw but warn, if the PartitionKey is two times annotated', () => {
@Model()
class InvalidModel {
class NotCoolButOkModel {
@PartitionKey()
partKeyA: string

@PartitionKey()
partKeyB: string
doppeltGemoppelt: string
}

return new InvalidModel()
}).toThrow()
const propertyMetaData = metadataForModel(NotCoolButOkModel).forProperty('doppeltGemoppelt')
expect(propertyMetaData).toBeDefined()
expect(propertyMetaData!.key).toEqual({ type: 'HASH' })
expect(logReceiver).toBeCalledTimes(1)
expect(logReceiver.mock.calls[0][0].level).toBe(LogLevel.WARNING)
})
})
})
1 change: 1 addition & 0 deletions src/decorator/impl/index/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function initOrUpdateIndex(indexType: IndexType, indexData: IndexData, ta
indexData,
)
break
// `default` is actually unnecessary - but could only be removed by cast or nonNullAssertion of `propertyMetadata`
default:
throw new Error(`unsupported index type ${indexType}`)
}
Expand Down
11 changes: 8 additions & 3 deletions src/decorator/impl/key/partition-key.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/**
* @module decorators
*/
import { createOptModelLogger } from '../../../logger/logger'
import { PropertyMetadata } from '../../metadata/property-metadata.model'
import { initOrUpdateProperty } from '../property/init-or-update-property.function'
import { KEY_PROPERTY } from '../property/key-property.const'

const logger = createOptModelLogger('@PartitionKey')

export function PartitionKey(): PropertyDecorator {
return (target: any, propertyKey: string | symbol) => {
if (typeof propertyKey === 'string') {
Expand All @@ -14,9 +17,11 @@ export function PartitionKey(): PropertyDecorator {
const existingPartitionKeys = properties.filter(property => property.key && property.key.type === 'HASH')
if (existingPartitionKeys.length) {
if (properties.find(property => property.name === propertyKey)) {
// just ignore this and go on, somehow the partition key gets defined
// tslint:disable-next-line:no-console
console.warn(`this is the second execution to define the partitionKey for property ${propertyKey}`)
// just ignore this and go on, somehow the partition key gets defined two times
logger.warn(
`this is the second execution to define the partitionKey for property ${propertyKey}`,
target.constructor,
)
} else {
throw new Error(
'only one partition key is allowed per model, if you want to define key for indexes use one of these decorators: ' +
Expand Down
17 changes: 17 additions & 0 deletions src/decorator/impl/model/errors.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @module decorators
*/

/**
* @hidden
*/
export const modelErrors = {
gsiMultiplePk: (indexName: string, propDbName: string) =>
`there is already a partition key defined for global secondary index ${indexName} (property name: ${propDbName})`,
gsiMultipleSk: (indexName: string, propDbName: string) =>
`there is already a sort key defined for global secondary index ${indexName} (property name: ${propDbName})`,
lsiMultipleSk: (indexName: string, propDbName: string) =>
`only one sort key can be defined for the same local secondary index, ${propDbName} is already defined as sort key for index ${indexName}`,
lsiRequiresPk: (indexName: string, propDbName: string) =>
`the local secondary index ${indexName} requires the partition key to be defined`,
}
70 changes: 70 additions & 0 deletions src/decorator/impl/model/model.decorator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// tslint:disable:max-classes-per-file
import { GSIPartitionKey } from '../index/gsi-partition-key.decorator'
import { GSISortKey } from '../index/gsi-sort-key.decorator'
import { LSISortKey } from '../index/lsi-sort-key.decorator'
import { PartitionKey } from '../key/partition-key.decorator'
import { modelErrors } from './errors.const'
import { Model } from './model.decorator'

const IX_NAME = 'anIndexName'

describe('@model decorator', () => {
describe('getGlobalSecondaryIndexes', () => {
// throws on applying decorator

it('throws when defining multiple partitionKeys for same gsi', () => {
expect(() => {
// @ts-ignore
@Model()
class FailModel {
@GSIPartitionKey(IX_NAME)
pk1: string
@GSIPartitionKey(IX_NAME)
pk2: string
@GSISortKey(IX_NAME)
sk1: string
}
}).toThrow(modelErrors.gsiMultiplePk(IX_NAME, 'pk2'))
})
it('throws when defining multiple sortKeys for same gsi', () => {
expect(() => {
// @ts-ignore
@Model()
class FailModel {
@GSIPartitionKey(IX_NAME)
pk1: string
@GSISortKey(IX_NAME)
sk1: string
@GSISortKey(IX_NAME)
sk2: string
}
}).toThrow(modelErrors.gsiMultipleSk(IX_NAME, 'sk2'))
})
})
describe('getLocalSecondaryIndexes', () => {
it('throws when defining LSI sortKey but no PartitionKey', () => {
expect(() => {
// @ts-ignore
@Model()
class FailModel {
@LSISortKey(IX_NAME)
sk1: string
}
}).toThrow(modelErrors.lsiRequiresPk(IX_NAME, 'sk1'))
})
it('throws when defining multiple sortKeys for same lsi', () => {
expect(() => {
// @ts-ignore
@Model()
class FailModel {
@PartitionKey()
pk1: string
@LSISortKey(IX_NAME)
sk1: string
@LSISortKey(IX_NAME)
sk2: string
}
}).toThrow(modelErrors.lsiMultipleSk(IX_NAME, 'sk2'))
})
})
})
Loading

0 comments on commit ab7175c

Please sign in to comment.