Skip to content

Commit 017bd28

Browse files
anatolzaktywalch
andauthored
fix: composite index projection response types now include pk/sk composite attributes (#560)
* fix: composite index projection types now include pk/sk composite attributes (#558, #559) * add another test * add standard index type and runtime tests to contrast with composite index behavior --------- Co-authored-by: Tyler W. Walch <tywalch@gmail.com>
1 parent 6342178 commit 017bd28

6 files changed

Lines changed: 1025 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,3 +647,7 @@ All notable changes to this project will be documented in this file. Breaking ch
647647

648648
## [3.7.3]
649649
- [Issue #556](https://github.com/tywalch/electrodb/issues/556); Fixed `where` clause on projected indexes exposing all entity attributes instead of only projected attributes when the index has non-empty SK composites. Calling `.where()` directly now correctly restricts to projected attributes, matching the existing behavior of `.gte().where()` and other SK operation chains.
650+
651+
## [3.7.4]
652+
- [Issue #558](https://github.com/tywalch/electrodb/issues/558); Fixed composite index (`type: "composite"`) projection types being too narrow. `keys_only` returned `{}` and array projections omitted pk/sk composite attributes from the response type, even though DynamoDB always returns them as native columns. Response types, `attributes` parameter, and collection queries now correctly include composite key attributes.
653+
- [Issue #559](https://github.com/tywalch/electrodb/issues/559); Fixed composite index query response types marking pk/sk composite attributes as optional even though composite indexes are sparse — items only appear in the index if all key attributes are present, so they are always defined in query results.

index.d.ts

Lines changed: 103 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,7 +2800,7 @@ type ServiceQueryGoTerminalOptions<
28002800
E[EntityResultName]["schema"]
28012801
>
28022802
? Extract<
2803-
IndexProjectedAttributeNames<
2803+
IndexAvailableAttributeNames<
28042804
A,
28052805
F,
28062806
C,
@@ -2855,7 +2855,7 @@ type GoQueryTerminalOptions<
28552855
hydrate?: false | undefined;
28562856
attributes?: S extends Schema<infer A, infer F, infer C>
28572857
? ReadonlyArray<
2858-
Extract<IndexProjectedAttributeNames<A, F, C, S, I>, Attributes>
2858+
Extract<IndexAvailableAttributeNames<A, F, C, S, I>, Attributes>
28592859
>
28602860
: ReadonlyArray<Attributes>;
28612861
}
@@ -3047,23 +3047,21 @@ export type EntityCollectionResponse<
30473047
Attr,
30483048
Index extends keyof S["indexes"],
30493049
Options extends ServiceQueryGoTerminalOptions<any, any, any, Attr>,
3050-
> = Array<{
3051-
[Name in keyof ResponseItem as Name extends Attr
3052-
? Options["hydrate"] extends true
3053-
? Name
3054-
: Index extends keyof S["indexes"]
3055-
? "projection" extends keyof S["indexes"][Index]
3056-
? S["indexes"][Index]["projection"] extends ReadonlyArray<infer P>
3057-
? Name extends P
3058-
? Name
3059-
: never
3060-
: S["indexes"][Index]["projection"] extends "keys_only"
3061-
? never
3062-
: Name
3063-
: Name
3064-
: Name
3065-
: never]: ResponseItem[Name];
3066-
}>;
3050+
> = Array<
3051+
Options["hydrate"] extends true
3052+
? { [Name in keyof ResponseItem as Name extends Attr ? Name : never]: ResponseItem[Name] }
3053+
: RequireCompositeKeys<
3054+
{
3055+
[Name in keyof ResponseItem as Name extends Attr
3056+
? Name extends IndexResponseProjectedNames<S, Index>
3057+
? Name
3058+
: never
3059+
: never]: ResponseItem[Name];
3060+
},
3061+
S,
3062+
Index
3063+
>
3064+
>;
30673065

30683066
export type ServiceQueryRecordsGo<
30693067
E extends { [name: string]: Entity<any, any, any, any> },
@@ -3154,32 +3152,67 @@ export type ServiceQueryRecordsGo<
31543152
}>;
31553153
};
31563154

3155+
export type IndexResponseProjectedNames<
3156+
S extends Schema<string, string, string>,
3157+
I extends keyof S["indexes"] | undefined,
3158+
> = I extends keyof S["indexes"]
3159+
? "projection" extends keyof S["indexes"][I]
3160+
? S["indexes"][I]["projection"] extends ReadonlyArray<infer P>
3161+
? S["indexes"][I] extends { type: "composite" }
3162+
? S extends Schema<infer A, infer F, infer C>
3163+
? P | IndexCompositeAttributeNames<A, F, C, S, I>
3164+
: P
3165+
: P
3166+
: S["indexes"][I]["projection"] extends "keys_only"
3167+
? S["indexes"][I] extends { type: "composite" }
3168+
? S extends Schema<infer A, infer F, infer C>
3169+
? IndexCompositeAttributeNames<A, F, C, S, I>
3170+
: never
3171+
: never
3172+
: unknown
3173+
: unknown
3174+
: unknown;
3175+
3176+
// For composite indexes, items only appear when all key attributes are present (sparse index).
3177+
// This makes composite key attributes required in the response, only for type: "composite" indexes.
3178+
type RequireCompositeKeys<
3179+
Result,
3180+
S extends Schema<string, string, string>,
3181+
I extends keyof S["indexes"] | undefined,
3182+
> = I extends keyof S["indexes"]
3183+
? S["indexes"][I] extends { type: "composite" }
3184+
? S extends Schema<infer A, infer F, infer C>
3185+
? IndexCompositeAttributeNames<A, F, C, S, I> & keyof Result extends infer Keys extends keyof Result
3186+
? { [K in keyof Result as K extends Keys ? never : K]: Result[K] } & { [K in Keys]-?: Result[K] } extends infer Merged
3187+
? { [K in keyof Merged]: Merged[K] }
3188+
: Result
3189+
: Result
3190+
: Result
3191+
: Result
3192+
: Result;
3193+
31573194
export type IndexResponse<
31583195
Options extends GoQueryTerminalOptions<keyof Item, S, any>,
31593196
Item,
31603197
S extends Schema<string, string, string>,
31613198
I extends keyof S["indexes"] | undefined = undefined,
31623199
> = Options extends GoQueryTerminalOptions<infer Attr, S>
31633200
? {
3164-
data: Array<{
3165-
[Name in keyof Item as Name extends Attr
3166-
? Options["hydrate"] extends true
3167-
? Name
3168-
: I extends keyof S["indexes"]
3169-
? "projection" extends keyof S["indexes"][I]
3170-
? S["indexes"][I]["projection"] extends ReadonlyArray<
3171-
infer P
3172-
>
3173-
? Name extends P
3174-
? Name
3175-
: never
3176-
: S["indexes"][I]["projection"] extends 'keys_only'
3177-
? never
3178-
: Name
3179-
: Name
3180-
: Name
3181-
: never]: Item[Name];
3182-
}>;
3201+
data: Array<
3202+
Options["hydrate"] extends true
3203+
? { [Name in keyof Item as Name extends Attr ? Name : never]: Item[Name] }
3204+
: RequireCompositeKeys<
3205+
{
3206+
[Name in keyof Item as Name extends Attr
3207+
? Name extends IndexResponseProjectedNames<S, I>
3208+
? Name
3209+
: never
3210+
: never]: Item[Name];
3211+
},
3212+
S,
3213+
I
3214+
>
3215+
>;
31833216
cursor: string | null;
31843217
}
31853218
: {
@@ -4051,6 +4084,24 @@ export type IndexProjectedAttributeNames<
40514084
: A
40524085
: A;
40534086

4087+
export type IndexAvailableAttributeNames<
4088+
A extends string,
4089+
F extends string,
4090+
C extends string,
4091+
S extends Schema<A, F, C>,
4092+
I extends keyof S["indexes"] | undefined,
4093+
> = I extends keyof S["indexes"]
4094+
? S["indexes"][I]["projection"] extends ReadonlyArray<infer R extends A>
4095+
? S["indexes"][I] extends { type: "composite" }
4096+
? R | IndexCompositeAttributeNames<A, F, C, S, I>
4097+
: R
4098+
: S["indexes"][I]["projection"] extends "keys_only"
4099+
? S["indexes"][I] extends { type: "composite" }
4100+
? IndexCompositeAttributeNames<A, F, C, S, I>
4101+
: never
4102+
: A
4103+
: A;
4104+
40544105
export type CollectionProjectedAttributeNames<
40554106
E extends { [entityName: string]: Entity<any, any, any, any> },
40564107
Collections extends CollectionAssociations<E>,
@@ -4754,6 +4805,20 @@ export type IndexSKCompositeAttributes<
47544805
I extends keyof S["indexes"],
47554806
> = Pick<SKCompositeAttributes<A, F, C, S>, I>;
47564807

4808+
export type IndexCompositeAttributeNames<
4809+
A extends string,
4810+
F extends string,
4811+
C extends string,
4812+
S extends Schema<A, F, C>,
4813+
I extends keyof S["indexes"],
4814+
> = (
4815+
S["indexes"][I]["pk"]["composite"] extends ReadonlyArray<infer PK extends A> ? PK : never
4816+
) | (
4817+
S["indexes"][I] extends IndexWithSortKey
4818+
? S["indexes"][I]["sk"]["composite"] extends ReadonlyArray<infer SK extends A> ? SK : never
4819+
: never
4820+
);
4821+
47574822
export type TableIndexPKAttributes<
47584823
A extends string,
47594824
F extends string,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"KeySchema": [
3+
{ "AttributeName": "pk", "KeyType": "HASH" },
4+
{ "AttributeName": "sk", "KeyType": "RANGE" }
5+
],
6+
"AttributeDefinitions": [
7+
{ "AttributeName": "pk", "AttributeType": "S" },
8+
{ "AttributeName": "sk", "AttributeType": "S" },
9+
{ "AttributeName": "tenantId", "AttributeType": "S" },
10+
{ "AttributeName": "age", "AttributeType": "N" },
11+
{ "AttributeName": "email", "AttributeType": "S" },
12+
{ "AttributeName": "gsi5pk", "AttributeType": "S" },
13+
{ "AttributeName": "gsi5sk", "AttributeType": "S" }
14+
],
15+
"GlobalSecondaryIndexes": [
16+
{
17+
"IndexName": "gsi1-index",
18+
"KeySchema": [
19+
{ "AttributeName": "tenantId", "KeyType": "HASH" },
20+
{ "AttributeName": "age", "KeyType": "RANGE" }
21+
],
22+
"Projection": { "ProjectionType": "KEYS_ONLY" }
23+
},
24+
{
25+
"IndexName": "gsi2-index",
26+
"KeySchema": [
27+
{ "AttributeName": "tenantId", "KeyType": "HASH" },
28+
{ "AttributeName": "email", "KeyType": "RANGE" }
29+
],
30+
"Projection": {
31+
"ProjectionType": "INCLUDE",
32+
"NonKeyAttributes": ["name", "__edb_e__", "__edb_v__"]
33+
}
34+
},
35+
{
36+
"IndexName": "gsi3-index",
37+
"KeySchema": [
38+
{ "AttributeName": "email", "KeyType": "HASH" }
39+
],
40+
"Projection": { "ProjectionType": "KEYS_ONLY" }
41+
},
42+
{
43+
"IndexName": "gsi4-index",
44+
"KeySchema": [
45+
{ "AttributeName": "email", "KeyType": "HASH" }
46+
],
47+
"Projection": {
48+
"ProjectionType": "INCLUDE",
49+
"NonKeyAttributes": ["name", "age", "__edb_e__", "__edb_v__"]
50+
}
51+
},
52+
{
53+
"IndexName": "gsi5-index",
54+
"KeySchema": [
55+
{ "AttributeName": "gsi5pk", "KeyType": "HASH" },
56+
{ "AttributeName": "gsi5sk", "KeyType": "RANGE" }
57+
],
58+
"Projection": { "ProjectionType": "ALL" }
59+
}
60+
],
61+
"BillingMode": "PAY_PER_REQUEST"
62+
}

0 commit comments

Comments
 (0)