diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ada0314..19a7a4e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -536,4 +536,14 @@ All notable changes to this project will be documented in this file. Breaking ch ## [2.15.0] - 2024-09-19 ### Updated - Updated `@aws-sdk/lib-dynamodb` dependency from pinned version `3.395.0` to latest release `^3.654.0`. This impacts users using the v3 aws-sdk. -- Adds dependency `@aws-sdk/util-dynamodb` for unmarshalling functionality. \ No newline at end of file +- Adds dependency `@aws-sdk/util-dynamodb` for unmarshalling functionality. + +## [3.0.0] +### Changed +- ElectroDB is changing how it generates query parameters to give more control to users. Prior to `v3`, query operations that used the `gt`, `lte`, or `between` methods would incur additional post-processing, including additional filter expressions and some sort key hacks. The post-processing was an attempt to bridge an interface gap between attribute-level considerations and key-level considerations. Checkout the GitHub issue championed by @rcoundon and @PaulJNewell77 [here](https://github.com/tywalch/electrodb/issues/228) to learn more. With `v3`, ElectroDB will not apply post-processing to queries of any type and abstains from adding implicit/erroneous filter expressions to queries _by default_. This change should provide additional control to users to achieve more advanced queries, but also introduces some additional complexity. There are many factors related to sorting and using comparison queries that are not intuitive, and the simplest way to mitigate this is by using additional [filter expressions](https://electrodb.dev/en/queries/filters/) to ensure the items returned will match expectations. To ease migration and adoption, I have added a new execution option called `compare`; To recreate `v2` functionality without further changes, use the execution option `{ compare: "v2" }`. This value is marked as deprecated and will be removed at a later date, but should allow users to safely upgrade to `v3` and experiment with the impact of this change on their existing data. The new `compare` option has other values that will continue to see support, however; to learn more about this new option, checkout [Comparison Queries](https://electrodb.dev/en/queries/query#comparison-queries). +- The `validate` callback on attributes now expects a strict return type of `boolean`. Additionally, the semantic meaning of a boolean response has _flipped_. The callback should return `true` for "valid" values and `false` for "invalid" values. If your validation function throws an error, ElectroDB will still behave as it previously did in `v2`, by catching and wrapping the error. +- Providing the execution option `limit` on queries now _only_ applies a `Limit` parameter to its request to DynamoDB. Previously, the `limit` option would cause ElectroDB to effectively "seek" DynamoDB until the limit was _at least_ reached. The execution option `count` can be used in similar cases where `limit` was used, but performance may vary depending on your data and use case. +### Removed +- The execution options `includeKeys` and `raw` were deprecated in version `2.0.0` and have now been removed in favor of the execution option `data`. To migrate from `v2`, use the options `{ data: "includeKeys" }` and `{ data: "raw" }` respectively. +### Fixed +- Response typing and formatting logic for `delete` diff --git a/README.md b/README.md index aae6b1b6..67834e32 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,19 @@ _Please submit issues/feedback or reach out on Twitter [@tinkertamper](https://twitter.com/tinkertamper)._ +

ElectroDB v3 now released

+ +

Visit the v3 migration page to learn more about this new update.

+ --- -

New: Documentation now found at ElectroDB.dev

+

Documentation now found at ElectroDB.dev

-

ElectroDB's new website for Documentation is now live at www.ElectroDB.dev.

+

ElectroDB's new website for Documentation is now live at electrodb.dev.

--- -

Introducing: The NEW ElectroDB Playground

+

The NEW ElectroDB Playground

