diff --git a/src/queryBuilders/getItemQueryBuilder.integration.test.ts b/src/queryBuilders/getItemQueryBuilder.integration.test.ts index 16993e7..59db2e0 100644 --- a/src/queryBuilders/getItemQueryBuilder.integration.test.ts +++ b/src/queryBuilders/getItemQueryBuilder.integration.test.ts @@ -76,7 +76,7 @@ describe("GetItemQueryBuilder", () => { expect(Object.keys(data!).length).toBe(2); }); - it("handles selecting attributes from arrays and tuples", async () => { + it("handles selecting attributes from arrays", async () => { const data = await tsynamoClient .getItemFrom("myOtherTable") .keys({ @@ -91,6 +91,20 @@ describe("GetItemQueryBuilder", () => { expect(data?.cats?.length).toEqual(1); expect(data?.cats?.[0].age).toEqual(TEST_DATA[6].cats[1].age); }); + it("handles selecting attributes from tuples", async () => { + const data = await tsynamoClient + .getItemFrom("myOtherTable") + .keys({ + userId: TEST_DATA[6].userId, + stringTimestamp: "123", + }) + .consistentRead(true) + .attributes(["tuplez[0]"]) + .execute(); + + expect(Object.keys(data!).length).toBe(1); + expect(data?.tuplez?.[0]).toEqual(TEST_DATA[6].tuplez[0]); + }); it("can't await instance directly", async () => { expect( diff --git a/src/typeHelpers.ts b/src/typeHelpers.ts index 02814ad..caffd73 100644 --- a/src/typeHelpers.ts +++ b/src/typeHelpers.ts @@ -181,6 +181,20 @@ export type ObjectKeyPaths = : // Leaf value reached, don't return anything never; +type IsTuple = T extends [any, ...any] ? true : false; + +/** + * Generate union from 0 to N + * RangeToN<3> => 0 | 1 | 2 | 3 + */ +type RangeToN< + N extends number, + Result extends any[] = [] +> = Result["length"] extends N + ? Result[number] + : RangeToN; + +// T extends (number extends T['length'] ? [] : any[]) export type ObjectFullPaths = // if `T` is an object or array T extends Record @@ -194,10 +208,18 @@ export type ObjectFullPaths = `${Key}` | `${Key}.${ObjectFullPaths}` : // If it's not an object, check if its an array T[Key] extends (infer A)[] - ? // If it's an array concatenate the key, array accessors, and the rest of the path recursively - | `${Key}` - | `${Key}[${number}]` - | `${Key}[${number}].${ObjectFullPaths}` + ? IsTuple extends true + ? // If tuple create array concatenate the key, specific array accessor for each index of the tuple, and the rest of the path recursively + | `${Key}` + | `${Key}${ObjectFullPaths<{ + [SpecificKey in RangeToN< + T[Key]["length"] + > as `[${SpecificKey}]`]: T[Key][SpecificKey]; + }>}` + : // If it's an array concatenate the key, array accessors, and the rest of the path recursively + | `${Key}` + | `${Key}[${number}]` + | `${Key}[${number}].${ObjectFullPaths}` : `${Key}` : // unreachable branch (if key is symbol) never diff --git a/test/testFixture.ts b/test/testFixture.ts index 93cc407..afd72a4 100644 --- a/test/testFixture.ts +++ b/test/testFixture.ts @@ -28,7 +28,7 @@ export interface DDB { stringTimestamp: SortKey; somethingElse: number; someBoolean: boolean; - tuple: [ + tuplez: [ { beer: string; }, @@ -174,7 +174,7 @@ export const TEST_DATA = [ stringTimestamp: "123", somethingElse: -5, someBoolean: true, - tuple: [ + tuplez: [ { beer: "karhu", },