diff --git a/package-lock.json b/package-lock.json index fb836a9..122cf0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@ginger.io/beyonce", - "version": "0.0.26", + "version": "0.0.39", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index dbd15c2..8c0c0f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ginger.io/beyonce", - "version": "0.0.38", + "version": "0.0.39", "description": "Type-safe DynamoDB query builder for TypeScript. Designed with single-table architecture in mind.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/main/codegen/generateGSIs.ts b/src/main/codegen/generateGSIs.ts index 41fbd0a..feb797d 100644 --- a/src/main/codegen/generateGSIs.ts +++ b/src/main/codegen/generateGSIs.ts @@ -31,7 +31,7 @@ function generateGSIsForTable(table: Table): string { const sk = sortKey.replace("$", "") const models = fieldToModels[pk].map((_) => `${_}Model`).join(", ") - return `const ${name}GSI = ${table.name}Table.gsi("${name}") + return `export const ${name}GSI = ${table.name}Table.gsi("${name}") .models([${models}]) .partitionKey("${pk}") .sortKey("${sk}") diff --git a/src/main/dynamo/Beyonce.ts b/src/main/dynamo/Beyonce.ts index a39e975..d67ea2f 100644 --- a/src/main/dynamo/Beyonce.ts +++ b/src/main/dynamo/Beyonce.ts @@ -39,7 +39,11 @@ export class Beyonce { private jayz?: JayZ private consistentReads: boolean - constructor(private table: Table, dynamo: DynamoDB, options: Options = {}) { + constructor( + private table: Table, + dynamo: DynamoDB, + options: Options = {} + ) { this.client = new DynamoDB.DocumentClient({ service: dynamo }) if (options.jayz !== undefined) { diff --git a/src/main/dynamo/GSI.ts b/src/main/dynamo/GSI.ts index 954d70f..1a52a61 100644 --- a/src/main/dynamo/GSI.ts +++ b/src/main/dynamo/GSI.ts @@ -5,11 +5,15 @@ import { ExtractFields } from "./types" type StringKey = keyof ExtractFields & string -export class GSI> { +export class GSI< + PK extends string, + SK extends string, + T extends Model +> { private modelTags: string[] constructor( - private table: Table, + private table: Table, readonly name: string, private models: T[], private partitionKeyName: StringKey, @@ -25,29 +29,35 @@ export class GSI> { } } -export class GSIBuilder { - constructor(private table: Table, private name: string) {} +export class GSIBuilder { + constructor(private table: Table, private name: string) {} - models>(models: T[]) { + models>( + models: T[] + ): GSIKeyBuilder { return new GSIKeyBuilder(this.table, this.name, models) } } -class GSIKeyBuilder> { +class GSIKeyBuilder< + PK extends string, + SK extends string, + T extends Model +> { private partitionKeyName?: StringKey constructor( - private table: Table, + private table: Table, private name: string, private models: T[] ) {} - partitionKey(partitionKeyName: StringKey): this { + partitionKey(partitionKeyName: PK | SK | StringKey): this { this.partitionKeyName = partitionKeyName return this } - sortKey(sortKeyName: StringKey): GSI { + sortKey(sortKeyName: PK | SK | StringKey): GSI { return new GSI( this.table, this.name, diff --git a/src/main/dynamo/QueryBuilder.ts b/src/main/dynamo/QueryBuilder.ts index aa61318..6ecdd98 100644 --- a/src/main/dynamo/QueryBuilder.ts +++ b/src/main/dynamo/QueryBuilder.ts @@ -1,8 +1,8 @@ import { JayZ } from "@ginger.io/jay-z" import { DynamoDB } from "aws-sdk" +import { QueryExpressionBuilder } from "./expressions/QueryExpressionBuilder" import { groupModelsByType } from "./groupModelsByType" import { PartitionKey, PartitionKeyAndSortKeyPrefix } from "./keys" -import { QueryExpressionBuilder } from "./expressions/QueryExpressionBuilder" import { Table } from "./Table" import { GroupedModels, TaggedModel } from "./types" import { decryptOrPassThroughItem, toJSON } from "./util" diff --git a/src/main/dynamo/Table.ts b/src/main/dynamo/Table.ts index f3f91c1..89e02d9 100644 --- a/src/main/dynamo/Table.ts +++ b/src/main/dynamo/Table.ts @@ -3,16 +3,16 @@ import { Model, PartitionKeyBuilder } from "./Model" import { Partition } from "./Partition" import { TaggedModel } from "./types" -export class Table { +export class Table { readonly tableName: string - readonly partitionKeyName: string - readonly sortKeyName: string + readonly partitionKeyName: PK + readonly sortKeyName: SK private encryptionBlacklist: Set constructor(config: { name: string - partitionKeyName: string - sortKeyName: string + partitionKeyName: PK + sortKeyName: SK encryptionBlacklist?: string[] }) { this.tableName = config.name @@ -38,7 +38,7 @@ export class Table { return new Partition(models) } - gsi(name: string): GSIBuilder { + gsi(name: string): GSIBuilder { return new GSIBuilder(this, name) } diff --git a/src/test/codegen/generateCode.test.ts b/src/test/codegen/generateCode.test.ts index 463619a..2409138 100644 --- a/src/test/codegen/generateCode.test.ts +++ b/src/test/codegen/generateCode.test.ts @@ -233,7 +233,8 @@ Tables: sortKey: $id `) - expect(result).toContain(`const modelByIdGSI = LibraryTable.gsi("modelById") + expect(result) + .toContain(`export const modelByIdGSI = LibraryTable.gsi("modelById") .models([AuthorModel, BookModel]) .partitionKey("model") .sortKey("id") diff --git a/src/test/dynamo/Beyonce.test.ts b/src/test/dynamo/Beyonce.test.ts index 8304985..426f502 100644 --- a/src/test/dynamo/Beyonce.test.ts +++ b/src/test/dynamo/Beyonce.test.ts @@ -5,7 +5,7 @@ import { Beyonce } from "../../main/dynamo/Beyonce" import { aMusicianWithTwoSongs, byModelAndIdGSI, - byNameAndIdGSI, + invertedIndexGSI, ModelType, MusicianModel, MusicianPartition, @@ -195,8 +195,8 @@ describe("Beyonce", () => { await testGSIByModel() }) - it("should query GSI by name", async () => { - await testGSIByName() + it("should query inverted index GSI", async () => { + await testInvertedIndexGSI() }) it("should write multiple items at once", async () => { @@ -275,9 +275,9 @@ describe("Beyonce", () => { await testGSIByModel(jayZ) }) - it("should query GSI by name with jayZ", async () => { + it("should query inverted index GSI by name with jayZ", async () => { const jayZ = await createJayZ() - await testGSIByName(jayZ) + await testInvertedIndexGSI(jayZ) }) it("should write multiple items at once with jayZ", async () => { @@ -545,16 +545,53 @@ async function testGSIByModel(jayZ?: JayZ) { expect(result).toEqual({ musician: [], song: [song1, song2] }) } -async function testGSIByName(jayZ?: JayZ) { +async function testInvertedIndexGSI(jayZ?: JayZ) { const db = await setup(jayZ) - const [musician, song1, song2] = aMusicianWithTwoSongs() - await Promise.all([db.put(musician), db.put(song1), db.put(song2)]) - const result = await db - .queryGSI(byNameAndIdGSI.name, byNameAndIdGSI.key(musician.name)) + const santana = MusicianModel.create({ + id: "1", + name: "Santana", + details: { + description: "famous guitarist", + }, + }) + + const slash = MusicianModel.create({ + id: "2", + name: "Slash", + details: { + description: "another famous guitarist", + }, + }) + + const santanasSong = SongModel.create({ + musicianId: santana.id, + id: "1", + title: "A song where Slash and Santana play together", + mp3: Buffer.from("fake-data", "utf8"), + }) + + const slashesSong = SongModel.create({ + musicianId: slash.id, + id: "1", // note the same id as above + title: "A song where Slash and Santana play together", + mp3: Buffer.from("fake-data", "utf8"), + }) + + await db.batchPutWithTransaction({ + items: [santana, slash, santanasSong, slashesSong], + }) + + // Now when we query our inverted index, pk and sk are reversed, + // so song id: 1 => [santanasSong, slashesSong] + const { song: songs } = await db + .queryGSI( + invertedIndexGSI.name, + invertedIndexGSI.key(`${ModelType.Song}-1`) + ) .exec() - expect(result).toEqual({ musician: [musician] }) + expect(songs).toEqual([santanasSong, slashesSong]) } async function testBatchWriteWithTransaction(jayZ?: JayZ) { diff --git a/src/test/dynamo/models.ts b/src/test/dynamo/models.ts index eb45924..3d0a5eb 100644 --- a/src/test/dynamo/models.ts +++ b/src/test/dynamo/models.ts @@ -46,11 +46,11 @@ export const byModelAndIdGSI = table .partitionKey("model") .sortKey("id") -export const byNameAndIdGSI = table - .gsi("byNameAndId") - .models([MusicianModel]) - .partitionKey("name") - .sortKey("id") +export const invertedIndexGSI = table + .gsi("invertedIndex") + .models([MusicianModel, SongModel]) + .partitionKey("sk") + .sortKey("pk") export function aMusicianWithTwoSongs(): [Musician, Song, Song] { const musician = MusicianModel.create({ diff --git a/src/test/dynamo/util.ts b/src/test/dynamo/util.ts index f47a240..1c92475 100644 --- a/src/test/dynamo/util.ts +++ b/src/test/dynamo/util.ts @@ -43,10 +43,10 @@ export async function setup(jayz?: JayZ): Promise { GlobalSecondaryIndexes: [ { - IndexName: "byNameAndId", + IndexName: "invertedIndex", KeySchema: [ - { AttributeName: "name", KeyType: "HASH" }, - { AttributeName: "id", KeyType: "RANGE" }, + { AttributeName: "sk", KeyType: "HASH" }, + { AttributeName: "pk", KeyType: "RANGE" }, ], Projection: { ProjectionType: "ALL",