dyngoose
Version:
Elegant DynamoDB object modeling for Typescript
279 lines (228 loc) • 8.94 kB
text/typescript
import { expect, should } from 'chai'
import * as Decorator from '../decorator'
import { QueryError } from '../errors'
import { Table } from '../table'
import * as Query from './index'
@Decorator.Table({ name: 'QueryGlobalSecondaryIndexCardTable', backup: false })
class Card extends Table {
@Decorator.PrimaryKey('id', 'title')
public static readonly primaryKey: Query.PrimaryKey<Card, number, string>
@Decorator.GlobalSecondaryIndex({ primaryKey: 'title' })
public static readonly hashTitleIndex: Query.GlobalSecondaryIndex<Card>
@Decorator.GlobalSecondaryIndex({ hashKey: 'title', rangeKey: 'id' })
public static readonly fullTitleIndex: Query.GlobalSecondaryIndex<Card>
@Decorator.GlobalSecondaryIndex({ hashKey: 'id', rangeKey: 'title' })
public static readonly filterableTitleIndex: Query.GlobalSecondaryIndex<Card>
@Decorator.GlobalSecondaryIndex({
hashKey: 'id',
projection: 'INCLUDE',
nonKeyAttributes: ['title'],
})
public static readonly includeTestIndex: Query.GlobalSecondaryIndex<Card>
@Decorator.Attribute.Number()
public id: number
@Decorator.Attribute.String()
public title: string
@Decorator.Attribute.Number()
public count: number
customMethod(): number {
return 1
}
}
describe('Query/GlobalSecondaryIndex', () => {
beforeEach(async () => {
await Card.createTable()
})
afterEach(async () => {
await Card.deleteTable()
})
describe('hash only index', () => {
describe('#get', () => {
it('should throw error when no range key is specified', async () => {
let exception
try {
await Card.filterableTitleIndex.get({ id: 10 })
} catch (ex) {
exception = ex
}
should().exist(exception)
})
it('should find item', async () => {
await Card.new({ id: 10, title: 'abc', count: 1 }).save()
// this .get() will perform a query with a limit of 1,
// so it will get the first matching record
const card = await Card.filterableTitleIndex.get({ id: 10, title: 'abc' })
should().exist(card)
if (card != null) {
expect(card.id).to.eq(10)
expect(card.title).to.eq('abc')
expect(card.count).to.eq(1)
}
})
})
describe('#query', () => {
it('should find items', async () => {
await Card.new({ id: 10, title: 'abc' }).save()
await Card.new({ id: 11, title: 'abd' }).save()
await Card.new({ id: 12, title: 'abd' }).save()
const res = await Card.hashTitleIndex.query({ title: 'abd' })
expect(res.records.length).to.eq(2)
expect(res.records[0].id).to.eq(12)
expect(res.records[1].id).to.eq(11)
})
it('should not return items when aborted', async () => {
const abortController = new AbortController()
await Card.new({ id: 10, title: 'abc' }).save()
await Card.new({ id: 11, title: 'abd' }).save()
await Card.new({ id: 12, title: 'abd' }).save()
abortController.abort()
let exception
try {
await Card.hashTitleIndex.query({ title: 'abd' }, { abortSignal: abortController.signal })
} catch (ex) {
exception = ex
}
should().exist(exception)
})
it('should return an empty array when no items match', async () => {
const res = await Card.hashTitleIndex.query({ title: '404' })
expect(res[0]).to.not.eq(null)
expect(res.records.length).to.eq(0)
expect(res.length).to.eq(0)
expect(res.count).to.eq(0)
expect(res.map(i => i)[0]).to.eq(undefined)
for (const card of res.records) {
expect(card).to.eq('does not exist')
}
for (const card of res) {
expect(card).to.eq('does not exist')
}
})
it('should complain when HASH key is not provided', async () => {
await Card.hashTitleIndex.query({ id: 10 }).then(
() => {
expect(true).to.be.eq('false')
},
(err) => {
expect(err).to.be.instanceOf(QueryError)
expect(err.message).to.contain('Cannot perform')
},
)
})
it('should complain when HASH key attempts to use unsupported operator', async () => {
await Card.hashTitleIndex.query({ title: ['<>', 'abd'] }).then(
() => {
expect(true).to.be.eq('false')
},
(err) => {
expect(err).to.be.instanceOf(QueryError)
expect(err.message).to.contain('DynamoDB only supports')
},
)
})
it('should allow use of query operators for RANGE', async () => {
await Card.new({ id: 10, title: 'prefix/abc' }).save()
await Card.new({ id: 10, title: 'prefix/123' }).save()
await Card.new({ id: 10, title: 'prefix/xyz' }).save()
const res = await Card.filterableTitleIndex.query({ id: 10, title: ['beginsWith', 'prefix/'] })
expect(res.records.length).to.eq(3)
expect(res.records[0].id).to.eq(10)
expect(res.records[1].id).to.eq(10)
expect(res.records[2].id).to.eq(10)
})
it('should complain when using unsupported query operators for RANGE', async () => {
await Card.filterableTitleIndex.query({ id: 10, title: ['contains', 'prefix/'] }).then(
() => {
expect(true).to.be.eq('false')
},
(err) => {
expect(err).to.be.instanceOf(QueryError)
expect(err.message).to.contain('Cannot use')
},
)
})
})
describe('#scan', () => {
const cardIds = [111, 222, 333, 444, 555]
beforeEach(async () => {
for (const cardId of cardIds) {
await Card.new({ id: cardId, title: cardId.toString() }).save()
}
})
it('should return results', async () => {
const res1 = await Card.hashTitleIndex.scan()
const res2 = await Card.hashTitleIndex.scan(null, { limit: 2 })
const res3 = await Card.hashTitleIndex.scan(null, { limit: 2, exclusiveStartKey: res2.lastEvaluatedKey })
expect(res1.records.map((r) => r.id)).to.have.all.members(cardIds)
expect(cardIds).to.include.members(res2.records.map((r) => r.id))
expect(cardIds).to.include.members(res3.records.map((r) => r.id))
})
it('should not return results when aborted', async () => {
const abortController = new AbortController()
abortController.abort()
let exception
try {
await Card.hashTitleIndex.scan(null, undefined, { abortSignal: abortController.signal })
} catch (ex) {
exception = ex
}
should().exist(exception)
})
})
})
describe('hash and range index', () => {
describe('#query', () => {
it('should find items', async () => {
await Card.new({ id: 10, title: 'abc' }).save()
await Card.new({ id: 11, title: 'abd' }).save()
await Card.new({ id: 12, title: 'abd' }).save()
await Card.new({ id: 13, title: 'abd' }).save()
const res = await Card.fullTitleIndex.query({
title: 'abd',
id: ['>=', 12],
}, {
rangeOrder: 'DESC',
})
expect(res.records.length).to.eq(2)
expect(res.records[0].id).to.eq(13)
expect(res.records[1].id).to.eq(12)
})
})
describe('#scan', () => {
const cardIds = [111, 222, 333, 444, 555]
beforeEach(async () => {
for (const cardId of cardIds) {
await Card.new({ id: cardId, title: cardId.toString() }).save()
}
})
it('should support filters', async () => {
const search = await Card.fullTitleIndex.scan({
id: ['includes', cardIds],
})
expect(search.records.map((r) => r.id)).to.have.all.members(cardIds)
})
it('should work without filters', async () => {
const res1 = await Card.fullTitleIndex.scan()
const res2 = await Card.fullTitleIndex.scan(null, { limit: 2 })
const res3 = await Card.fullTitleIndex.scan(null, { limit: 2, exclusiveStartKey: res2.lastEvaluatedKey })
expect(res1.records.map((r) => r.id)).to.have.all.members(cardIds)
expect(cardIds).to.include.members(res2.records.map((r) => r.id))
expect(cardIds).to.include.members(res3.records.map((r) => r.id))
})
})
})
describe('include projection index', () => {
it('allows you to query and returns the nonKeyAttributes', async () => {
const newCard = Card.new({ id: 10, title: 'abc' })
newCard.count = 10
await newCard.save()
const card = await Card.includeTestIndex.get({ id: 10 })
should().exist(card)
if (card != null) {
expect(card.id).to.eq(10)
expect(card.title).to.eq('abc')
should().not.exist(card.count)
}
})
})
})