diff --git a/index.d.ts b/index.d.ts index 392624ae..95007a41 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2536,12 +2536,9 @@ export interface QueryOptions { listeners?: Array; logger?: ElectroEventListener; data?: "raw" | "includeKeys" | "attributes"; - - /** @depricated use 'data=raw' instead */ - raw?: boolean; - /** @depricated use 'data=includeKeys' instead */ - includeKeys?: boolean; order?: "asc" | "desc"; + + consistent?: boolean; } // subset of QueryOptions @@ -2581,7 +2578,7 @@ export interface PutQueryOptions extends QueryOptions { response?: "default" | "none" | "all_old" | "all_new"; } -export interface ParamOptions { +export type ParamOptions = { cursor?: string | null; params?: object; table?: string; @@ -2594,7 +2591,8 @@ export interface ParamOptions { | "all_new" | "updated_new"; order?: "asc" | "desc"; -} + consistent?: boolean; +} & QueryExecutionComparisonParts; export interface BulkOptions extends QueryOptions { unprocessed?: "raw" | "item"; @@ -2609,11 +2607,6 @@ export type OptionalDefaultEntityIdentifiers = { interface GoBatchGetTerminalOptions { data?: "raw" | "includeKeys" | "attributes"; - /** @depricated use 'data=raw' instead */ - raw?: boolean; - /** @depricated use 'data=raw' instead */ - includeKeys?: boolean; - table?: string; limit?: number; params?: object; @@ -2626,15 +2619,23 @@ interface GoBatchGetTerminalOptions { preserveBatchOrder?: boolean; listeners?: Array; logger?: ElectroEventListener; + consistent?: boolean; } -interface ServiceQueryGoTerminalOptions { +export type ExecutionOptionCompare = "keys" | "attributes" | "v2"; + +export type QueryExecutionComparisonParts = { + compare?: "keys" | "attributes" +} | { + /** + * @deprecated 'v2' exists to ease user migration to ElectroDB v3. Support for this option will eventually be dropped. + */ + compare?: "v2" +} + +type ServiceQueryGoTerminalOptions = { cursor?: string | null; data?: "raw" | "includeKeys" | "attributes"; - /** @depricated use 'data=raw' instead */ - raw?: boolean; - /** @depricated use 'data=raw' instead */ - includeKeys?: boolean; table?: string; limit?: number; params?: object; @@ -2645,15 +2646,12 @@ interface ServiceQueryGoTerminalOptions { logger?: ElectroEventListener; order?: "asc" | "desc"; hydrate?: boolean; -} + consistent?: boolean; +} & QueryExecutionComparisonParts; -interface GoQueryTerminalOptions { +type GoQueryTerminalOptions = { cursor?: string | null; data?: "raw" | "includeKeys" | "attributes"; - /** @depricated use 'data=raw' instead */ - raw?: boolean; - /** @depricated use 'data=raw' instead */ - includeKeys?: boolean; table?: string; limit?: number; count?: number; @@ -2666,7 +2664,8 @@ interface GoQueryTerminalOptions { logger?: ElectroEventListener; order?: "asc" | "desc"; hydrate?: boolean; -} + consistent?: boolean; +} & QueryExecutionComparisonParts; interface TransactWriteQueryOptions { data?: "raw" | "includeKeys" | "attributes"; @@ -2688,13 +2687,15 @@ interface TransactGetQueryOptions { attributes?: ReadonlyArray; listeners?: Array; logger?: ElectroEventListener; + consistent?: boolean; } -export interface ParamTerminalOptions { +export type ParamTerminalOptions = { table?: string; limit?: number; params?: object; originalErr?: boolean; + consistent?: boolean; attributes?: ReadonlyArray; response?: | "default" @@ -2704,7 +2705,7 @@ export interface ParamTerminalOptions { | "all_new" | "updated_new"; order?: "asc" | "desc"; -} +} & QueryExecutionComparisonParts; type GoBatchGetTerminal< A extends string, @@ -2915,11 +2916,11 @@ export type DeleteRecordOperationGo = < ? O["response"] extends "all_old" ? Promise<{ data: T | null }> : O["response"] extends "default" - ? Promise<{ data: Keys | null }> - : O["response"] extends "none" - ? Promise<{ data: null }> - : Promise<{ data: Keys | null }> - : Promise<{ data: Keys | null }> + ? Promise<{ data: Keys }> + : O["response"] extends "none" + ? Promise<{ data: null }> + : Promise<{ data: Keys | null }> + : Promise<{ data: Keys }> : never; export type BatchWriteGo = ( @@ -3000,10 +3001,7 @@ export interface NestedBooleanAttribute { readonly get?: (val: boolean, item: any) => boolean | undefined | void; readonly set?: (val?: boolean, item?: any) => boolean | undefined | void; readonly default?: boolean | (() => boolean); - readonly validate?: - | ((val: boolean) => boolean) - | ((val: boolean) => void) - | ((val: boolean) => string | void); + readonly validate?: ((val: boolean) => boolean) readonly field?: string; } @@ -3015,10 +3013,7 @@ export interface BooleanAttribute { readonly get?: (val: boolean, item: any) => boolean | undefined | void; readonly set?: (val?: boolean, item?: any) => boolean | undefined | void; readonly default?: boolean | (() => boolean); - readonly validate?: - | ((val: boolean) => boolean) - | ((val: boolean) => void) - | ((val: boolean) => string | void); + readonly validate?: ((val: boolean) => boolean) readonly field?: string; readonly label?: string; readonly watch?: ReadonlyArray | "*"; @@ -3036,10 +3031,7 @@ export interface NestedNumberAttribute { readonly get?: (val: number, item: any) => number | undefined | void; readonly set?: (val?: number, item?: any) => number | undefined | void; readonly default?: number | (() => number); - readonly validate?: - | ((val: number) => boolean) - | ((val: number) => void) - | ((val: number) => string | void); + readonly validate?: ((val: number) => boolean) readonly field?: string; } @@ -3051,10 +3043,7 @@ export interface NumberAttribute { readonly get?: (val: number, item: any) => number | undefined | void; readonly set?: (val?: number, item?: any) => number | undefined | void; readonly default?: number | (() => number); - readonly validate?: - | ((val: number) => boolean) - | ((val: number) => void) - | ((val: number) => string | void); + readonly validate?: ((val: number) => boolean) readonly field?: string; readonly label?: string; readonly watch?: ReadonlyArray | "*"; @@ -3074,8 +3063,6 @@ export interface NestedStringAttribute { readonly default?: string | (() => string); readonly validate?: | ((val: string) => boolean) - | ((val: string) => void) - | ((val: string) => string | void) | RegExp; readonly field?: string; } @@ -3090,8 +3077,6 @@ export interface StringAttribute { readonly default?: string | (() => string); readonly validate?: | ((val: string) => boolean) - | ((val: string) => void) - | ((val: string) => string | void) | RegExp; readonly field?: string; readonly label?: string; @@ -3110,10 +3095,7 @@ export interface NestedEnumAttribute { readonly get?: (val: any, item: any) => any | undefined | void; readonly set?: (val?: any, item?: any) => any | undefined | void; readonly default?: string | (() => string); - readonly validate?: - | ((val: any) => boolean) - | ((val: any) => void) - | ((val: any) => string | void); + readonly validate?: ((val: any) => boolean) readonly field?: string; readonly label?: string; } @@ -3126,10 +3108,7 @@ export interface EnumAttribute { readonly get?: (val: any, item: any) => any | undefined | void; readonly set?: (val?: any, item?: any) => any | undefined | void; readonly default?: string | (() => string); - readonly validate?: - | ((val: any) => boolean) - | ((val: any) => void) - | ((val: any) => string | void); + readonly validate?: ((val: any) => boolean) readonly field?: string; readonly label?: string; readonly watch?: ReadonlyArray | "*"; @@ -3143,10 +3122,7 @@ export interface NestedAnyAttribute { readonly get?: (val: any, item: any) => any | undefined | void; readonly set?: (val?: any, item?: any) => any | undefined | void; readonly default?: any | (() => any); - readonly validate?: - | ((val: any) => boolean) - | ((val: any) => void) - | ((val: any) => string | void); + readonly validate?: ((val: any) => boolean) readonly field?: string; } @@ -3158,10 +3134,7 @@ export interface AnyAttribute { readonly get?: (val: any, item: any) => any | undefined | void; readonly set?: (val?: any, item?: any) => any | undefined | void; readonly default?: any | (() => any); - readonly validate?: - | ((val: any) => boolean) - | ((val: any) => void) - | ((val: any) => string | void); + readonly validate?: ((val: any) => boolean) readonly field?: string; readonly label?: string; readonly watch?: ReadonlyArray | "*"; @@ -3184,10 +3157,7 @@ export interface NestedMapAttribute { item?: any, ) => Record | undefined | void; readonly default?: Record | (() => Record); - readonly validate?: - | ((val: Record) => boolean) - | ((val: Record) => void) - | ((val: Record) => string | void); + readonly validate?: ((val: Record) => boolean) readonly field?: string; } @@ -3208,10 +3178,7 @@ export interface MapAttribute { item?: any, ) => Record | undefined | void; readonly default?: Record | (() => Record); - readonly validate?: - | ((val: Record) => boolean) - | ((val: Record) => void) - | ((val: Record) => string | void); + readonly validate?: ((val: Record) => boolean) readonly field?: string; readonly watch?: ReadonlyArray | "*"; } @@ -3231,10 +3198,7 @@ export interface NestedCustomListAttribute { item?: any, ) => Array | undefined | void; readonly default?: Array | (() => Array); - readonly validate?: - | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void); + readonly validate?: ((val: Array) => boolean) } export interface NestedStringListAttribute { @@ -3249,8 +3213,6 @@ export interface NestedStringListAttribute { readonly default?: string | (() => string); readonly validate?: | ((val: string) => boolean) - | ((val: string) => void) - | ((val: string) => string | void) | RegExp; readonly field?: string; }; @@ -3266,10 +3228,7 @@ export interface NestedStringListAttribute { item?: any, ) => Array | undefined | void; readonly default?: Array | (() => Array); - readonly validate?: - | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void); + readonly validate?: ((val: Array) => boolean) } export interface StringListAttribute { @@ -3284,8 +3243,6 @@ export interface StringListAttribute { readonly default?: string | (() => string); readonly validate?: | ((val: string) => boolean) - | ((val: string) => void) - | ((val: string) => string | void) | RegExp; readonly field?: string; }; @@ -3301,10 +3258,7 @@ export interface StringListAttribute { item?: any, ) => Array | undefined | void; readonly default?: Array | (() => Array); - readonly validate?: - | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void); + readonly validate?: ((val: Array) => boolean) readonly watch?: ReadonlyArray | "*"; } @@ -3323,10 +3277,7 @@ export interface NestedNumberListAttribute { item?: any, ) => Array | undefined | void; readonly default?: Array | (() => Array); - readonly validate?: - | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void); + readonly validate?: ((val: Array) => boolean) readonly field?: string; } @@ -3345,10 +3296,7 @@ export interface NumberListAttribute { item?: any, ) => Array | undefined | void; readonly default?: Array | (() => Array); - readonly validate?: - | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void); + readonly validate?: ((val: Array) => boolean) readonly field?: string; readonly watch?: ReadonlyArray | "*"; } @@ -3368,10 +3316,7 @@ export interface NestedMapListAttribute { item?: any, ) => Record[] | undefined | void; readonly default?: Record[] | (() => Record[]); - readonly validate?: - | ((val: Record[]) => boolean) - | ((val: Record[]) => void) - | ((val: Record[]) => string | void); + readonly validate?: ((val: Record[]) => boolean) readonly field?: string; } @@ -3390,10 +3335,7 @@ export interface NestedAnyListAttribute { item?: any, ) => Record[] | undefined | void; readonly default?: Record[] | (() => Record[]); - readonly validate?: - | ((val: Record[]) => boolean) - | ((val: Record[]) => void) - | ((val: Record[]) => string | void); + readonly validate?: ((val: Record[]) => boolean) readonly field?: string; } @@ -3412,10 +3354,7 @@ export interface MapListAttribute { item?: any, ) => Record[] | undefined | void; readonly default?: Record[] | (() => Record[]); - readonly validate?: - | ((val: Record[]) => boolean) - | ((val: Record[]) => void) - | ((val: Record[]) => string | void); + readonly validate?: ((val: Record[]) => boolean) readonly field?: string; readonly watch?: ReadonlyArray | "*"; } @@ -3435,10 +3374,7 @@ export interface CustomListAttribute { item?: any, ) => Array | undefined | void; readonly default?: Array | (() => Array); - readonly validate?: - | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void); + readonly validate?: ((val: Array) => boolean); readonly field?: string; readonly watch?: ReadonlyArray | "*"; } @@ -3458,10 +3394,7 @@ export interface AnyListAttribute { item?: any, ) => Array | undefined | void; readonly default?: Array | (() => Array); - readonly validate?: - | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void); + readonly validate?: ((val: Array) => boolean) readonly field?: string; readonly watch?: ReadonlyArray | "*"; } @@ -3483,8 +3416,6 @@ export interface NestedStringSetAttribute { readonly default?: Array | (() => Array); readonly validate?: | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void) | RegExp; readonly field?: string; } @@ -3504,11 +3435,7 @@ export interface NestedEnumNumberSetAttribute { item?: any, ) => Array | undefined | void; readonly default?: Array | (() => Array); - readonly validate?: - | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void) - | RegExp; + readonly validate?: ((val: Array) => boolean) readonly field?: string; } @@ -3527,11 +3454,7 @@ export interface EnumNumberSetAttribute { item?: any, ) => Array | undefined | void; readonly default?: Array | (() => Array); - readonly validate?: - | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void) - | RegExp; + readonly validate?: ((val: Array) => boolean) readonly field?: string; readonly watch?: ReadonlyArray | "*"; } @@ -3553,8 +3476,6 @@ export interface NestedEnumStringSetAttribute { readonly default?: Array | (() => Array); readonly validate?: | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void) | RegExp; readonly field?: string; } @@ -3576,8 +3497,6 @@ export interface EnumStringSetAttribute { readonly default?: Array | (() => Array); readonly validate?: | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void) | RegExp; readonly field?: string; readonly watch?: ReadonlyArray | "*"; @@ -3600,8 +3519,6 @@ export interface StringSetAttribute { readonly default?: Array | (() => Array); readonly validate?: | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void) | RegExp; readonly field?: string; readonly watch?: ReadonlyArray | "*"; @@ -3622,10 +3539,7 @@ export interface NestedNumberSetAttribute { item?: any, ) => Array | undefined | void; readonly default?: Array | (() => Array); - readonly validate?: - | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void); + readonly validate?: ((val: Array) => boolean) readonly field?: string; } @@ -3644,10 +3558,7 @@ export interface NumberSetAttribute { item?: any, ) => Array | undefined | void; readonly default?: Array | (() => Array); - readonly validate?: - | ((val: Array) => boolean) - | ((val: Array) => void) - | ((val: Array) => string | void); + readonly validate?: ((val: Array) => boolean) readonly field?: string; readonly watch?: ReadonlyArray | "*"; } @@ -3671,10 +3582,7 @@ type CustomAttribute = { readonly get?: (val: any, item: any) => any | undefined | void; readonly set?: (val?: any, item?: any) => any | undefined | void; readonly default?: any | (() => any); - readonly validate?: - | ((val: any) => boolean) - | ((val: any) => void) - | ((val: any) => string | void); + readonly validate?: ((val: any) => boolean) readonly field?: string; readonly watch?: ReadonlyArray | "*"; }; @@ -3687,10 +3595,7 @@ type NestedCustomAttribute = { readonly get?: (val: any, item: any) => any | undefined | void; readonly set?: (val?: any, item?: any) => any | undefined | void; readonly default?: any | (() => any); - readonly validate?: - | ((val: any) => boolean) - | ((val: any) => void) - | ((val: any) => string | void); + readonly validate?: ((val: any) => boolean) readonly field?: string; }; @@ -5365,7 +5270,6 @@ export class Entity< TableIndexCompositeAttributes >; query: Queries; - conversions: Conversions; parse>>( item: ParseSingleInput, @@ -5402,6 +5306,13 @@ export class Entity< client: any; } +declare function createConversions< + A extends string, + F extends string, + C extends string, + S extends Schema, +>(entity: Entity): Conversions; + export class TransactWriteEntity< A extends string, F extends string, @@ -5776,10 +5687,7 @@ type CustomAttributeDefinition = { readonly get?: (val: T, item: any) => T | undefined | void; readonly set?: (val?: T, item?: any) => T | undefined | void; readonly default?: T | (() => T); - readonly validate?: - | ((val: T) => boolean) - | ((val: T) => void) - | ((val: T) => string | void); + readonly validate?: ((val: T) => boolean) readonly field?: string; readonly watch?: ReadonlyArray | "*"; }; @@ -5809,4 +5717,4 @@ declare function createSchema< F extends string, C extends string, S extends Schema, ->(schema: S): S; +>(schema: S): S; \ No newline at end of file diff --git a/index.js b/index.js index 323b6df1..0a3849f3 100644 --- a/index.js +++ b/index.js @@ -15,15 +15,24 @@ const { ElectroUserValidationError, ElectroAttributeValidationError, } = require("./src/errors"); +const { createConversions } = require("./src/conversions"); + +const { + ComparisonTypes +} = require('./src/types'); module.exports = { Entity, Service, ElectroError, createSchema, + ComparisonTypes, CustomAttributeType, createCustomAttribute, ElectroValidationError, createGetTransaction, createWriteTransaction, + ElectroUserValidationError, + ElectroAttributeValidationError, + createConversions, }; diff --git a/index.test-d.ts b/index.test-d.ts index a0531669..95a09f55 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -12,9 +12,7 @@ import { expectError, expectAssignable, expectNotAssignable, - expectNotType, } from "tsd"; -import * as tests from "./test/tests.test-d"; type Resolve = T extends Function | string | number | boolean ? T @@ -582,19 +580,17 @@ expectError({ }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", @@ -614,20 +610,18 @@ expectAssignable({ }); expectNotAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", attributes: ["attrz1"], }); expectNotAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", @@ -850,66 +844,58 @@ type DeleteBatchParamsParamsWithoutSK = Parameter< >; expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "all_old", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "all_old", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "all_old", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "all_old", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", }); @@ -934,38 +920,34 @@ expectNotAssignable({ }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", @@ -987,19 +969,62 @@ entityWithoutSK.delete({ attr1: "asbc" }).where((attr, op) => { }); // Results -expectAssignable>( +expectAssignable>( entityWithSK .delete({ attr1: "abc", attr2: "def" }) .go({ response: "all_old" }) .then((res) => res.data), ); -expectAssignable>( + +expectAssignable>( + entityWithSK + .delete({ attr1: "abc", attr2: "def" }) + .go({ response: "default" }) + .then((res) => res.data), +); + +expectAssignable>( + entityWithSK + .delete({ attr1: "abc", attr2: "def" }) + .go() + .then((res) => res.data), +); + +expectAssignable>( + entityWithSK + .delete({ attr1: "abc", attr2: "def" }) + .go({ response: "none" }) + .then((res) => res.data), +); + +expectAssignable>( entityWithoutSK .delete({ attr1: "abc" }) .go({ response: "all_old" }) .then((res) => res.data), ); +expectAssignable>( + entityWithoutSK + .delete({ attr1: "abc" }) + .go({ response: "none" }) + .then((res) => res.data), +); + +expectAssignable>( + entityWithoutSK + .delete({ attr1: "abc" }) + .go({ response: "default" }) + .then((res) => res.data), +); + +expectAssignable>( + entityWithoutSK + .delete({ attr1: "abc" }) + .go() + .then((res) => res.data), +); + expectAssignable<"paramtest">( entityWithSK.delete({ attr1: "abc", attr2: "def" }).params<"paramtest">(), ); @@ -1153,35 +1178,31 @@ type PutBatchParamsParamsWithoutSK = Parameter< >; expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "all_old", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "all_old", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "all_old", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "all_old", }); @@ -1205,39 +1226,35 @@ expectError({ }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", }); expectNotAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", response: "all_old", }); expectNotAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", @@ -1245,39 +1262,35 @@ expectNotAssignable({ }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", }); expectNotAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", response: "all_old", }); expectNotAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", concurrency: 10, unprocessed: "raw", @@ -1431,32 +1444,28 @@ type CreateParamsParams = Parameter; type CreateParamsParamsWithoutSK = Parameter; expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", }); @@ -1608,18 +1617,16 @@ type UpdateParamsParams = Parameter; type UpdateParamsParamsWithoutSK = Parameter; expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "updated_new", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "updated_new", }); @@ -1761,18 +1768,16 @@ type PatchParamsParams = Parameter; type PatchParamsParamsWithoutSK = Parameter; expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "updated_new", }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", response: "updated_new", }); @@ -1876,34 +1881,30 @@ type MatchParamsParams = Parameter; type MatchParamsParamsWithoutSK = Parameter; expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", pages: 123, }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", pages: 123, }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", pages: 123, }); expectAssignable({ - includeKeys: true, + data: 'includeKeys', originalErr: true, params: {}, - raw: true, table: "abc", pages: 123, }); @@ -2635,10 +2636,9 @@ expectType(expectedAfterQueryChainMethods); type GoParams = Parameter; expectAssignable({ table: "df", - raw: true, params: {}, originalErr: true, - includeKeys: true, + data: 'includeKeys', pages: 123, }); complexService.collections diff --git a/package-lock.json b/package-lock.json index c4beb4c1..f65689c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "electrodb", - "version": "2.14.3", + "version": "3.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "electrodb", - "version": "2.14.3", + "version": "3.0.0", "license": "ISC", "dependencies": { "@aws-sdk/lib-dynamodb": "^3.654.0", diff --git a/package.json b/package.json index 38e000a0..35281ffa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "electrodb", - "version": "2.15.0", + "version": "3.0.0", "description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb", "main": "index.js", "scripts": { diff --git a/src/clauses.js b/src/clauses.js index aaee3261..c2406380 100644 --- a/src/clauses.js +++ b/src/clauses.js @@ -9,6 +9,7 @@ const { KeyTypes, IndexTypes, UpsertOperations, + ComparisonTypes, } = require("./types"); const { AttributeOperationProxy, @@ -119,13 +120,27 @@ let clauses = { pk, ); state.setSK(composites); - // we must apply eq on filter on all provided because if the user then does a sort key operation, it'd actually then unexpect results - if (sk.length > 1) { - state.filterProperties(FilterOperationNames.eq, { - ...unused, - ...composites, - }); - } + state.beforeBuildParams(({ options, state }) => { + const accessPattern = + entity.model.translations.indexes.fromIndexToAccessPattern[ + state.query.index + ]; + + if ( + options.compare === ComparisonTypes.attributes || + options.compare === ComparisonTypes.v2 + ) { + if ( + !entity.model.indexes[accessPattern].sk.isFieldRef && + sk.length > 1 + ) { + state.filterProperties(FilterOperationNames.eq, { + ...unused, + ...composites, + }); + } + } + }); }) .whenOptions(({ options, state }) => { if (!options.ignoreOwnership && !state.getParams()) { @@ -204,11 +219,13 @@ let clauses = { .whenOptions(({ state, options }) => { if (!options.ignoreOwnership && !state.getParams()) { state.unsafeApplyFilter( + {}, FilterOperationNames.eq, entity.identifiers.entity, entity.getName(), ); state.unsafeApplyFilter( + {}, FilterOperationNames.eq, entity.identifiers.version, entity.getVersion(), @@ -324,9 +341,9 @@ let clauses = { const attributes = state.getCompositeAttributes(); const filter = state.query.filter[ExpressionTypes.ConditionExpression]; const { pk, sk } = entity._getPrimaryIndexFieldNames(); - filter.unsafeSet(FilterOperationNames.exists, pk); + filter.unsafeSet({}, FilterOperationNames.exists, pk); if (sk) { - filter.unsafeSet(FilterOperationNames.exists, sk); + filter.unsafeSet({}, FilterOperationNames.exists, sk); } const pkComposite = entity._expectFacets(facets, attributes.pk); state.addOption("_includeOnResponseItem", pkComposite); @@ -432,14 +449,15 @@ let clauses = { const { pk } = state.query.keys; const sk = state.query.keys.sk[0]; - const { updatedKeys, setAttributes, indexKey, deletedKeys = [] } = entity._getPutKeys( - pk, - sk && sk.facets, - onlySetAppliedData, - ); + const { + updatedKeys, + setAttributes, + indexKey, + deletedKeys = [], + } = entity._getPutKeys(pk, sk && sk.facets, onlySetAppliedData); for (const deletedKey of deletedKeys) { - state.query.update.remove(deletedKey) + state.query.update.remove(deletedKey); } // calculated here but needs to be used when building the params @@ -540,9 +558,9 @@ let clauses = { const attributes = state.getCompositeAttributes(); const filter = state.query.filter[ExpressionTypes.ConditionExpression]; const { pk, sk } = entity._getPrimaryIndexFieldNames(); - filter.unsafeSet(FilterOperationNames.notExists, pk); + filter.unsafeSet({}, FilterOperationNames.notExists, pk); if (sk) { - filter.unsafeSet(FilterOperationNames.notExists, sk); + filter.unsafeSet({}, FilterOperationNames.notExists, sk); } return state .setMethod(MethodTypes.put) @@ -570,9 +588,9 @@ let clauses = { const attributes = state.getCompositeAttributes(); const filter = state.query.filter[ExpressionTypes.ConditionExpression]; const { pk, sk } = entity._getPrimaryIndexFieldNames(); - filter.unsafeSet(FilterOperationNames.exists, pk); + filter.unsafeSet({}, FilterOperationNames.exists, pk); if (sk) { - filter.unsafeSet(FilterOperationNames.exists, sk); + filter.unsafeSet({}, FilterOperationNames.exists, sk); } const pkComposite = entity._expectFacets(facets, attributes.pk); state.addOption("_includeOnResponseItem", pkComposite); @@ -923,15 +941,20 @@ let clauses = { pk, ); state.setSK(state.buildQueryComposites(facets, sk)); - // we must apply eq on filter on all provided because if the user then does a sort key operation, it'd actually then unexpect results - if (sk.length > 1) { - state.filterProperties(FilterOperationNames.eq, { - ...unused, - ...composites, - }); - } state.whenOptions(({ options, state }) => { + if ( + options.compare === ComparisonTypes.attributes || + options.compare === ComparisonTypes.v2 + ) { + if (sk.length > 1) { + state.filterProperties(FilterOperationNames.eq, { + ...unused, + ...composites, + }); + } + } + if ( state.query.options.indexType === IndexTypes.clustered && Object.keys(composites).length < sk.length && @@ -940,11 +963,13 @@ let clauses = { ) { state .unsafeApplyFilter( + {}, FilterOperationNames.eq, entity.identifiers.entity, entity.getName(), ) .unsafeApplyFilter( + {}, FilterOperationNames.eq, entity.identifiers.version, entity.getVersion(), @@ -983,15 +1008,34 @@ let clauses = { state.query.index ]; - if (!entity.model.indexes[accessPattern].sk.isFieldRef) { - state.filterProperties(FilterOperationNames.lte, endingSk.composites); - } - return state .setType(QueryTypes.and) .setSK(endingSk.composites) .setType(QueryTypes.between) - .setSK(startingSk.composites); + .setSK(startingSk.composites) + .beforeBuildParams(({ options, state }) => { + if ( + options.compare === ComparisonTypes.attributes || + options.compare === ComparisonTypes.v2 + ) { + if (!entity.model.indexes[accessPattern].sk.isFieldRef) { + state.filterProperties( + FilterOperationNames.lte, + endingSk.composites, + { asPrefix: true }, + ); + } + if (options.compare === ComparisonTypes.attributes) { + if (!entity.model.indexes[accessPattern].sk.isFieldRef) { + state.filterProperties( + FilterOperationNames.gte, + startingSk.composites, + { asPrefix: true }, + ); + } + } + } + }); } catch (err) { state.setError(err); return state; @@ -1032,14 +1076,23 @@ let clauses = { pk, ); state.setSK(composites); - const accessPattern = - entity.model.translations.indexes.fromIndexToAccessPattern[ - state.query.index - ]; - - if (!entity.model.indexes[accessPattern].sk.isFieldRef) { - state.filterProperties(FilterOperationNames.gt, composites); - } + state.beforeBuildParams(({ options, state }) => { + if ( + options.compare === ComparisonTypes.attributes || + options.compare === ComparisonTypes.v2 + ) { + const accessPattern = + entity.model.translations.indexes.fromIndexToAccessPattern[ + state.query.index + ]; + + if (!entity.model.indexes[accessPattern].sk.isFieldRef) { + state.filterProperties(FilterOperationNames.gt, composites, { + asPrefix: true, + }); + } + } + }); }); } catch (err) { state.setError(err); @@ -1058,6 +1111,27 @@ let clauses = { return state.setType(QueryTypes.gte).ifSK((state) => { const attributes = state.getCompositeAttributes(); state.setSK(state.buildQueryComposites(facets, attributes.sk)); + state.beforeBuildParams(({ options, state }) => { + const { composites } = state.identifyCompositeAttributes( + facets, + attributes.sk, + attributes.pk, + ); + if (options.compare === ComparisonTypes.attributes) { + const accessPattern = + entity.model.translations.indexes.fromIndexToAccessPattern[ + state.query.index + ]; + if ( + !entity.model.indexes[accessPattern].sk.isFieldRef && + attributes.sk.length > 1 + ) { + state.filterProperties(FilterOperationNames.gte, composites, { + asPrefix: true, + }); + } + } + }); }); } catch (err) { state.setError(err); @@ -1081,6 +1155,19 @@ let clauses = { pk, ); state.setSK(composites); + state.beforeBuildParams(({ options, state }) => { + if (options.compare === ComparisonTypes.attributes) { + const accessPattern = + entity.model.translations.indexes.fromIndexToAccessPattern[ + state.query.index + ]; + if (!entity.model.indexes[accessPattern].sk.isFieldRef) { + state.filterProperties(FilterOperationNames.lt, composites, { + asPrefix: true, + }); + } + } + }); }); } catch (err) { state.setError(err); @@ -1104,13 +1191,23 @@ let clauses = { pk, ); state.setSK(composites); - const accessPattern = - entity.model.translations.indexes.fromIndexToAccessPattern[ - state.query.index - ]; - if (!entity.model.indexes[accessPattern].sk.isFieldRef) { - state.filterProperties(FilterOperationNames.lte, composites); - } + + state.beforeBuildParams(({ options, state }) => { + if ( + options.compare === ComparisonTypes.attributes || + options.compare === ComparisonTypes.v2 + ) { + const accessPattern = + entity.model.translations.indexes.fromIndexToAccessPattern[ + state.query.index + ]; + if (!entity.model.indexes[accessPattern].sk.isFieldRef) { + state.filterProperties(FilterOperationNames.lte, composites, { + asPrefix: true, + }); + } + } + }); }); } catch (err) { state.setError(err); @@ -1430,14 +1527,20 @@ class ChainState { }; } - applyFilter(operation, name, ...values) { + applyFilter(operation, name, values, filterOptions) { if ( - (FilterOperationNames[operation] !== undefined) & (name !== undefined) && - values.length > 0 + FilterOperationNames[operation] !== undefined && + name !== undefined && + values !== undefined ) { const attribute = this.attributes[name]; if (attribute !== undefined) { - this.unsafeApplyFilter(operation, attribute.field, ...values); + this.unsafeApplyFilter( + filterOptions, + operation, + attribute.field, + values, + ); } } return this; @@ -1452,28 +1555,28 @@ class ChainState { const attribute = this.attributes[name]; if (attribute !== undefined) { const filter = this.query.filter[ExpressionTypes.ConditionExpression]; - filter.unsafeSet(operation, attribute.field, ...values); + filter.unsafeSet({}, operation, attribute.field, ...values); } } return this; } - unsafeApplyFilter(operation, name, ...values) { + unsafeApplyFilter(filterOptions = {}, operation, name, values) { if ( (FilterOperationNames[operation] !== undefined) & (name !== undefined) && - values.length > 0 + values !== undefined ) { const filter = this.query.filter[ExpressionTypes.FilterExpression]; - filter.unsafeSet(operation, name, ...values); + filter.unsafeSet(filterOptions, operation, name, values); } return this; } - filterProperties(operation, obj = {}) { + filterProperties(operation, obj = {}, filterOptions = {}) { for (const property in obj) { const value = obj[property]; if (value !== undefined) { - this.applyFilter(operation, property, value); + this.applyFilter(operation, property, value, filterOptions); } } return this; @@ -1551,8 +1654,11 @@ class ChainState { fn({ options, state: this }); }); } + + return this; } + // these are ran before "beforeBuildParams" applyWithOptions(options = {}) { this.applyAfterOptions.forEach((fn) => fn(options)); } @@ -1563,6 +1669,7 @@ class ChainState { fn({ options, state: this }); }); } + return this; } applyBeforeBuildParams(options = {}) { diff --git a/src/client.js b/src/client.js index 808dca79..afa79012 100644 --- a/src/client.js +++ b/src/client.js @@ -1,5 +1,5 @@ -const lib = require('@aws-sdk/lib-dynamodb') -const util = require('@aws-sdk/util-dynamodb') +const lib = require("@aws-sdk/lib-dynamodb"); +const util = require("@aws-sdk/util-dynamodb"); const { isFunction } = require("./validations"); const { ElectroError, ErrorCodes } = require("./errors"); const DocumentClientVersions = { @@ -11,12 +11,12 @@ const unmarshallItem = (value) => { const unmarshall = util.unmarshall || ((val) => val); try { value.Item = unmarshall(value.Item); - } catch(err) { - console.error('Internal Error: Failed to unmarshal input', err); + } catch (err) { + console.error("Internal Error: Failed to unmarshal input", err); } return value; -} +}; const v3Methods = ["send"]; const v2Methods = [ diff --git a/src/conversions.js b/src/conversions.js new file mode 100644 index 00000000..d2e359b5 --- /dev/null +++ b/src/conversions.js @@ -0,0 +1,68 @@ +function createConversions(entity) { + const conversions = { + fromComposite: { + toKeys: (composite, options = {}) => + entity._fromCompositeToKeys({ provided: composite }, options), + toCursor: (composite) => + entity._fromCompositeToCursor( + { provided: composite }, + { strict: "all" }, + ), + }, + fromKeys: { + toCursor: (keys) => entity._fromKeysToCursor({ provided: keys }, {}), + toComposite: (keys) => entity._fromKeysToComposite({ provided: keys }), + }, + fromCursor: { + toKeys: (cursor) => entity._fromCursorToKeys({ provided: cursor }), + toComposite: (cursor) => + entity._fromCursorToComposite({ provided: cursor }), + }, + byAccessPattern: {}, + }; + + for (let accessPattern in entity.model.indexes) { + let index = entity.model.indexes[accessPattern].index; + conversions.byAccessPattern[accessPattern] = { + fromKeys: { + toCursor: (keys) => + entity._fromKeysToCursorByIndex({ indexName: index, provided: keys }), + toComposite: (keys) => + entity._fromKeysToCompositeByIndex({ + indexName: index, + provided: keys, + }), + }, + fromCursor: { + toKeys: (cursor) => + entity._fromCursorToKeysByIndex({ + indexName: index, + provided: cursor, + }), + toComposite: (cursor) => + entity._fromCursorToCompositeByIndex({ + indexName: index, + provided: cursor, + }), + }, + fromComposite: { + toCursor: (composite) => + entity._fromCompositeToCursorByIndex( + { indexName: index, provided: composite }, + { strict: "all" }, + ), + toKeys: (composite, options = {}) => + entity._fromCompositeToKeysByIndex( + { indexName: index, provided: composite }, + options, + ), + }, + }; + } + + return conversions; +} + +module.exports = { + createConversions, +}; diff --git a/src/entity.js b/src/entity.js index 15a51d3f..91cb57d8 100644 --- a/src/entity.js +++ b/src/entity.js @@ -23,10 +23,12 @@ const { ResultOrderOption, ResultOrderParam, IndexTypes, - PartialComparisons, + KeyAttributesComparisons, MethodTypeTranslation, TransactionCommitSymbol, CastKeyOptions, + ComparisonTypes, + DataOptions, } = require("./types"); const { FilterFactory } = require("./filters"); const { FilterOperations } = require("./operations"); @@ -40,9 +42,9 @@ const e = require("./errors"); const v = require("./validations"); const ImpactedIndexTypeSource = { - composite: 'composite', - provided: 'provided', -} + composite: "composite", + provided: "provided", +}; class Entity { constructor(model, config = {}) { @@ -76,27 +78,6 @@ class Entity { ); this.query = {}; - this.conversions = { - fromComposite: { - toKeys: (composite, options = {}) => - this._fromCompositeToKeys({ provided: composite }, options), - toCursor: (composite) => - this._fromCompositeToCursor( - { provided: composite }, - { strict: "all" }, - ), - }, - fromKeys: { - toCursor: (keys) => this._fromKeysToCursor({ provided: keys }, {}), - toComposite: (keys) => this._fromKeysToComposite({ provided: keys }), - }, - fromCursor: { - toKeys: (cursor) => this._fromCursorToKeys({ provided: cursor }), - toComposite: (cursor) => - this._fromCursorToComposite({ provided: cursor }), - }, - byAccessPattern: {}, - }; for (let accessPattern in this.model.indexes) { let index = this.model.indexes[accessPattern].index; this.query[accessPattern] = (...values) => { @@ -111,42 +92,6 @@ class Entity { options, ).query(...values); }; - - this.conversions.byAccessPattern[accessPattern] = { - fromKeys: { - toCursor: (keys) => - this._fromKeysToCursorByIndex({ indexName: index, provided: keys }), - toComposite: (keys) => - this._fromKeysToCompositeByIndex({ - indexName: index, - provided: keys, - }), - }, - fromCursor: { - toKeys: (cursor) => - this._fromCursorToKeysByIndex({ - indexName: index, - provided: cursor, - }), - toComposite: (cursor) => - this._fromCursorToCompositeByIndex({ - indexName: index, - provided: cursor, - }), - }, - fromComposite: { - toCursor: (composite) => - this._fromCompositeToCursorByIndex( - { indexName: index, provided: composite }, - { strict: "all" }, - ), - toKeys: (composite, options = {}) => - this._fromCompositeToKeysByIndex( - { indexName: index, provided: composite }, - options, - ), - }, - }; } this.config.identifiers = config.identifiers || {}; @@ -633,7 +578,7 @@ class Entity { _safeMinimum(...values) { let eligibleNumbers = []; for (let value of values) { - if (typeof value === 'number') { + if (typeof value === "number") { eligibleNumbers.push(value); } } @@ -733,16 +678,14 @@ class Entity { ExclusiveStartKey = undefined; } let pages = this._normalizePagesValue(config.pages); - let max = this._normalizeLimitValue(config.limit); let iterations = 0; let count = 0; let hydratedUnprocessed = []; const shouldHydrate = config.hydrate && method === MethodTypes.query; do { - let limit = max === undefined ? parameters.Limit : max - count; let response = await this._exec( method, - { ExclusiveStartKey, ...parameters, Limit: limit }, + { ExclusiveStartKey, ...parameters }, config, ); @@ -750,17 +693,17 @@ class Entity { response = this.formatResponse(response, parameters.IndexName, { ...config, - includeKeys: shouldHydrate || config.includeKeys, + data: + shouldHydrate && + (!config.data || config.data === DataOptions.attributes) + ? "includeKeys" + : config.data, ignoreOwnership: shouldHydrate || config.ignoreOwnership, }); - - if (config.raw) { + if (config.data === DataOptions.raw) { return response; } else if (config._isCollectionQuery) { for (const entity in response.data) { - if (max) { - count += response.data[entity].length; - } let items = response.data[entity]; if (shouldHydrate && items.length) { const hydrated = await config.hydrator( @@ -778,8 +721,8 @@ class Entity { results[entity] = [...results[entity], ...items]; } } else if (Array.isArray(response.data)) { - let prevCount = count - if (!!max || !!config.count) { + let prevCount = count; + if (config.count) { count += response.data.length; } let items = response.data; @@ -801,7 +744,10 @@ class Entity { results = [...results, ...items]; if (moreItemsThanRequired || count === config.count) { const lastItem = results[results.length - 1]; - ExclusiveStartKey = this._fromCompositeToKeysByIndex({ indexName, provided: lastItem }); + ExclusiveStartKey = this._fromCompositeToKeysByIndex({ + indexName, + provided: lastItem, + }); break; } } else { @@ -810,8 +756,9 @@ class Entity { iterations++; } while ( ExclusiveStartKey && - (pages === AllPages || (config.count !== undefined || iterations < pages)) && - (max === undefined || count < max) && + (pages === AllPages || + config.count !== undefined || + iterations < pages) && (config.count === undefined || count < config.count) ); @@ -869,14 +816,13 @@ class Entity { } cleanseRetrievedData(item = {}, options = {}) { - let { includeKeys } = options; let data = {}; let names = this.model.schema.translationForRetrieval; for (let [attr, value] of Object.entries(item)) { let name = names[attr]; if (name) { data[name] = value; - } else if (includeKeys) { + } else if (options.data === DataOptions.includeKeys) { data[attr] = value; } } @@ -963,14 +909,14 @@ class Entity { let results = {}; if (validations.isFunction(config.parse)) { results = config.parse(config, response); - } else if (config.raw && !config._isPagination) { + } else if (config.data === DataOptions.raw && !config._isPagination) { if (response.TableName) { results = {}; } else { results = response; } } else if ( - config.raw && + config.data === DataOptions.raw && (config._isPagination || config.lastEvaluatedKeyRaw) ) { results = response; @@ -1372,7 +1318,7 @@ class Entity { _formatReturnPager(config, lastEvaluatedKey) { let page = lastEvaluatedKey || null; - if (config.raw || config.pager === Pager.raw) { + if (config.data === DataOptions.raw || config.pager === Pager.raw) { return page; } return config.formatCursor.serialize(page) || null; @@ -1380,7 +1326,7 @@ class Entity { _formatExclusiveStartKey({ config, indexName = TableIndex }) { let exclusiveStartKey = config.cursor; - if (config.raw || config.pager === Pager.raw) { + if (config.data === DataOptions.raw || config.pager === Pager.raw) { return ( this._trimKeysToIndex({ provided: exclusiveStartKey, indexName }) || null @@ -1683,6 +1629,9 @@ class Entity { response: "default", cursor: null, data: "attributes", + consistent: undefined, + compare: ComparisonTypes.keys, + complete: false, ignoreOwnership: false, _providedIgnoreOwnership: false, _isPagination: false, @@ -1717,6 +1666,23 @@ class Entity { } } + if (typeof option.compare === "string") { + const type = ComparisonTypes[option.compare.toLowerCase()]; + if (type) { + config.compare = type; + if (type === ComparisonTypes.v2 && option.complete === undefined) { + config.complete = true; + } + } else { + throw new e.ElectroError( + e.ErrorCodes.InvalidOptions, + `Invalid value for query option "compare" provided. Valid options include ${u.commaSeparatedString( + Object.keys(ComparisonTypes), + )}, received: "${option.compare}"`, + ); + } + } + if (typeof option.response === "string" && option.response.length) { const format = ReturnValues[option.response]; if (format === undefined) { @@ -1728,13 +1694,14 @@ class Entity { Object.keys(ReturnValues), )}.`, ); - } - config.response = format; - if (context.operation === MethodTypes.transactWrite) { - config.params.ReturnValuesOnConditionCheckFailure = - FormatToReturnValues[format]; - } else { - config.params.ReturnValues = FormatToReturnValues[format]; + } else if (format !== ReturnValues.default) { + config.response = format; + if (context.operation === MethodTypes.transactWrite) { + config.params.ReturnValuesOnConditionCheckFailure = + FormatToReturnValues[format]; + } else { + config.params.ReturnValues = FormatToReturnValues[format]; + } } } @@ -1801,12 +1768,20 @@ class Entity { } if (option.data) { + if (!DataOptions[option.data]) { + throw new e.ElectroError( + e.ErrorCodes.InvalidOptions, + `Query option 'data' must be one of ${u.commaSeparatedString( + Object.keys(DataOptions), + )}.`, + ); + } config.data = option.data; switch (option.data) { - case "raw": + case DataOptions.raw: config.raw = true; break; - case "includeKeys": + case DataOptions.includeKeys: config.includeKeys = true; break; } @@ -1814,11 +1789,19 @@ class Entity { if (option.count !== undefined) { if (typeof option.count !== "number" || option.count < 1) { - throw new e.ElectroError(e.ErrorCodes.InvalidOptions, `Query option 'count' must be of type 'number' and greater than zero.`); + throw new e.ElectroError( + e.ErrorCodes.InvalidOptions, + `Query option 'count' must be of type 'number' and greater than zero.`, + ); } config.count = option.count; } + if (option.consistent === true) { + config.consistent = true; + config.params.ConsistentRead = true; + } + if (option.limit !== undefined) { config.limit = option.limit; config.params.Limit = option.limit; @@ -2064,7 +2047,7 @@ class Entity { return parameters; } - const requiresRawResponse = !!config.raw; + const requiresRawResponse = config.data === DataOptions.raw; const enforcesOwnership = !config.ignoreOwnership; const requiresUserInvolvedPagination = TerminalOperation[config.terminalOperation] === TerminalOperation.page; @@ -2258,7 +2241,7 @@ class Entity { let keys = this._makeParameterKey(indexBase, pk, ...sk); // trim empty key values (this can occur when keys are defined by users) for (let key in keys) { - if (keys[key] === undefined || keys[key] === '') { + if (keys[key] === undefined || keys[key] === "") { delete keys[key]; } } @@ -2266,26 +2249,25 @@ class Entity { let keyExpressions = this._expressionAttributeBuilder(keys); const expressionAttributeNames = this._mergeExpressionsAttributes( - filter.getNames(), - keyExpressions.ExpressionAttributeNames, + filter.getNames(), + keyExpressions.ExpressionAttributeNames, ); const expressionAttributeValues = this._mergeExpressionsAttributes( - filter.getValues(), - keyExpressions.ExpressionAttributeValues, + filter.getValues(), + keyExpressions.ExpressionAttributeValues, ); - let params = { TableName: this.getTableName(), }; if (Object.keys(expressionAttributeNames).length) { - params['ExpressionAttributeNames'] = expressionAttributeNames; + params["ExpressionAttributeNames"] = expressionAttributeNames; } if (Object.keys(expressionAttributeValues).length) { - params['ExpressionAttributeValues'] = expressionAttributeValues; + params["ExpressionAttributeValues"] = expressionAttributeValues; } let filterExpressions = []; @@ -2306,7 +2288,7 @@ class Entity { } if (filterExpressions.length) { - params.FilterExpression = filterExpressions.join(' AND '); + params.FilterExpression = filterExpressions.join(" AND "); } return params; @@ -2365,7 +2347,13 @@ class Entity { indexKey, updatedKeys, deletedKeys = [], - } = this._getUpdatedKeys(pk, sk, attributesAndComposites, removed, update.composites); + } = this._getUpdatedKeys( + pk, + sk, + attributesAndComposites, + removed, + update.composites, + ); const accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[TableIndex]; for (const path of Object.keys(preparedUpdateValues)) { @@ -2645,7 +2633,7 @@ class Entity { return expressions; } - _makeQueryKeys(state) { + _makeQueryKeys(state, options) { let consolidatedQueryFacets = this._consolidateQueryFacets( state.query.keys.sk, ); @@ -2660,13 +2648,17 @@ class Entity { isCollection: state.query.options._isCollectionQuery, }); default: - return this._makeIndexKeysWithoutTail(state, consolidatedQueryFacets); + return this._makeIndexKeysWithoutTail( + state, + consolidatedQueryFacets, + options, + ); } } /* istanbul ignore next */ _queryParams(state = {}, options = {}) { - const indexKeys = this._makeQueryKeys(state); + const indexKeys = this._makeQueryKeys(state, options); let parameters = {}; switch (state.query.type) { case QueryTypes.is: @@ -2707,6 +2699,7 @@ class Entity { break; case QueryTypes.between: parameters = this._makeBetweenQueryParams( + state.query.options, state.query.index, state.query.filter[ExpressionTypes.FilterExpression], indexKeys.pk, @@ -2722,6 +2715,8 @@ class Entity { state.query.type, state.query.filter[ExpressionTypes.FilterExpression], indexKeys, + options, + state.query.options, ); break; default: @@ -2739,31 +2734,50 @@ class Entity { }); } - _makeBetweenQueryParams(index, filter, pk, ...sk) { + _makeBetweenQueryParams(queryOptions, index, filter, pk, ...sk) { let keyExpressions = this._queryKeyExpressionAttributeBuilder( index, pk, ...sk, ); + delete keyExpressions.ExpressionAttributeNames["#sk2"]; + + const customExpressions = { + names: (queryOptions.expressions && queryOptions.expressions.names) || {}, + values: + (queryOptions.expressions && queryOptions.expressions.values) || {}, + expression: + (queryOptions.expressions && queryOptions.expressions.expression) || "", + }; + let params = { TableName: this.getTableName(), ExpressionAttributeNames: this._mergeExpressionsAttributes( filter.getNames(), keyExpressions.ExpressionAttributeNames, + customExpressions.names, ), ExpressionAttributeValues: this._mergeExpressionsAttributes( filter.getValues(), keyExpressions.ExpressionAttributeValues, + customExpressions.values, ), KeyConditionExpression: `#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2`, }; + if (index) { params["IndexName"] = index; } - if (filter.build()) { - params.FilterExpression = filter.build(); + + let expressions = [customExpressions.expression, filter.build()] + .filter(Boolean) + .join(" AND "); + + if (expressions.length) { + params.FilterExpression = expressions; } + return params; } @@ -2881,28 +2895,52 @@ class Entity { return merged; } + _getComparisonOperator(comparison, skType, comparisonType) { + if (skType === "number") { + return Comparisons[comparison]; + } else if ( + comparisonType === ComparisonTypes.attributes || + comparisonType === ComparisonTypes.v2 + ) { + return KeyAttributesComparisons[comparison]; + } else { + return Comparisons[comparison]; + } + } + /* istanbul ignore next */ _makeComparisonQueryParams( index = TableIndex, comparison = "", filter = {}, indexKeys = {}, + options = {}, + queryOptions = {}, ) { const { pk } = indexKeys; const sk = indexKeys.sk[0]; - let operator = - typeof sk === "number" - ? Comparisons[comparison] - : PartialComparisons[comparison]; - + let operator = this._getComparisonOperator( + comparison, + typeof sk, + options.compare, + ); if (!operator) { throw new Error( `Unexpected comparison operator "${comparison}", expected ${u.commaSeparatedString( - Object.values(PartialComparisons), + Object.keys(KeyAttributesComparisons), )}`, ); } + + let customExpressions = { + names: (queryOptions.expressions && queryOptions.expressions.names) || {}, + values: + (queryOptions.expressions && queryOptions.expressions.values) || {}, + expression: + (queryOptions.expressions && queryOptions.expressions.expression) || "", + }; + let keyExpressions = this._queryKeyExpressionAttributeBuilder( index, pk, @@ -2914,27 +2952,40 @@ class Entity { ExpressionAttributeNames: this._mergeExpressionsAttributes( filter.getNames(), keyExpressions.ExpressionAttributeNames, + customExpressions.names, ), ExpressionAttributeValues: this._mergeExpressionsAttributes( filter.getValues(), keyExpressions.ExpressionAttributeValues, + customExpressions.values, ), KeyConditionExpression: `#pk = :pk and #sk1 ${operator} :sk1`, }; + if (index) { params["IndexName"] = index; } - if (filter.build()) { - params.FilterExpression = filter.build(); + + let expressions = [customExpressions.expression, filter.build()] + .filter(Boolean) + .join(" AND "); + + if (expressions.length) { + params.FilterExpression = expressions; } + return params; } - _expectIndexFacets(attributes, facets, { utilizeIncludedOnlyIndexes, skipConditionCheck } = {}) { + _expectIndexFacets( + attributes, + facets, + { utilizeIncludedOnlyIndexes, skipConditionCheck } = {}, + ) { let [isIncomplete, { incomplete, complete }] = this._getIndexImpact( attributes, facets, - { utilizeIncludedOnlyIndexes, skipConditionCheck }, + { utilizeIncludedOnlyIndexes, skipConditionCheck }, ); if (isIncomplete) { @@ -2963,7 +3014,8 @@ class Entity { _makeKeysFromAttributes(indexes, attributes, conditions) { let indexKeys = {}; for (let [index, keyTypes] of Object.entries(indexes)) { - const shouldMakeKeys = !this._indexConditionIsDefined(index) || conditions[index]; + const shouldMakeKeys = + !this._indexConditionIsDefined(index) || conditions[index]; if (!shouldMakeKeys && index !== TableIndex) { continue; } @@ -2994,7 +3046,10 @@ class Entity { _makePutKeysFromAttributes(indexes, attributes) { let indexKeys = {}; for (let index of indexes) { - const shouldMakeKeys = this.model.indexes[this.model.translations.indexes.fromIndexToAccessPattern[index]].condition(attributes); + const shouldMakeKeys = + this.model.indexes[ + this.model.translations.indexes.fromIndexToAccessPattern[index] + ].condition(attributes); if (!shouldMakeKeys) { continue; } @@ -3019,11 +3074,15 @@ class Entity { ); let deletedKeys = []; - for (const [indexName, condition] of Object.entries(completeFacets.conditions)) { + for (const [indexName, condition] of Object.entries( + completeFacets.conditions, + )) { if (!condition) { deletedKeys.push(this.model.translations.keys[indexName][KeyTypes.pk]); if (this.model.translations.keys[indexName][KeyTypes.sk]) { - deletedKeys.push(this.model.translations.keys[indexName][KeyTypes.sk]); + deletedKeys.push( + this.model.translations.keys[indexName][KeyTypes.sk], + ); } } } @@ -3071,7 +3130,7 @@ class Entity { const removedKeyImpact = this._expectIndexFacets( { ...removed }, { ...keyAttributes }, - { skipConditionCheck: true } + { skipConditionCheck: true }, ); // complete facets, only includes impacted facets which likely does not include the updateIndex which then needs to be added here. @@ -3091,11 +3150,15 @@ class Entity { let updatedKeys = {}; let deletedKeys = []; let indexKey = {}; - for (const [indexName, condition] of Object.entries(completeFacets.conditions)) { + for (const [indexName, condition] of Object.entries( + completeFacets.conditions, + )) { if (!condition) { deletedKeys.push(this.model.translations.keys[indexName][KeyTypes.pk]); if (this.model.translations.keys[indexName][KeyTypes.sk]) { - deletedKeys.push(this.model.translations.keys[indexName][KeyTypes.sk]); + deletedKeys.push( + this.model.translations.keys[indexName][KeyTypes.sk], + ); } } } @@ -3122,9 +3185,9 @@ class Entity { let hasPrefix = indexHasSk && this.model.prefixes[index].sk.prefix !== undefined; let hasPostfix = - indexHasSk && this.model.prefixes[index].sk.prefix !== undefined; + indexHasSk && this.model.prefixes[index].sk.prefix !== undefined; if (noImpactSk && noAttributeSk) { - let key = hasPrefix ? this.model.prefixes[index].sk.prefix : ''; + let key = hasPrefix ? this.model.prefixes[index].sk.prefix : ""; if (hasPostfix) { key = `${key}${this.model.prefixes[index].sk.postfix}`; } @@ -3146,12 +3209,19 @@ class Entity { } _indexConditionIsDefined(index) { - const definition = this.model.indexes[this.model.translations.indexes.fromIndexToAccessPattern[index]]; + const definition = + this.model.indexes[ + this.model.translations.indexes.fromIndexToAccessPattern[index] + ]; return definition && definition.conditionDefined; } /* istanbul ignore next */ - _getIndexImpact(attributes = {}, included = {}, { utilizeIncludedOnlyIndexes, skipConditionCheck } = {}) { + _getIndexImpact( + attributes = {}, + included = {}, + { utilizeIncludedOnlyIndexes, skipConditionCheck } = {}, + ) { // beware: this entire algorithm stinks and needs to be completely refactored. It does redundant loops and fights // itself the whole way through. I am sorry. let includedFacets = Object.keys(included); @@ -3166,21 +3236,26 @@ class Entity { facets[attribute] = attributes[attribute]; indexes.forEach((definition) => { const { index, type } = definition; - impactedIndexes[index] = impactedIndexes[index] || {}; - impactedIndexes[index][type] = impactedIndexes[index][type] || []; - impactedIndexes[index][type].push(attribute); - impactedIndexTypes[index] = impactedIndexTypes[index] || {}; - impactedIndexTypes[index][type] = this.model.translations.keys[index][type]; + impactedIndexes[index] = impactedIndexes[index] || {}; + impactedIndexes[index][type] = impactedIndexes[index][type] || []; + impactedIndexes[index][type].push(attribute); + impactedIndexTypes[index] = impactedIndexTypes[index] || {}; + impactedIndexTypes[index][type] = + this.model.translations.keys[index][type]; - impactedIndexTypeSources[index] = impactedIndexTypeSources[index] || {}; - impactedIndexTypeSources[index][type] = ImpactedIndexTypeSource.provided; + impactedIndexTypeSources[index] = + impactedIndexTypeSources[index] || {}; + impactedIndexTypeSources[index][type] = + ImpactedIndexTypeSource.provided; }); } } // this function is used to determine key impact for update `set`, update `delete`, and `put`. This block is currently only used by update `set` if (utilizeIncludedOnlyIndexes) { - for (const [index, { pk, sk }] of Object.entries(this.model.facets.byIndex)) { + for (const [index, { pk, sk }] of Object.entries( + this.model.facets.byIndex, + )) { // The main table index is handled somewhere else (messy I know), and we only want to do this processing if an // index condition is defined for backwards compatibility. Backwards compatibility is not required for this // change, but I have paranoid concerns of breaking changes around sparse indexes. @@ -3188,24 +3263,36 @@ class Entity { continue; } - if (pk && pk.length && pk.every(attr => included[attr] !== undefined)) { + if ( + pk && + pk.length && + pk.every((attr) => included[attr] !== undefined) + ) { pk.forEach((attr) => { facets[attr] = included[attr]; }); impactedIndexes[index] = impactedIndexes[index] || {}; impactedIndexes[index][KeyTypes.pk] = [...pk]; impactedIndexTypes[index] = impactedIndexTypes[index] || {}; - impactedIndexTypes[index][KeyTypes.pk] = this.model.translations.keys[index][KeyTypes.pk]; + impactedIndexTypes[index][KeyTypes.pk] = + this.model.translations.keys[index][KeyTypes.pk]; // flagging the impactedIndexTypeSource as `composite` means the entire key is only being impacted because // all composites are in `included`. This will help us determine if we need to evaluate the `condition` // callback for the index. If both the `sk` and `pk` were impacted because of `included` then we can skip // the condition check because the index doesn't need to be recalculated; - impactedIndexTypeSources[index] = impactedIndexTypeSources[index] || {}; - impactedIndexTypeSources[index][KeyTypes.pk] = impactedIndexTypeSources[index][KeyTypes.pk] || ImpactedIndexTypeSource.composite; + impactedIndexTypeSources[index] = + impactedIndexTypeSources[index] || {}; + impactedIndexTypeSources[index][KeyTypes.pk] = + impactedIndexTypeSources[index][KeyTypes.pk] || + ImpactedIndexTypeSource.composite; } - if (sk && sk.length && sk.every(attr => included[attr] !== undefined)) { + if ( + sk && + sk.length && + sk.every((attr) => included[attr] !== undefined) + ) { if (this.model.translations.keys[index][KeyTypes.sk]) { sk.forEach((attr) => { facets[attr] = included[attr]; @@ -3213,61 +3300,66 @@ class Entity { impactedIndexes[index] = impactedIndexes[index] || {}; impactedIndexes[index][KeyTypes.sk] = [...sk]; impactedIndexTypes[index] = impactedIndexTypes[index] || {}; - impactedIndexTypes[index][KeyTypes.sk] = this.model.translations.keys[index][KeyTypes.sk]; + impactedIndexTypes[index][KeyTypes.sk] = + this.model.translations.keys[index][KeyTypes.sk]; // flagging the impactedIndexTypeSource as `composite` means the entire key is only being impacted because // all composites are in `included`. This will help us determine if we need to evaluate the `condition` // callback for the index. If both the `sk` and `pk` were impacted because of `included` then we can skip // the condition check because the index doesn't need to be recalculated; - impactedIndexTypeSources[index] = impactedIndexTypeSources[index] || {}; - impactedIndexTypeSources[index][KeyTypes.sk] = impactedIndexTypeSources[index][KeyTypes.sk] || ImpactedIndexTypeSource.composite; + impactedIndexTypeSources[index] = + impactedIndexTypeSources[index] || {}; + impactedIndexTypeSources[index][KeyTypes.sk] = + impactedIndexTypeSources[index][KeyTypes.sk] || + ImpactedIndexTypeSource.composite; } } } } - let indexesWithMissingComposites = Object.entries(this.model.facets.byIndex) - .map(([index, definition]) => { - const { pk, sk } = definition; - let impacted = impactedIndexes[index]; - let impact = { - index, - definition, - missing: [] - }; - if (impacted) { - let missingPk = - impacted[KeyTypes.pk] && impacted[KeyTypes.pk].length !== pk.length; - let missingSk = - impacted[KeyTypes.sk] && impacted[KeyTypes.sk].length !== sk.length; - if (missingPk) { - impact.missing = [ - ...impact.missing, - ...pk.filter((attr) => { - return ( - !impacted[KeyTypes.pk].includes(attr) && - !includedFacets.includes(attr) - ); - }), - ]; - } - if (missingSk) { - impact.missing = [ - ...impact.missing, - ...sk.filter( - (attr) => - !impacted[KeyTypes.sk].includes(attr) && - !includedFacets.includes(attr), - ), - ]; - } - if (!missingPk && !missingSk) { - completedIndexes.push(index); - } + let indexesWithMissingComposites = Object.entries( + this.model.facets.byIndex, + ).map(([index, definition]) => { + const { pk, sk } = definition; + let impacted = impactedIndexes[index]; + let impact = { + index, + definition, + missing: [], + }; + if (impacted) { + let missingPk = + impacted[KeyTypes.pk] && impacted[KeyTypes.pk].length !== pk.length; + let missingSk = + impacted[KeyTypes.sk] && impacted[KeyTypes.sk].length !== sk.length; + if (missingPk) { + impact.missing = [ + ...impact.missing, + ...pk.filter((attr) => { + return ( + !impacted[KeyTypes.pk].includes(attr) && + !includedFacets.includes(attr) + ); + }), + ]; + } + if (missingSk) { + impact.missing = [ + ...impact.missing, + ...sk.filter( + (attr) => + !impacted[KeyTypes.sk].includes(attr) && + !includedFacets.includes(attr), + ), + ]; } + if (!missingPk && !missingSk) { + completedIndexes.push(index); + } + } - return impact; - }); + return impact; + }); let incomplete = []; for (const { index, missing, definition } of indexesWithMissingComposites) { @@ -3277,28 +3369,53 @@ class Entity { // is meaningless and ElectroDB should uphold its obligation to keep keys and attributes in sync. // `index === TableIndex` is a special case where we don't need to check the condition because the main table is immutable // `!this._indexConditionIsDefined(index)` means the index doesn't have a condition defined, so we can skip the check - if (skipConditionCheck || index === TableIndex || !indexConditionIsDefined) { + if ( + skipConditionCheck || + index === TableIndex || + !indexConditionIsDefined + ) { incomplete.push({ index, missing }); conditions[index] = true; continue; } - const memberAttributeIsImpacted = impactedIndexTypeSources[index] && (impactedIndexTypeSources[index][KeyTypes.pk] === ImpactedIndexTypeSource.provided || impactedIndexTypeSources[index][KeyTypes.sk] === ImpactedIndexTypeSource.provided); - const allMemberAttributesAreIncluded = definition.all.every(({name}) => included[name] !== undefined); + const memberAttributeIsImpacted = + impactedIndexTypeSources[index] && + (impactedIndexTypeSources[index][KeyTypes.pk] === + ImpactedIndexTypeSource.provided || + impactedIndexTypeSources[index][KeyTypes.sk] === + ImpactedIndexTypeSource.provided); + const allMemberAttributesAreIncluded = definition.all.every( + ({ name }) => included[name] !== undefined, + ); if (memberAttributeIsImpacted || allMemberAttributesAreIncluded) { // the `missing` array will contain indexes that are partially provided, but that leaves cases where the pk or // sk of an index is complete but not both. Both cases are invalid if `indexConditionIsDefined=true` const missingAttributes = definition.all - .filter(({name}) => attributes[name] === undefined && included[name] === undefined || missing.includes(name)) - .map(({name}) => name) + .filter( + ({ name }) => + (attributes[name] === undefined && + included[name] === undefined) || + missing.includes(name), + ) + .map(({ name }) => name); if (missingAttributes.length) { - throw new e.ElectroError(e.ErrorCodes.IncompleteIndexCompositesAttributesProvided, `Incomplete composite attributes provided for index ${index}. Write operations that include composite attributes, for indexes with a condition callback defined, must always provide values for every index composite. This is to ensure consistency between index values and attribute values. Missing composite attributes identified: ${u.commaSeparatedString(missingAttributes)}`); + throw new e.ElectroError( + e.ErrorCodes.IncompleteIndexCompositesAttributesProvided, + `Incomplete composite attributes provided for index ${index}. Write operations that include composite attributes, for indexes with a condition callback defined, must always provide values for every index composite. This is to ensure consistency between index values and attribute values. Missing composite attributes identified: ${u.commaSeparatedString( + missingAttributes, + )}`, + ); } - const accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[index]; - let shouldMakeKeys = !!this.model.indexes[accessPattern].condition({...attributes, ...included}); + const accessPattern = + this.model.translations.indexes.fromIndexToAccessPattern[index]; + let shouldMakeKeys = !!this.model.indexes[accessPattern].condition({ + ...attributes, + ...included, + }); // this helps identify which conditions were checked (key is present) and what the result was (true/false) conditions[index] = shouldMakeKeys; @@ -3313,7 +3430,12 @@ class Entity { incomplete = incomplete.filter(({ missing }) => missing.length); let isIncomplete = !!incomplete.length; - let complete = { facets, indexes: completedIndexes, impactedIndexTypes, conditions }; + let complete = { + facets, + indexes: completedIndexes, + impactedIndexTypes, + conditions, + }; return [isIncomplete, { incomplete, complete }]; } @@ -3555,34 +3677,32 @@ class Entity { return prefix; } - _makeKeyTransforms(queryType) { + _makeKeyTransforms(queryType, options = {}) { const transforms = []; const shiftUp = (val) => u.shiftSortOrder(val, 1); const noop = (val) => val; - switch (queryType) { - case QueryTypes.between: - transforms.push(noop, shiftUp); - break; - case QueryTypes.lte: - case QueryTypes.gt: - transforms.push(shiftUp); - break; - default: - transforms.push(noop); - break; + if (options.compare !== ComparisonTypes.v2) { + transforms.push(noop); + } else if (queryType === QueryTypes.between) { + transforms.push(noop, shiftUp); + } else if (queryType === QueryTypes.lte || queryType === QueryTypes.gt) { + transforms.push(shiftUp); + } else { + transforms.push(noop); } + return transforms; } /* istanbul ignore next */ - _makeIndexKeysWithoutTail(state = {}, skFacets = []) { + _makeIndexKeysWithoutTail(state = {}, skFacets = [], options) { const index = state.query.index || TableIndex; this._validateIndex(index); const pkFacets = state.query.keys.pk || {}; const excludePostfix = state.query.options.indexType === IndexTypes.clustered && state.query.options._isCollectionQuery; - const transforms = this._makeKeyTransforms(state.query.type); + const transforms = this._makeKeyTransforms(state.query.type, options); if (!skFacets.length) { skFacets.push({}); } @@ -4078,8 +4198,8 @@ class Entity { let indexScope = index.scope || ""; if (index.index === undefined && v.isFunction(index.condition)) { throw new e.ElectroError( - e.ErrorCodes.InvalidIndexCondition, - `The index option 'condition' is only allowed on secondary indexes`, + e.ErrorCodes.InvalidIndexCondition, + `The index option 'condition' is only allowed on secondary indexes`, ); } @@ -4184,7 +4304,7 @@ class Entity { } } - let definition= { + let definition = { pk, sk, hasSk, diff --git a/src/errors.js b/src/errors.js index ba028f61..72e82de3 100644 --- a/src/errors.js +++ b/src/errors.js @@ -219,8 +219,8 @@ const ErrorCodes = { }, IncompleteIndexCompositesAttributesProvided: { code: 2012, - section: 'invalid-index-composite-attributes-provided', - name: 'IncompleteIndexCompositesAttributesProvided', + section: "invalid-index-composite-attributes-provided", + name: "IncompleteIndexCompositesAttributesProvided", sym: ErrorCode, }, InvalidAttribute: { diff --git a/src/operations.js b/src/operations.js index a0fce097..5a80bccb 100644 --- a/src/operations.js +++ b/src/operations.js @@ -313,12 +313,15 @@ class AttributeOperationProxy { return AttributeOperationProxy.pathProxy(() => { const { commit, root, target, builder } = build(); const attribute = target.getChild(prop); - const nestedAny = attribute.type === AttributeTypes.any && - // if the name doesn't match that's because we are nested under 'any' - attribute.name !== prop; + const nestedAny = + attribute.type === AttributeTypes.any && + // if the name doesn't match that's because we are nested under 'any' + attribute.name !== prop; let field; if (attribute === undefined) { - throw new Error(`Invalid attribute "${prop}" at path "${target.path}.${prop}"`); + throw new Error( + `Invalid attribute "${prop}" at path "${target.path}.${prop}"`, + ); } else if (nestedAny) { field = prop; } else { diff --git a/src/schema.js b/src/schema.js index 26c6e0e3..7396f83a 100644 --- a/src/schema.js +++ b/src/schema.js @@ -8,6 +8,7 @@ const { PathTypes, TableIndex, ItemOperations, + DataOptions, } = require("./types"); const AttributeTypeNames = Object.keys(AttributeTypes); const ValidFacetTypes = [ @@ -466,26 +467,18 @@ class Attribute { if (typeof definition === "function") { return (val) => { try { - let reason = definition(val); - const isValid = !reason; - if (isValid) { - return [isValid, []]; - } else if (typeof reason === "boolean") { - return [ - isValid, - [ - new e.ElectroUserValidationError( - this.path, - "Invalid value provided", - ), - ], - ]; - } else { - return [ - isValid, - [new e.ElectroUserValidationError(this.path, reason)], - ]; - } + let isValid = !!definition(val); + return [ + isValid, + isValid + ? [] + : [ + new e.ElectroUserValidationError( + this.path, + "Invalid value provided", + ), + ], + ]; } catch (err) { return [false, [new e.ElectroUserValidationError(this.path, err)]]; } @@ -1687,14 +1680,13 @@ class Schema { } translateFromFields(item = {}, options = {}) { - let { includeKeys } = options; let data = {}; let names = this.translationForRetrieval; for (let [attr, value] of Object.entries(item)) { let name = names[attr]; if (name) { data[name] = value; - } else if (includeKeys) { + } else if (options.data === DataOptions.includeKeys) { data[attr] = value; } } diff --git a/src/service.js b/src/service.js index 9ae82d15..2b560be1 100644 --- a/src/service.js +++ b/src/service.js @@ -9,11 +9,11 @@ const { TransactionMethods, KeyCasing, ServiceVersions, - Pager, ElectroInstance, ElectroInstanceTypes, ModelVersions, IndexTypes, + DataOptions, } = require("./types"); const { FilterFactory } = require("./filters"); const { FilterOperations } = require("./operations"); @@ -345,7 +345,7 @@ class Service { } cleanseRetrievedData(index = TableIndex, entities, data = {}, config = {}) { - if (config.raw) { + if (config.data === DataOptions.raw) { return data; } const identifiers = getEntityIdentifiers(entities); @@ -462,7 +462,7 @@ class Service { let options = { // expressions, // DynamoDB doesnt return what I expect it would when provided with these entity filters parse: (options, data) => { - if (options.raw) { + if (options.data === DataOptions.raw) { return data; } return this.cleanseRetrievedData(index, entities, data, options); @@ -634,11 +634,11 @@ class Service { if (!scopeMatch) { collectionDifferences.push( - `The index scope value provided "${ - providedIndex.scope || "undefined" - }" does not match established index scope value "${ - definition.scope || "undefined" - }" on index "${providedIndexName}". Index scope options must match across all entities participating in a collection`, + `The index scope value provided "${ + providedIndex.scope || "undefined" + }" does not match established index scope value "${ + definition.scope || "undefined" + }" on index "${providedIndexName}". Index scope options must match across all entities participating in a collection`, ); } diff --git a/src/transaction.js b/src/transaction.js index aba8cb73..7aef39e9 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -1,4 +1,4 @@ -const { TableIndex, TransactionMethods } = require("./types"); +const { TableIndex, TransactionMethods, DataOptions } = require("./types"); const { getEntityIdentifiers, matchToEntityAlias } = require("./entity"); function cleanseCanceledData( @@ -7,7 +7,7 @@ function cleanseCanceledData( data = {}, config = {}, ) { - if (config.raw) { + if (config.data === DataOptions.raw) { return data; } const identifiers = getEntityIdentifiers(entities); @@ -52,7 +52,7 @@ function cleanseTransactionData( data = {}, config = {}, ) { - if (config.raw) { + if (config.data === DataOptions.raw) { return data; } const identifiers = getEntityIdentifiers(entities); @@ -139,7 +139,7 @@ function createTransaction(options) { const response = await driver.go(method, params, { ...options, parse: (options, data) => { - if (options.raw) { + if (options.data === DataOptions.raw) { return data; } else if (data.canceled) { canceled = true; diff --git a/src/types.js b/src/types.js index 410a6a93..52b7a965 100644 --- a/src/types.js +++ b/src/types.js @@ -3,11 +3,23 @@ const KeyTypes = { sk: "sk", }; +const DataOptions = { + raw: "raw", + includeKeys: "includeKeys", + attributes: "attributes", +}; + const BatchWriteTypes = { batch: "batch", concurrent: "concurrent", }; +const ComparisonTypes = { + keys: "keys", + attributes: "attributes", + v2: "v2", +}; + const QueryTypes = { and: "and", gte: "gte", @@ -86,7 +98,7 @@ const Comparisons = { gt: ">", }; -const PartialComparisons = { +const KeyAttributesComparisons = { lt: "<", gte: ">=", @@ -332,6 +344,7 @@ module.exports = { ValueTypes, TableIndex, MethodTypes, + DataOptions, Comparisons, BuilderTypes, ReturnValues, @@ -341,6 +354,7 @@ module.exports = { AttributeTypes, EntityVersions, CastKeyOptions, + ComparisonTypes, ServiceVersions, ExpressionTypes, ElectroInstance, @@ -348,13 +362,13 @@ module.exports = { UnprocessedTypes, AttributeWildCard, TerminalOperation, - PartialComparisons, FormatToReturnValues, AttributeProxySymbol, ElectroInstanceTypes, MethodTypeTranslation, EventSubscriptionTypes, DynamoDBAttributeTypes, + KeyAttributesComparisons, AttributeMutationMethods, AllPages, ResultOrderOption, @@ -363,4 +377,5 @@ module.exports = { TransactionOperations, TransactionMethods, UpsertOperations, + BatchWriteTypes, }; diff --git a/src/validations.js b/src/validations.js index f26b1731..d616ffd3 100644 --- a/src/validations.js +++ b/src/validations.js @@ -122,7 +122,7 @@ const Index = { scope: { type: "string", required: false, - } + }, }, }, sk: { @@ -178,7 +178,7 @@ const Index = { type: "any", required: false, format: "isFunction", - } + }, }, }; diff --git a/src/where.js b/src/where.js index c345b70f..95bb0cc3 100644 --- a/src/where.js +++ b/src/where.js @@ -27,7 +27,8 @@ class FilterExpression extends ExpressionState { return !expression.replace(/\n|\r|\w/g, "").trim(); } - add(newExpression) { + add(newExpression, filterOptions = {}) { + const asPrefix = !!filterOptions.asPrefix; let expression = ""; let existingExpression = this.expression; if ( @@ -39,13 +40,19 @@ class FilterExpression extends ExpressionState { if (isEmpty) { return existingExpression; } - let existingNeedsParens = + + if ( + !asPrefix && !existingExpression.startsWith("(") && - !existingExpression.endsWith(")"); - if (existingNeedsParens) { + !existingExpression.endsWith(")") + ) { existingExpression = `(${existingExpression})`; } - expression = `${existingExpression} AND ${newExpression}`; + if (asPrefix) { + expression = `(${newExpression}) AND ${existingExpression}`; + } else { + expression = `${existingExpression} AND ${newExpression}`; + } } else { expression = this._trim(newExpression); } @@ -53,7 +60,7 @@ class FilterExpression extends ExpressionState { } // applies operations without verifying them against known attributes. Used internally for key conditions. - unsafeSet(operation, name, ...values) { + unsafeSet(filterOptions, operation, name, ...values) { const { template } = FilterOperations[operation] || {}; if (template === undefined) { throw new Error( @@ -68,7 +75,7 @@ class FilterExpression extends ExpressionState { names.prop, ...valueExpressions, ); - this.add(condition); + this.add(condition, filterOptions); } build() { diff --git a/test/connected.batch.spec.js b/test/connected.batch.spec.js index 91ff5b2c..dc3ca467 100644 --- a/test/connected.batch.spec.js +++ b/test/connected.batch.spec.js @@ -62,7 +62,7 @@ let schema = { type: "string", required: true, validate: (date) => - moment(date, "YYYY-MM-DD").isValid() ? "" : "Invalid date format", + moment(date, "YYYY-MM-DD").isValid() }, rent: { type: "string", @@ -510,7 +510,7 @@ describe("BatchGet", () => { response, resultsAll: [], unprocessedAll: [], - config: { includeKeys: true }, + config: { data: 'includeKeys' }, orderMaintainer: { getOrder: () => -1, }, diff --git a/test/connected.crud.spec.js b/test/connected.crud.spec.js index 0f3250c1..f9946c75 100644 --- a/test/connected.crud.spec.js +++ b/test/connected.crud.spec.js @@ -75,7 +75,7 @@ const createModel = (entityName) => { type: "string", required: true, validate: (date) => - moment(date, "YYYY-MM-DD").isValid() ? "" : "Invalid date format", + moment(date, "YYYY-MM-DD").isValid() }, rent: { type: "string", @@ -496,8 +496,6 @@ for (const [clientVersion, client] of [ required: true, validate: (date) => moment(date, "YYYY-MM-DD").isValid() - ? "" - : "Invalid date format", }, rent: { type: "string", @@ -3926,12 +3924,12 @@ for (const [clientVersion, client] of [ let someValue = "ABDEF"; let putRecord = await db .put({ id, date, someValue }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(putRecord).to.deep.equal({}); let getRecord = await db .get({ id, date }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); if (clientVersion === c.DocumentClientVersions.v2) { expect(getRecord).to.deep.equal({ @@ -3951,7 +3949,7 @@ for (const [clientVersion, client] of [ let updateRecord = await db .update({ id, date }) .set({ someValue }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); if (clientVersion === c.DocumentClientVersions.v2) { expect(updateRecord).to.deep.equal({}); @@ -3963,7 +3961,7 @@ for (const [clientVersion, client] of [ let queryRecord = await db.query .record({ id, date }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); if (clientVersion === c.DocumentClientVersions.v2) { expect(queryRecord).to.deep.equal({ @@ -3982,7 +3980,6 @@ for (const [clientVersion, client] of [ ScannedCount: 1, }); } else { - console.log({queryRecord}); expect(queryRecord).to.have.keys([ "Items", "Count", @@ -3992,7 +3989,7 @@ for (const [clientVersion, client] of [ } let recordWithKeys = await db .get({ id, date }) - .go({ includeKeys: true }) + .go({ data: 'includeKeys' }) .then((res) => res.data); expect(recordWithKeys).to.deep.equal({ id, @@ -4362,19 +4359,11 @@ for (const [clientVersion, client] of [ ExpressionAttributeNames: { "#pk": "gsi2pk", "#sk1": "gsi2sk", - "#prop6": "prop6", - "#prop7": "prop7", - "#prop8": "prop8", }, ExpressionAttributeValues: { - ":prop60": record.prop6, - ":prop70": record.prop7, - ":prop80": record.prop8, ":pk": `$test#prop5_${record.prop5}`, ":sk1": `$dummy_1#prop6_${record.prop6}#prop7_${record.prop7}#prop8_${record.prop8}`, }, - FilterExpression: - "(#prop6 = :prop60) AND #prop7 = :prop70 AND #prop8 = :prop80", IndexName: "gsi2pk-gsi2sk-index", }); let beforeUpdate = await Dummy.query @@ -4409,19 +4398,11 @@ for (const [clientVersion, client] of [ ExpressionAttributeNames: { "#pk": "gsi2pk", "#sk1": "gsi2sk", - "#prop6": "prop6", - "#prop7": "prop7", - "#prop8": "prop8", }, ExpressionAttributeValues: { - ":prop60": record.prop6, - ":prop70": record.prop7, - ":prop80": record.prop8, ":pk": `$test#prop5_${prop5}`, ":sk1": `$dummy_1#prop6_${record.prop6}#prop7_${record.prop7}#prop8_${record.prop8}`, }, - FilterExpression: - "(#prop6 = :prop60) AND #prop7 = :prop70 AND #prop8 = :prop80", IndexName: "gsi2pk-gsi2sk-index", }); let afterUpdate = await Dummy.query @@ -4472,19 +4453,11 @@ for (const [clientVersion, client] of [ ExpressionAttributeNames: { "#pk": "gsi2pk", "#sk1": "gsi2sk", - "#prop6": "prop6", - "#prop7": "prop7", - "#prop8": "prop8", }, ExpressionAttributeValues: { - ":prop60": record.prop6, - ":prop70": record.prop7, - ":prop80": record.prop8, ":pk": `$test#prop5_${record.prop5}`, ":sk1": `$dummy_1#prop6_${record.prop6}#prop7_${record.prop7}#prop8_${record.prop8}`, }, - FilterExpression: - "(#prop6 = :prop60) AND #prop7 = :prop70 AND #prop8 = :prop80", IndexName: "gsi2pk-gsi2sk-index", }); let beforeUpdate = await Dummy.query @@ -4519,19 +4492,11 @@ for (const [clientVersion, client] of [ ExpressionAttributeNames: { "#pk": "gsi2pk", "#sk1": "gsi2sk", - "#prop6": "prop6", - "#prop7": "prop7", - "#prop8": "prop8", }, ExpressionAttributeValues: { - ":prop60": record.prop6, - ":prop70": record.prop7, - ":prop80": record.prop8, ":pk": `$test#prop5_${prop5}`, ":sk1": `$dummy_1#prop6_${record.prop6}#prop7_${record.prop7}#prop8_${record.prop8}`, }, - FilterExpression: - "(#prop6 = :prop60) AND #prop7 = :prop70 AND #prop8 = :prop80", IndexName: "gsi2pk-gsi2sk-index", }); let afterUpdate = await Dummy.query @@ -4688,7 +4653,7 @@ for (const [clientVersion, client] of [ // THIS IS A FOOLISH TEST: IT ONLY FULLY WORKS WHEN THE TABLE USED FOR TESTING IS FULL OF RECORDS. THIS WILL HAVE TO DO UNTIL I HAVE TIME TO FIGURE OUT A PROPER WAY MOCK DDB. let MallStores = new Entity(model, { client }); let results = await MallStores.scan - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => [res.cursor, res.data]); expect(results).to.be.an("array").and.have.length(2); // Scan may not return records, dont force a bad test then @@ -4700,7 +4665,7 @@ for (const [clientVersion, client] of [ expect(stores.Items[0]).to.have.a.property("pk"); expect(stores.Items[0]).to.have.a.property("sk"); let [nextIndex, nextStores] = await MallStores.scan - .go({ cursor: index, raw: true }) + .go({ cursor: index, data: 'raw' }) .then((res) => [res.cursor, res.data]); expect(nextIndex).to.not.deep.equal(index); expect(nextStores.Items).to.be.an("array"); @@ -4764,8 +4729,6 @@ for (const [clientVersion, client] of [ required: true, validate: (date) => moment(date, "YYYY-MM-DD").isValid() - ? "" - : "Invalid date format", }, rent: { type: "string", @@ -4883,17 +4846,11 @@ for (const [clientVersion, client] of [ ExpressionAttributeNames: { "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#unitId": "unitId", - "#buildingId": "buildingId", }, ExpressionAttributeValues: { ":pk": "mallz_eastpointe", ":sk1": "b_buildingz#u_g1#s_", - ":buildingId0": "BuildingZ", - ":unitId0": "G1", }, - FilterExpression: - "(#buildingId = :buildingId0) AND #unitId = :unitId0", IndexName: "gsi1pk-gsi1sk-index", }); expect(leasesParams).to.deep.equal({ @@ -4902,17 +4859,11 @@ for (const [clientVersion, client] of [ ExpressionAttributeNames: { "#pk": "gsi2pk", "#sk1": "gsi2sk", - "#leaseEnd": "leaseEnd", - "#storeId": "storeId", }, ExpressionAttributeValues: { ":pk": "m_eastpointe", ":sk1": "l_2020-01-20#s_lattelarrys#b_", - ":leaseEnd0": "2020-01-20", - ":storeId0": "LatteLarrys", }, - FilterExpression: - "(#leaseEnd = :leaseEnd0) AND #storeId = :storeId0", IndexName: "gsi2pk-gsi2sk-index", }); expect(shopParams).to.deep.equal({ @@ -4921,17 +4872,11 @@ for (const [clientVersion, client] of [ ExpressionAttributeNames: { "#pk": "gsi4pk", "#sk1": "gsi4sk", - "#buildingId": "buildingId", - "#mallId": "mallId", }, ExpressionAttributeValues: { - ":buildingId0": "BuildingZ", - ":mallId0": "EastPointe", ":pk": "$facettest_1#store_lattelarrys", ":sk1": `$${ENTITY}#mall_eastpointe#building_buildingz#unit_`, }, - FilterExpression: - "(#mallId = :mallId0) AND #buildingId = :buildingId0", IndexName: "gsi4pk-gsi4sk-index", }); expect(createParams).to.deep.equal({ diff --git a/test/connected.page.spec.js b/test/connected.page.spec.js index 9732da5f..89622797 100644 --- a/test/connected.page.spec.js +++ b/test/connected.page.spec.js @@ -394,59 +394,11 @@ describe("Query Pagination", () => { } while (cursor !== null); }).timeout(10000); - it(`should continue to query until '${limitOption}' option is reached`, async () => { - const ExclusiveStartKey = { key: "hi" }; - const [one, two, three, four, five, six] = tasks.data; - const { client, calls } = createClient({ - mockResponses: [ - { - Items: [one, two, three], - LastEvaluatedKey: ExclusiveStartKey, - }, - { - Items: [four, five, six], - LastEvaluatedKey: ExclusiveStartKey, - }, - { - Items: [], - LastEvaluatedKey: ExclusiveStartKey, - }, - { - Items: [], - LastEvaluatedKey: undefined, - }, - { - Items: [], - LastEvaluatedKey: ExclusiveStartKey, - }, - ], - }); - const pages = 3; - const limit = 5; - const entity = new Tasks(TasksModel, { client, table }); - const results = await entity.query - .task({ task: "my_task" }) - .go({ pages, [limitOption]: limit }) - .then((res) => res.data); - expect(results).to.be.an("array").with.length(limitOption === 'count' ? 5 : 6); - expect(calls).to.have.length(2); - for (let i = 0; i < calls.length; i++) { - const call = calls[i]; - if (i === 0) { - expect(call.ExclusiveStartKey).to.be.undefined; - } else { - expect(call.ExclusiveStartKey.key).to.equal("hi"); - expect(call.ExclusiveStartKey.key === ExclusiveStartKey.key).to.be - .true; - } - } - }); - it(`should throw if '${limitOption}' option is less than one or not a valid number`, async () => { const employee = "employee"; const project = "project"; const message = limitOption === 'limit' - ? "Query option 'limit' must be of type 'number' and greater than zero. - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-limit-option" + ? "Error thrown by DynamoDB client: \"Limit must be greater than or equal to 1\" - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#aws-error" : "Query option 'count' must be of type 'number' and greater than zero. - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-options" const result1 = await service.collections .assignments({ employee }) @@ -464,14 +416,6 @@ describe("Query Pagination", () => { expect(result2.success).to.be.false; expect(result2.message).to.equal(message); - const result3 = await service.collections - .assignments({ employee }) - .go({ [limitOption]: "weasel" }) - .then(() => ({ success: true })) - .catch((err) => ({ success: false, message: err.message })); - expect(result3.success).to.be.false; - expect(result3.message).to.equal(message); - const result4 = await tasks.query .projects({ project }) .go({ [limitOption]: -1 }) @@ -487,14 +431,6 @@ describe("Query Pagination", () => { .catch((err) => ({ success: false, message: err.message })); expect(result5.success).to.be.false; expect(result5.message).to.equal(message); - - const result6 = await tasks.query - .projects({ project }) - .go({ [limitOption]: "weasel" }) - .then(() => ({ success: true })) - .catch((err) => ({ success: false, message: err.message })); - expect(result6.success).to.be.false; - expect(result6.message).to.equal(message); }); } @@ -517,7 +453,7 @@ describe("Query Pagination", () => { expect(limit).to.be.lessThan(occurrences); const results = await tasks.query .projects({ project }) - .go({ limit, raw: true }) + .go({ limit, data: 'raw' }) .then((res) => res.data); expect(results.Items).to.be.an("array").and.have.length(limit); expect(created.calls).to.be.an("array").and.have.length(1); @@ -534,7 +470,7 @@ describe("Query Pagination", () => { let keys = new Set(); let [next, results] = await tasks.query .projects({ project: Tasks.projects[0] }) - .go({ cursor, raw: true, limit }) + .go({ cursor, data: 'raw', limit }) .then((res) => [res.cursor, res.data]); if (next !== null && count > 1) { expect(next).to.have.keys(["sk", "pk", "gsi1sk", "gsi1pk"]); @@ -698,24 +634,24 @@ describe("Query Pagination", () => { const id = uuid(); const queries = [ ['query operation using default execution options', () => entity1.query.record({ id }).go()], - ['query operation with raw flag', () => entity1.query.record({ id }).go({ raw: true })], - ['query operation with includeKeys flag', () => entity1.query.record({ id }).go({ includeKeys: true })], + ['query operation with raw flag', () => entity1.query.record({ id }).go({ data: 'raw' })], + ['query operation with includeKeys flag', () => entity1.query.record({ id }).go({ data: 'includeKeys' })], ['query operation with ignoreOwnership flag', () => entity1.query.record({ id }).go({ ignoreOwnership: true })], // ['scan query using default execution options', () => entity1.scan.go()], - // ['scan query with raw flag', () => entity1.scan.go({ raw: true })], - // ['scan query with includeKeys flag', () => entity1.scan.go({ includeKeys: true })], + // ['scan query with raw flag', () => entity1.scan.go({ data: 'raw' })], + // ['scan query with includeKeys flag', () => entity1.scan.go({ data: 'includeKeys' })], // ['scan query with ignoreOwnership flag', () => entity1.scan.go({ ignoreOwnership: true })], ['match query using default execution options', () => entity1.match({ id }).go()], - ['match query with raw flag', () => entity1.match({ id }).go({ raw: true })], - ['match query with includeKeys flag', () => entity1.match({ id }).go({ includeKeys: true })], + ['match query with raw flag', () => entity1.match({ id }).go({ data: 'raw' })], + ['match query with includeKeys flag', () => entity1.match({ id }).go({ data: 'includeKeys' })], ['match query with ignoreOwnership flag', () => entity1.match({ id }).go({ ignoreOwnership: true })], ['find query using default execution options', () => entity1.find({ id }).go()], - ['find query with raw flag', () => entity1.find({ id }).go({ raw: true })], - ['find query with includeKeys flag', () => entity1.find({ id }).go({ includeKeys: true })], + ['find query with raw flag', () => entity1.find({ id }).go({ data: 'raw' })], + ['find query with includeKeys flag', () => entity1.find({ id }).go({ data: 'includeKeys' })], ['find query with ignoreOwnership flag', () => entity1.find({ id }).go({ ignoreOwnership: true })], ['collection query using default execution options', () => service.collections.test({ id }).go()], - ['collection query with raw flag', () => service.collections.test({ id }).go({ raw: true })], - ['collection query with includeKeys flag', () => service.collections.test({ id }).go({ includeKeys: true })], + ['collection query with raw flag', () => service.collections.test({ id }).go({ data: 'raw' })], + ['collection query with includeKeys flag', () => service.collections.test({ id }).go({ data: 'includeKeys' })], ['collection query with ignoreOwnership flag', () => service.collections.test({ id }).go({ ignoreOwnership: true })], ]; @@ -774,7 +710,7 @@ describe("Query Pagination", () => { // }).timeout(10000); it("Should paginate and return raw results", async () => { - let results = await tasks.scan.go({ raw: true }); + let results = await tasks.scan.go({ data: 'raw' }); expect(results).to.have.keys(["cursor", "data"]); expect(results.data.Items).to.not.be.undefined; expect(results.data.Items).to.be.an("array"); @@ -954,7 +890,7 @@ describe("Query Pagination", () => { }, ], }); - const pages = 3; + const pages = 'all'; const limit = 5; const entity = new Tasks(TasksModel, { client, table }); const results = await entity.query @@ -962,7 +898,7 @@ describe("Query Pagination", () => { .go({ pages, limit }) .then((res) => res.data); expect(results).to.be.an("array").with.length(6); - expect(calls).to.have.length(2); + expect(calls).to.have.length(4); for (let i = 0; i < calls.length; i++) { const call = calls[i]; if (i === 0) { @@ -1120,65 +1056,6 @@ describe("Query Pagination", () => { } }); - it("collection query should continue to query until 'limit' option is reached", async () => { - const ExclusiveStartKey = { key: "hi" }; - const tasks = new Tasks(makeTasksModel(), { client, table }); - const tasks2 = new Tasks(makeTasksModel(), { client, table }); - - await tasks.load(10); - await tasks2.load(10); - const [one, two, three, four, five, six] = tasks.data; - const [seven, eight, nine, ten] = tasks2.data; - const created = createClient({ - mockResponses: [ - { - Items: [one, two, three, seven, eight, nine], - LastEvaluatedKey: ExclusiveStartKey, - }, - { - Items: [four, five, six, ten], - LastEvaluatedKey: ExclusiveStartKey, - }, - { - Items: [], - LastEvaluatedKey: ExclusiveStartKey, - }, - { - Items: [], - LastEvaluatedKey: undefined, - }, - { - Items: [], - LastEvaluatedKey: ExclusiveStartKey, - }, - ], - }); - const service = new Service( - { tasks, tasks2 }, - { client: created.client, table }, - ); - const pages = 3; - const limit = 9; - const employee = "my_employee"; - const results = await service.collections - .assignments({ employee }) - .go({ pages, limit }) - .then((res) => res.data); - expect(results.tasks).to.be.an("array").with.length(6); - expect(results.tasks2).to.be.an("array").with.length(4); - expect(created.calls).to.have.length(2); - for (let i = 0; i < created.calls.length; i++) { - const call = created.calls[i]; - if (i === 0) { - expect(call.ExclusiveStartKey).to.be.undefined; - } else { - expect(call.ExclusiveStartKey.key).to.equal("hi"); - expect(call.ExclusiveStartKey.key === ExclusiveStartKey.key).to.be - .true; - } - } - }); - it("collection query should only count entities belonging to the collection entities to fulfill 'limit' option requirements", async () => { const ExclusiveStartKey = { key: "hi" }; const tasks = new Tasks(makeTasksModel(), { client, table }); @@ -1216,7 +1093,7 @@ describe("Query Pagination", () => { { tasks, tasks2 }, { client: created.client, table }, ); - const pages = 3; + const pages = 'all' // 3; const limit = 6; const employee = "my_employee"; const results = await service.collections @@ -1225,7 +1102,7 @@ describe("Query Pagination", () => { .then((res) => res.data); expect(results.tasks).to.be.an("array").with.length(4); expect(results.tasks2).to.be.an("array").with.length(3); - expect(created.calls).to.have.length(2); + expect(created.calls).to.have.length(4); for (let i = 0; i < created.calls.length; i++) { const call = created.calls[i]; if (i === 0) { diff --git a/test/connected.service.spec.js b/test/connected.service.spec.js index 38418d23..e265cba9 100644 --- a/test/connected.service.spec.js +++ b/test/connected.service.spec.js @@ -1,9 +1,8 @@ const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms)); process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1; -const { Entity, clauses } = require("../src/entity"); +const { Entity } = require("../src/entity"); const { Service } = require("../src/service"); const { expect } = require("chai"); -const moment = require("moment"); const uuid = require("uuid").v4; const DynamoDB = require("aws-sdk/clients/dynamodb"); const table = "electro"; @@ -505,6 +504,11 @@ describe("Service Connected", () => { entityThree: [recordThree], }); + expect(collectionF).to.deep.equal({ + entityTwo: [recordTwo], + entityThree: [recordThree], + }); + expect(collectionG).to.deep.equal({ entityTwo: [recordTwo], }); @@ -898,7 +902,7 @@ describe("Entities with custom identifiers and versions", () => { let collectionA = await service.collections .collectionA({ prop1 }) .where(({ prop2 }, { eq }) => eq(prop2, "prop2Value")) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data) .then((data) => ({ success: true, data })) .catch((err) => ({ success: false, err })); diff --git a/test/connected.update.spec.js b/test/connected.update.spec.js index 5fa7c799..7132bf4b 100644 --- a/test/connected.update.spec.js +++ b/test/connected.update.spec.js @@ -1,6 +1,5 @@ process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1; const { Entity } = require("../src/entity"); -const { Service } = require("../src/service"); const { expect } = require("chai"); const uuid = require("uuid").v4; const moment = require("moment"); @@ -1705,7 +1704,7 @@ describe("Update Item", () => { const itemBefore = await users .get({ username }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(itemBefore).to.deep.equal({ @@ -1758,7 +1757,7 @@ describe("Update Item", () => { const itemAfter = await users .get({ username }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(itemAfter).to.deep.equal({ @@ -1852,7 +1851,7 @@ describe("Update Item", () => { const itemBefore = await users .get({ username }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(itemBefore).to.deep.equal({ @@ -1904,7 +1903,7 @@ describe("Update Item", () => { const itemAfter = await users .get({ username }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(itemAfter).to.deep.equal({ @@ -1999,7 +1998,7 @@ describe("Update Item", () => { const itemBefore = await users .get({ username }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(itemBefore).to.deep.equal({ diff --git a/test/connected.where.spec.js b/test/connected.where.spec.js index 945ad8e1..32e5c89f 100644 --- a/test/connected.where.spec.js +++ b/test/connected.where.spec.js @@ -1,8 +1,7 @@ const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms)); process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1; -const { Entity, clauses } = require("../src/entity"); +const { Entity } = require("../src/entity"); const { expect } = require("chai"); -const moment = require("moment"); const uuidV4 = require("uuid").v4; const DynamoDB = require("aws-sdk/clients/dynamodb"); const client = new DynamoDB.DocumentClient({ @@ -72,7 +71,7 @@ describe("Where Clause Queries", () => { ]; let penRows = []; before(async () => { - let results = await Promise.all( + await Promise.all( animals.map((animal) => { let row = uuidV4(); penRows.push({ pen, row, animal }); @@ -278,7 +277,7 @@ describe("Where Clause Queries", () => { ${name(animal)} = ${value(animal, penRow.animal)} AND ${notExists(dangerous)} `, ) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(results).to.be.empty; let after = await WhereTests.get(penRow) @@ -321,7 +320,7 @@ describe("Where Clause Queries", () => { let doesExist = await WhereTests.patch(penRow) .set({ dangerous: true }) .where(({ dangerous }, { notExists }) => notExists(dangerous)) - .go({ raw: true }) + .go({ data: 'raw' }) .then(() => false) .catch(() => true); expect(doesExist).to.be.true; diff --git a/test/generation/comparisonTests/generate.ts b/test/generation/comparisonTests/generate.ts new file mode 100644 index 00000000..8db6e693 --- /dev/null +++ b/test/generation/comparisonTests/generate.ts @@ -0,0 +1,16 @@ +import { resolve } from 'path'; +import { writeFileSync } from 'fs'; +import { generateInitialConfiguration, generateTestCases } from "./testCaseGenerators"; + +function main() { + const configuration = generateInitialConfiguration(); + const testCases = generateTestCases(configuration); + const withoutEvaluation = testCases.filter((testCase) => testCase.evaluations.length === 0); + if (withoutEvaluation.length > 0) { + console.log(JSON.stringify({ withoutEvaluation }, null, 4)); + throw new Error(`Some did not have evaluations, ${withoutEvaluation.length}`); + } + writeFileSync(resolve(__dirname, './testCases.json'), JSON.stringify(testCases, null, 2)); +} + +main(); \ No newline at end of file diff --git a/test/generation/comparisonTests/inputs.ts b/test/generation/comparisonTests/inputs.ts new file mode 100644 index 00000000..d5eda5d6 --- /dev/null +++ b/test/generation/comparisonTests/inputs.ts @@ -0,0 +1,178 @@ +import { Entity, EntityRecord, ExecutionOptionCompare, Service } from "../../../index"; +import { bool } from "aws-sdk/clients/signer"; + +const table = 'electro'; + +export const Attraction = new Entity({ + model: { + entity: 'attraction', + service: 'comparison', + version: '1' + }, + attributes: { + name: { + type: 'string' + }, + country: { + type: 'string' + }, + state: { + type: 'string' + }, + county: { + type: 'string' + }, + city: { + type: 'string' + }, + zip: { + type: 'string' + } + }, + indexes: { + location: { + pk: { + field: 'pk', + composite: ['country', 'state'] + }, + sk: { + field: 'sk', + composite: ['county', 'city', 'zip', 'name'] + } + }, + clusteredLocation: { + index: 'gsi1pk-gsi1sk-index', + collection: ['clusteredRegion'], + type: 'clustered', + pk: { + field: 'gsi1pk', + composite: ['country', 'state'] + }, + sk: { + field: 'gsi1sk', + composite: ['county', 'city', 'zip', 'name'] + } + }, + isolatedLocation: { + index: 'gsi2pk-gsi2sk-index', + collection: ['isolatedRegion'], + type: 'isolated', + pk: { + field: 'gsi2pk', + composite: ['country', 'state'] + }, + sk: { + field: 'gsi2sk', + composite: ['county', 'city', 'zip', 'name'] + } + } + } +}, { table }); + +export const AttractionService = new Service({ Attraction }); + +type Exhaustive = { + [K in T]: K; +} + +export type AttractionIndexes = keyof typeof Attraction['query']; + +export const indexes: Exhaustive = { + clusteredLocation: 'clusteredLocation', + isolatedLocation: 'isolatedLocation', + location: 'location', +} + +export const indexTypes: Record = { + clusteredLocation: 'clustered', + isolatedLocation: 'isolated', + location: 'isolated', +} + +export const indexHasCollection: Record = { + clusteredLocation: true, + isolatedLocation: true, + location: false, +} + +type AttractionCollections = keyof typeof AttractionService['collections']; + +export const collections: Exhaustive = { + clusteredRegion: 'clusteredRegion', + isolatedRegion: 'isolatedRegion', +} + +export const collectionTypes: Record = { + clusteredRegion: 'clustered', + isolatedRegion: 'isolated', +} + +export const comparisons: Exhaustive = { + attributes: 'attributes', + keys: "keys", + v2: 'v2', +}; + +export type Operators = "" | "gte" | "gt" | "lt" | "lte" | "between" | "begins"; + +export const operators: Exhaustive = { + "": "", + gte: "gte", + gt: "gt", + lt: "lt", + lte: "lte", + between: "between", + begins: "begins", +} + +export const data: EntityRecord = { + country: 'USA', + state: 'Wisconsin', + county: 'Dane', + city: 'Madison', + zip: '53713', + name: 'Veterans Memorial Coliseum', +} + +export const { + country, + county, + state, + city, + zip, + name +} = data; + +export const pks = new Set(['country', 'state']); +export const sks = new Set(['county', 'city', 'zip', 'name']); + +export const operatorParts = { + "": [{country, state, county, city}], + gte: [{country, state}, {county, city}], + gt: [{country, state}, {county, city}], + lt: [{country, state}, {county, city}], + lte: [{country, state}, {county, city}], + between: [{country, state}, {county, city: 'Madison'}, {county, city: 'Marshall'}], + begins: [{country, state}, {county, city}], +} as const; + +export type OperatorParts = typeof operatorParts + +export type ValuesOf = { + [K in keyof T]: T[K] +}[keyof T]; + +export type ConfigurationItem = + { + hasCollection: boolean; + operator: Operators; + parts: ValuesOf; + type: 'clustered' | 'isolated'; + compare?: ExecutionOptionCompare; + } & ({ + target: "Entity"; + index: AttractionIndexes; +} | { + target: "Service"; + collection: AttractionCollections; +}); \ No newline at end of file diff --git a/test/generation/comparisonTests/testCaseGenerators.ts b/test/generation/comparisonTests/testCaseGenerators.ts new file mode 100644 index 00000000..88499c1b --- /dev/null +++ b/test/generation/comparisonTests/testCaseGenerators.ts @@ -0,0 +1,396 @@ +import { expect } from "chai"; +import { v4 as uuid } from "uuid"; +import { QueryInput } from "aws-sdk/clients/dynamodb"; +import { + Attraction, + AttractionService, collections, collectionTypes, + comparisons, + ConfigurationItem, + indexes, indexHasCollection, + indexTypes, operatorParts, + operators, + pks, + sks +} from "./inputs"; + +export type BitShiftEvaluation = { + name: 'bitShift'; + isShifted: boolean; +} + +export type AttributeFiltersPresence = 'all' | 'some' | 'none'; + +export type AttributeFiltersEvaluation = { + name: 'attributeFilters'; + presence: AttributeFiltersPresence; + occurrences: number; + attributes: string[]; +} + +export type FilterExpressionPresentEvaluation = { + name: 'filterExpressionPresent'; + isPresent: boolean; +} + +export type TestEvaluation = + | BitShiftEvaluation + | AttributeFiltersEvaluation + | FilterExpressionPresentEvaluation; + +export type TestEvaluationName = TestEvaluation['name']; + +export const TestEvaluationNames: Record = { + filterExpressionPresent: 'filterExpressionPresent', + attributeFilters: 'attributeFilters', + bitShift: 'bitShift', +} + +export type TestCase = { + input: ConfigurationItem; + output: QueryInput; + evaluations: TestEvaluation[]; + id: string; +}; + +export function generateParams(options: ConfigurationItem): QueryInput { + const [ first, second, third ] = options.parts; + if (options.target === 'Entity') { + if (!options.operator) { + return Attraction.query[options.index](first).params({ compare: options.compare }); + } else if (options.operator === 'between') { + return Attraction.query[options.index](first).between(second ?? {}, third ?? {}).params({ compare: options.compare }); + } else { + return Attraction.query[options.index](first)[options.operator](second ?? {}).params({ compare: options.compare }); + } + } else { + if (options.collection === 'clusteredRegion') { + if (options.operator === '') { + return AttractionService.collections.clusteredRegion(first).params({ compare: options.compare }); + } else if (options.operator === 'between') { + return AttractionService.collections.clusteredRegion(first).between(second ?? {}, third ?? {}).params({ compare: options.compare }) + } else { + return AttractionService.collections.clusteredRegion(first)[options.operator](second ?? {}).params({ compare: options.compare }); + } + } else { + return AttractionService.collections.isolatedRegion(first).params({ compare: options.compare }); + } + } +} + +function countOccurrences(str: string, subStr: string): number { + if (subStr.length === 0) { + return 0; + } + + let count = 0; + let pos = str.indexOf(subStr); + + while (pos !== -1) { + count++; + pos = str.indexOf(subStr, pos + subStr.length); + } + + return count; +} + +function isString(val: unknown): val is string { + return typeof val === 'string' +} + +export const testEvaluations = { + attributeFilters: (testCase: TestCase, evaluation: AttributeFiltersEvaluation, params: QueryInput) => { + let expression = params.FilterExpression || ''; + const uniqueAttributes = new Set(evaluation.attributes); + let attributeCount = uniqueAttributes.size; + let seenCount = 0; + for (const attribute of uniqueAttributes) { + try { + const occurrences = countOccurrences(expression, `#${attribute}`); + expect(occurrences).to.equal(evaluation.occurrences); + if (occurrences > 0) { + seenCount++; + } + } catch(err: any) { + err.message = `${err.message} ${JSON.stringify({evaluation, attribute, params, testCase}, null, 2)}`; + throw err; + } + } + + try { + switch (evaluation.presence) { + case "all": + expect(seenCount).to.equal(evaluation.occurrences === 0 ? 0 : attributeCount); + break; + case "none": + expect(seenCount).to.equal(0); + break; + case "some": + expect(seenCount).to.be.lessThan(attributeCount); + break; + } + } catch(err: any) { + err.message = `${err.message} ${JSON.stringify({evaluation, testCase, params}, null, 2)}`; + throw err; + } + }, + filterExpressionPresent: (testCase: TestCase, evaluation: FilterExpressionPresentEvaluation, params: QueryInput) => { + try { + let expression = params.FilterExpression; + + evaluation.isPresent + ? expect(expression).to.not.be.undefined + : expect(expression).to.be.undefined; + } catch(err: any) { + err.message = `${err.message} ${JSON.stringify({evaluation, testCase, params}, null, 2)}`; + throw err; + } + }, + bitShift: (testCase: TestCase, evaluation: BitShiftEvaluation, params: QueryInput) => { + // madison -> madison + let expectedLastLetter = 'n'; + let attributeName = ':sk1'; + if (testCase.input.compare === 'v2') { + if (['lte', 'gt', 'between'].includes(testCase.input.operator)) { + // madison -> madisoo + expectedLastLetter = 'o'; + if (testCase.input.operator === 'between') { + attributeName = ':sk2'; + // mashall -> mashalm + expectedLastLetter = 'm'; + } + } + } + + let attribute: string | undefined = undefined; + if (params.ExpressionAttributeValues && attributeName in params.ExpressionAttributeValues) { + const value = params.ExpressionAttributeValues[attributeName]; + if (isString(value)) { + attribute = value; + } + } + + try { + if (attribute === undefined) { + throw new Error(`Attribute is not present in ExpressionAttributeNames: ${attributeName}`) + } + const lastLetter: string = attribute[attribute.length - 1]; + const isEqual = lastLetter === expectedLastLetter; + expect(isEqual).to.equal(evaluation.isShifted); + } catch(err: any) { + err.message = `${err.message} ${JSON.stringify({testCase, evaluation, attribute, params}, null, 2)}`; + throw err; + } + }, +} as const; + +export function evaluateTestCase(testCase: TestCase, evaluation: TestEvaluation, params: QueryInput) { + switch(evaluation.name) { + case "filterExpressionPresent": + return testEvaluations.filterExpressionPresent(testCase, evaluation, params); + case "bitShift": + return testEvaluations.bitShift(testCase, evaluation, params); + case "attributeFilters": + return testEvaluations.attributeFilters(testCase, evaluation, params); + default: + throw new Error( + `Unknown evaluation: ${JSON.stringify({ evaluation, params })}`, + ); + } + +} + +export function assertsIsTestEvaluation(maybeEvaluation: unknown): asserts maybeEvaluation is TestEvaluation { + const isTestEvaluation = typeof maybeEvaluation === 'object' && maybeEvaluation && 'name' in maybeEvaluation && typeof maybeEvaluation.name === 'string' && maybeEvaluation.name in TestEvaluationNames; + if (!isTestEvaluation) { + throw new Error('Unknown test evaluation'); + } +} + +export function generateInitialConfiguration() { + const config: ConfigurationItem[] = []; + + for (const compare of Object.values(comparisons)) { + for (const operator of Object.values(operators)) { + for (const index of Object.values(indexes)) { + const type = indexTypes[index]; + const hasCollection = indexHasCollection[index]; + const parts = operatorParts[operator] + config.push({ + hasCollection, + operator, + compare, + target: 'Entity', + parts, + index, + type, + }); + } + + for (const collection of Object.values(collections)) { + const parts = operatorParts[operator]; + const type = collectionTypes[collection]; + if (type === 'clustered') { + config.push({ + hasCollection: true, + target: 'Service', + collection, + operator, + compare, + parts, + type, + }); + } + } + } + + for (const collection of Object.values(collections)) { + const operator = '' as const; + const parts = operatorParts[operator]; + const type = collectionTypes[collection]; + if (type === 'isolated') { + config.push({ + target: 'Service', + hasCollection: true, + collection, + operator, + compare, + parts, + type, + }); + } + } + } + + return config; +} + +export function makeDeterministicHash(config: unknown): string { + switch (typeof config) { + case "undefined": + case "string": + case "boolean": + case "bigint": + case "number": + return `${config}`; + case "symbol": + return `${config.description}`; + case "function": + return `${config.toString()}`; + case "object": + if (!config) { + return 'null'; + } else if (Array.isArray(config)) { + return config.sort().map(c => makeDeterministicHash(c)).join(','); + } + return Object.entries(config) + .sort(([a], [z]) => a.localeCompare(z)) + .map(([key, value]) => { + return `${key}:${makeDeterministicHash(value)}`; + }) + .join('|'); + } +} + +function makeDeterministicTestCaseId(config: ConfigurationItem): string { + const compare = config.compare; + const newConfig: Omit & { compare?: string } = {...config}; + delete newConfig.compare; + return `${compare}::${makeDeterministicHash(newConfig)}` +} + +export function generateTestCases(configuration: ConfigurationItem[]): TestCase[] { + const testCases: TestCase[] = []; + for (const config of configuration) { + const params = generateParams(config); + + testCases.push({ + id: `${makeDeterministicTestCaseId(config)}`, + input: config, + output: params, + evaluations: [], + }); + } + + return testCases.map((testCase): TestCase => { + const allAttributes: string[] = testCase.input.parts.reduce((attrs, obj) => { + return [...attrs, ...Object.keys(obj)]; + }, []); + const pkAttributes = allAttributes.filter((attr) => pks.has(attr)); + const skAttributes = allAttributes.filter((attr) => sks.has(attr)); + if (!testCase.input.compare || testCase.input.compare === 'keys') { + testCase.evaluations.push({ + attributes: allAttributes, + name: 'attributeFilters', + presence: 'none', + occurrences: 0, + }); + + const hasFilters = testCase.input.target === 'Service' || testCase.input.type === 'clustered'; + testCase.evaluations.push({ + name: 'filterExpressionPresent', + isPresent: hasFilters, + }); + + } else if (testCase.input.compare === 'attributes') { + let presence: AttributeFiltersPresence = 'all'; + let occurrences = 1; + if (testCase.input.operator === 'between') { + occurrences = 2; + } else if (testCase.input.operator === 'begins') { + presence = 'none'; + occurrences = 0; + } + if (testCase.input.target === 'Entity') { + testCase.evaluations.push({ + attributes: skAttributes, + name: 'attributeFilters', + presence: presence, + occurrences, + }); + } + + } else if (testCase.input.compare === 'v2') { + const v2ImpactedOperators = ['lte', 'gt', 'between', '']; + const v2ShiftOperators = ['lte', 'gt', 'between']; + const shouldAddSkFilters = v2ImpactedOperators.includes(testCase.input.operator); + const shouldShift = v2ShiftOperators.includes(testCase.input.operator); + + if (testCase.input.target === 'Entity') { + // testCase.evaluations.push({ + // name: 'attributeFilters', + // presence: 'all', + // occurrences: 1, + // attributes: skAttributes, + // }); + + testCase.evaluations.push({ + occurrences: shouldAddSkFilters ? 1 : 0, + attributes: skAttributes, + name: 'attributeFilters', + presence: 'all', + }); + + if (shouldShift) { + testCase.evaluations.push({ + name: 'bitShift', + isShifted: true, + }); + } + } + } + + if ((testCase.input.target === 'Service' || testCase.input.type === 'clustered')) { + const skipFilter = testCase.input.target !== 'Service' && testCase.input.compare === 'v2' && !['', 'gte', 'lt', 'begins', 'lte', 'gt', 'between'].includes(testCase.input.operator); + testCase.evaluations.push({ + occurrences: skipFilter ? 0 : 1, + presence: 'all', + name: 'attributeFilters', + attributes: ['__edb_v__', '__edb_e__'] + }); + } + + return testCase; + }); +} + + diff --git a/test/generation/comparisonTests/testCases.json b/test/generation/comparisonTests/testCases.json new file mode 100644 index 00000000..96b001b7 --- /dev/null +++ b/test/generation/comparisonTests/testCases.json @@ -0,0 +1,4830 @@ +[ + { + "id": "attributes::hasCollection:true|index:clusteredLocation|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison#zip_" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#county = :county0) AND #city = :city0 AND #__edb_e__ = :__edb_e__0 AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:isolatedLocation|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison#zip_" + }, + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#county = :county0) AND #city = :city0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + } + ] + }, + { + "id": "attributes::hasCollection:false|index:location|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison#zip_" + }, + "FilterExpression": "(#county = :county0) AND #city = :city0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + } + ] + }, + { + "id": "attributes::collection:clusteredRegion|hasCollection:true|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "", + "compare": "attributes", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction) AND (#county = :county0) AND #city = :city0" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:clusteredLocation|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "gte", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#city >= :city0) AND (#county >= :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:isolatedLocation|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "gte", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#city >= :city0) AND #county >= :county0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + } + ] + }, + { + "id": "attributes::hasCollection:false|index:location|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "gte", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "FilterExpression": "(#city >= :city0) AND #county >= :county0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + } + ] + }, + { + "id": "attributes::collection:clusteredRegion|hasCollection:true|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "gte", + "compare": "attributes", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction) AND (#city >= :city0) AND #county >= :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:clusteredLocation|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "gt", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#city > :city0) AND (#county > :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:isolatedLocation|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "gt", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#city > :city0) AND #county > :county0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + } + ] + }, + { + "id": "attributes::hasCollection:false|index:location|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "gt", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "FilterExpression": "(#city > :city0) AND #county > :county0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + } + ] + }, + { + "id": "attributes::collection:clusteredRegion|hasCollection:true|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "gt", + "compare": "attributes", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction) AND (#city > :city0) AND #county > :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:clusteredLocation|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "lt", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#city < :city0) AND (#county < :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:isolatedLocation|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "lt", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#city < :city0) AND #county < :county0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + } + ] + }, + { + "id": "attributes::hasCollection:false|index:location|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "lt", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "FilterExpression": "(#city < :city0) AND #county < :county0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + } + ] + }, + { + "id": "attributes::collection:clusteredRegion|hasCollection:true|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "lt", + "compare": "attributes", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction) AND (#city < :city0) AND #county < :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:clusteredLocation|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "lte", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#city <= :city0) AND (#county <= :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:isolatedLocation|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "lte", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + } + ] + }, + { + "id": "attributes::hasCollection:false|index:location|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "lte", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "FilterExpression": "(#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 1 + } + ] + }, + { + "id": "attributes::collection:clusteredRegion|hasCollection:true|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "lte", + "compare": "attributes", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction) AND (#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:clusteredLocation|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "between", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":county0": "Dane", + ":city0": "Marshall", + ":county1": "Dane", + ":city1": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":sk2": "$clusteredregion#county_dane#city_marshall" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#city >= :city1) AND (#county >= :county1) AND (#city <= :city0) AND (#county <= :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 2 + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:isolatedLocation|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "between", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Marshall", + ":county1": "Dane", + ":city1": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison", + ":sk2": "$isolatedregion#attraction_1#county_dane#city_marshall" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#city >= :city1) AND (#county >= :county1) AND (#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 2 + } + ] + }, + { + "id": "attributes::hasCollection:false|index:location|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "between", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Marshall", + ":county1": "Dane", + ":city1": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison", + ":sk2": "$attraction_1#county_dane#city_marshall" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "FilterExpression": "(#city >= :city1) AND (#county >= :county1) AND (#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all", + "occurrences": 2 + } + ] + }, + { + "id": "attributes::collection:clusteredRegion|hasCollection:true|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "between", + "compare": "attributes", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Marshall", + ":county1": "Dane", + ":city1": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":sk2": "$clusteredregion#county_dane#city_marshall", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction) AND (#city >= :city1) AND (#county >= :county1) AND (#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:clusteredLocation|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "begins", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::hasCollection:true|index:isolatedLocation|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "begins", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + } + ] + }, + { + "id": "attributes::hasCollection:false|index:location|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "begins", + "compare": "attributes", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + } + }, + "evaluations": [ + { + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + } + ] + }, + { + "id": "attributes::collection:clusteredRegion|hasCollection:true|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "begins", + "compare": "attributes", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "attributes::collection:isolatedRegion|hasCollection:true|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Service|type:isolated", + "input": { + "target": "Service", + "hasCollection": true, + "collection": "isolatedRegion", + "operator": "", + "compare": "attributes", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:clusteredLocation|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison#zip_" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:isolatedLocation|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison#zip_" + }, + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::hasCollection:false|index:location|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison#zip_" + } + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::collection:clusteredRegion|hasCollection:true|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "", + "compare": "keys", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:clusteredLocation|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "gte", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:isolatedLocation|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "gte", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::hasCollection:false|index:location|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "gte", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::collection:clusteredRegion|hasCollection:true|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "gte", + "compare": "keys", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:clusteredLocation|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "gt", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 > :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:isolatedLocation|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "gt", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 > :sk1", + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::hasCollection:false|index:location|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "gt", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 > :sk1" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::collection:clusteredRegion|hasCollection:true|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "gt", + "compare": "keys", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 > :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:clusteredLocation|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "lt", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:isolatedLocation|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "lt", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::hasCollection:false|index:location|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "lt", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::collection:clusteredRegion|hasCollection:true|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "lt", + "compare": "keys", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:clusteredLocation|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "lte", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 <= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:isolatedLocation|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "lte", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 <= :sk1", + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::hasCollection:false|index:location|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "lte", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 <= :sk1" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::collection:clusteredRegion|hasCollection:true|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "lte", + "compare": "keys", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 <= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:clusteredLocation|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "between", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":sk2": "$clusteredregion#county_dane#city_marshall" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:isolatedLocation|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "between", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison", + ":sk2": "$isolatedregion#attraction_1#county_dane#city_marshall" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::hasCollection:false|index:location|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "between", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison", + ":sk2": "$attraction_1#county_dane#city_marshall" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::collection:clusteredRegion|hasCollection:true|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "between", + "compare": "keys", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":sk2": "$clusteredregion#county_dane#city_marshall", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:clusteredLocation|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "begins", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::hasCollection:true|index:isolatedLocation|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "begins", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::hasCollection:false|index:location|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "begins", + "compare": "keys", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + } + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": false + } + ] + }, + { + "id": "keys::collection:clusteredRegion|hasCollection:true|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "begins", + "compare": "keys", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "keys::collection:isolatedRegion|hasCollection:true|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Service|type:isolated", + "input": { + "target": "Service", + "hasCollection": true, + "collection": "isolatedRegion", + "operator": "", + "compare": "keys", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "attributes": [ + "country", + "state", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "none", + "occurrences": 0 + }, + { + "name": "filterExpressionPresent", + "isPresent": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison#zip_" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#county = :county0) AND #city = :city0 AND #__edb_e__ = :__edb_e__0 AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison#zip_" + }, + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#county = :county0) AND #city = :city0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + } + ] + }, + { + "id": "v2::hasCollection:false|index:location|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison#zip_" + }, + "FilterExpression": "(#county = :county0) AND #city = :city0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + } + ] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "", + "compare": "v2", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction) AND (#county = :county0) AND #city = :city0" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "gte", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "occurrences": 0, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "gte", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [ + { + "occurrences": 0, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + } + ] + }, + { + "id": "v2::hasCollection:false|index:location|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "gte", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1" + }, + "evaluations": [ + { + "occurrences": 0, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + } + ] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "gte", + "compare": "v2", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "gt", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#city > :city0) AND (#county > :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "name": "bitShift", + "isShifted": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "gt", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#city > :city0) AND #county > :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "name": "bitShift", + "isShifted": true + } + ] + }, + { + "id": "v2::hasCollection:false|index:location|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "gt", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "FilterExpression": "(#city > :city0) AND #county > :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "name": "bitShift", + "isShifted": true + } + ] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "gt", + "compare": "v2", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madisoo", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction) AND (#city > :city0) AND #county > :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "lt", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "occurrences": 0, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "lt", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [ + { + "occurrences": 0, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + } + ] + }, + { + "id": "v2::hasCollection:false|index:location|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "lt", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1" + }, + "evaluations": [ + { + "occurrences": 0, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + } + ] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "lt", + "compare": "v2", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "lte", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#city <= :city0) AND (#county <= :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "name": "bitShift", + "isShifted": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "lte", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "name": "bitShift", + "isShifted": true + } + ] + }, + { + "id": "v2::hasCollection:false|index:location|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "lte", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "FilterExpression": "(#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "name": "bitShift", + "isShifted": true + } + ] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "lte", + "compare": "v2", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madisoo", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction) AND (#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "between", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":county0": "Dane", + ":city0": "Marshall", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":sk2": "$clusteredregion#county_dane#city_marshalm" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#city <= :city0) AND (#county <= :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "name": "bitShift", + "isShifted": true + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "between", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Marshall", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison", + ":sk2": "$isolatedregion#attraction_1#county_dane#city_marshalm" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "name": "bitShift", + "isShifted": true + } + ] + }, + { + "id": "v2::hasCollection:false|index:location|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "between", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Marshall", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison", + ":sk2": "$attraction_1#county_dane#city_marshalm" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "FilterExpression": "(#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "attributes": [ + "county", + "city", + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "name": "bitShift", + "isShifted": true + } + ] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "between", + "compare": "v2", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Marshall", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":sk2": "$clusteredregion#county_dane#city_marshalm", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction) AND (#city <= :city0) AND #county <= :county0" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "begins", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [ + { + "occurrences": 0, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + }, + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "begins", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [ + { + "occurrences": 0, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + } + ] + }, + { + "id": "v2::hasCollection:false|index:location|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "begins", + "compare": "v2", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + } + }, + "evaluations": [ + { + "occurrences": 0, + "attributes": [ + "county", + "city" + ], + "name": "attributeFilters", + "presence": "all" + } + ] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "begins", + "compare": "v2", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + }, + { + "id": "v2::collection:isolatedRegion|hasCollection:true|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Service|type:isolated", + "input": { + "target": "Service", + "hasCollection": true, + "collection": "isolatedRegion", + "operator": "", + "compare": "v2", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [ + { + "occurrences": 1, + "presence": "all", + "name": "attributeFilters", + "attributes": [ + "__edb_v__", + "__edb_e__" + ] + } + ] + } +] \ No newline at end of file diff --git a/test/generation/comparisonTests/versionTwoTestCases.json b/test/generation/comparisonTests/versionTwoTestCases.json new file mode 100644 index 00000000..38561406 --- /dev/null +++ b/test/generation/comparisonTests/versionTwoTestCases.json @@ -0,0 +1,1116 @@ +[ + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison#zip_" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#county = :county0) AND #city = :city0 AND #__edb_e__ = :__edb_e__0 AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison#zip_" + }, + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#county = :county0) AND #city = :city0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:false|index:location|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison#zip_" + }, + "FilterExpression": "(#county = :county0) AND #city = :city0" + }, + "evaluations": [] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction) AND (#county = :county0) AND #city = :city0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "gte", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "gte", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:false|index:location|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "gte", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1" + }, + "evaluations": [] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "gte", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "gt", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#county > :county0) AND #city > :city0 AND #__edb_e__ = :__edb_e__0 AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "gt", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#county > :county0) AND #city > :city0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:false|index:location|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "gt", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "FilterExpression": "(#county > :county0) AND #city > :city0" + }, + "evaluations": [] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "gt", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#county > :county0) AND #city > :city0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "lt", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "lt", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:false|index:location|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "lt", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1" + }, + "evaluations": [] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "lt", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "lte", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#county <= :county0) AND #city <= :city0 AND #__edb_e__ = :__edb_e__0 AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "lte", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#county <= :county0) AND #city <= :city0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:false|index:location|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "lte", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "FilterExpression": "(#county <= :county0) AND #city <= :city0" + }, + "evaluations": [] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "lte", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Madison", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madisoo" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#county <= :county0) AND #city <= :city0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "between", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Marshall", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":sk2": "$clusteredregion#county_dane#city_marshalm" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#county <= :county0) AND #city <= :city0 AND #__edb_e__ = :__edb_e__0 AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "between", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Marshall", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison", + ":sk2": "$isolatedregion#attraction_1#county_dane#city_marshalm" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#county <= :county0) AND #city <= :city0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:false|index:location|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "between", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Marshall", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison", + ":sk2": "$attraction_1#county_dane#city_marshalm" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "FilterExpression": "(#county <= :county0) AND #city <= :city0" + }, + "evaluations": [] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "between", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + }, + { + "county": "Dane", + "city": "Marshall" + } + ], + "type": "clustered" + }, + "output": { + "TableName": "electro", + "ExpressionAttributeNames": { + "#county": "county", + "#city": "city", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":county0": "Dane", + ":city0": "Marshall", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":sk2": "$clusteredregion#county_dane#city_marshalm" + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#county <= :county0) AND #city <= :city0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:clusteredLocation|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered", + "input": { + "hasCollection": true, + "operator": "begins", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "clusteredLocation", + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "gsi1pk", + "#sk1": "gsi1sk" + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:true|index:isolatedLocation|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": true, + "operator": "begins", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "isolatedLocation", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion#attraction_1#county_dane#city_madison" + }, + "IndexName": "gsi2pk-gsi2sk-index" + }, + "evaluations": [] + }, + { + "id": "v2::hasCollection:false|index:location|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated", + "input": { + "hasCollection": false, + "operator": "begins", + "target": "Entity", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "index": "location", + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$attraction_1#county_dane#city_madison" + } + }, + "evaluations": [] + }, + { + "id": "v2::collection:clusteredRegion|hasCollection:true|operator:begins|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered", + "input": { + "hasCollection": true, + "target": "Service", + "collection": "clusteredRegion", + "operator": "begins", + "parts": [ + { + "country": "USA", + "state": "Wisconsin" + }, + { + "county": "Dane", + "city": "Madison" + } + ], + "type": "clustered" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi1pk", + "#sk1": "gsi1sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$clusteredregion#county_dane#city_madison", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi1pk-gsi1sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [] + }, + { + "id": "v2::collection:isolatedRegion|hasCollection:true|operator:|parts:city:Madison|country:USA|county:Dane|state:Wisconsin|target:Service|type:isolated", + "input": { + "target": "Service", + "hasCollection": true, + "collection": "isolatedRegion", + "operator": "", + "parts": [ + { + "country": "USA", + "state": "Wisconsin", + "county": "Dane", + "city": "Madison" + } + ], + "type": "isolated" + }, + "output": { + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + "ExpressionAttributeNames": { + "#pk": "gsi2pk", + "#sk1": "gsi2sk", + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$isolatedregion", + ":__edb_e___Attraction": "attraction", + ":__edb_v___Attraction": "1" + }, + "IndexName": "gsi2pk-gsi2sk-index", + "FilterExpression": "(#__edb_e__ = :__edb_e___Attraction AND #__edb_v__ = :__edb_v___Attraction)" + }, + "evaluations": [] + } +] \ No newline at end of file diff --git a/test/offline.batch.spec.js b/test/offline.batch.spec.js index 09008576..8deab7f7 100644 --- a/test/offline.batch.spec.js +++ b/test/offline.batch.spec.js @@ -55,7 +55,7 @@ let schema = { type: "string", required: true, validate: (date) => - moment(date, "YYYY-MM-DD").isValid() ? "" : "Invalid date format", + moment(date, "YYYY-MM-DD").isValid(), }, rent: { type: "string", diff --git a/test/offline.entity.spec.js b/test/offline.entity.spec.js index 472aaa39..56289215 100644 --- a/test/offline.entity.spec.js +++ b/test/offline.entity.spec.js @@ -51,7 +51,7 @@ let schema = { type: "string", required: true, validate: (date) => - moment(date, "YYYY-MM-DD").isValid() ? "" : "Invalid date format", + moment(date, "YYYY-MM-DD").isValid() }, rent: { type: "string", @@ -213,9 +213,9 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk" }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityonf", + ":sk1": "$collectiona#entityone", }, - KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 > :sk1", }, lt: { TableName: "test", @@ -240,9 +240,9 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk" }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityonf", + ":sk1": "$collectiona#entityone", }, - KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 <= :sk1", }, main: { KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -268,7 +268,7 @@ describe("Entity", () => { ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone", - ":sk2": "$collectiona#entityonf", + ":sk2": "$collectiona#entityone", }, KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", @@ -295,15 +295,12 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityone#prop3_val4", - ":prop30": "val3", + ":sk1": "$collectiona#entityone#prop3_val3", }, - FilterExpression: "#prop3 > :prop30", - KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 > :sk1", }, lt: { TableName: "test", @@ -328,15 +325,12 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", }, ExpressionAttributeValues: { - ":prop30": "val3", ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityone#prop3_val4", + ":sk1": "$collectiona#entityone#prop3_val3", }, - FilterExpression: "#prop3 <= :prop30", - KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 <= :sk1", }, main: { KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -361,15 +355,12 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", }, ExpressionAttributeValues: { - ":prop30": "val3", ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_val3", - ":sk2": "$collectiona#entityone#prop3_val4", + ":sk2": "$collectiona#entityone#prop3_val3", }, - FilterExpression: "#prop3 <= :prop30", KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", }, @@ -395,63 +386,48 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", }, ExpressionAttributeValues: { - ":prop30": "val3", ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityone#prop3_val4", - ":prop30": "val3a", - ":prop31": "val3", + ":sk1": "$collectiona#entityone#prop3_val3", }, - FilterExpression: "(#prop3 = :prop30) AND #prop3 > :prop31", - KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 > :sk1", }, lt: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_val3", - ":prop30": "val3a", }, - KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", - FilterExpression: "#prop3 = :prop30", + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1" }, gte: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_val3", - ":prop30": "val3a", }, KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", - FilterExpression: "#prop3 = :prop30", }, lte: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", }, ExpressionAttributeValues: { - ":prop30": "val3a", - ":prop31": "val3", ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityone#prop3_val4", + ":sk1": "$collectiona#entityone#prop3_val3", }, - FilterExpression: "(#prop3 = :prop30) AND #prop3 <= :prop31", - KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 <= :sk1", }, main: { KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -459,14 +435,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_val3a#prop4_", - ":prop30": "val3a", }, - FilterExpression: "#prop3 = :prop30", }, begins: { KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -474,30 +447,23 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_val3", - ":prop30": "val3a", }, - FilterExpression: "#prop3 = :prop30", }, between: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", }, ExpressionAttributeValues: { - ":prop30": "val3a", - ":prop31": "val3", ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_val3", - ":sk2": "$collectiona#entityone#prop3_val4", + ":sk2": "$collectiona#entityone#prop3_val3", }, - FilterExpression: "(#prop3 = :prop30) AND #prop3 <= :prop31", KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", }, @@ -523,64 +489,48 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", - "#prop4": "prop4", }, ExpressionAttributeValues: { - ":prop30": "val3", ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityone#prop3_val3#prop4_val5", - ":prop40": "val4", + ":sk1": "$collectiona#entityone#prop3_val3#prop4_val4", }, - FilterExpression: "(#prop4 = :prop40) AND #prop3 > :prop30", - KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 > :sk1", }, lt: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_val3#prop4_val4", - ":prop40": "val4", }, KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", - FilterExpression: "#prop4 = :prop40", }, gte: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_val3#prop4_val4", - ":prop40": "val4", }, KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", - FilterExpression: "#prop4 = :prop40", }, lte: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", - "#prop4": "prop4", }, ExpressionAttributeValues: { - ":prop30": "val3", ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityone#prop3_val3#prop4_val5", - ":prop40": "val4", + ":sk1": "$collectiona#entityone#prop3_val3#prop4_val4", }, - FilterExpression: "(#prop4 = :prop40) AND #prop3 <= :prop30", - KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 <= :sk1", }, main: { KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -588,14 +538,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_", - ":prop40": "val4", }, - FilterExpression: "#prop4 = :prop40", }, begins: { KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -603,31 +550,23 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_val3#prop4_val4", - ":prop40": "val4", }, - FilterExpression: "#prop4 = :prop40", }, between: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop3": "prop3", - "#prop4": "prop4", }, ExpressionAttributeValues: { - ":prop30": "val3", ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_val3#prop4_val4", - ":sk2": "$collectiona#entityone#prop3_val3#prop4_val5", - ":prop40": "val4", + ":sk2": "$collectiona#entityone#prop3_val3#prop4_val4", }, - FilterExpression: "(#prop4 = :prop40) AND #prop3 <= :prop30", KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", }, @@ -651,60 +590,48 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityonf", - ":prop40": "val4", + ":sk1": "$collectiona#entityone", }, - KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", - FilterExpression: "#prop4 = :prop40", + KeyConditionExpression: "#pk = :pk and #sk1 > :sk1", }, lt: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone", - ":prop40": "val4", }, KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", - FilterExpression: "#prop4 = :prop40", }, gte: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone", - ":prop40": "val4", }, KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", - FilterExpression: "#prop4 = :prop40", }, lte: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityonf", - ":prop40": "val4", + ":sk1": "$collectiona#entityone", }, - KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", - FilterExpression: "#prop4 = :prop40", + KeyConditionExpression: "#pk = :pk and #sk1 <= :sk1", }, main: { KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -712,14 +639,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_", - ":prop40": "val4", }, - FilterExpression: "#prop4 = :prop40", }, begins: { KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -727,29 +651,23 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone", - ":prop40": "val4", }, - FilterExpression: "#prop4 = :prop40", }, between: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone", - ":sk2": "$collectiona#entityonf", - ":prop40": "val4", + ":sk2": "$collectiona#entityone", }, - FilterExpression: "#prop4 = :prop40", KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", }, @@ -775,46 +693,35 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop5": "prop5", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityonf", - ":prop50": "val5", - ":prop40": "val4", + ":sk1": "$collectiona#entityone", }, - FilterExpression: "(#prop4 = :prop40) AND #prop5 > :prop50", - KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 > :sk1", }, lt: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone", - ":prop40": "val4", }, KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", - FilterExpression: "#prop4 = :prop40", }, gte: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone", - ":prop40": "val4", }, - FilterExpression: "#prop4 = :prop40", KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", }, lte: { @@ -822,17 +729,12 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop5": "prop5", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", - ":sk1": "$collectiona#entityonf", - ":prop50": "val5", - ":prop40": "val4", + ":sk1": "$collectiona#entityone", }, - FilterExpression: "(#prop4 = :prop40) AND #prop5 <= :prop50", - KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 <= :sk1", }, main: { KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -840,14 +742,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone#prop3_", - ":prop40": "val4", }, - FilterExpression: "#prop4 = :prop40", }, begins: { KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -855,31 +754,23 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone", - ":prop40": "val4", }, - FilterExpression: "#prop4 = :prop40", }, between: { TableName: "test", ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#prop5": "prop5", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$test_1#prop1_val1#prop2_val2", ":sk1": "$collectiona#entityone", - ":sk2": "$collectiona#entityonf", - ":prop50": "val5", - ":prop40": "val4", + ":sk2": "$collectiona#entityone", }, - FilterExpression: "(#prop4 = :prop40) AND #prop5 <= :prop50", KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", }, @@ -2084,14 +1975,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#buildingId": "buildingId", }, ExpressionAttributeValues: { - ":buildingId0": "BuildingA", ":pk": `$MallStoreDirectory_1#mall_${mall}`.toLowerCase(), ":sk1": `$MallStores#building_${building}#unit_`.toLowerCase(), }, - FilterExpression: "#buildingId = :buildingId0", IndexName: "gsi1pk-gsi1sk-index", TableName: "StoreDirectory", KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -2103,19 +1991,13 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#buildingId": "buildingId", - "#storeId": "storeId", }, ExpressionAttributeValues: { - ":buildingId0": "BuildingA", - ":storeId0": "LatteLarrys", ":pk": `$MallStoreDirectory_1#mall_${mall}`.toLowerCase(), ":sk1": `$MallStores#building_${building}#unit_`.toLowerCase(), }, IndexName: "gsi1pk-gsi1sk-index", TableName: "StoreDirectory", - FilterExpression: - "(#buildingId = :buildingId0) AND #storeId = :storeId0", KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", }); let beingsWithThree = MallStores.query @@ -2125,17 +2007,12 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#buildingId": "buildingId", - "#unitId": "unitId", }, ExpressionAttributeValues: { - ":buildingId0": "BuildingA", - ":unitId0": "B54", ":pk": `$MallStoreDirectory_1#mall_${mall}`.toLowerCase(), ":sk1": `$MallStores#building_${building}#unit_${unit}#store_`.toLowerCase(), }, - FilterExpression: "(#buildingId = :buildingId0) AND #unitId = :unitId0", IndexName: "gsi1pk-gsi1sk-index", TableName: "StoreDirectory", KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", @@ -2147,20 +2024,12 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#buildingId": "buildingId", - "#unitId": "unitId", - "#storeId": "storeId", }, ExpressionAttributeValues: { - ":buildingId0": "BuildingA", - ":unitId0": "B54", - ":storeId0": "LatteLarrys", ":pk": `$MallStoreDirectory_1#mall_${mall}`.toLowerCase(), ":sk1": `$MallStores#building_${building}#unit_${unit}#store_${store}`.toLowerCase(), }, - FilterExpression: - "(#buildingId = :buildingId0) AND #unitId = :unitId0 AND #storeId = :storeId0", IndexName: "gsi1pk-gsi1sk-index", TableName: "StoreDirectory", KeyConditionExpression: "#pk = :pk and #sk1 = :sk1", @@ -2177,18 +2046,12 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#buildingId": "buildingId", - "#unitId": "unitId", }, ExpressionAttributeValues: { - ":unitId0": "B54", - ":buildingId0": "BuildingF", ":pk": `$MallStoreDirectory_1#mall_${mall}`.toLowerCase(), ":sk1": `$MallStores#building_${buildingOne}#unit_B54`.toLowerCase(), - ":sk2": `$MallStores#building_${buildingTwo}#unit_B55`.toLowerCase(), + ":sk2": `$MallStores#building_${buildingTwo}#unit_B54`.toLowerCase(), }, - FilterExpression: - "(#buildingId <= :buildingId0) AND #unitId <= :unitId0", IndexName: "gsi1pk-gsi1sk-index", TableName: "StoreDirectory", KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", @@ -2200,24 +2063,15 @@ describe("Entity", () => { .params(); expect(queryUnitsBetweenTwo).to.deep.equal({ ExpressionAttributeNames: { - "#buildingId": "buildingId", "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#unitId": "unitId", }, ExpressionAttributeValues: { - ":unitId0": "F6", - ":buildingId0": "BuildingA", ":pk": `$MallStoreDirectory_1#mall_${mall}`.toLowerCase(), ":sk1": `$MallStores#building_${building}#unit_${unitOne}`.toLowerCase(), - ":sk2": `$MallStores#building_${building}#unit_${shiftSortOrder( - unitTwo, - 1, - )}`.toLowerCase(), + ":sk2": `$MallStores#building_${building}#unit_${unitTwo}`.toLowerCase(), }, - FilterExpression: - "(#buildingId = :buildingId0) AND #unitId <= :unitId0", IndexName: "gsi1pk-gsi1sk-index", TableName: "StoreDirectory", KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", @@ -2231,21 +2085,12 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#storeId": "storeId", - "#buildingId": "buildingId", }, ExpressionAttributeValues: { - ":storeId0": "LatteLarrys", - ":buildingId0": "BuildingA", ":pk": `$MallStoreDirectory_1#mall_${mall}`.toLowerCase(), ":sk1": `$MallStores#building_${building}`.toLowerCase(), - ":sk2": `$MallStores#building_${shiftSortOrder( - building, - 1, - )}`.toLowerCase(), + ":sk2": `$MallStores#building_${building}`.toLowerCase(), }, - FilterExpression: - "(#buildingId = :buildingId0) AND #storeId <= :storeId0", IndexName: "gsi1pk-gsi1sk-index", TableName: "StoreDirectory", KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", @@ -2260,20 +2105,14 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#buildingId": "buildingId", }, ExpressionAttributeValues: { - ":buildingId0": "BuildingA", ":pk": `$MallStoreDirectory_1#mall_${mall}`.toLowerCase(), - ":sk1": `$MallStores#building_${shiftSortOrder( - building, - 1, - )}`.toLowerCase(), + ":sk1": `$MallStores#building_${building}`.toLowerCase(), }, - FilterExpression: "#buildingId > :buildingId0", IndexName: "gsi1pk-gsi1sk-index", TableName: "StoreDirectory", - KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 > :sk1", }); let queryBegins = MallStores.query @@ -2315,20 +2154,14 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#buildingId": "buildingId", }, ExpressionAttributeValues: { - ":buildingId0": "BuildingA", ":pk": `$MallStoreDirectory_1#mall_${mall}`.toLowerCase(), - ":sk1": `$MallStores#building_${shiftSortOrder( - building, - 1, - )}`.toLowerCase(), + ":sk1": `$MallStores#building_${building}`.toLowerCase(), }, - FilterExpression: "#buildingId <= :buildingId0", IndexName: "gsi1pk-gsi1sk-index", TableName: "StoreDirectory", - KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 <= :sk1", }); let queryUnitsLt = MallStores.query @@ -2617,16 +2450,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "partition_key", "#sk1": "sort_key", - "#mall": "mall", - "#stores": "stores", }, ExpressionAttributeValues: { - ":mall0": "Defg", - ":stores0": 1, ":pk": "$MallStoreDirectory#id_Abcd", ":sk1": "$MYCOLLECTION#MALLSTORES_1#MALL_DEFG#STORES_1", }, - FilterExpression: "(#mall = :mall0) AND #stores = :stores0", }); expect(query2).to.deep.equal({ KeyConditionExpression: "#pk = :pk and #sk1 = :sk1", @@ -2634,13 +2462,8 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "partition_key_idx1", "#sk1": "sort_key_idx1", - "#stores": "stores", - "#id": "id", }, - FilterExpression: "(#id = :id0) AND #stores = :stores0", ExpressionAttributeValues: { - ":id0": "Abcd", - ":stores0": 1, ":pk": "$MALLSTOREDIRECTORY#MALL_DEFG", ":sk1": "$otherCollection#MallStores_1#id_Abcd#stores_1", }, @@ -2945,16 +2768,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "partition_key", "#sk1": "sort_key", - "#mall": "mall", - "#stores": "stores", }, ExpressionAttributeValues: { - ":mall0": "Defg", - ":stores0": 1, ":pk": "mIxEdCaSe#Abcd", ":sk1": "MALL#DEFG#STORES#1", }, - FilterExpression: "(#mall = :mall0) AND #stores = :stores0", }); expect(query2).to.deep.equal({ KeyConditionExpression: "#pk = :pk and #sk1 = :sk1", @@ -2962,16 +2780,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "partition_key_idx1", "#sk1": "sort_key_idx1", - "#id": "id", - "#stores": "stores", }, ExpressionAttributeValues: { - ":id0": "Abcd", - ":stores0": 1, ":pk": "MALL#DEFG", ":sk1": "iD#Abcd#sToReS#1", }, - FilterExpression: "(#id = :id0) AND #stores = :stores0", IndexName: "idx1", }); expect(scanParams).to.deep.equal({ @@ -3350,16 +3163,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "partition_key", "#sk1": "sort_key", - "#mall": "mall", - "#stores": "stores", }, ExpressionAttributeValues: { - ":mall0": "defg", - ":stores0": 1, ":pk": "$mallstoredirectory_1#id_abcd", ":sk1": "$mallstores#mall_defg#stores_1", }, - FilterExpression: "(#mall = :mall0) AND #stores = :stores0", }); expect(query2).to.deep.equal({ KeyConditionExpression: "#pk = :pk and #sk1 = :sk1", @@ -3367,16 +3175,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "partition_key_idx1", "#sk1": "sort_key_idx1", - "#stores": "stores", - "#id": "id", }, ExpressionAttributeValues: { - ":id0": "abcd", - ":stores0": 1, ":pk": "$mallstoredirectory_1#mall_defg", ":sk1": "$mallstores#id_abcd#stores_1", }, - FilterExpression: "(#id = :id0) AND #stores = :stores0", IndexName: "idx1", }); expect(scanParams).to.deep.equal({ @@ -4249,21 +4052,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "pk", "#sk1": "sk", - "#dateTime": "dateTime", - "#prop2": "prop2", - "#prop3": "prop3", - "#prop4": "prop4", }, ExpressionAttributeValues: { - ":dateTime0": data.date, - ":prop20": "PROPERTY2", - ":prop30": "PROPERTY3", - ":prop40": "PROPERTY4", ":pk": "$mallstoredirectory_1#i_identifier#prop1_property1", ":sk1": "$mallstores#d_2020-11-16#prop2_property2#p3_property3", - }, - FilterExpression: - "(#prop4 = :prop40) AND #dateTime = :dateTime0 AND #prop2 = :prop20 AND #prop3 = :prop30", + } }); expect(mixedFacetTemplatesParams).to.deep.equal({ KeyConditionExpression: "#pk = :pk and #sk1 = :sk1", @@ -4271,21 +4064,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#dateTime": "dateTime", - "#prop2": "prop2", - "#prop1": "prop1", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "$mallstoredirectory_1#i_identifier#p3_property3", ":sk1": "2020-11-16#p2_property2#propzduce_property2", - ":prop10": "PROPERTY1", - ":prop20": "PROPERTY2", - ":prop40": "PROPERTY4", - ":dateTime0": data.date, }, - FilterExpression: - "(#prop1 = :prop10) AND #prop4 = :prop40 AND #dateTime = :dateTime0 AND #prop2 = :prop20", IndexName: "gsi1", }); expect(justTemplateParams).to.deep.equal({ @@ -4294,19 +4077,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi2pk", "#sk1": "gsi2sk", - "#dateTime": "dateTime", - "#prop2": "prop2", - "#prop4": "prop4", }, ExpressionAttributeValues: { ":pk": "idz_identifier#property1#third_property3", ":sk1": "2020-11-16|property2", - ":prop20": "PROPERTY2", - ":prop40": "PROPERTY4", - ":dateTime0": data.date, }, - FilterExpression: - "(#prop4 = :prop40) AND #dateTime = :dateTime0 AND #prop2 = :prop20", IndexName: "gsi2", }); expect(moreMixedParams).to.deep.equal({ @@ -4315,19 +4090,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi3pk", "#sk1": "gsi3sk", - "#prop1": "prop1", - "#storeLocationId": "storeLocationId", - "#prop4": "prop4", }, ExpressionAttributeValues: { - ":prop10": "PROPERTY1", - ":prop40": "PROPERTY4", - ":storeLocationId0": "IDENTIFIER", ":pk": "2020-11-16#p2_property2#propz3_property3", ":sk1": "$mallstores#prop1_property1#four_property4", }, - FilterExpression: - "(#storeLocationId = :storeLocationId0) AND #prop1 = :prop10 AND #prop4 = :prop40", IndexName: "gsi3", }); expect(noSkFacetArrayParams).to.deep.equal({ @@ -4992,26 +4759,21 @@ describe("Entity", () => { KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", TableName: "StoreDirectory", ExpressionAttributeNames: { - "#mall": "mall", "#building": "buildingId", + "#mall": "mall", "#unit": "unitId", "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#buildingId": "buildingId", - "#unitId": "unitId", }, ExpressionAttributeValues: { - ":buildingId0": "123", - ":unitId0": "123", - ":mall0": "123", ":building0": "123", + ":mall0": "123", ":unit0": "123", ":pk": "$mallstoredirectory_1#mall_123", ":sk1": "$mallstores#building_123#unit_123#store_", }, + FilterExpression: "#mall = :mall0 AND #building = :building0 AND #unit = :unit0", IndexName: "gsi1pk-gsi1sk-index", - FilterExpression: - "(#buildingId = :buildingId0) AND #unitId = :unitId0 AND #mall = :mall0 AND #building = :building0 AND #unit = :unit0", }); expect(keys).to.be.deep.equal([ { name: "mall", type: "pk" }, @@ -5033,16 +4795,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#unitId": "unitId", - "#buildingId": "buildingId", }, ExpressionAttributeValues: { ":pk": "$mallstoredirectory_1#mall_123", ":sk1": "$mallstores#building_123#unit_123#store_", - ":buildingId0": "123", - ":unitId0": "123", }, - FilterExpression: "(#buildingId = :buildingId0) AND #unitId = :unitId0", IndexName: "gsi1pk-gsi1sk-index", }); expect(keys).to.be.deep.equal([ @@ -5383,20 +5140,18 @@ describe("Entity", () => { "#leaseEnd": "leaseEnd", "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#buildingId": "buildingId", }, ExpressionAttributeValues: { ":rent0": "50.00", ":mall0": "EastPointe", ":leaseEnd0": "20200101", ":leaseEnd1": "20200401", - ":buildingId0": "BuildingA", ":pk": "$MallStoreDirectory_1#mall_EastPointe".toLowerCase(), ":sk1": "$MallStores#building_BuildingA#unit_".toLowerCase(), }, KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", FilterExpression: - "(#buildingId = :buildingId0) AND (#rent >= :rent0 AND #mall = :mall0) OR (#leaseEnd between :leaseEnd0 and :leaseEnd1)", + "(#rent >= :rent0 AND #mall = :mall0) OR (#leaseEnd between :leaseEnd0 and :leaseEnd1)", }); }); @@ -5420,20 +5175,18 @@ describe("Entity", () => { "#leaseEnd": "leaseEnd", "#pk": "gsi1pk", "#sk1": "gsi1sk", - "#buildingId": "buildingId", }, ExpressionAttributeValues: { ":rent0": "50.00", ":mall0": "EastPointe", ":leaseEnd0": "20200101", ":leaseEnd1": "20200401", - ":buildingId0": "BuildingA", ":pk": "$MallStoreDirectory_1#mall_EastPointe".toLowerCase(), ":sk1": "$MallStores#building_BuildingA#unit_".toLowerCase(), }, KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", FilterExpression: - "(#buildingId = :buildingId0) AND (#rent >= :rent0 AND #mall = :mall0) OR (#leaseEnd between :leaseEnd0 and :leaseEnd1)", + "(#rent >= :rent0 AND #mall = :mall0) OR (#leaseEnd between :leaseEnd0 and :leaseEnd1)", }); }); it("Should add filtered fields to the between params", () => { @@ -5458,25 +5211,21 @@ describe("Entity", () => { "#leaseEnd": "leaseEnd", "#pk": "gsi3pk", "#sk1": "gsi3sk", - "#buildingId": "buildingId", - "#category": "category", }, ExpressionAttributeValues: { - ":buildingId0": "BuildingF", ":rent0": "50.00", ":mall0": "EastPointe", ":leaseEnd0": "20200101", ":leaseEnd1": "20200401", - ":category0": "food/coffee", ":pk": "$MallStoreDirectory_1#mall_EastPointe".toLowerCase(), ":sk1": "$MallStores#category_food/coffee#building_BuildingA".toLowerCase(), ":sk2": - "$MallStores#category_food/coffee#building_BuildingG".toLowerCase(), + "$MallStores#category_food/coffee#building_BuildingF".toLowerCase(), }, KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", FilterExpression: - "(#category = :category0) AND #buildingId <= :buildingId0 AND (#rent >= :rent0 AND #mall = :mall0) OR (#leaseEnd between :leaseEnd0 and :leaseEnd1)", + "(#rent >= :rent0 AND #mall = :mall0) OR (#leaseEnd between :leaseEnd0 and :leaseEnd1)", }); }); it("Should add filtered fields to the comparison params", () => { @@ -5489,7 +5238,7 @@ describe("Entity", () => { let leasesAboutToExpire = MallStores.query .leases({ mall }) .lte({ leaseEnd }) - .rentsLeaseEndFilter({ lowRent, beginning, end, location }) + .where(({ rent, leaseEnd, mall }, {gte, eq, between}) => `(${gte(rent, lowRent)} AND ${eq(mall, location)}) OR ${between(leaseEnd, beginning, end)}`) .params(); expect(leasesAboutToExpire).to.be.deep.equal({ @@ -5505,15 +5254,14 @@ describe("Entity", () => { ExpressionAttributeValues: { ":rent0": "50.00", ":mall0": "EastPointe", - ":leaseEnd0": "20201231", - ":leaseEnd1": "20200101", - ":leaseEnd2": "20200401", + ":leaseEnd0": beginning, + ":leaseEnd1": end, ":pk": "$MallStoreDirectory_1#mall_EastPointe".toLowerCase(), - ":sk1": "$MallStores#leaseEnd_20201232".toLowerCase(), + ":sk1": "$MallStores#leaseEnd_20201231".toLowerCase(), }, - KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 <= :sk1", FilterExpression: - "(#leaseEnd <= :leaseEnd0) AND (#rent >= :rent0 AND #mall = :mall0) OR (#leaseEnd between :leaseEnd1 and :leaseEnd2)", + "(#rent >= :rent0 AND #mall = :mall0) OR (#leaseEnd between :leaseEnd0 and :leaseEnd1)", }); }); }); diff --git a/test/offline.options.spec.js b/test/offline.options.spec.js index 62e500d4..2bb8f5f6 100644 --- a/test/offline.options.spec.js +++ b/test/offline.options.spec.js @@ -394,7 +394,7 @@ describe("Query Options", () => { type: "string", required: true, validate: (date) => - moment(date, "YYYY-MM-DD").isValid() ? "" : "Invalid date format", + moment(date, "YYYY-MM-DD").isValid() }, rent: { type: "string", diff --git a/test/offline.validations.spec.js b/test/offline.validations.spec.js index c6833461..70647f34 100644 --- a/test/offline.validations.spec.js +++ b/test/offline.validations.spec.js @@ -229,7 +229,7 @@ describe("Model Validation", () => { ExpressionAttributeValues: { ":pk": "$#id_abc", ":sk1": "$user_1#id_abc", - ":sk2": "$user_1#id_abd", + ":sk2": "$user_1#id_abc", }, KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", }, @@ -253,9 +253,9 @@ describe("Model Validation", () => { }, ExpressionAttributeValues: { ":pk": "$#id_abc", - ":sk1": "$user_1#id_abd", + ":sk1": "$user_1#id_abc", }, - KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 > :sk1", }, queryLte: { TableName: "electro", @@ -265,9 +265,9 @@ describe("Model Validation", () => { }, ExpressionAttributeValues: { ":pk": "$#id_abc", - ":sk1": "$user_1#id_abd", + ":sk1": "$user_1#id_abc", }, - KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 <= :sk1", }, queryLt: { TableName: "electro", @@ -457,7 +457,7 @@ describe("Model Validation", () => { ExpressionAttributeValues: { ":pk": "$#accountid_acct1#id_abc", ":sk1": "$user_1#id_abc", - ":sk2": "$user_1#id_abd", + ":sk2": "$user_1#id_abc", }, KeyConditionExpression: "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", }, @@ -481,9 +481,9 @@ describe("Model Validation", () => { }, ExpressionAttributeValues: { ":pk": "$#accountid_acct1#id_abc", - ":sk1": "$user_1#id_abd", + ":sk1": "$user_1#id_abc", }, - KeyConditionExpression: "#pk = :pk and #sk1 >= :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 > :sk1", }, queryLte: { TableName: "electro", @@ -493,9 +493,9 @@ describe("Model Validation", () => { }, ExpressionAttributeValues: { ":pk": "$#accountid_acct1#id_abc", - ":sk1": "$user_1#id_abd", + ":sk1": "$user_1#id_abc", }, - KeyConditionExpression: "#pk = :pk and #sk1 < :sk1", + KeyConditionExpression: "#pk = :pk and #sk1 <= :sk1", }, queryLt: { TableName: "electro", diff --git a/test/schema.test-d.ts b/test/schema.test-d.ts index 7a3f0b9e..8aa2ddb8 100644 --- a/test/schema.test-d.ts +++ b/test/schema.test-d.ts @@ -266,6 +266,7 @@ const schemaWithCustomAttribute = createSchema({ }, validate: (attr) => { expectType(attr); + return true; }, }), }, diff --git a/test/ts_connected.client.spec.ts b/test/ts_connected.client.spec.ts index 65d6022b..49d842a1 100644 --- a/test/ts_connected.client.spec.ts +++ b/test/ts_connected.client.spec.ts @@ -303,6 +303,7 @@ describe("dynamodb sdk client compatibility", () => { expect(val).to.be.an("array").and.have.length(1); expect(val).to.deep.equal(prop3); called.validate = true; + return true; }, type: "set", items: "string", diff --git a/test/ts_connected.comparisons.spec.ts b/test/ts_connected.comparisons.spec.ts new file mode 100644 index 00000000..ca7e19d9 --- /dev/null +++ b/test/ts_connected.comparisons.spec.ts @@ -0,0 +1,108 @@ +import { expect } from "chai"; +import { + generateParams, + assertsIsTestEvaluation, + TestCase, + evaluateTestCase, +} from "./generation/comparisonTests/testCaseGenerators"; +import testCases from './generation/comparisonTests/testCases.json'; +import v2TestCases from './generation/comparisonTests/versionTwoTestCases.json'; + +const v2TestCaseLookup = v2TestCases.reduce>((lookup, item) => { + lookup[item.id] = item; + return lookup; +}, {}); + +function assertsIsTestCase(item: unknown): asserts item is TestCase { + if (!item) { + throw new Error('Is not test case'); + } +} + +describe('when using the comparison execution option', () => { + for (const testCase of testCases) { + assertsIsTestCase(testCase); + describe(`when executing a comparison query with the ${testCase.input.operator === '' ? 'starts_with' : testCase.input.operator} operator against a ${testCase.input.type} ${testCase.input.target === 'Entity' ? 'index' : 'collection'} with the comparison option ${testCase.input.compare}`, () => { + for (const evaluation of testCase.evaluations) { + it(`should evaluate ${evaluation.name} with the options: ${JSON.stringify(evaluation)}`, () => { + try { + assertsIsTestEvaluation(evaluation); + const params = generateParams(testCase.input); + evaluateTestCase(testCase, evaluation, params); + expect(params).to.deep.equal(testCase.output); + + // if "keys" also try it undefined (because keys is the default) + if (testCase.input.compare === "keys") { + assertsIsTestEvaluation(evaluation); + const defaultParams = generateParams({ ...testCase.input, compare: undefined }); + evaluateTestCase(testCase, evaluation, defaultParams); + expect(defaultParams).to.deep.equal(testCase.output); + expect(defaultParams).to.deep.equal(params); + } + } catch(err: any) { + err.message = `error on test case ${testCase.id}: ${err.message}`; + throw err; + } + }); + } + + if (testCase.input.compare === 'v2') { + // each exception has been verified + const exceptions = { + nowHasIdentifierFilters: [ + 'v2::collection:clusteredRegion|hasCollection:true|operator:gte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered', + 'v2::collection:clusteredRegion|hasCollection:true|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered', + 'v2::collection:clusteredRegion|hasCollection:true|operator:lt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered', + 'v2::collection:clusteredRegion|hasCollection:true|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Service|type:clustered', + 'v2::collection:clusteredRegion|hasCollection:true|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Service|type:clustered', + ], + minorFilterOrderChanges: [ + 'v2::hasCollection:true|index:clusteredLocation|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered', + 'v2::hasCollection:true|index:isolatedLocation|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated', + 'v2::hasCollection:false|index:location|operator:gt|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated', + 'v2::hasCollection:true|index:clusteredLocation|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:clustered', + 'v2::hasCollection:true|index:isolatedLocation|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated', + 'v2::hasCollection:false|index:location|operator:lte|parts:country:USA|state:Wisconsin,city:Madison|county:Dane|target:Entity|type:isolated', + 'v2::hasCollection:true|index:clusteredLocation|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:clustered', + 'v2::hasCollection:true|index:isolatedLocation|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:isolated', + 'v2::hasCollection:false|index:location|operator:between|parts:country:USA|state:Wisconsin,city:Madison|county:Dane,city:Marshall|county:Dane|target:Entity|type:isolated', + ] + } + + it('should result in the same query parameter output as v2 electrodb', () => { + try { + const v3Params = generateParams(testCase.input); + const v2TestCase = v2TestCaseLookup[testCase.id]; + expect(v2TestCase).to.not.be.undefined; + const v2Params = v2TestCase.output; + if (exceptions.nowHasIdentifierFilters.includes(testCase.id)) { + if (v3Params.ExpressionAttributeNames) { + delete v3Params.ExpressionAttributeNames['#__edb_e__'] + delete v3Params.ExpressionAttributeNames['#__edb_v__'] + } + if (v3Params.ExpressionAttributeValues) { + delete v3Params.ExpressionAttributeValues[":__edb_e___Attraction"] + delete v3Params.ExpressionAttributeValues[":__edb_v___Attraction"] + } + if (v3Params.FilterExpression) { + delete v3Params.FilterExpression; + } + delete v2Params.ExpressionAttributeNames['#__edb_e__'] + delete v2Params.ExpressionAttributeNames['#__edb_v__'] + delete v2Params.ExpressionAttributeValues[":__edb_e___Attraction"] + delete v2Params.ExpressionAttributeValues[":__edb_v___Attraction"] + delete v2Params.FilterExpression; + } else if (exceptions.minorFilterOrderChanges.includes(testCase.id)) { + delete v3Params.FilterExpression; + delete v2Params.FilterExpression; + } + expect(v3Params).to.deep.equal(v2Params); + } catch(err: any) { + err.message = `testcase id: ${testCase.id} ${err.message}`; + throw err; + } + }); + } + }); + } +}); \ No newline at end of file diff --git a/test/ts_connected.complex.spec.ts b/test/ts_connected.complex.spec.ts index 66b0c284..5f045bc1 100644 --- a/test/ts_connected.complex.spec.ts +++ b/test/ts_connected.complex.spec.ts @@ -53,7 +53,7 @@ function getEntity(helper: Helper) { default: () => "abc", validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("stringVal", value); @@ -69,7 +69,7 @@ function getEntity(helper: Helper) { default: () => "abc", validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("stringVal2", value); @@ -82,7 +82,7 @@ function getEntity(helper: Helper) { }, enumVal: { type: ["abc", "def"] as const, - validate: (value: "abc" | "def") => undefined, + validate: (value: "abc" | "def") => true, default: () => "abc", get: (value: "abc" | "def") => { helper.triggerGetter("enumVal", value); @@ -97,7 +97,7 @@ function getEntity(helper: Helper) { type: "number", validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, default: () => 123, get: (value) => { @@ -113,7 +113,7 @@ function getEntity(helper: Helper) { type: "boolean", validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, default: () => true, get: (value) => { @@ -130,7 +130,7 @@ function getEntity(helper: Helper) { items: "string", validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("stringSetValue", "items", value); @@ -148,7 +148,7 @@ function getEntity(helper: Helper) { default: "def", validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("stringListValue", "items", value); @@ -162,7 +162,7 @@ function getEntity(helper: Helper) { default: () => { return []; }, - validate: (value: string[]) => undefined, + validate: (value: string[]) => true, get: (value: string[]) => { helper.triggerGetter("stringListValue", value); return value; @@ -178,7 +178,7 @@ function getEntity(helper: Helper) { type: "number", validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, default: 0, get: (value) => { @@ -191,7 +191,7 @@ function getEntity(helper: Helper) { }, }, default: [], - validate: (value: number[]) => undefined, + validate: (value: number[]) => true, get: (value: number[]) => { helper.triggerGetter("numberListValue", value); return value; @@ -211,7 +211,7 @@ function getEntity(helper: Helper) { default: "def", validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("stringVal", value); @@ -227,7 +227,7 @@ function getEntity(helper: Helper) { default: 5, validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("numVal", value); @@ -243,7 +243,7 @@ function getEntity(helper: Helper) { default: false, validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("boolValue", value); @@ -256,7 +256,7 @@ function getEntity(helper: Helper) { }, enumVal: { type: ["abc", "def"] as const, - validate: (value: "abc" | "def") => undefined, + validate: (value: "abc" | "def") => true, default: () => "abc", get: (value: "abc" | "def") => { helper.triggerGetter("enumVal", value); @@ -270,7 +270,7 @@ function getEntity(helper: Helper) { }, validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, default: { numVal: 123, @@ -302,7 +302,7 @@ function getEntity(helper: Helper) { default: () => "abc", validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("stringVal", value); @@ -318,7 +318,7 @@ function getEntity(helper: Helper) { default: () => 10, validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("numVal", value); @@ -334,7 +334,7 @@ function getEntity(helper: Helper) { default: () => false, validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("boolValue", value); @@ -347,7 +347,7 @@ function getEntity(helper: Helper) { }, enumVal: { type: ["abc", "def"] as const, - validate: (value: "abc" | "def") => undefined, + validate: (value: "abc" | "def") => true, default: () => "abc", get: (value: "abc" | "def") => { helper.triggerGetter("enumVal", value); @@ -365,7 +365,7 @@ function getEntity(helper: Helper) { default: "abc", validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("stringListValue", "string", value); @@ -377,7 +377,7 @@ function getEntity(helper: Helper) { }, }, default: ["xyz"], - validate: (value: string[]) => undefined, + validate: (value: string[]) => true, get: (value: string[]) => { helper.triggerGetter("stringListValue", value); return value; @@ -394,7 +394,7 @@ function getEntity(helper: Helper) { default: () => 100, validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("numberListValue", "items", value); @@ -406,7 +406,7 @@ function getEntity(helper: Helper) { }, }, default: [123, 123], - validate: (value: number[]) => undefined, + validate: (value: number[]) => true, get: (value: number[]) => { helper.triggerGetter("mapValue", value); return value; @@ -426,7 +426,7 @@ function getEntity(helper: Helper) { default: "def", validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("stringVal", value); @@ -442,7 +442,7 @@ function getEntity(helper: Helper) { default: 100, validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("numVal", value); @@ -458,7 +458,7 @@ function getEntity(helper: Helper) { default: () => false, validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("boolValue", value); @@ -471,7 +471,7 @@ function getEntity(helper: Helper) { }, enumVal: { type: ["abc", "def"] as const, - validate: (value: "abc" | "def") => undefined, + validate: (value: "abc" | "def") => true, default: () => "abc", get: (value: "abc" | "def") => { helper.triggerGetter("enumVal", value); @@ -486,7 +486,7 @@ function getEntity(helper: Helper) { default: {}, validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("map", "mapListValue", "map", value); @@ -498,7 +498,7 @@ function getEntity(helper: Helper) { }, }, default: [{ stringVal: "xyz" }, {}], - validate: (value: Record[]) => undefined, + validate: (value: Record[]) => true, get: (value: Record[]) => { helper.triggerGetter("map", "mapListValue", value); return value; @@ -512,7 +512,7 @@ function getEntity(helper: Helper) { default: {}, validate: (value) => { helper.triggerValidate(value); - return undefined; + return true; }, get: (value) => { helper.triggerGetter("map", value); @@ -685,7 +685,7 @@ describe("Simple Crud On Complex Entity", () => { type: "string", default: "def", validate: (value) => { - return undefined; + return true; }, get: (value) => { return value; @@ -698,7 +698,7 @@ describe("Simple Crud On Complex Entity", () => { type: "number", default: 5, validate: (value) => { - return undefined; + return true; }, get: (value) => { return value; @@ -711,7 +711,7 @@ describe("Simple Crud On Complex Entity", () => { type: "boolean", default: false, validate: (value) => { - return undefined; + return true; }, get: (value) => { return value; @@ -722,7 +722,7 @@ describe("Simple Crud On Complex Entity", () => { }, enumVal: { type: ["abc", "def"] as const, - validate: (value: "abc" | "def") => undefined, + validate: (value: "abc" | "def") => true, default: () => "abc", get: (value: "abc" | "def") => { return value; @@ -733,7 +733,7 @@ describe("Simple Crud On Complex Entity", () => { }, }, validate: (value) => { - return undefined; + return true; }, default: { stringVal: "abc", diff --git a/test/ts_connected.crud.spec.ts b/test/ts_connected.crud.spec.ts index d84b4e83..d070076e 100644 --- a/test/ts_connected.crud.spec.ts +++ b/test/ts_connected.crud.spec.ts @@ -6,6 +6,7 @@ import { EntityItem, QueryResponse, Service, + createConversions, } from "../index"; import { expect } from "chai"; import { v4 as uuid } from "uuid"; @@ -80,7 +81,7 @@ describe("Entity", () => { type: "string", required: true, validate: (date: string) => - moment(date, "YYYY-MM-DD").isValid() ? "" : "Invalid date format", + moment(date, "YYYY-MM-DD").isValid() }, rent: { type: "string", @@ -1060,7 +1061,7 @@ describe("Entity", () => { type: "string", required: true, validate: (date) => - moment(date, "YYYY-MM-DD").isValid() ? "" : "Invalid date format", + moment(date, "YYYY-MM-DD").isValid() }, rent: { type: "string", @@ -1967,12 +1968,12 @@ describe("Entity", () => { let someValue = "ABDEF"; let putRecord = await db .put({ id, date, someValue }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(putRecord).to.deep.equal({}); let getRecord = await db .get({ id, date }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(getRecord).to.deep.equal({ Item: { @@ -1988,12 +1989,12 @@ describe("Entity", () => { let updateRecord = await db .update({ id, date }) .set({ someValue }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(updateRecord).to.deep.equal({}); let queryRecord = await db.query .record({ id, date }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(queryRecord).to.deep.equal({ Items: [ @@ -2012,7 +2013,7 @@ describe("Entity", () => { }); let recordWithKeys = await db .get({ id, date }) - .go({ includeKeys: true }) + .go({ data: 'includeKeys' }) .then((res) => res.data); expect(recordWithKeys).to.deep.equal({ id, @@ -2357,19 +2358,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi2pk", "#sk1": "gsi2sk", - "#prop6": "prop6", - "#prop7": "prop7", - "#prop8": "prop8", }, ExpressionAttributeValues: { - ":prop60": record.prop6, - ":prop70": record.prop7, - ":prop80": record.prop8, ":pk": `$test#prop5_${record.prop5}`, ":sk1": `$dummy_1#prop6_${record.prop6}#prop7_${record.prop7}#prop8_${record.prop8}`, }, - FilterExpression: - "(#prop6 = :prop60) AND #prop7 = :prop70 AND #prop8 = :prop80", IndexName: "gsi2pk-gsi2sk-index", }); let beforeUpdate = await Dummy.query @@ -2404,19 +2397,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi2pk", "#sk1": "gsi2sk", - "#prop6": "prop6", - "#prop7": "prop7", - "#prop8": "prop8", }, ExpressionAttributeValues: { - ":prop60": record.prop6, - ":prop70": record.prop7, - ":prop80": record.prop8, ":pk": `$test#prop5_${prop5}`, ":sk1": `$dummy_1#prop6_${record.prop6}#prop7_${record.prop7}#prop8_${record.prop8}`, }, - FilterExpression: - "(#prop6 = :prop60) AND #prop7 = :prop70 AND #prop8 = :prop80", IndexName: "gsi2pk-gsi2sk-index", }); let afterUpdate = await Dummy.query @@ -2464,19 +2449,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi2pk", "#sk1": "gsi2sk", - "#prop6": "prop6", - "#prop7": "prop7", - "#prop8": "prop8", }, ExpressionAttributeValues: { ":pk": `$test#prop5_${record.prop5}`, ":sk1": `$dummy_1#prop6_${record.prop6}#prop7_${record.prop7}#prop8_${record.prop8}`, - ":prop60": record.prop6, - ":prop70": record.prop7, - ":prop80": record.prop8, }, - FilterExpression: - "(#prop6 = :prop60) AND #prop7 = :prop70 AND #prop8 = :prop80", IndexName: "gsi2pk-gsi2sk-index", }); let beforeUpdate = await Dummy.query @@ -2510,19 +2487,11 @@ describe("Entity", () => { ExpressionAttributeNames: { "#pk": "gsi2pk", "#sk1": "gsi2sk", - "#prop6": "prop6", - "#prop7": "prop7", - "#prop8": "prop8", }, ExpressionAttributeValues: { ":pk": `$test#prop5_${prop5}`, ":sk1": `$dummy_1#prop6_${record.prop6}#prop7_${record.prop7}#prop8_${record.prop8}`, - ":prop60": record.prop6, - ":prop70": record.prop7, - ":prop80": record.prop8, }, - FilterExpression: - "(#prop6 = :prop60) AND #prop7 = :prop70 AND #prop8 = :prop80", IndexName: "gsi2pk-gsi2sk-index", }); let afterUpdate = await Dummy.query @@ -2729,6 +2698,20 @@ describe("Entity", () => { { client, table: "electro" }, ); + it('should return the correct response deleting an item that does not exist', async () => { + const id = uuid(); + const sub = uuid(); + + const noneResponse = await db.delete({ id, sub }).go({ response: 'none' }); + expect(noneResponse.data).to.be.null; + const allOldResponse = await db.delete({ id, sub }).go({ response: 'all_old' }); + expect(allOldResponse.data).to.be.null; + + const unspecifiedResponse = await db.delete({ id, sub }).go(); + const defaultResponse = await db.delete({ id, sub }).go({ response: 'default' }); + expect(unspecifiedResponse.data).to.deep.equal(defaultResponse.data).and.to.deep.equal({ id, sub }); + }); + for (const method of ["update", "patch"] as const) { it(`should return just the new results when using ${method}`, async () => { const id = uuid(); @@ -4363,7 +4346,7 @@ describe("attributes query option", () => { attr1: item.attr1, attr2: item.attr2, }) - .go({ raw: true, attributes: ["attr2", "attr9", "attr5", "attr10"] }) + .go({ data: 'raw', attributes: ["attr2", "attr9", "attr5", "attr10"] }) .then((res) => res.data); expect(getRaw).to.deep.equal({ Item: { @@ -4378,7 +4361,7 @@ describe("attributes query option", () => { attr1: item.attr1, attr2: item.attr2, }) - .go({ raw: true, attributes: ["attr2", "attr9", "attr5", "attr10"] }) + .go({ data: 'raw', attributes: ["attr2", "attr9", "attr5", "attr10"] }) .then((res) => res.data); expect(queryRawGo).to.deep.equal({ Items: [ @@ -4401,7 +4384,7 @@ describe("attributes query option", () => { }) .go({ cursor: null, - raw: true, + data: 'raw', attributes: ["attr2", "attr9", "attr5", "attr10"], }); @@ -4622,7 +4605,7 @@ describe("attributes query option", () => { }, ]); expect(params.ProjectionExpression).to.equal( - "#attr5, #attr4, #pk, #sk1, #attr2, #prop9, #attr10, #__edb_e__, #__edb_v__", + "#pk, #sk1, #attr2, #prop9, #attr5, #attr10, #__edb_e__, #__edb_v__", ); }); @@ -4668,7 +4651,7 @@ describe("attributes query option", () => { }, ]); expect(params.ProjectionExpression).to.equal( - "#attr5, #attr4, #pk, #sk1, #attr2, #prop9, #attr10, #__edb_e__, #__edb_v__", + "#pk, #sk1, #attr2, #prop9, #attr5, #attr10, #__edb_e__, #__edb_v__", ); }); @@ -5531,6 +5514,8 @@ describe("upsert", () => { expect(typeof put.data.createdAt).to.equal("number"); expect(typeof put.data.updatedAt).to.equal("number"); + await sleep(1); + const upserted = await entity .upsert({ accountNumber, @@ -6359,6 +6344,7 @@ describe("conversion use cases: pagination", () => { }); it("should let you use a entity level toCursor conversion to create a cursor based on the last item returned", async () => { + const conversions = createConversions(entity); const { items, accountId } = await loadItems(); const limit = 10; let iterations = 0; @@ -6370,7 +6356,7 @@ describe("conversion use cases: pagination", () => { .go({ cursor, limit }); results = results.concat(response.data); if (response.data[response.data.length - 1]) { - cursor = entity.conversions.fromComposite.toCursor( + cursor = conversions.fromComposite.toCursor( response.data[response.data.length - 1], ); } else { @@ -6385,6 +6371,7 @@ describe("conversion use cases: pagination", () => { }); it("should let you use the index specific toCursor conversion to create a cursor based on the last item returned", async () => { + const conversions = createConversions(entity); const { items, accountId } = await loadItems(); const limit = 10; let iterations = 0; @@ -6397,7 +6384,7 @@ describe("conversion use cases: pagination", () => { results = results.concat(response.data); if (response.data[response.data.length - 1]) { cursor = - entity.conversions.byAccessPattern.records.fromComposite.toCursor( + conversions.byAccessPattern.records.fromComposite.toCursor( response.data[response.data.length - 1], ); } else { @@ -6411,3 +6398,68 @@ describe("conversion use cases: pagination", () => { ); }); }); + +describe('consistent read', () => { + const entity = new Entity({ + model: { + version: '1', + service: 'tests', + entity: 'consistent-read', + }, + attributes: { + name: { + type: 'string' + }, + type: { + type: 'string' + } + }, + indexes: { + record: { + pk: { + field: 'pk', + composite: ['name'] + }, + sk: { + field: 'sk', + composite: ['type'] + } + } + } + }, { table }); + + const name = 'dr. strangelove'; + const type = 'satire'; + + async function resolves(promise: Promise) { + return promise.then(() => true).catch(() => false); + } + + it('should apply consistent read options to dynamodb parameters to a get request', async () => { + const getParams = entity.get({name, type}).params({consistent: true}); + expect(getParams.ConsistentRead).to.be.true; + // dynamodb does not cry + expect(await resolves(entity.get({name, type}).go({consistent: true}))); + }); + + it('should apply consistent read options to dynamodb parameters to a batchGet request', async () => { + const batchGetParams = entity.get([{ name, type }]).params({consistent: true}); + expect(batchGetParams[0].RequestItems.electro.ConsistentRead).to.be.true; + // dynamodb does not cry + expect(await resolves(entity.get([{ name, type }]).go({consistent: true}))); + }); + + it('should apply consistent read options to dynamodb parameters to a query request', async () => { + const queryParams = entity.query.record({name}).params({consistent: true}); + expect(queryParams.ConsistentRead).to.be.true; + // dynamodb does not cry + expect(await resolves(entity.query.record({name}).go({consistent: true}))); + }); + + it('should apply consistent read options to dynamodb parameters to a scan request', async () => { + const scanParams = entity.scan.params({consistent: true}); + expect(scanParams.ConsistentRead).to.be.true; + // dynamodb does not cry + expect(await resolves(entity.scan.go({consistent: true}))); + }); +}) diff --git a/test/ts_connected.entity.spec.ts b/test/ts_connected.entity.spec.ts index 8ef11edd..697a4e1c 100644 --- a/test/ts_connected.entity.spec.ts +++ b/test/ts_connected.entity.spec.ts @@ -1,5 +1,5 @@ import { DocumentClient, PutItemInput } from "aws-sdk/clients/dynamodb"; -import { Entity, EntityRecord, createWriteTransaction, ElectroEvent } from "../"; +import { Entity, EntityRecord, createWriteTransaction, ElectroEvent, createConversions, Service } from "../"; import { expect } from "chai"; import { v4 as uuid } from "uuid"; const u = require("../src/util"); @@ -233,6 +233,8 @@ describe("conversions", () => { { table, client }, ); + const conversions = createConversions(entity); + const validateMatchingCorrespondence = (options: { label: string; pkComposite: string[]; @@ -281,25 +283,25 @@ describe("conversions", () => { }; const evaluateFromComposite = (item: typeof record) => { - const cursor = entity.conversions.fromComposite.toCursor(item); + const cursor = conversions.fromComposite.toCursor(item); expect(cursor).not.to.be.null; - const keys = entity.conversions.fromComposite.toKeys(item); + const keys = conversions.fromComposite.toKeys(item); expect(keys).not.to.be.null; - const cursorFromKeys = entity.conversions.fromKeys.toCursor(keys!); + const cursorFromKeys = conversions.fromKeys.toCursor(keys!); expect(cursorFromKeys).not.to.be.null; expect(cursor).to.equal(cursorFromKeys); - const keysFromCursor = entity.conversions.fromCursor.toKeys(cursor!); + const keysFromCursor = conversions.fromCursor.toKeys(cursor!); expect(keysFromCursor).not.to.be.null; - const compositeFromCursor = entity.conversions.fromCursor.toComposite( + const compositeFromCursor = conversions.fromCursor.toComposite( cursor!, ); expect(compositeFromCursor).not.to.be.null; - const compositeFromKeys = entity.conversions.fromKeys.toComposite(keys!); + const compositeFromKeys = conversions.fromKeys.toComposite(keys!); expect(compositeFromKeys).not.to.be.null; expect(keys).to.deep.equal(keysFromCursor); @@ -346,35 +348,37 @@ describe("conversions", () => { } }; + // const conversions = createConversions(entity); + const evaluateFromKeys = (keys: any) => { - const item = entity.conversions.fromKeys.toComposite(keys); + const item = conversions.fromKeys.toComposite(keys); if (!item) { throw new Error("Item not defined!"); } // @ts-ignore - const cursor = entity.conversions.fromComposite.toCursor(item); + const cursor = conversions.fromComposite.toCursor(item); expect(cursor).not.to.be.null; - const keysFromCursor = entity.conversions.fromCursor.toKeys(cursor!); + const keysFromCursor = conversions.fromCursor.toKeys(cursor!); expect(keysFromCursor).not.to.be.null; - const cursorFromKeys = entity.conversions.fromKeys.toCursor( + const cursorFromKeys = conversions.fromKeys.toCursor( keysFromCursor!, ); expect(cursorFromKeys).not.to.be.null; expect(cursor).to.equal(cursorFromKeys); - const compositeFromCursor = entity.conversions.fromCursor.toComposite( + const compositeFromCursor = conversions.fromCursor.toComposite( cursor!, ); expect(compositeFromCursor).not.to.be.null; // @ts-ignore - const keysFromComposite = entity.conversions.fromComposite.toKeys(item); + const keysFromComposite = conversions.fromComposite.toKeys(item); expect(keysFromComposite).not.to.be.null; expect(keysFromCursor).to.deep.equal(keysFromComposite); - const compositeFromKeys = entity.conversions.fromKeys.toComposite( + const compositeFromKeys = conversions.fromKeys.toComposite( keysFromComposite!, ); @@ -428,34 +432,34 @@ describe("conversions", () => { item: typeof record, ) => { const cursor = - entity.conversions.byAccessPattern[accessPattern].fromComposite.toCursor( + conversions.byAccessPattern[accessPattern].fromComposite.toCursor( item, ); expect(cursor).not.to.be.null; const keys = - entity.conversions.byAccessPattern[accessPattern].fromComposite.toKeys( + conversions.byAccessPattern[accessPattern].fromComposite.toKeys( item, ); expect(keys).not.to.be.null; - const cursorFromKeys = entity.conversions.byAccessPattern[ + const cursorFromKeys = conversions.byAccessPattern[ accessPattern ].fromKeys.toCursor(keys!); expect(cursorFromKeys).not.to.be.null; expect(cursor).to.equal(cursorFromKeys); - const keysFromCursor = entity.conversions.byAccessPattern[ + const keysFromCursor = conversions.byAccessPattern[ accessPattern ].fromCursor.toKeys(cursor!); expect(keysFromCursor).not.to.be.null; - const compositeFromCursor = entity.conversions.byAccessPattern[ + const compositeFromCursor = conversions.byAccessPattern[ accessPattern ].fromCursor.toComposite(cursor!); expect(compositeFromCursor).not.to.be.null; - const compositeFromKeys = entity.conversions.byAccessPattern[ + const compositeFromKeys = conversions.byAccessPattern[ accessPattern ].fromKeys.toComposite(keys!); expect(compositeFromKeys).not.to.be.null; @@ -507,39 +511,39 @@ describe("conversions", () => { keys: any, ) => { const item = - entity.conversions.byAccessPattern[accessPattern].fromKeys.toComposite( + conversions.byAccessPattern[accessPattern].fromKeys.toComposite( keys, ); if (!item) { throw new Error("Item not defined!"); } // @ts-ignore - const cursor = entity.conversions.byAccessPattern[accessPattern].fromComposite.toCursor(item); + const cursor = conversions.byAccessPattern[accessPattern].fromComposite.toCursor(item); expect(cursor).not.to.be.null; - const keysFromCursor = entity.conversions.byAccessPattern[ + const keysFromCursor = conversions.byAccessPattern[ accessPattern ].fromCursor.toKeys(cursor!); expect(keysFromCursor).not.to.be.null; - const cursorFromKeys = entity.conversions.byAccessPattern[ + const cursorFromKeys = conversions.byAccessPattern[ accessPattern ].fromKeys.toCursor(keysFromCursor!); expect(cursorFromKeys).not.to.be.null; expect(cursor).to.equal(cursorFromKeys); - const compositeFromCursor = entity.conversions.byAccessPattern[ + const compositeFromCursor = conversions.byAccessPattern[ accessPattern ].fromCursor.toComposite(cursor!); expect(compositeFromCursor).not.to.be.null; // @ts-ignore - const keysFromComposite =entity.conversions.byAccessPattern[accessPattern].fromComposite.toKeys(item); + const keysFromComposite =conversions.byAccessPattern[accessPattern].fromComposite.toKeys(item); expect(keysFromComposite).not.to.be.null; expect(keysFromCursor).to.deep.equal(keysFromComposite); expect(Object.entries(compositeFromCursor!).length).to.be.greaterThan(0); - const compositeFromKeys = entity.conversions.byAccessPattern[ + const compositeFromKeys = conversions.byAccessPattern[ accessPattern ].fromKeys.toComposite(keysFromComposite!); expect(!!compositeFromKeys).to.be.true; @@ -672,6 +676,8 @@ describe("conversions", () => { { table }, ); + const conversions = createConversions(entity); + function getConversion( target: ConversionTest["target"], strict?: "all" | "pk" | "none", @@ -679,19 +685,19 @@ describe("conversions", () => { switch (target) { case "byCategory": return (composite: any) => - entity.conversions.byAccessPattern.byCategory.fromComposite.toKeys( + conversions.byAccessPattern.byCategory.fromComposite.toKeys( composite, { strict }, ); case "byOrganization": return (composite: any) => - entity.conversions.byAccessPattern.byOrganization.fromComposite.toKeys( + conversions.byAccessPattern.byOrganization.fromComposite.toKeys( composite, { strict }, ); case "top": return (composite: any) => - entity.conversions.fromComposite.toKeys(composite, { strict }); + conversions.fromComposite.toKeys(composite, { strict }); default: throw new Error(`Unknown target: "${target}"`); } @@ -2724,24 +2730,26 @@ describe('index scope', () => { const batchPutParams = withScope.put([{prop1: 'abc', prop2: 'def'}]).params(); expect(batchPutParams[0].RequestItems.electro[0].PutRequest.Item.pk).to.equal('$test_scope1#prop1_abc'); - const keys = withScope.conversions.fromComposite.toKeys({prop1: 'abc', prop2: 'def'}); + const conversions = createConversions(withScope); + + const keys = conversions.fromComposite.toKeys({prop1: 'abc', prop2: 'def'}); expect(keys.pk).to.equal('$test_scope1#prop1_abc'); expect(keys.gsi1pk).to.equal('$test_scope2#prop2_def'); - const keysComposite = withScope.conversions.fromKeys.toComposite(keys); + const keysComposite = conversions.fromKeys.toComposite(keys); expect(keysComposite).to.deep.equal({prop1: 'abc', prop2: 'def'}); - const indexKeys = withScope.conversions.byAccessPattern.test.fromComposite.toKeys({prop1: 'abc', prop2: 'def'}); + const indexKeys = conversions.byAccessPattern.test.fromComposite.toKeys({prop1: 'abc', prop2: 'def'}); expect(indexKeys.pk).to.equal('$test_scope1#prop1_abc'); - const indexKeysComposite = withScope.conversions.byAccessPattern.test.fromKeys.toComposite(indexKeys); + const indexKeysComposite = conversions.byAccessPattern.test.fromKeys.toComposite(indexKeys); expect(indexKeysComposite).to.deep.equal({prop1: 'abc', prop2: 'def'}); - const reverseKeys = withScope.conversions.byAccessPattern.reverse.fromComposite.toKeys({prop1: 'abc', prop2: 'def'}); + const reverseKeys = conversions.byAccessPattern.reverse.fromComposite.toKeys({prop1: 'abc', prop2: 'def'}); expect(reverseKeys.gsi1pk).to.equal('$test_scope2#prop2_def'); expect(keys.pk).to.equal('$test_scope1#prop1_abc'); - const reverseKeysComposite = withScope.conversions.byAccessPattern.reverse.fromKeys.toComposite(reverseKeys); + const reverseKeysComposite = conversions.byAccessPattern.reverse.fromKeys.toComposite(reverseKeys); expect(reverseKeysComposite).to.deep.equal({prop1: 'abc', prop2: 'def'}); }); @@ -4915,4 +4923,526 @@ describe("index condition", () => { }); }); +describe('execution option compare', () => { + const Attraction = new Entity({ + model: { + entity: 'attraction', + service: 'comparison', + version: '1' + }, + attributes: { + name: { + type: 'string' + }, + country: { + type: 'string' + }, + state: { + type: 'string' + }, + county: { + type: 'string' + }, + city: { + type: 'string' + }, + zip: { + type: 'string' + } + }, + indexes: { + location: { + collection: ['inRegion'], + type: 'clustered', + pk: { + field: 'pk', + composite: ['country', 'state'] + }, + sk: { + field: 'sk', + composite: ['county', 'city', 'zip', 'name'] + } + } + } + }, { table }); + + describe('when using compare option keys', () => { + const country = 'USA'; + const state = 'Wisconsin'; + const county = 'Dane'; + const city = 'Madison'; + const zip = '53713'; + const name = 'Veterans Memorial Coliseum'; + + it('should impact entity parameters that when using gt', () => { + const defaultParams = Attraction.query.location({country, state}).gt({county, city}).params(); + const keyParams = Attraction.query.location({country, state}).gt({county, city}).params({ compare: 'keys' }); + const attributeParams = Attraction.query.location({country, state}).gt({county, city}).params({ compare: 'attributes' }); + const v2Params = Attraction.query.location({country, state}).gt({county, city}).params({ compare: 'v2' }); + expect(defaultParams).to.deep.equal(keyParams); + expect(keyParams).to.deep.equal({ + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + }, + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 > :sk1" + }); + expect(attributeParams).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#city": "city", + "#county": "county", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":city0": "Madison", + ":county0": "Dane", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + }, + "FilterExpression": "(#city > :city0) AND (#county > :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "TableName": "electro" + }); + expect(v2Params).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#city": "city", + "#county": "county", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":city0": "Madison", + ":county0": "Dane", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madisoo", + }, + "FilterExpression": "(#city > :city0) AND (#county > :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "TableName": "electro" + }); + }); + + it('should impact entity parameters when using lte', () => { + const defaultParams = Attraction.query.location({country, state}).lte({county, city}).params(); + const keyParams = Attraction.query.location({country, state}).lte({county, city}).params({ compare: 'keys' }); + const attributeParams = Attraction.query.location({country, state}).lte({county, city}).params({ compare: 'attributes' }); + const v2Params = Attraction.query.location({country, state}).lte({county, city}).params({ compare: 'v2' }); + expect(defaultParams).to.deep.equal(keyParams); + expect(keyParams).to.deep.equal({ + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "pk", + "#sk1": "sk" + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + }, + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 <= :sk1" + }); + expect(attributeParams).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#city": "city", + "#county": "county", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":city0": "Madison", + ":county0": "Dane", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + }, + "FilterExpression": "(#city <= :city0) AND (#county <= :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "TableName": "electro" + }); + expect(v2Params).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#city": "city", + "#county": "county", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":city0": "Madison", + ":county0": "Dane", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madisoo", + }, + "FilterExpression": "(#city <= :city0) AND (#county <= :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "TableName": "electro", + }); + }); + + it('should impact entity parameters when using between', () => { + const defaultParams = Attraction.query.location({country, state}).between( + { county, city: "Madison" }, + { county, city: "Marshall" }, + ).params(); + const keyParams = Attraction.query.location({country, state}).between( + { county, city: "Madison" }, + { county, city: "Marshall" }, + ).params({ compare: 'keys' }); + const attributeParams = Attraction.query.location({country, state}).between( + { county, city: "Madison" }, + { county, city: "Marshall" }, + ).params({ compare: 'attributes' }); + const v2Params = Attraction.query.location({country, state}).between( + { county, city: "Madison" }, + { county, city: "Marshall" }, + ).params({ compare: 'v2' }); + expect(defaultParams).to.deep.equal(keyParams); + expect(keyParams).to.deep.equal({ + "TableName": "electro", + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "pk", + "#sk1": "sk" + }, + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + ":sk2": "$inregion#county_dane#city_marshall", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + }, + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2" + }); + expect(attributeParams).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#city": "city", + "#county": "county", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":city0": "Marshall", + ":city1": "Madison", + ":county0": "Dane", + ":county1": "Dane", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + ":sk2": "$inregion#county_dane#city_marshall" + }, + "FilterExpression": "(#city >= :city1) AND (#county >= :county1) AND (#city <= :city0) AND (#county <= :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "TableName": "electro" + }); + expect(v2Params).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#city": "city", + "#county": "county", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":city0": "Marshall", + ":county0": "Dane", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + ":sk2": "$inregion#county_dane#city_marshalm", + }, + "FilterExpression": "(#city <= :city0) AND (#county <= :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2", + "TableName": "electro", + }); + }); + + it('should impact entity parameters for gte', () => { + const defaultParameters = Attraction.query.location({country, state}).gte({county, city}).params() + const keysParams = Attraction.query.location({country, state}).gte({county, city}).params({ compare: 'keys' }); + const attributesParams = Attraction.query.location({country, state}).gte({county, city}).params({ compare: 'attributes' }); + const v2Params = Attraction.query.location({country, state}).gte({county, city}).params({ compare: 'v2' }); + expect(defaultParameters).to.deep.equal(keysParams); + expect(keysParams).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + }, + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "TableName": "electro" + }); + expect(attributesParams).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#city": "city", + "#county": "county", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":city0": "Madison", + ":county0": "Dane", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + }, + "FilterExpression": "(#city >= :city0) AND (#county >= :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "TableName": "electro" + }); + expect(v2Params).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + }, + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 >= :sk1", + "TableName": "electro", + }); + }); + + it('should impact entity parameters for lt', () => { + const defaultParameters = Attraction.query.location({country, state}).lt({county, city}).params(); + const keysParams = Attraction.query.location({country, state}).lt({county, city}).params({ compare: 'keys' }); + const attributesParams = Attraction.query.location({country, state}).lt({county, city}).params({ compare: 'attributes' }); + const v2Params = Attraction.query.location({country, state}).lt({county, city}).params({ compare: 'v2' }); + expect(defaultParameters).to.deep.equal(keysParams); + expect(keysParams).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + }, + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "TableName": "electro" + }); + expect(attributesParams).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#city": "city", + "#county": "county", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":city0": "Madison", + ":county0": "Dane", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + }, + "FilterExpression": "(#city < :city0) AND (#county < :county0) AND (#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "TableName": "electro", + }); + expect(v2Params).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + }, + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and #sk1 < :sk1", + "TableName": "electro" + }); + }); + + it('should impact entity parameters for begins', () => { + const defaultParameters = Attraction.query.location({country, state}).begins({county, city}).params(); + const keysParams = Attraction.query.location({country, state}).begins({county, city}).params({ compare: 'keys' }); + const attributesParams = Attraction.query.location({country, state}).begins({county, city}).params({ compare: 'attributes' }); + const v2Params = Attraction.query.location({country, state}).begins({county, city}).params({ compare: 'v2' }); + expect(defaultParameters).to.deep.equal(keysParams); + expect(keysParams).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + }, + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro" + }); + expect(attributesParams).to.deep.equal(keysParams); + expect(attributesParams).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + }, + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro", + }); + expect(v2Params).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison", + }, + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro" + }); + }); + it('should impact entity parameters for implicit begins', () => { + const defaultParameters = Attraction.query.location({country, state, county, city}).params(); + const keysParams = Attraction.query.location({country, state, county, city}).params({ compare: 'keys' }); + const attributesParams = Attraction.query.location({country, state, county, city}).params({ compare: 'attributes' }); + const v2Params = Attraction.query.location({country, state, county, city}).params({ compare: 'v2' }); + expect(defaultParameters).to.deep.equal(keysParams); + expect(keysParams).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison#zip_", + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + }, + "FilterExpression": "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro" + }); + expect(attributesParams).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#city": "city", + "#county": "county", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":city0": "Madison", + ":county0": "Dane", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison#zip_", + }, + "FilterExpression": "(#county = :county0) AND #city = :city0 AND #__edb_e__ = :__edb_e__0 AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro" + }); + expect(v2Params).to.deep.equal({ + "ExpressionAttributeNames": { + "#__edb_e__": "__edb_e__", + "#__edb_v__": "__edb_v__", + "#city": "city", + "#county": "county", + "#pk": "pk", + "#sk1": "sk", + }, + "ExpressionAttributeValues": { + ":__edb_e__0": "attraction", + ":__edb_v__0": "1", + ":city0": "Madison", + ":county0": "Dane", + ":pk": "$comparison#country_usa#state_wisconsin", + ":sk1": "$inregion#county_dane#city_madison#zip_" + }, + "FilterExpression": "(#county = :county0) AND #city = :city0 AND #__edb_e__ = :__edb_e__0 AND #__edb_v__ = :__edb_v__0", + "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)", + "TableName": "electro" + }); + }); + }); +}) diff --git a/test/ts_connected.service.spec.ts b/test/ts_connected.service.spec.ts index c7cc7c72..1a20c33a 100644 --- a/test/ts_connected.service.spec.ts +++ b/test/ts_connected.service.spec.ts @@ -1020,7 +1020,7 @@ describe("index types and operations", () => { .primaryCollection({ prop1: keys.isolatedPrimaryPkKey, }) - .go(); + .go({ compare: 'v2' }); expect(isolatedPrimaryCollectionData.cursor).to.be.null; compareItems( @@ -1037,7 +1037,7 @@ describe("index types and operations", () => { .secondaryCollection({ prop4: keys.isolatedSecondaryPkKey, }) - .go(); + .go({ compare: 'v2' }); expect(isolatedSecondaryCollectionData.cursor).to.be.null; compareItems( @@ -1054,7 +1054,7 @@ describe("index types and operations", () => { .primaryCollection({ prop1: keys.clusteredPrimaryPkKey, }) - .go(); + .go({ compare: 'v2' }); expect(clusteredPrimaryCollectionData.cursor).to.be.null; compareItems( @@ -1071,7 +1071,7 @@ describe("index types and operations", () => { .secondaryCollection({ prop4: keys.clusteredSecondaryPkKey, }) - .go(); + .go({ compare: 'v2' }); expect(clusteredSecondaryCollectionData.cursor).to.be.null; compareItems( @@ -1092,7 +1092,7 @@ describe("index types and operations", () => { prop1: keys.clusteredPrimaryPkKey, prop2: 5, }) - .go(); + .go({ compare: 'v2' }); expect(clusteredPrimaryCollectionPartialSKData.cursor).to.be.null; compareItems(clusteredPrimaryCollectionPartialSKData.data.entity1, [ @@ -1108,7 +1108,7 @@ describe("index types and operations", () => { prop4: keys.clusteredSecondaryPkKey, prop5: 5, }) - .go(); + .go({ compare: 'v2' }); expect(clusteredSecondaryCollectionPartialSKData.cursor).to.be.null; compareItems(clusteredSecondaryCollectionPartialSKData.data.entity1, [ @@ -1166,23 +1166,23 @@ describe("index types and operations", () => { case "between": return clusteredPrimaryCollectionPartialSKOperation .between({ prop2: first }, { prop2: last }) - .go(); + .go({ compare: 'v2' }); case "gte": return clusteredPrimaryCollectionPartialSKOperation .gte({ prop2: first }) - .go(); + .go({ compare: 'v2' }); case "gt": return clusteredPrimaryCollectionPartialSKOperation .gt({ prop2: first }) - .go(); + .go({ compare: 'v2' }); case "lte": return clusteredPrimaryCollectionPartialSKOperation .lte({ prop2: first }) - .go(); + .go({ compare: 'v2' }); case "lt": return clusteredPrimaryCollectionPartialSKOperation .lt({ prop2: first }) - .go(); + .go({ compare: 'v2' }); } })(); @@ -1207,23 +1207,23 @@ describe("index types and operations", () => { case "between": return clusteredSecondaryCollectionPartialSKOperation .between({ prop5: first }, { prop5: last }) - .go(); + .go({ compare: 'v2' }); case "gte": return clusteredSecondaryCollectionPartialSKOperation .gte({ prop5: first }) - .go(); + .go({ compare: 'v2' }); case "gt": return clusteredSecondaryCollectionPartialSKOperation .gt({ prop5: first }) - .go(); + .go({ compare: 'v2' }); case "lte": return clusteredSecondaryCollectionPartialSKOperation .lte({ prop5: first }) - .go(); + .go({ compare: 'v2' }); case "lt": return clusteredSecondaryCollectionPartialSKOperation .lt({ prop5: first }) - .go(); + .go({ compare: 'v2' }); } })(); @@ -1287,15 +1287,15 @@ describe("index types and operations", () => { case "between": return entity1PrimaryQuery .between({ prop2: first }, { prop2: last }) - .go(); + .go({ compare: 'v2' }); case "gte": - return entity1PrimaryQuery.gte({ prop2: first }).go(); + return entity1PrimaryQuery.gte({ prop2: first }).go({ compare: 'v2' }); case "gt": - return entity1PrimaryQuery.gt({ prop2: first }).go(); + return entity1PrimaryQuery.gt({ prop2: first }).go({ compare: 'v2' }); case "lte": - return entity1PrimaryQuery.lte({ prop2: first }).go(); + return entity1PrimaryQuery.lte({ prop2: first }).go({ compare: 'v2' }); case "lt": - return entity1PrimaryQuery.lt({ prop2: first }).go(); + return entity1PrimaryQuery.lt({ prop2: first }).go({ compare: 'v2' }); } })(); @@ -1312,15 +1312,15 @@ describe("index types and operations", () => { case "between": return entity1SecondaryQuery .between({ prop5: first }, { prop5: last }) - .go(); + .go({ compare: 'v2' }); case "gte": - return entity1SecondaryQuery.gte({ prop5: first }).go(); + return entity1SecondaryQuery.gte({ prop5: first }).go({ compare: 'v2' }); case "gt": - return entity1SecondaryQuery.gt({ prop5: first }).go(); + return entity1SecondaryQuery.gt({ prop5: first }).go({ compare: 'v2' }); case "lte": - return entity1SecondaryQuery.lte({ prop5: first }).go(); + return entity1SecondaryQuery.lte({ prop5: first }).go({ compare: 'v2' }); case "lt": - return entity1SecondaryQuery.lt({ prop5: first }).go(); + return entity1SecondaryQuery.lt({ prop5: first }).go({ compare: 'v2' }); } })(); @@ -1370,15 +1370,15 @@ describe("index types and operations", () => { case "between": return entity1PrimaryQuery .between({ prop2: first }, { prop2: last }) - .go(); + .go({ compare: 'v2' }); case "gte": - return entity1PrimaryQuery.gte({ prop2: first }).go(); + return entity1PrimaryQuery.gte({ prop2: first }).go({ compare: 'v2' }); case "gt": - return entity1PrimaryQuery.gt({ prop2: first }).go(); + return entity1PrimaryQuery.gt({ prop2: first }).go({ compare: 'v2' }); case "lte": - return entity1PrimaryQuery.lte({ prop2: first }).go(); + return entity1PrimaryQuery.lte({ prop2: first }).go({ compare: 'v2' }); case "lt": - return entity1PrimaryQuery.lt({ prop2: first }).go(); + return entity1PrimaryQuery.lt({ prop2: first }).go({ compare: 'v2' }); } })(); @@ -1399,15 +1399,15 @@ describe("index types and operations", () => { case "between": return entity1SecondaryQuery .between({ prop5: first }, { prop5: last }) - .go(); + .go({ compare: 'v2' }); case "gte": - return entity1SecondaryQuery.gte({ prop5: first }).go(); + return entity1SecondaryQuery.gte({ prop5: first }).go({ compare: 'v2' }); case "gt": - return entity1SecondaryQuery.gt({ prop5: first }).go(); + return entity1SecondaryQuery.gt({ prop5: first }).go({ compare: 'v2' }); case "lte": - return entity1SecondaryQuery.lte({ prop5: first }).go(); + return entity1SecondaryQuery.lte({ prop5: first }).go({ compare: 'v2' }); case "lt": - return entity1SecondaryQuery.lt({ prop5: first }).go(); + return entity1SecondaryQuery.lt({ prop5: first }).go({ compare: 'v2' }); } })(); @@ -1450,15 +1450,15 @@ describe("index types and operations", () => { case "between": return entity1PrimaryQuery .between({ prop2: first }, { prop2: last }) - .go(); + .go({ compare: 'v2' }); case "gte": - return entity1PrimaryQuery.gte({ prop2: first }).go(); + return entity1PrimaryQuery.gte({ prop2: first }).go({ compare: 'v2' }); case "gt": - return entity1PrimaryQuery.gt({ prop2: first }).go(); + return entity1PrimaryQuery.gt({ prop2: first }).go({ compare: 'v2' }); case "lte": - return entity1PrimaryQuery.lte({ prop2: first }).go(); + return entity1PrimaryQuery.lte({ prop2: first }).go({ compare: 'v2' }); case "lt": - return entity1PrimaryQuery.lt({ prop2: first }).go(); + return entity1PrimaryQuery.lt({ prop2: first }).go({ compare: 'v2' }); } })(); @@ -1475,15 +1475,15 @@ describe("index types and operations", () => { case "between": return entity1SecondaryQuery .between({ prop5: first }, { prop5: last }) - .go(); + .go({ compare: 'v2' }); case "gte": - return entity1SecondaryQuery.gte({ prop5: first }).go(); + return entity1SecondaryQuery.gte({ prop5: first }).go({ compare: 'v2' }); case "gt": - return entity1SecondaryQuery.gt({ prop5: first }).go(); + return entity1SecondaryQuery.gt({ prop5: first }).go({ compare: 'v2' }); case "lte": - return entity1SecondaryQuery.lte({ prop5: first }).go(); + return entity1SecondaryQuery.lte({ prop5: first }).go({ compare: 'v2' }); case "lt": - return entity1SecondaryQuery.lt({ prop5: first }).go(); + return entity1SecondaryQuery.lt({ prop5: first }).go({ compare: 'v2' }); } })(); @@ -1491,7 +1491,7 @@ describe("index types and operations", () => { compareItems(entity1SecondaryQueryResults.data, entity1Items); }); - it(`should iterate through only the specified entity regardless of type using a sort key the ${operation} sort key operation on a single attribute sort key`, async () => { + it(`should iterate through only the specified entity regardless of type using a sort key the ${operation} sort key operation on a single attribute sort key on ${indexType} index type`, async () => { const { loaded, keys, services } = await initSingleSKTests(); const filterByOperation = (item: { prop5: number; @@ -1533,15 +1533,15 @@ describe("index types and operations", () => { case "between": return entity1PrimaryQuery .between({ prop2: first }, { prop2: last }) - .go(); + .go({ compare: 'v2' }); case "gte": - return entity1PrimaryQuery.gte({ prop2: first }).go(); + return entity1PrimaryQuery.gte({ prop2: first }).go({ compare: 'v2' }); case "gt": - return entity1PrimaryQuery.gt({ prop2: first }).go(); + return entity1PrimaryQuery.gt({ prop2: first }).go({ compare: 'v2' }); case "lte": - return entity1PrimaryQuery.lte({ prop2: first }).go(); + return entity1PrimaryQuery.lte({ prop2: first }).go({ compare: 'v2' }); case "lt": - return entity1PrimaryQuery.lt({ prop2: first }).go(); + return entity1PrimaryQuery.lt({ prop2: first }).go({ compare: 'v2' }); } })(); @@ -1562,15 +1562,15 @@ describe("index types and operations", () => { case "between": return entity1SecondaryQuery .between({ prop5: first }, { prop5: last }) - .go(); + .go({ compare: 'v2' }); case "gte": - return entity1SecondaryQuery.gte({ prop5: first }).go(); + return entity1SecondaryQuery.gte({ prop5: first }).go({ compare: 'v2' }); case "gt": - return entity1SecondaryQuery.gt({ prop5: first }).go(); + return entity1SecondaryQuery.gt({ prop5: first }).go({ compare: 'v2' }); case "lte": - return entity1SecondaryQuery.lte({ prop5: first }).go(); + return entity1SecondaryQuery.lte({ prop5: first }).go({ compare: 'v2' }); case "lt": - return entity1SecondaryQuery.lt({ prop5: first }).go(); + return entity1SecondaryQuery.lt({ prop5: first }).go({ compare: 'v2' }); } })(); @@ -1920,21 +1920,19 @@ it("should add entity filter when clustered index is partially provided on entit KeyConditionExpression: "#pk = :pk and begins_with(#sk1, :sk1)", TableName: "electro", ExpressionAttributeNames: { - "#prop2": "prop2", "#__edb_e__": "__edb_e__", "#__edb_v__": "__edb_v__", "#pk": "pk", "#sk1": "sk", }, ExpressionAttributeValues: { - ":prop20": 123, ":__edb_e__0": entity1Name, ":__edb_v__0": "1", ":pk": `$${serviceName}#prop1_abc`, ":sk1": "$primarycollection#prop2_123#prop3_", }, FilterExpression: - "(#prop2 = :prop20) AND #__edb_e__ = :__edb_e__0 AND #__edb_v__ = :__edb_v__0", + "(#__edb_e__ = :__edb_e__0) AND #__edb_v__ = :__edb_v__0", }); }); diff --git a/test/ts_connected.update.spec.ts b/test/ts_connected.update.spec.ts index c4cac412..3ad4bee5 100644 --- a/test/ts_connected.update.spec.ts +++ b/test/ts_connected.update.spec.ts @@ -3030,7 +3030,7 @@ describe("Update Item", () => { const itemBefore = await users .get({ username }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(itemBefore).to.deep.equal({ @@ -3083,7 +3083,7 @@ describe("Update Item", () => { const itemAfter = await users .get({ username }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(itemAfter).to.deep.equal({ @@ -3177,7 +3177,7 @@ describe("Update Item", () => { const itemBefore = await users .get({ username }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(itemBefore).to.deep.equal({ @@ -3229,7 +3229,7 @@ describe("Update Item", () => { const itemAfter = await users .get({ username }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(itemAfter).to.deep.equal({ @@ -3527,7 +3527,7 @@ describe("Update Item", () => { const itemBefore = await users .get({ username }) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(itemBefore).to.deep.equal({ @@ -4267,6 +4267,7 @@ describe("Update Item", () => { cityId: { validate: () => { counter.cityId++; + return true; }, type: "string", required: true, @@ -4274,6 +4275,7 @@ describe("Update Item", () => { mallId: { validate: () => { counter.mallId++; + return true; }, type: "string", required: true, @@ -4281,6 +4283,7 @@ describe("Update Item", () => { storeId: { validate: () => { counter.storeId++; + return true; }, type: "string", required: true, @@ -4288,6 +4291,7 @@ describe("Update Item", () => { buildingId: { validate: () => { counter.buildingId++; + return true; }, type: "string", required: true, @@ -4295,6 +4299,7 @@ describe("Update Item", () => { unitId: { validate: () => { counter.unitId++; + return true; }, type: "string", required: true, @@ -4302,6 +4307,7 @@ describe("Update Item", () => { category: { validate: () => { counter.category++; + return true; }, type: [ "spite store", @@ -4317,6 +4323,7 @@ describe("Update Item", () => { leaseEndDate: { validate: () => { counter.leaseEndDate++; + return true; }, type: "string", required: true, @@ -4324,6 +4331,7 @@ describe("Update Item", () => { rent: { validate: () => { counter.rent++; + return true; }, type: "number", required: true, @@ -4331,6 +4339,7 @@ describe("Update Item", () => { discount: { validate: () => { counter.discount++; + return true; }, type: "number", required: false, @@ -4339,6 +4348,7 @@ describe("Update Item", () => { tenant: { validate: () => { counter.tenant++; + return true; }, type: "set", items: "string", @@ -4346,12 +4356,14 @@ describe("Update Item", () => { deposit: { validate: () => { counter.deposit++; + return true; }, type: "number", }, rentalAgreement: { validate: () => { counter.rentalAgreement++; + return true; }, type: "list", items: { @@ -4360,6 +4372,7 @@ describe("Update Item", () => { type: { validate: () => { counter.rentalAgreementChildren.type++; + return true; }, type: "string", required: true, @@ -4367,6 +4380,7 @@ describe("Update Item", () => { detail: { validate: () => { counter.rentalAgreementChildren.detail++; + return true; }, type: "string", required: true, @@ -4377,6 +4391,7 @@ describe("Update Item", () => { tags: { validate: () => { counter.tags++; + return true; }, type: "set", items: "string", @@ -4384,6 +4399,7 @@ describe("Update Item", () => { contact: { validate: () => { counter.contact++; + return true; }, type: "set", items: "string", @@ -4391,6 +4407,7 @@ describe("Update Item", () => { leaseHolders: { validate: () => { counter.leaseHolders++; + return true; }, type: "set", items: "string", @@ -4398,18 +4415,21 @@ describe("Update Item", () => { petFee: { validate: () => { counter.petFee++; + return true; }, type: "number", }, totalFees: { validate: () => { counter.totalFees++; + return true; }, type: "number", }, listAttribute: { validate: () => { counter.listAttribute++; + return true; }, type: "list", items: { @@ -4418,6 +4438,7 @@ describe("Update Item", () => { setAttribute: { validate: () => { counter.listAttributeChildren.setAttribute++; + return true; }, type: "set", items: "string", @@ -4428,12 +4449,14 @@ describe("Update Item", () => { mapAttribute: { validate: () => { counter.mapAttribute++; + return true; }, type: "map", properties: { mapProperty: { validate: () => { counter.mapAttributeChildren.mapProperty++; + return true; }, type: "string", }, @@ -4603,9 +4626,9 @@ describe("Update Item", () => { .go() .then(() => false) .catch((err) => err.message); - expect(error).to.be.string( - 'Invalid value for attribute "stringVal": Failed model defined regex', - ); + expect(error).to.be.string( + 'Invalid value for attribute "stringVal": Failed model defined regex', + ); }); it("should validate string sets", async () => { const stringVal = `abc${uuid()}`; diff --git a/test/ts_connected.validations.spec.ts b/test/ts_connected.validations.spec.ts index affd17da..fd21ec0e 100644 --- a/test/ts_connected.validations.spec.ts +++ b/test/ts_connected.validations.spec.ts @@ -99,7 +99,7 @@ describe("padding validation", () => { describe("user attribute validation", () => { describe("root primitives user validation", () => { - it("should interpret a true response value as an invalid attribute value", async () => { + it("should interpret a false response value as an invalid attribute value", async () => { const prop1 = uuid(); const prop2 = "value2"; const invalidProp2 = "invalid_value"; @@ -116,7 +116,7 @@ describe("user attribute validation", () => { }, prop2: { type: "string", - validate: (value) => value === invalidProp2, + validate: (value) => value !== invalidProp2, }, }, indexes: { @@ -157,71 +157,6 @@ describe("user attribute validation", () => { expect(updateResult.fields).to.have.length(1); }); - it("should interpret a string return value as a validation error message", async () => { - const prop1 = uuid(); - const prop2 = "value2"; - const invalidProp2 = "invalid_value"; - const invalidValueMessage = "Oh no! invalid value!"; - const entity = new Entity( - { - model: { - service: "MallStoreDirectory", - entity: "MallStores", - version: "1", - }, - attributes: { - prop1: { - type: "string", - }, - prop2: { - type: "string", - validate: (value) => { - if (value === invalidProp2) { - return invalidValueMessage; - } - }, - }, - }, - indexes: { - record: { - pk: { - field: "partition_key", - composite: ["prop1"], - }, - }, - }, - }, - { table, client }, - ); - await entity.put({ prop1, prop2 }).go(); - await entity.update({ prop1 }).set({ prop2 }).go({ response: "all_new" }); - const [putSuccess, putResult] = await entity - .put({ prop1, prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - const [updateSuccess, updateResult] = await entity - .update({ prop1 }) - .set({ prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - expect(putSuccess).to.be.false; - expect(putResult.message).to.equal( - invalidValueMessage + - " - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - expect(updateSuccess).to.be.false; - expect(updateResult.message).to.equal( - invalidValueMessage + - " - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - expect(putResult.fields).to.have.length(1); - expect(updateResult.fields).to.have.length(1); - }); - it("should wrap a thrown error in the user validation callback with an ElectroError and make it available on a 'causes' array", async () => { const prop1 = uuid(); const prop2 = "value2"; @@ -245,6 +180,7 @@ describe("user attribute validation", () => { if (value === invalidProp2) { throw error; } + return true; }, }, }, @@ -291,7 +227,7 @@ describe("user attribute validation", () => { }); }); describe("root maps user validation", () => { - it("should interpret a true response value as an invalid attribute value", async () => { + it("should interpret a false response value as an invalid attribute value", async () => { const prop1 = uuid(); const prop2 = { prop4: "value4", @@ -327,7 +263,7 @@ describe("user attribute validation", () => { validate: (value = {}) => { const value3PresentButValue5IsNot = value.prop3 !== undefined && value.prop5 === undefined; - return value3PresentButValue5IsNot; + return !value3PresentButValue5IsNot; }, }, }, @@ -413,6 +349,7 @@ describe("user attribute validation", () => { if (value.prop3 !== undefined && value.prop5 === undefined) { throw new MyCustomError(message); } + return true; }, }, }, @@ -460,87 +397,6 @@ describe("user attribute validation", () => { expect(updateResult.fields[0].field).to.equal("prop2"); }); - it("should interpret a string return value as a validation error message", async () => { - const prop1 = uuid(); - const prop2 = { - prop4: "value4", - }; - const invalidProp2 = { - prop3: "value3", - prop4: "value4", - }; - const invalidValueMessage = "Oh no! invalid value!"; - const entity = new Entity( - { - model: { - service: "MallStoreDirectory", - entity: "MallStores", - version: "1", - }, - attributes: { - prop1: { - type: "string", - }, - prop2: { - type: "map", - properties: { - prop3: { - type: "string", - }, - prop4: { - type: "string", - }, - prop5: { - type: "string", - }, - }, - validate: (value = {}) => { - const value3PresentButValue5IsNot = - value.prop3 !== undefined && value.prop5 === undefined; - if (value3PresentButValue5IsNot) { - return invalidValueMessage; - } - }, - }, - }, - indexes: { - record: { - pk: { - field: "partition_key", - composite: ["prop1"], - }, - }, - }, - }, - { table, client }, - ); - await entity.put({ prop1, prop2 }).go(); - await entity.update({ prop1 }).set({ prop2 }).go({ response: "all_new" }); - const [putSuccess, putResult] = await entity - .put({ prop1, prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - const [updateSuccess, updateResult] = await entity - .update({ prop1 }) - .set({ prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - expect(putSuccess).to.be.false; - expect(putResult.message).to.equal( - "Oh no! invalid value! - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - expect(updateSuccess).to.be.false; - expect(updateResult.message).to.equal( - "Oh no! invalid value! - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - expect(putResult.fields).to.have.length(1); - expect(updateResult.fields).to.have.length(1); - }); - it("should validate a maps properties before itself", async () => { const prop1 = uuid(); const prop2 = { @@ -569,6 +425,7 @@ describe("user attribute validation", () => { validate: (value) => { validationExecutions.push("property"); validationExecutionTypes.push(typeof value); + return true; }, }, prop4: { @@ -576,6 +433,7 @@ describe("user attribute validation", () => { validate: (value) => { validationExecutions.push("property"); validationExecutionTypes.push(typeof value); + return true; }, }, prop5: { @@ -583,12 +441,14 @@ describe("user attribute validation", () => { validate: (value) => { validationExecutions.push("property"); validationExecutionTypes.push(typeof value); + return true; }, }, }, validate: (value) => { validationExecutions.push("map"); validationExecutionTypes.push(typeof value); + return true; }, }, }, @@ -627,7 +487,7 @@ describe("user attribute validation", () => { ]); }); - it("should interpret a true response value as an invalid attribute value on individual property values", async () => { + it("should interpret a false response value as an invalid attribute value on individual property values", async () => { const prop1 = uuid(); const prop2 = { prop3: "value4", @@ -656,19 +516,19 @@ describe("user attribute validation", () => { prop3: { type: "string", validate: (value) => { - return value !== prop2.prop3; + return value === prop2.prop3; }, }, prop4: { type: "number", validate: (value) => { - return value !== prop2.prop4; + return value === prop2.prop4; }, }, prop5: { type: "boolean", validate: (value) => { - return !value; + return !!value; }, }, }, @@ -712,104 +572,6 @@ describe("user attribute validation", () => { expect(updateResult.fields).to.have.length(3); }); - it("should interpret a string return value as a validation error message on individual property values", async () => { - const prop1 = uuid(); - const prop2 = { - prop3: "value4", - prop4: 12345, - prop5: true, - }; - const invalidProp2 = { - prop3: "value3", - prop4: 12346, - prop5: false, - }; - const invalidValueMessage = "Oh no! invalid value!"; - const entity = new Entity( - { - model: { - service: "MallStoreDirectory", - entity: "MallStores", - version: "1", - }, - attributes: { - prop1: { - type: "string", - }, - prop2: { - type: "map", - properties: { - prop3: { - type: "string", - validate: (value) => { - if (value !== prop2.prop3) { - return invalidValueMessage; - } - }, - }, - prop4: { - type: "number", - validate: (value) => { - if (value !== prop2.prop4) { - return invalidValueMessage; - } - }, - }, - prop5: { - type: "boolean", - validate: (value) => { - if (value !== prop2.prop5) { - return invalidValueMessage; - } - }, - }, - }, - }, - }, - indexes: { - record: { - pk: { - field: "partition_key", - composite: ["prop1"], - }, - }, - }, - }, - { table, client }, - ); - await entity.put({ prop1, prop2 }).go(); - await entity.update({ prop1 }).set({ prop2 }).go({ response: "all_new" }); - const [putSuccess, putResult] = await entity - .put({ prop1, prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - const [updateSuccess, updateResult] = await entity - .update({ prop1 }) - .set({ prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - expect(putSuccess).to.be.false; - expect(putResult.message).to.equal( - [invalidValueMessage, invalidValueMessage, invalidValueMessage].join( - ", ", - ) + - " - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - expect(updateSuccess).to.be.false; - expect(updateResult.message).to.equal( - [invalidValueMessage, invalidValueMessage, invalidValueMessage].join( - ", ", - ) + - " - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - expect(putResult.fields).to.have.length(3); - expect(updateResult.fields).to.have.length(3); - }); - it("should wrap a thrown error in the user validation callback with an ElectroError and make it available on a 'causes' array", async () => { const prop1 = uuid(); const prop2 = { @@ -844,6 +606,7 @@ describe("user attribute validation", () => { if (value !== prop2.prop3) { throw error; } + return true; }, }, prop4: { @@ -852,6 +615,7 @@ describe("user attribute validation", () => { if (value !== prop2.prop4) { throw error; } + return true; }, }, prop5: { @@ -860,6 +624,7 @@ describe("user attribute validation", () => { if (value !== prop2.prop5) { throw error; } + return true; }, }, }, @@ -916,7 +681,7 @@ describe("user attribute validation", () => { }); }); describe("root lists user validation", () => { - it("should interpret a true response value as an invalid attribute value", async () => { + it("should interpret a false response value as an invalid attribute value", async () => { const prop1 = uuid(); const prop2 = ["value1", "value2"]; const invalidProp2 = ["value1"]; @@ -937,7 +702,7 @@ describe("user attribute validation", () => { type: "string", }, validate: (value: string[] = []) => { - return value.length === 1; + return value.length !== 1; }, }, }, @@ -979,74 +744,6 @@ describe("user attribute validation", () => { expect(updateResult.fields).to.have.length(1); }); - it("should interpret a string return value as a validation error message", async () => { - const prop1 = uuid(); - const prop2 = ["value1", "value2"]; - const invalidProp2 = ["value1"]; - const invalidValueMessage = "Oh no! invalid value!"; - const entity = new Entity( - { - model: { - service: "MallStoreDirectory", - entity: "MallStores", - version: "1", - }, - attributes: { - prop1: { - type: "string", - }, - prop2: { - type: "list", - items: { - type: "string", - }, - validate: (value: string[] = []) => { - if (value.length === 1) { - return invalidValueMessage; - } - }, - }, - }, - indexes: { - record: { - pk: { - field: "partition_key", - composite: ["prop1"], - }, - }, - }, - }, - { table, client }, - ); - await entity.put({ prop1, prop2 }).go(); - await entity.update({ prop1 }).set({ prop2 }).go({ response: "all_new" }); - const [putSuccess, putResult] = await entity - .put({ prop1, prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - const [updateSuccess, updateResult] = await entity - .update({ prop1 }) - .set({ prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - expect(putSuccess).to.be.false; - expect(putResult.message).to.equal( - invalidValueMessage + - " - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - expect(updateSuccess).to.be.false; - expect(updateResult.message).to.equal( - invalidValueMessage + - " - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - expect(putResult.fields).to.have.length(1); - expect(updateResult.fields).to.have.length(1); - }); - it("should validate a lists properties before itself", async () => { const prop1 = uuid(); const prop2 = ["value1", "value2"]; @@ -1070,6 +767,7 @@ describe("user attribute validation", () => { validate: (value: string) => { validationExecutions.push("property"); validationExecutionTypes.push(typeof value); + return true; }, }, validate: (value: string[] | undefined) => { @@ -1077,6 +775,7 @@ describe("user attribute validation", () => { validationExecutionTypes.push( Array.isArray(value) ? "array" : typeof value, ); + return true; }, }, }, @@ -1111,7 +810,7 @@ describe("user attribute validation", () => { ]); }); - it("should interpret a true response value as an invalid attribute value on individual item values", async () => { + it("should interpret a false response value as an invalid attribute value on individual item values", async () => { const prop1 = uuid(); const prop2 = ["value1", "value2"]; const invalidProp2 = ["value3", "value4", "value5"]; @@ -1131,7 +830,7 @@ describe("user attribute validation", () => { items: { type: "string", validate: (value: string) => { - return !prop2.find((val) => val === value); + return !!prop2.find((val) => val === value); }, }, }, @@ -1197,71 +896,6 @@ describe("user attribute validation", () => { expect(updateResult.fields).to.have.length(3); }); - it("should interpret a string return value as a validation error message on individual item values", async () => { - const prop1 = uuid(); - const prop2 = ["value1", "value2"]; - const invalidProp2 = ["value3", "value4", "value5"]; - const invalidValueMessage = "Oh no! invalid value!"; - const invalidValueError = - [invalidValueMessage, invalidValueMessage, invalidValueMessage].join( - ", ", - ) + - " - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute"; - const entity = new Entity( - { - model: { - service: "MallStoreDirectory", - entity: "MallStores", - version: "1", - }, - attributes: { - prop1: { - type: "string", - }, - prop2: { - type: "list", - items: { - type: "string", - validate: (value: string) => { - if (!prop2.find((val) => val === value)) { - return invalidValueMessage; - } - }, - }, - }, - }, - indexes: { - record: { - pk: { - field: "partition_key", - composite: ["prop1"], - }, - }, - }, - }, - { table, client }, - ); - await entity.put({ prop1, prop2 }).go(); - await entity.update({ prop1 }).set({ prop2 }).go({ response: "all_new" }); - const [putSuccess, putResult] = await entity - .put({ prop1, prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - const [updateSuccess, updateResult] = await entity - .update({ prop1 }) - .set({ prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - expect(putSuccess).to.be.false; - expect(putResult.message.trim()).to.equal(invalidValueError); - expect(updateSuccess).to.be.false; - expect(updateResult.message).to.equal(invalidValueError); - }); - it("should not validate a parent list of a nested property if the property fails", async () => { const prop1 = uuid(); const prop2 = ["value1", "value2"]; @@ -1293,6 +927,7 @@ describe("user attribute validation", () => { validationExecutionTypes.push( Array.isArray(value) ? "array" : typeof value, ); + return true; }, }, }, @@ -1319,14 +954,18 @@ describe("user attribute validation", () => { expect(validationExecutions).to.deep.equal([ "property", "property", + "list", "property", "property", + "list", ]); expect(validationExecutionTypes).to.deep.equal([ "string", "string", + "array", "string", "string", + "array", ]); }); @@ -1381,6 +1020,7 @@ describe("user attribute validation", () => { validate: (value) => { validationExecutions.push("map"); validationExecutionTypes.push(typeof value); + return true; }, }, }, @@ -1408,17 +1048,21 @@ describe("user attribute validation", () => { "property", "property", "property", + "map", "property", "property", "property", + "map", ]); expect(validationExecutionTypes).to.deep.equal([ "string", "number", "boolean", + "object", "string", "number", "boolean", + "object", ]); }); @@ -1448,6 +1092,7 @@ describe("user attribute validation", () => { if (value.length === 1) { throw error; } + return true; }, }, }, @@ -1493,7 +1138,7 @@ describe("user attribute validation", () => { }); describe("root set user validation", () => { - it("should interpret a true response value as an invalid attribute value", async () => { + it("should interpret a false response value as an invalid attribute value", async () => { const prop1 = uuid(); const prop2 = ["value1", "value2"]; const invalidProp2 = ["value1"]; @@ -1512,7 +1157,7 @@ describe("user attribute validation", () => { type: "set", items: "string", validate: (value: string[] = []) => { - return value.length === 1; + return value.length !== 1; }, }, }, @@ -1552,70 +1197,6 @@ describe("user attribute validation", () => { ); }); - it("should interpret a string return value as a validation error message", async () => { - const prop1 = uuid(); - const prop2 = ["value1", "value2"]; - const invalidProp2 = ["value1"]; - const invalidValueMessage = "Oh no! invalid value!"; - const entity = new Entity( - { - model: { - service: "MallStoreDirectory", - entity: "MallStores", - version: "1", - }, - attributes: { - prop1: { - type: "string", - }, - prop2: { - type: "set", - items: "string", - validate: (value: string[] = []) => { - if (value.length === 1) { - return invalidValueMessage; - } - }, - }, - }, - indexes: { - record: { - pk: { - field: "partition_key", - composite: ["prop1"], - }, - }, - }, - }, - { table, client }, - ); - await entity.put({ prop1, prop2 }).go(); - await entity.update({ prop1 }).set({ prop2 }).go({ response: "all_new" }); - const [putSuccess, putResult] = await entity - .put({ prop1, prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - const [updateSuccess, updateResult] = await entity - .update({ prop1 }) - .set({ prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - expect(putSuccess).to.be.false; - expect(putResult.message).to.equal( - invalidValueMessage + - " - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - expect(updateSuccess).to.be.false; - expect(updateResult.message).to.equal( - invalidValueMessage + - " - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - }); - it("should wrap a thrown error in the user validation callback with an ElectroError and make it available on a 'causes' array", async () => { const prop1 = uuid(); const prop2 = ["value1", "value2"]; @@ -1640,6 +1221,7 @@ describe("user attribute validation", () => { if (value.length === 1) { throw error; } + return true; }, }, }, @@ -1684,70 +1266,6 @@ describe("user attribute validation", () => { expect(updateResult.fields).to.be.an("array").with.length(1); expect(updateResult.fields[0].cause).to.equal(error); }); - - it("should interpret a string return value as a validation error message", async () => { - const prop1 = uuid(); - const prop2 = ["value1", "value2"]; - const invalidProp2 = ["value1"]; - const invalidValueMessage = "Oh no! invalid value!"; - const entity = new Entity( - { - model: { - service: "MallStoreDirectory", - entity: "MallStores", - version: "1", - }, - attributes: { - prop1: { - type: "string", - }, - prop2: { - type: "set", - items: "string", - validate: (value: string[] = []) => { - if (value.length === 1) { - return invalidValueMessage; - } - }, - }, - }, - indexes: { - record: { - pk: { - field: "partition_key", - composite: ["prop1"], - }, - }, - }, - }, - { table, client }, - ); - await entity.put({ prop1, prop2 }).go(); - await entity.update({ prop1 }).set({ prop2 }).go({ response: "all_new" }); - const [putSuccess, putResult] = await entity - .put({ prop1, prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - const [updateSuccess, updateResult] = await entity - .update({ prop1 }) - .set({ prop2: invalidProp2 }) - .go() - .then((res) => res.data) - .then((data) => [true, data]) - .catch((err) => [false, err]); - expect(putSuccess).to.be.false; - expect(putResult.message).to.equal( - invalidValueMessage + - " - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - expect(updateSuccess).to.be.false; - expect(updateResult.message).to.equal( - invalidValueMessage + - " - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#invalid-attribute", - ); - }); }); }); diff --git a/test/ts_connected.where.spec.ts b/test/ts_connected.where.spec.ts index ec78e0e3..f0c67761 100644 --- a/test/ts_connected.where.spec.ts +++ b/test/ts_connected.where.spec.ts @@ -433,7 +433,7 @@ describe("Where Clause Queries", () => { ${name(animal)} = ${value(animal, penRow.animal)} AND ${notExists(dangerous)} `, ) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data); expect(results).to.be.empty; let after = await WhereTests.get(penRow) @@ -476,7 +476,7 @@ describe("Where Clause Queries", () => { let doesExist = await WhereTests.patch(penRow) .set({ dangerous: true }) .where(({ dangerous }, { notExists }) => notExists(dangerous)) - .go({ raw: true }) + .go({ data: 'raw' }) .then((res) => res.data) .then(() => false) .catch(() => true); diff --git a/www/src/components/PageContent/LinkButton.tsx b/www/src/components/PageContent/LinkButton.tsx index ff90ea85..db896579 100644 --- a/www/src/components/PageContent/LinkButton.tsx +++ b/www/src/components/PageContent/LinkButton.tsx @@ -13,7 +13,7 @@ type Props = { }; const LinkButton: FunctionComponent = ({ href, text, alt }) => { - const style = alt ? { backgroundColor: "#4a82b5" } : {}; + const style = alt ? { backgroundColor: "#f9bd00" } : {}; return ( {text} diff --git a/www/src/config.ts b/www/src/config.ts index 4bdc2c8e..1dde3522 100644 --- a/www/src/config.ts +++ b/www/src/config.ts @@ -57,6 +57,7 @@ export const SIDEBAR: Sidebar = { text: "Single-Table Relationships", link: "en/core-concepts/single-table-relationships", }, + { text: "Upgrade to v3", link: "en/core-concepts/v3-migration" } ], "Data Modeling": [ { text: "Entities", link: "en/modeling/entities" }, diff --git a/www/src/pages/en/core-concepts/v3-migration.mdx b/www/src/pages/en/core-concepts/v3-migration.mdx new file mode 100644 index 00000000..da022811 --- /dev/null +++ b/www/src/pages/en/core-concepts/v3-migration.mdx @@ -0,0 +1,35 @@ +--- +title: Upgrade to v3 +description: Upgrade to v3 +keywords: + - electrodb + - v3 + - migrate + - 3 +layout: ../../../layouts/MainLayout.astro +--- + +## Migrating from ElectroDB v2 to v3 + +ElectroDB v3 is the next major update after v2. You can also join the discussion or ask questions [here](https://github.com/tywalch/electrodb/discussions/424). + +## Breaking changes + +### Removes execution options `includeKeys` and `raw` + +The execution options `includeKeys` and `raw` were deprecated in version `2.0.0` and have now been removed in favor of the execution option `data`. To migrate from `v2`, use the options `{ data: "includeKeys" }` and `{ data: "raw" }` respectively. + +### The `limit` execution option no longer plays a role in pagination + +Providing the execution option `limit` on queries now _only_ applies a `Limit` parameter to its request to DynamoDB. Previously, the `limit` option would cause ElectroDB to effectively "seek" DynamoDB until the limit was _at least_ reached. The execution option `count` can be used in similar cases where `limit` was used, but performance may vary depending on your data and use case. + +### Changes to `validate` callback on attributes +The `validate` callback on attributes now expects a strict return type of `boolean`. Additionally, the semantic meaning of a boolean response has _flipped_. The callback should return `true` for "valid" values and `false` for "invalid" values. If your validation function throws an error, ElectroDB will still behave as it previously did in `v2`, by catching and wrapping the error. + +### Significant changes to default behavior when performing `gt`, `lte`, and `between` queries + +ElectroDB is changing how it generates query parameters to give more control to users. Prior to `v3`, query operations that used the `gt`, `lte`, or `between` methods would incur additional post-processing, including additional filter expressions and some sort key hacks. The post-processing was an attempt to bridge an interface gap between attribute-level considerations and key-level considerations. Checkout the GitHub issue championed by @rcoundon and @PaulJNewell77 [here](https://github.com/tywalch/electrodb/issues/228) to learn more. + +With `v3`, ElectroDB will not apply post-processing to queries of any type and abstains from adding implicit/erroneous filter expressions to queries _by default_. This change should provide additional control to users to achieve more advanced queries, but also introduces some additional complexity. There are many factors related to sorting and using comparison queries that are not intuitive, and the simplest way to mitigate this is by using additional [filter expressions](https://electrodb.dev/en/queries/filters/) to ensure the items returned will match expectations. + +To ease migration and adoption, I have added a new execution option called `compare`; To recreate `v2` functionality without further changes, use the execution option `{ compare: "v2" }`. This value is marked as deprecated and will be removed at a later date, but should allow users to safely upgrade to `v3` and experiment with the impact of this change on their existing data. The new `compare` option has other values that will continue to see support, however; to learn more about this new option, checkout [Comparison Queries](https://electrodb.dev/en/queries/query#comparison-queries). diff --git a/www/src/pages/en/examples/library-system.mdx b/www/src/pages/en/examples/library-system.mdx index 9650305b..a91c1c66 100644 --- a/www/src/pages/en/examples/library-system.mdx +++ b/www/src/pages/en/examples/library-system.mdx @@ -522,11 +522,11 @@ const { data, cursor } = await book.query.releases({ bookTitle: "it" }).go(); Queries can be provided partially like just the year, the year and month, etc. ```typescript -book.query.releases({ bookTitle: "it" }).gte({ releaseDate: "1990" }).go(); +book.query.releases({ bookTitle: "it" }).gte({ releaseDate: "1990-00-00" }).go(); book.query .releases({ bookTitle: "it" }) - .between({ releaseDate: "1990" }, { releaseDate: "2019" }) + .between({ releaseDate: "1990-00-00" }, { releaseDate: "2019-99-99" }) .go(); ``` diff --git a/www/src/pages/en/modeling/indexes.mdx b/www/src/pages/en/modeling/indexes.mdx index 446a7be6..73c4f564 100644 --- a/www/src/pages/en/modeling/indexes.mdx +++ b/www/src/pages/en/modeling/indexes.mdx @@ -272,14 +272,14 @@ This option changes how ElectroDB formats your keys for storage, so it is an imp By default, and when omitted, ElectroDB will create your index as an `isolated` index. Isolated indexes optimizes your index structure for faster and more efficient retrieval of items within an individual Entity. _Choose_ `isolated` if you have strong access pattern requirements to retrieve only records for only your entity on that index. While an `isolated` index is more limited in its ability to be used in a [collection](/en/modeling/collections), it can perform better than a `clustered` index if a collection contains a highly unequal distribution of entities within a collection. -_Don't choose_ `isolated` if the primary use-cases for your index is to query across entities -- this index type does limit the extent to which indexes can be leveraged to improve query efficiency. +_Do not choose_ `isolated` if the primary use-cases for your index is to query across entities -- this index type does limit the extent to which indexes can be leveraged to improve query efficiency. ### Clustered Indexes When your index type is defined as `clustered`, ElectroDB will optimize your index for relationships within a partition. Clustered indexes optimize your index structure for more homogenous partitions, which allows for more efficient queries across multiple entities. _Choose_ `clustered` if you have a high degree of grouped or similar data that needs to be frequently accessed together. This index works best in [collections](/en/modeling/collections) when member entities are more evenly distributed within a partition. -_Don't choose_ `clustered` if your need to query across entities is secondary to its primary purpose -- this index type limits the efficiency of querying your individual entity. +_Do not choose_ `clustered` if your need to query across entities is secondary to its primary purpose -- this index type limits the efficiency of querying your individual entity. ### Isolated vs Clustered diff --git a/www/src/pages/en/mutations/batch-delete.mdx b/www/src/pages/en/mutations/batch-delete.mdx index 4bee885f..855002db 100644 --- a/www/src/pages/en/mutations/batch-delete.mdx +++ b/www/src/pages/en/mutations/batch-delete.mdx @@ -20,7 +20,7 @@ import ExampleSetup from "../../../partials/entity-query-example-setup.mdx"; Provide all table index composite attributes in an array of objects to the `delete` method to batch delete records. -> Performing a Batch Delete will return an array of "unprocessed" records. An empty array signifies all records were processed. If you want the raw DynamoDB response you can always use the option `{raw: true}`, more detail found here: [Execution Options](#execution-options). +> Performing a Batch Delete will return an array of "unprocessed" records. An empty array signifies all records were processed. If you want the raw DynamoDB response you can always use the option `{ data: "raw" }`, more detail found here: [Execution Options](#execution-options). > Additionally, when performing a BatchWrite the `.params()` method will return an _array_ of parameters, rather than just the parameters for one docClient query. This is because ElectroDB BatchWrite queries larger than the docClient's limit of 25 records. If the number of records you are requesting is above the BatchWrite threshold of 25 records, ElectroDB will make multiple requests to DynamoDB and return the results in a single array. By default, ElectroDB will make these requests in series, one after another. If you are confident your table can handle the throughput, you can use the [Execution Option](#execution-options) `concurrent`. This value can be set to any number greater than zero, and will execute that number of requests simultaneously. @@ -85,7 +85,7 @@ let unprocessed = await StoreLocations.delete([ } ``` -Elements of the `unprocessed` array are unlike results received from a query. Instead of containing all the attributes of a record, an unprocessed record only includes the composite attributes defined in the Table Index. This is in keeping with DynamoDB's practice of returning only Keys in the case of unprocessed records. For convenience, ElectroDB will return these keys as composite attributes, but you can pass the [execution option](#execution-options) `{unprocessed:"raw"}` override this behavior and return the Keys as they came from DynamoDB. +Elements of the `unprocessed` array are unlike results received from a query. Instead of containing all the attributes of a record, an unprocessed record only includes the composite attributes defined in the Table Index. This is in keeping with DynamoDB's practice of returning only Keys in the case of unprocessed records. For convenience, ElectroDB will return these keys as composite attributes, but you can pass the [execution option](#execution-options) `{ unprocessed:"raw" }` override this behavior and return the Keys as they came from DynamoDB. ## Execution Options diff --git a/www/src/pages/en/mutations/batch-put.mdx b/www/src/pages/en/mutations/batch-put.mdx index 44dd2407..8414b28e 100644 --- a/www/src/pages/en/mutations/batch-put.mdx +++ b/www/src/pages/en/mutations/batch-put.mdx @@ -19,7 +19,7 @@ import ExampleSetup from "../../../partials/entity-query-example-setup.mdx"; Provide all _required_ Attributes as defined in the model to create records as an _array_ to `.put()`. **ElectroDB** will enforce any defined validations, defaults, casting, and field aliasing. Another convenience ElectroDB provides, is accepting BatchWrite arrays _larger_ than the 25 record limit. This is achieved making multiple, "parallel", requests to DynamoDB for batches of 25 records at a time. A failure with any of these requests will cause the query to throw, so be mindful of your table's configured throughput. -> Performing a Batch Put will return an array of "unprocessed" records. An empty array signifies all records returned were processed. If you want the raw DynamoDB response you can always use the option `{raw: true}`, more detail found here: [Execution Options](#execution-options). +> Performing a Batch Put will return an array of "unprocessed" records. An empty array signifies all records returned were processed. If you want the raw DynamoDB response you can always use the option `{ data: "raw" }`, more detail found here: [Execution Options](#execution-options). > Additionally, when performing a BatchWrite the `.params()` method will return an _array_ of parameters, rather than just the parameters for one docClient query. This is because ElectroDB BatchWrite queries larger than the docClient's limit of 25 records. If the number of records you are requesting is above the BatchWrite threshold of 25 records, ElectroDB will make multiple requests to DynamoDB and return the results in a single array. By default, ElectroDB will make these requests in series, one after another. If you are confident your table can handle the throughput, you can use the [Execution Option](#execution-options) `concurrent`. This value can be set to any number greater than zero, and will execute that number of requests simultaneously. @@ -122,7 +122,7 @@ const unprocessed = await StoreLocations.put([ } ``` -Elements of the `unprocessed` array are unlike results received from a query. Instead of containing all the attributes of a record, an unprocessed record only includes the composite attributes defined in the Table Index. This is in keeping with DynamoDB's practice of returning only Keys in the case of unprocessed records. For convenience, ElectroDB will return these keys as composite attributes, but you can pass the [execution option](#execution-options) `{unprocessed:"raw"}` override this behavior and return the Keys as they came from DynamoDB. +Elements of the `unprocessed` array are unlike results received from a query. Instead of containing all the attributes of a record, an unprocessed record only includes the composite attributes defined in the Table Index. This is in keeping with DynamoDB's practice of returning only Keys in the case of unprocessed records. For convenience, ElectroDB will return these keys as composite attributes, but you can pass the [execution option](#execution-options) `{ unprocessed:"raw" }` override this behavior and return the Keys as they came from DynamoDB. ## Execution Options diff --git a/www/src/pages/en/queries/batch-get.mdx b/www/src/pages/en/queries/batch-get.mdx index 153bda32..8812e07c 100644 --- a/www/src/pages/en/queries/batch-get.mdx +++ b/www/src/pages/en/queries/batch-get.mdx @@ -85,7 +85,7 @@ The `results` array are records that were returned DynamoDB as `Responses` on th > By default, ElectroDB will return items without concern for order. If the order returned by ElectroDB must match the order provided, the [execution option](#execution-options) `preserveBatchOrder` can be used. When enabled, ElectroDB will ensure the order returned by a batchGet will be the same as the order provided. When enabled, if a record is returned from DynamoDB as "unprocessed" ([read more here](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html)), ElectroDB will return a null value at that index. -Elements of the `unprocessed` array are unlike results received from a query. Instead of containing all the attributes of a record, an unprocessed record only includes the composite attributes defined in the Table Index. This is in keeping with DynamoDB's practice of returning only Keys in the case of unprocessed records. For convenience, ElectroDB will return these keys as composite attributes, but you can pass the [execution option](#execution-options) `{unprocessed:"raw"}` override this behavior and return the Keys as they came from DynamoDB. +Elements of the `unprocessed` array are unlike results received from a query. Instead of containing all the attributes of a record, an unprocessed record only includes the composite attributes defined in the Table Index. This is in keeping with DynamoDB's practice of returning only Keys in the case of unprocessed records. For convenience, ElectroDB will return these keys as composite attributes, but you can pass the [execution option](#execution-options) `{ unprocessed:"raw" }` override this behavior and return the Keys as they came from DynamoDB. ## Execution Options diff --git a/www/src/pages/en/queries/filters.mdx b/www/src/pages/en/queries/filters.mdx index 1cc1dff7..79df3924 100644 --- a/www/src/pages/en/queries/filters.mdx +++ b/www/src/pages/en/queries/filters.mdx @@ -11,11 +11,11 @@ keywords: layout: ../../../layouts/MainLayout.astro --- -Building thoughtful indexes can make queries simple and performant. Sometimes you need to filter results down further or add conditions to an update/patch/upsert/put/create/delete/remove action. +Building thoughtful indexes can make queries simple and performant, though sometimes you need to apply further filters to your query. ## FilterExpressions -Below is the traditional way you would add a `FilterExpression` to Dynamo's DocumentClient directly alongside how you would accomplish the same using the `where` method. +Below demonstrates how to add a `FilterExpression` to Dynamo's DocumentClient directly alongside how you would accomplish the same using the `where` method. ### Example @@ -98,11 +98,9 @@ animals.query ```typescript animals.query .habitats({ habitat: "Africa", enclosure: "5b" }) - .where( - (attr, op) => ` + .where((attr, op) => ` ${op.eq(attr.dangerous, true)} OR ${op.notExists(attr.lastFed)} - `, - ) + `) .where(({ birthday }, { between }) => { const today = Date.now(); const lastMonth = today - 1000 * 60 * 60 * 24 * 30; @@ -118,15 +116,13 @@ type GetAnimalOptions = { habitat: string; keepers: string[]; }; + function getAnimals(options: GetAnimalOptions) { const { habitat, keepers } = options; - const query = animals.query.exhibit({ habitat }); - - for (const name of keepers) { - query.where(({ keeper }, { eq }) => eq(keeper, name)); - } - - return query.go(); + return animals.query.exhibit({ habitat }) + .where(({ keeper }, { eq }) => { + return keepers.map((name) => eq(keeper, name)).join(' AND '); + }).go() } const { data, cursor } = await getAnimals({ @@ -137,7 +133,7 @@ const { data, cursor } = await getAnimals({ ## Operations -[![Try it out!](https://img.shields.io/badge/electrodb-try_out_this_example_›-%23f9bd00?style=for-the-badge&logo=amazondynamodb&labelColor=1a212a)](https://electrodb.fun/?#code/JYWwDg9gTgLgBAbwKIDsbBgTwDRwMoCmUAbsAMYEC+cAZlBCHAEQEA2BZM9AJgEZMAoAWQgoAzvAAWAQ14ZpMMXAC8cANpMAgnXLSmuLVE7l9zAErTgKAGLQCEpgF040pSPEwA3AKxgCcAAlZeXhVXwIIGjgZORgFMTUUAFcQXiJHb2FRCRcUUGlWJVUUAgB3OFR0LAAKBAE4OBAIbjYALkR6hq6xIlIKdqYALwgIfU6uuAI0DEwB6TyQArExiYbiIjFgUQGARhW4SmxOhS5gXiSYe3a6iZiQ6-Gu8Pa7uMUj1bgoAgBHJOBvtx2lwkgQPl1Do8pmRWBAxElvg9Ps9mBIoFYAOb7BqQibzfKsJGrFFMNGY7Fdb5-AEEIFwEFgx64rooaQgAhEiYkskoLFM8ENbjzDFECBJMScp6YPwDXgjdjzCnMhrAMQABW+GNZaElDRJcogCpQggmyrgclgkiFsw6yOlHOYyVSRBNEIFcG4wAIMF19PtAx6MApKsuIAlqNOvNdOPdAGsCAQ-FBfdzI3zTe71pd0az0fMU-6Izn027HpEaGIwMWCzLmKxVUH3SGCGHfXrC0xFmBg10qxAk+grrbPiy2Q6biOuR2eSXJ2bPhaYFbpDaJ5P27XSWnoyP56sUBBLm3idPt49PpRzxCr5eM49WK4YNZaTWHVvizuzQ+JM-uAAhVcr1TD9+XvR8kGIAokgULYUFfAMzzvCZvxgCCoJg0QAPgotySZTpmSsFoAA8hzXOBKw4UApl9MBY2PBoaC9Vg6SYWiewmERwDhDAHQ0fFFlYJwmwOYSxDo4dJ0YtgWLE9iuk4yBNiPdQmFZdknBvUCJgIIjJDODBfUInSBgxTYdlogBaUzgB2MSLKMoie1o+jaCYljrPM2M5IaBTuOUjRXgUISb1E8SyM+KTmJMszZOEjiGEUnj2g0aFYXhb5TCYFDfw0i8tJEgRDkQOJeHYAZhggAB9RY8hoewg0oABKTIAHoACo2vqNq4AANWkPN0GyOBRGiCBykwMU4DIeY4HFfwl38AADUpJCIAhFqmh85q6lqhBalq4E0cjMXYVzWGzYakwwlA4AEfilgAOiSMAhUuWp7sJZh0BFKBTDUt8ACt5m9JgmoEABIB7A1qeNEyIAYACkIH8JAiMPEwwYaB6VrW6pqhOKBcH7RqVAAPkuh7fnxmAuAeoVeVFcVcAZRrGs6B6MQgapmr2g6js2XlTsY86iEuohrrgUoDpaNEkk4BFMTuhZHue16CHe5XPqYb6XVwf6BiBkoGrZyHoYQWGk0R5GKjR9AyFBtmsZx748YQemfrFMQioQX4mrJyYfmqd3GbEZmoFBVn2c57nWoOgBZJJzuAMBTvcT1BvEOALLgRb+0WpWCTEJ6XoUdWEA+gYdd+vWxwN4HjYhqHvRhhNLeYJGUdtjHHbgbHVpd6muCJsASeUcn84mAASBB+0pwOCbp4UQ7DiPqE0AA5AAROBp9ng9UKIhsxEHqAHuy2lGtvHOe45rmeYEfa4ATpOU-8cVpBFJRIhz531rgapQAp3IBgQ6W82YfSLqrUuGsCSV2AD9P6tdmCGxBmDU2zdzat3hu3a2qN0b20xr3X+eMCbD1HuPR4u8wBzxPovBm9Amb0nDgQS+cAADyZgd4z2ofvVGR9aHn24JfToi0b7ENqIuZcmBvZpBgKUBMKA-ZjwklNbI8AYDNBXCoOAm9S4PQPKUGOjx3A5BQnHUQS5tEaOtFnOAOwAAMji4DdQAGz2OcXANxHiABMAAWDxABmex3gJjfBgAiG6sj5FTGqJI60uAzEWMkMzTRmAeY4hvtHe+j8mDcEwGpEwZ1syKxoEkFAxgRoihgJoTWx9Ao+kCMEN4uALYbHaDONQjgSYThMfAP4RBMDaIgQ9fpUBMCU10vpGAtR6lNRCbQaA-9elwH+sNKIrSoBiG6Y8UZ4zxGYLhlAb2vtyEB2qBsmu7JWbzKvmEiJcBdm3yMZeLIHg4AbKKOoToTAO423wfsJgABhfqho4B-lcLGKwggMhCCqTUwu1QmAWCsLYb4DgWlYM2c1IAA) +[![Try it out!](https://img.shields.io/badge/electrodb-try_out_this_example_›-%23f9bd00?style=for-the-badge&logo=amazondynamodb&labelColor=1a212a)](https://electrodb.fun/?#code/JYWwDg9gTgLgBAbwKIDsbBgTwL5wGZQQhwBEApgDZkDGMhAJgEYkBQL1EKAzvABYCGjDPxhc4AXjgBtEgEECwavxIAaUrKi1Fq0gCV+wFADFoZHiQC6cfmI7cYAbhZYwZOAAlBw+JJdkIeHACQjAiXFIoAK4gjGRQFk7snDzWKKD8FGKSKGQA7nCo6FgAFAgscHAgEPSUAFyI5RVNXHEAbopk9SQAXhAQqo1NcGRoGJhd-GkgGVwDQxWtcVzAnF0AjHNw2CqNInTAjJEwZvVlQ8Hep4NNfvUXoaI783BQZACOkcCv9PV0kWRPJrba4jagUCBcSKvK7PW6kHhQQwAc02FWBQ0m6QoMPmcJICORqKarw+XzIPzgfwB13RTRQ-BAnQasMwri6BJQKJpgIq9EmSLiEEiXBxQzxjD6VEmRNpFWAXAACq8kfS0KKbqymSQJRApShWENZXAhLBeHzxszcZqulEYnEDUCeXB6MAyDB1RU8S0YES5ccQCL4ftOQ60U6ANZkMiuKAeynWoOIkPc66LY5J-iIyZxr3BrmGp0BPBcMBJpE5hMkCjyn1Ov1kANxz2V6ZgX1NUsQGPoE6W54VemMptWtmJwnXZ5G54mmBm-gWs79sWVjn5pdbOvzFAQY7D5ej-F50P97AToFn08F64UGwwIzkisH1fHo03nj3+gAIQXZ9zZZfTpvjASCtBkkQiCsKCPlqz4pkMQEgWBEGcN+0HskeNKNLShg1AAHr2i4VCWNCgCMcZgOGe74K6FAUiQFHtkMHDgBCGBMjImLTBQliblOcBcJRfb9ngNF0QJjFNMxkDLLu0gkIOZCWBecFNGQuG8AcGBxjhaldEiyxrBRAC0+nAGsAlGTpuHthRVEiZQdGmYZ4YSRUUmsbJMj3CIPEXpuAl2aJekGeJm5MUQ0lsfUMiguCkKvDoVa3h+SmTipwLbIgoSMFQXS9BAAD60xpHgZg+tgACUiR+HAADibqyFMGQAPJgOgyQSJa3nuh4XgPE4FSRtGSz1KuUgJCw2CJHgkQoFonBwAKMCNViXDFF27XcPU9XLU1FCtZtXAVZadgpAgQR9SIahDTGYi4JIG2QVwA0vG6UIoKkq0AHQfHEmBfWpGkhKUF0hCIWwVY0X25LwcRkMUIM3XEG6IMMbwQxIAB8QmvDA71wEjUBcF9rYIwpx3iNj7zFITajkxVX0AFYQIYxQAORwM1uhwGzVVYQzSIQMUkOniwS0rVxa2Lt1XT6IYJivOYgKE4GUjXCQABSEBuEguE7toTokAAwpmupwJ+NjhoYDoWDslUOEAA) The `attributes` object contains every Attribute defined in the Entity's Model. The `operations` object contains the following methods: diff --git a/www/src/pages/en/queries/pagination.mdx b/www/src/pages/en/queries/pagination.mdx index 2afae1dd..54a544a5 100644 --- a/www/src/pages/en/queries/pagination.mdx +++ b/www/src/pages/en/queries/pagination.mdx @@ -29,8 +29,7 @@ The terminal method `go()` accepts a `cursor` when executing a `query` or `scan` ```typescript const results1 = await MallStores.query.leases({ mallId }).go(); // no "cursor" passed to `.go()` -const results2 = await MallStores.query - .leases({ mallId }) +const results2 = await MallStores.query.leases({ mallId }) .go({ cursor: results1.cursor }); // Paginate by querying with the "cursor" from your first query ``` @@ -79,6 +78,35 @@ type GoResults = { }; ``` +## Execution Options + +### Count +The execution option `count` allows you to specify a specific number of items to be returned. This is often a difficult task with DynamoDB because queries do not always return a consistent number of items. If your query includes filters, and requires pagination, it can be even harder to return a specific number of items reliably. +When using `count`, ElectroDB will paginate your query against DynamoDB until the number of items matches the supplied `count`, create a custom cursor, and return the items found. This option is recommend for queries with numerous and/or strict attribute filters where end-user or external pagination is necessary. + +```typescript + +type GetItemsOptions = { + mallId: string; + cursor: string; + limit?: number; +} + +async function getLeases(options: GetItemsOptions) { + const { mallId, cursor, limit } = options; + + if (limit < 1 || limit >= 200) { + throw new Error('Limit must be at least 1 and at most 200'); + } + + return MallStores.query.leases({ mallId }) + .go({ cursor, count: limit }); +} +``` + +### Pages +The execution option `pages` allows you to automatically perform multiple queries against DynamoDB. By default, ElectroDB queries will perform one request against DynamoDB. With `pages`, you can specify the number of queries you'd like to occur under the hood before returning. Furthermore, if you would like ElectroDB to return all results for a given query (i.e., exhausting pagination for a given query automatically) you can use the option `{pages: "all"}`. Note that while option is convenient, it may not performant for some workflows. + ## Example Simple pagination example: @@ -107,4 +135,4 @@ async function getTeamMembers(team: string) { return members; } -``` +``` \ No newline at end of file diff --git a/www/src/pages/en/queries/query.mdx b/www/src/pages/en/queries/query.mdx index 22137fb0..2a6474ed 100644 --- a/www/src/pages/en/queries/query.mdx +++ b/www/src/pages/en/queries/query.mdx @@ -12,7 +12,7 @@ keywords: layout: ../../../layouts/MainLayout.astro --- -Queries are the _**most efficient**_ way to retrieve items from DynamoDB, which is why it is recommend to be very thoughtful when modeling your indexes. +Queries are the _**most efficient**_ way to retrieve items from DynamoDB, which is why it is recommended that you be very thoughtful when modeling your indexes. import ExampleSetup from "../../../partials/entity-query-example-setup.mdx"; @@ -22,11 +22,11 @@ import ExampleSetup from "../../../partials/entity-query-example-setup.mdx"; ElectroDB queries use DynamoDB's `query` method to retrieve items based on your table's indexes. -Forming a composite **Partition Key** and **Sort Key** is a critical step in planning **Access Patterns** in **DynamoDB**. When planning composite keys, it is crucial to consider the order in which they are _composed_. As of the time of writing this documentation, **DynamoDB** has the following constraints that should be taken into account when planning your **Access Patterns**: +Designing a composite **Partition Key** and **Sort Key** is a critical step in planning **Access Patterns** in **DynamoDB**. When planning composite keys, it is crucial to consider the order in which they are _composed_. As of the time of writing this documentation, **DynamoDB** has the following constraints that should be taken into account when planning your **Access Patterns**: 1. You must always supply the **Partition Key** in full for all queries to **DynamoDB**. 2. You currently have the following operators available to narrow results via your [**Sort Key**](#sort-key-operations) -3. To act on single record, you will need to know the full **Partition Key** and **Sort Key** for that record. +3. To act on a single record, you must know the full **Partition Key** and **Sort Key** for that record. ### Providing composite attributes to queries @@ -90,7 +90,7 @@ await StoreLocations.query #### Bad: Composite Attributes not included in order -This example will \_not throw, but will it ignore `unitId` because `storeId` was not supplied and because ElectroDB cannot logically build a valid sort key string without the all values being provided left to right. +This example will not throw, but will it ignore `unitId` because `storeId` was not supplied ElectroDB cannot logically build a valid sort key string without the all values being provided left to right. ```typescript await StoreLocations.query @@ -120,7 +120,7 @@ Unlike Partition Keys, Sort Keys can be partially provided. We can leverage this All queries require (_at minimum_) the **Composite Attributes** included in its defined **Partition Key**. **Composite Attributes** you define on the **Sort Key** can be partially supplied, but must be supplied in the order they are defined. -> Composite Attributes must be supplied in the order they are composed when invoking the **Access Pattern\***. This is because composite attributes are used to form a concatenated key string, and if attributes supplied out of order, it is not possible to fill the gaps in that concatenation. +> Composite Attributes must be supplied in the order they are composed when invoking the **Access Pattern\***. This is because composite attributes are used to form a concatenated key string, and if attributes are supplied out of order, it is not possible to fill the gaps in that concatenation. ### Sort Key Operations @@ -135,7 +135,7 @@ All queries require (_at minimum_) the **Composite Attributes** included in its #### Begins With Queries -One important consideration when using Sort Key Operations to make is when to use and not to use "begins". +One important consideration when using Sort Key Operations is when to use and not to use "begins". It is possible to supply partially supply Sort Key composite attributes. Sort Key attributes must be provided in the order they are defined, but it's possible to provide only a subset of the Sort Key Composite Attributes to ElectroDB. By default, when you supply a partial Sort Key in the Access Pattern method, ElectroDB will create a `beginsWith` query. The difference between that and using `.begins()` is that, with a `.begins()` query, ElectroDB will not post-pend the next composite attribute's label onto the query. @@ -216,7 +216,7 @@ await StoreLocations.query ```typescript await StoreLocations.query .leases({ storeId: "LatteLarrys" }) - .begins({ leaseEndDate: "2020" }) + .begins({ leaseEndDate: "2020-00-00" }) .go(); ``` @@ -225,7 +225,7 @@ await StoreLocations.query ```typescript await StoreLocations.query .leases({ storeId: "LatteLarrys" }) - .gt({ leaseEndDate: "2020-03" }) + .gt({ leaseEndDate: "2020-04-00" }) .go(); ``` @@ -234,7 +234,7 @@ await StoreLocations.query ```typescript await StoreLocations.query .leases({ storeId: "LatteLarrys" }) - .gte({ leaseEndDate: "2020-03" }) + .gte({ leaseEndDate: "2020-03-00" }) .go(); ``` @@ -243,7 +243,7 @@ await StoreLocations.query ```typescript await StoreLocations.query .leases({ storeId: "LatteLarrys" }) - .lt({ leaseEndDate: "2021-01" }) + .lt({ leaseEndDate: "2021-00-00" }) .go(); ``` @@ -252,7 +252,7 @@ await StoreLocations.query ```typescript await StoreLocations.query .leases({ storeId: "LatteLarrys" }) - .lte({ leaseEndDate: "2021-02" }) + .lte({ leaseEndDate: "2021-02-00" }) .go(); ``` @@ -261,7 +261,7 @@ await StoreLocations.query ```typescript await StoreLocations.query .leases({ storeId: "LatteLarrys" }) - .between({ leaseEndDate: "2010" }, { leaseEndDate: "2020" }) + .between({ leaseEndDate: "2010-00-00" }, { leaseEndDate: "2020-99-99" }) .go(); ``` @@ -270,7 +270,7 @@ await StoreLocations.query ```typescript await StoreLocations.query .leases({ storeId: "LatteLarrys" }) - .gte({ leaseEndDate: "2010" }) + .gte({ leaseEndDate: "2010-00-00" }) .where( (attr, op) => ` ${op.eq(attr.cityId, "Atlanta1")} AND ${op.contains( @@ -282,6 +282,15 @@ await StoreLocations.query .go(); ``` +## Comparison Queries +When performing comparison queries (e.g. `gt`, `gte`, `lte`, `lt`, `between`) it is often necessary to add additional filters. Understanding how a comparison query will impact the items returned can be challenging due to the unintuitive nature of sorting and multi-composite attribute Sort Keys. + +By default, ElectroDB builds comparison queries without applying attribute filters; the execution option `{ compare: "keys" }` and not specifying `compare` are functionally equivalent. In this way, the query's comparison applies to the item's "keys". This approach gives you complete control of your indexes, though your query may return unexpected data if you are not careful with item sorting. + +When using `{ compare: "attributes" }`, the `compare` option instructs ElectroDB to apply attribute filters on each Sort Key composite attribute provided. In this way, the query comparison applies to the item's "attributes." + +The option `{ compare: "v2" }` offers backwards compatibility with the ElectroDB `v2` API. This option exists to assist in migration, should be avoided for greenfield projects, and will be removed in the near future. + ## Execution Options import PartialExample from "../../../partials/query-options.mdx"; diff --git a/www/src/pages/en/recipes/item-counters.mdx b/www/src/pages/en/recipes/item-counters.mdx index 0e7ee77e..bdf1534f 100644 --- a/www/src/pages/en/recipes/item-counters.mdx +++ b/www/src/pages/en/recipes/item-counters.mdx @@ -362,7 +362,7 @@ function createMembersCountFn(options: CreateCountFnOptions): CountFn { .members({ accountId }) .go({ // `raw` allows you return the unprocess results directly from DynamoDB (escape hatch) - raw: true, + data: 'raw', // paginate through the results using the cursor cursor: next, // `params` allows you to append additional parameters to the DynamoDB query (escape hatch) @@ -379,7 +379,7 @@ function createUsersCountFn(options: CreateCountFnOptions): CountFn { return User.query.account({ accountId }) .go({ // `raw` allows you return the unprocess results directly from DynamoDB (escape hatch) - raw: true, + data: 'raw', // paginate through the results using the cursor cursor: next, // `params` allows you to append additional parameters to the DynamoDB query (escape hatch) @@ -396,7 +396,7 @@ function createUsersScanCountFn(options: CreateCountFnOptions): CountFn { .where((attr, op) => op.eq(attr.accountId, accountId)) .go({ // `raw` allows you return the unprocess results directly from DynamoDB (escape hatch) - raw: true, + data: 'raw', // paginate through the results using the cursor cursor: next, // `params` allows you to append additional parameters to the DynamoDB query (escape hatch) diff --git a/www/src/pages/en/recipes/keys-only-gsi.mdx b/www/src/pages/en/recipes/keys-only-gsi.mdx index fb87d6a1..c8b68058 100644 --- a/www/src/pages/en/recipes/keys-only-gsi.mdx +++ b/www/src/pages/en/recipes/keys-only-gsi.mdx @@ -175,7 +175,7 @@ If you do not wish to "hydrate" your query response, you still have ways you can ### Include Keys -The query execution options `{ ignoreOwnership: true, data: 'includeKeys' }` allow you to return keys on the `data` array. The option `ignoreOwnership` is also used here to limit filters applied to query. ElectroDB filters items that it cannot identify as belonging to an Entity; `KEYS_ONLY` items are then indistinguishable from unidentifiable items. +The query execution options `{ ignoreOwnership: true, data: 'includeKeys' }` allow you to return keys on the `data` array. The option `ignoreOwnership` is also used here to limit filters applied to query to identify item types. ElectroDB filters out items that it cannot identify as belonging to an Entity using identifier attributes (`__edb_e__`, and `__edb_v__`); `KEYS_ONLY` items are then indistinguishable from unidentifiable items. ```typescript // The elements in the `data` array are just the keys of the index, despite the typing saying otherwise. diff --git a/www/src/pages/en/reference/conversions.mdx b/www/src/pages/en/reference/conversions.mdx index d3019829..07299929 100644 --- a/www/src/pages/en/reference/conversions.mdx +++ b/www/src/pages/en/reference/conversions.mdx @@ -23,7 +23,7 @@ All conversion functions can be found on your entity under the `conversions` nam ### Example Entity ```typescript -import { Entity } from "electrodb"; +import { Entity, createConversions } from "electrodb"; const thing = new Entity( { @@ -79,16 +79,18 @@ const composite = { #### From Composite ```typescript +const conversions = createConversions(thing); + // from Composite to all possible index Keys -thing.conversions.fromComposite.toKeys(item); +conversions.fromComposite.toKeys(item); // from Composite to Cursor -thing.conversions.fromComposite.toCursor(item); +conversions.fromComposite.toCursor(item); // from Composite to Keys for a specific access pattern -thing.conversions.byAccessPattern.records.fromComposite.toKeys(item); +conversions.byAccessPattern.records.fromComposite.toKeys(item); // from Composite to Cursor for a specific access pattern -thing.conversions.byAccessPattern.records.fromComposite.toCursor(item); +conversions.byAccessPattern.records.fromComposite.toCursor(item); ``` #### To Keys Options @@ -96,18 +98,21 @@ thing.conversions.byAccessPattern.records.fromComposite.toCursor(item); When converting `composite` items to `keys`, you may also specify a `strict` mode which is passed as a second argument: ```typescript +const conversions = createConversions(thing); + // from Composite to all possible index Keys -thing.conversions.fromComposite.toKeys(item, { strict: "all" }); +conversions.fromComposite.toKeys(item, { strict: "all" }); // from Composite to Cursor -thing.conversions.fromComposite.toCursor(item, { strict: "all" }); +conversions.fromComposite.toCursor(item, { strict: "all" }); // from Composite to Keys for a specific access pattern -thing.conversions.byAccessPattern.records.fromComposite.toKeys(item, { +conversions.byAccessPattern.records.fromComposite.toKeys(item, { strict: "all", }); + // from Composite to Cursor for a specific access pattern -thing.conversions.byAccessPattern.records.fromComposite.toCursor(item, { +conversions.byAccessPattern.records.fromComposite.toCursor(item, { strict: "all", }); ``` @@ -117,7 +122,9 @@ thing.conversions.byAccessPattern.records.fromComposite.toCursor(item, { The following is a table explaining how the `strict` option impacts key creation. the `at root` option below refers to usage directly on the `conversions` namespace. For example: ```typescript -thing.conversions.fromComposite.toKeys(item, { strict: "all" }); +const conversions = createConversions(thing); + +conversions.fromComposite.toKeys(item, { strict: "all" }); ``` | strict | variation | access pattern type | description | @@ -150,6 +157,7 @@ const keys = { > Note: when converting Keys to Composite values, ElectroDB will destructure keys into individual attributes, but the casing of your keys will match that of your keys. Be mindful when using the composite values you receive when using case-sensitive functionality like `filter` if casing is not consistent between your keys and your attributes. ```typescript +const conv // from Keys to Composite attribute values thing.conversions.fromKeys.toComposite(keys); // from Keys to Cursor diff --git a/www/src/pages/en/reference/errors.mdx b/www/src/pages/en/reference/errors.mdx index d5d155ec..20fe4643 100644 --- a/www/src/pages/en/reference/errors.mdx +++ b/www/src/pages/en/reference/errors.mdx @@ -450,7 +450,7 @@ By default ElectroDB tries to keep the stack trace close to your code, ideally t When using pagination with a Service, ElectroDB will try to identify which Entity is associated with the supplied pager. This error can occur when you supply an invalid pager, or when you are using a different [pager option](/en/queries/pagination#pager) to a pager than what was used when retrieving it. Consult the section on [Pagination](/en/queries/pagination) to learn more. **What to do about it:** -If you are sure the pager you are passing to `.page()` is the same you received from `.page()` this could be an unexpected error. To mitigate the issue use the Query Option `{pager: "raw"}` and please open a support issue. +If you are sure the pager you are passing to `.page()` is the same you received from `.page()` this could be an unexpected error. To mitigate the issue use the Query Option `{ pager: "raw" }` and please open a support issue. ### Pager Not Unique diff --git a/www/src/partials/query-options.mdx b/www/src/partials/query-options.mdx index d5035104..483dae48 100644 --- a/www/src/partials/query-options.mdx +++ b/www/src/partials/query-options.mdx @@ -18,6 +18,7 @@ By default, **ElectroDB** enables you to work with records as the names and prop listeners Array<(event) => void>; attributes?: string[]; order?: 'asc' | 'desc'; + compare?: 'attributes' | 'keys' | 'v2'; }; ``` @@ -53,9 +54,7 @@ By default, **ElectroDB** interrogates items returned from a query for the prese ### limit **Default Value:** _none_ -Used a convenience wrapper for the DynamoDB parameter `Limit` [[read more](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#DDB-Query-request-Limit)]. When `limit` is paired option `pages:"all"`, ElectroDB will paginate DynamoDB until the limit is _at least_ reached or all items for that query have been returned; this means you may receive _more_ items than your specified `limit` but never less. - -> Note: If your use case requires returning a specific number of items at a time, the `count` option is the better option. +Used a convenience wrapper for the DynamoDB parameter `Limit` [[read more](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#DDB-Query-request-Limit)]. ### count **Default Value:** _none_ @@ -75,4 +74,12 @@ An array of callbacks that are invoked when [internal ElectroDB events](#events) ### logger **Default Value:** _none_ -A convenience option for a single event listener that semantically can be used for logging. \ No newline at end of file +A convenience option for a single event listener that semantically can be used for logging. + +### compare +**Default Value:** 'keys' +When performing comparison queries (e.g. `gt`, `gte`, `lte`, `lt`, `between`) it is often necessary to add additional filters. Understanding how a comparison query will impact the items returned can be challenging due to the unintuitive nature of sorting and multi-composite attribute Sort Keys. + +By default (`comparison: "keys"`), ElectroDB builds comparison queries without applying attribute filters. In this way, the query's comparison applies to the item's "keys". This approach gives you complete control of your indexes, though your query may return unexpected data if you are not careful with item sorting. + +When set to `attributes`, the `comparison` option instructs ElectroDB to apply attribute filters on each Sort Key composite attribute provided. In this way, the query comparison applies to the item's "attributes." \ No newline at end of file