diff --git a/CHANGELOG.md b/CHANGELOG.md index f3ecc2d8..1dac3d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -200,4 +200,8 @@ All notable changes to this project will be documented in this file. Breaking ch ## [1.10.2] - 2022-07-02 ### Fixed -- Reorganizing type definition files into single file again to appease the frontend dependency overlords in https://electrodb.fun \ No newline at end of file +- Reorganizing type definition files into single file again to appease the frontend dependency overlords in https://electrodb.fun + +## [1.11.0] - 2022-07-04 +### Added +- Adding support for "ProjectionExpressions" via the Query Option: `attributes` [[read more](./README#query-options)] \ No newline at end of file diff --git a/README.md b/README.md index f825b8e4..19e5f003 100644 --- a/README.md +++ b/README.md @@ -2607,9 +2607,10 @@ const formattedQueryResults = myEntity.parse(formattedQueryResults); Parse also accepts an optional `options` object as a second argument (see the section [Query Options](#query-options) to learn more). Currently, the following query options are relevant to the `parse()` method: -Option | Default | Notes ------------------ : ------- | ----- -`ignoreOwnership` | `true` | This property defaults to `true` here, unlike elsewhere in the application when it defaults to `false`. You can overwrite the default here with your own preference. +Option | Type | Default | Notes +----------------- : -------- : ------------------ | ----- +ignoreOwnership | boolean | `true` | This property defaults to `true` here, unlike elsewhere in the application when it defaults to `false`. You can overwrite the default here with your own preference. +attributes | string[] | _(all attributes)_ | The `attributes` option allows you to specify a subset of attributes to return # Building Queries > For hands-on learners: the following example can be followed along with **and** executed on runkit: https://runkit.com/tywalch/electrodb-building-queries @@ -4300,6 +4301,7 @@ By default, **ElectroDB** enables you to work with records as the names and prop logger?: (event) => void; listeners Array<(event) => void>; preserveBatchOrder?: boolean; + attributes?: string[]; }; ``` @@ -4307,6 +4309,7 @@ Option | Default | Description ------------------ | :------------------: | ----------- params | `{}` | Properties added to this object will be merged onto the params sent to the document client. Any conflicts with **ElectroDB** will favor the params specified here. table | _(from constructor)_ | Use a different table than the one defined in the [Service Options](#service-options) +attributes | _(all attributes)_ | The `attributes` query option allows you to specify ProjectionExpression Attributes for your `get` or `query` operation. As of `1.11.0` only root attributes are allowed to be specified. raw | `false` | Returns query results as they were returned by the docClient. includeKeys | `false` | By default, **ElectroDB** does not return partition, sort, or global keys in its response. pager | `"named"` | Used in with pagination (`.pages()`) calls to override ElectroDBs default behaviour to break apart `LastEvaluatedKeys` records into composite attributes. See more detail about this in the sections for [Pager Query Options](#pager-query-options). diff --git a/examples/taskapp/bin/load.js b/examples/taskapp/bin/load.js index 53c832b4..97026807 100644 --- a/examples/taskapp/bin/load.js +++ b/examples/taskapp/bin/load.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +/* istanbul ignore file */ const path = require("path"); const taskr = require("../src/taskr"); const Loader = require("../lib/loader"); diff --git a/examples/taskapp/lib/client/index.js b/examples/taskapp/lib/client/index.js index 684621f9..29639905 100644 --- a/examples/taskapp/lib/client/index.js +++ b/examples/taskapp/lib/client/index.js @@ -1,3 +1,4 @@ +/* istanbul ignore file */ const DynamoDB = require("aws-sdk/clients/dynamodb"); /** diff --git a/examples/taskapp/lib/loader/index.js b/examples/taskapp/lib/loader/index.js index 3df6edb5..e52789d7 100644 --- a/examples/taskapp/lib/loader/index.js +++ b/examples/taskapp/lib/loader/index.js @@ -1,3 +1,4 @@ +/* istanbul ignore file */ const makeLoader = require("./mock"); const makeTabler = require("../table"); const definition = require("../table/definition.json") diff --git a/examples/taskapp/lib/loader/mock.js b/examples/taskapp/lib/loader/mock.js index 6a842dff..80328cdd 100644 --- a/examples/taskapp/lib/loader/mock.js +++ b/examples/taskapp/lib/loader/mock.js @@ -1,3 +1,4 @@ +/* istanbul ignore file */ const uuid = require("uuid").v4; const moment = require("moment"); diff --git a/examples/taskapp/src/index.js b/examples/taskapp/src/index.js index 8d4cc344..8be4ee86 100644 --- a/examples/taskapp/src/index.js +++ b/examples/taskapp/src/index.js @@ -1,3 +1,4 @@ +/* istanbul ignore file */ const path = require("path"); const moment = require("moment"); const taskr = require("./taskr"); diff --git a/examples/taskapp/src/taskr.js b/examples/taskapp/src/taskr.js index 0efbd886..3ebc2892 100644 --- a/examples/taskapp/src/taskr.js +++ b/examples/taskapp/src/taskr.js @@ -1,3 +1,4 @@ +/* istanbul ignore file */ process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1; const { Service } = require("../../.."); const client = require("../lib/client"); diff --git a/examples/taskapp_typescript/src/client/index.ts b/examples/taskapp_typescript/src/client/index.ts index f1df2849..eaef8932 100644 --- a/examples/taskapp_typescript/src/client/index.ts +++ b/examples/taskapp_typescript/src/client/index.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import DynamoDB from "aws-sdk/clients/dynamodb"; /** diff --git a/examples/taskapp_typescript/src/index.ts b/examples/taskapp_typescript/src/index.ts index 05d30076..238491b7 100644 --- a/examples/taskapp_typescript/src/index.ts +++ b/examples/taskapp_typescript/src/index.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import path from "path"; import moment from "moment"; diff --git a/examples/taskapp_typescript/src/models/employees.ts b/examples/taskapp_typescript/src/models/employees.ts index d55871c6..30ceb493 100644 --- a/examples/taskapp_typescript/src/models/employees.ts +++ b/examples/taskapp_typescript/src/models/employees.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import {v4 as uuid} from "uuid"; import moment from "moment"; diff --git a/examples/taskapp_typescript/src/models/offices.ts b/examples/taskapp_typescript/src/models/offices.ts index 78596697..31e24b40 100644 --- a/examples/taskapp_typescript/src/models/offices.ts +++ b/examples/taskapp_typescript/src/models/offices.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ const schema = { "model": { "entity": "offices", diff --git a/examples/taskapp_typescript/src/models/tasks.ts b/examples/taskapp_typescript/src/models/tasks.ts index b7e30a1a..8d8f8752 100644 --- a/examples/taskapp_typescript/src/models/tasks.ts +++ b/examples/taskapp_typescript/src/models/tasks.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ const schema = { "model": { "entity": "tasks", diff --git a/examples/taskapp_typescript/src/taskr.ts b/examples/taskapp_typescript/src/taskr.ts index 10107358..6eb13e25 100644 --- a/examples/taskapp_typescript/src/taskr.ts +++ b/examples/taskapp_typescript/src/taskr.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = "1"; import {Entity, Service} from "../../../"; import client from "./client"; diff --git a/examples/versioncontrol/database/index.ts b/examples/versioncontrol/database/index.ts index 65ce7a70..45ed4141 100644 --- a/examples/versioncontrol/database/index.ts +++ b/examples/versioncontrol/database/index.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import {Service} from "../../../"; import {repositories} from "./repositories"; diff --git a/examples/versioncontrol/database/issues.ts b/examples/versioncontrol/database/issues.ts index 19587980..c42eec98 100644 --- a/examples/versioncontrol/database/issues.ts +++ b/examples/versioncontrol/database/issues.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import {Entity} from "../../../"; import moment from "moment"; import {NotYetViewed, TicketTypes, IssueTicket, StatusTypes, toStatusString, toStatusCode} from "./types"; diff --git a/examples/versioncontrol/database/pullrequests.ts b/examples/versioncontrol/database/pullrequests.ts index 38657feb..ca7a57d4 100644 --- a/examples/versioncontrol/database/pullrequests.ts +++ b/examples/versioncontrol/database/pullrequests.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import {Entity} from "../../../"; import moment from "moment"; import {NotYetViewed, TicketTypes, PullRequestTicket, StatusTypes, toStatusString, toStatusCode} from "./types"; diff --git a/examples/versioncontrol/database/repositories.ts b/examples/versioncontrol/database/repositories.ts index e9b81509..ea4229a7 100644 --- a/examples/versioncontrol/database/repositories.ts +++ b/examples/versioncontrol/database/repositories.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import {Entity} from "../../.."; import moment from "moment"; diff --git a/examples/versioncontrol/database/subscriptions.ts b/examples/versioncontrol/database/subscriptions.ts index af0d5c70..1b681987 100644 --- a/examples/versioncontrol/database/subscriptions.ts +++ b/examples/versioncontrol/database/subscriptions.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import {Entity} from "../../../"; import moment from "moment"; import {TicketTypes, IsNotTicket} from "./types"; diff --git a/examples/versioncontrol/database/types.ts b/examples/versioncontrol/database/types.ts index 5de80e5f..2d5097fd 100644 --- a/examples/versioncontrol/database/types.ts +++ b/examples/versioncontrol/database/types.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import {IssueCommentIds, PullRequestCommentIds} from "./index"; export const StatusTypes = ["Open", "Closed"] as const; diff --git a/examples/versioncontrol/database/users.ts b/examples/versioncontrol/database/users.ts index 87c5ae09..faae96c6 100644 --- a/examples/versioncontrol/database/users.ts +++ b/examples/versioncontrol/database/users.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import {Entity} from "../../.."; import moment from "moment"; diff --git a/examples/versioncontrol/index.ts b/examples/versioncontrol/index.ts index 65f70d0d..3e4345a5 100644 --- a/examples/versioncontrol/index.ts +++ b/examples/versioncontrol/index.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ // VersionControl (git) Service // This example demonstrates more advanced modeling techniques using ElectroDB diff --git a/index.d.ts b/index.d.ts index 796772b2..d1e710e4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -79,6 +79,7 @@ export type CollectionWhereCallback}, A extends string, F extends string, C extends string, S extends Schema, I extends Partial>, T> = (where: CollectionWhereCallback) => T; + export interface WhereRecordsActionOptions}, A extends string, F extends string, C extends string, S extends Schema, I extends Partial>, Items, IndexCompositeAttributes> { go: GoRecord; params: ParamRecord; @@ -349,6 +350,13 @@ export type CollectionItem, COLLECTION extends keyo : never> : never +export interface QueryBranches, ResponseItem, IndexCompositeAttributes> { + go: GoQueryTerminal; + params: ParamTerminal; + page: PageQueryTerminal; + where: WhereClause,QueryBranches> +} export interface RecordsActionOptions, Items, IndexCompositeAttributes> { @@ -359,11 +367,16 @@ export interface RecordsActionOptions, ResponseType> { - go: GoRecord; - params: ParamRecord; + go: GoGetTerminal; + params: ParamTerminal; where: WhereClause,SingleRecordOperationOptions>; } +export interface BatchGetRecordOperationOptions, ResponseType> { + go: GoBatchGetTerminal + params: ParamTerminal; +} + export interface PutRecordOperationOptions, ResponseType> { go: GoRecord; params: ParamRecord; @@ -406,17 +419,17 @@ export type RemoveRecord, SetAttr, IndexCompositeAttributes, TableItem> = DataUpdateMethod, SetRecordActionOptions> -interface QueryOperations, CompositeAttributes, TableItem, IndexCompositeAttributes> { - between: (skCompositeAttributesStart: CompositeAttributes, skCompositeAttributesEnd: CompositeAttributes) => RecordsActionOptions,IndexCompositeAttributes>; - gt: (skCompositeAttributes: CompositeAttributes) => RecordsActionOptions,IndexCompositeAttributes>; - gte: (skCompositeAttributes: CompositeAttributes) => RecordsActionOptions,IndexCompositeAttributes>; - lt: (skCompositeAttributes: CompositeAttributes) => RecordsActionOptions,IndexCompositeAttributes>; - lte: (skCompositeAttributes: CompositeAttributes) => RecordsActionOptions,IndexCompositeAttributes>; - begins: (skCompositeAttributes: CompositeAttributes) => RecordsActionOptions,IndexCompositeAttributes>; - go: GoRecord>; - params: ParamRecord; - page: PageRecord,IndexCompositeAttributes>; - where: WhereClause,RecordsActionOptions,IndexCompositeAttributes>> +interface QueryOperations, CompositeAttributes, ResponseItem, IndexCompositeAttributes> { + between: (skCompositeAttributesStart: CompositeAttributes, skCompositeAttributesEnd: CompositeAttributes) => QueryBranches; + gt: (skCompositeAttributes: CompositeAttributes) => QueryBranches; + gte: (skCompositeAttributes: CompositeAttributes) => QueryBranches; + lt: (skCompositeAttributes: CompositeAttributes) => QueryBranches; + lte: (skCompositeAttributes: CompositeAttributes) => QueryBranches; + begins: (skCompositeAttributes: CompositeAttributes) => QueryBranches; + go: GoQueryTerminal; + params: ParamTerminal; + page: PageQueryTerminal; + where: WhereClause,QueryBranches> } export type Queries> = { @@ -424,7 +437,7 @@ export type Queries extends infer SK // If there is no SK, dont show query operations (when an empty array is provided) ? [keyof SK] extends [never] - ? RecordsActionOptions[], AllTableIndexCompositeAttributes & Required> + ? QueryBranches, AllTableIndexCompositeAttributes & Required> // If there is no SK, dont show query operations (When no PK is specified) : S["indexes"][I] extends IndexWithSortKey ? QueryOperations< @@ -434,7 +447,7 @@ export type Queries, AllTableIndexCompositeAttributes & Required & SK > - : RecordsActionOptions[], AllTableIndexCompositeAttributes & Required & SK> + : QueryBranches, AllTableIndexCompositeAttributes & Required & SK> : never } @@ -464,7 +477,8 @@ export interface QueryOptions { } // subset of QueryOptions -export interface ParseOptions { +export interface ParseOptions { + attributes?: ReadonlyArray; ignoreOwnership?: boolean; } @@ -510,6 +524,162 @@ export type OptionalDefaultEntityIdentifiers = { __edb_v__?: string; } +interface GoBatchGetTerminalOptions { + raw?: boolean; + table?: string; + limit?: number; + params?: object; + includeKeys?: boolean; + originalErr?: boolean; + ignoreOwnership?: boolean; + pages?: number; + attributes?: ReadonlyArray; + unprocessed?: "raw" | "item"; + concurrency?: number; + preserveBatchOrder?: boolean; + listeners?: Array; + logger?: ElectroEventListener; +} + +interface GoQueryTerminalOptions { + raw?: boolean; + table?: string; + limit?: number; + params?: object; + includeKeys?: boolean; + originalErr?: boolean; + ignoreOwnership?: boolean; + pages?: number; + attributes?: ReadonlyArray; + listeners?: Array; + logger?: ElectroEventListener; +} + +interface PageQueryTerminalOptions extends GoQueryTerminalOptions { + pager?: "raw" | "item" | "named"; + raw?: boolean; + table?: string; + limit?: number; + includeKeys?: boolean; + originalErr?: boolean; + ignoreOwnership?: boolean; + attributes?: ReadonlyArray; + listeners?: Array; + logger?: ElectroEventListener; +} + +export interface ParamTerminalOptions { + table?: string; + limit?: number; + params?: object; + originalErr?: boolean; + attributes?: ReadonlyArray; + response?: "default" | "none" | 'all_old' | 'updated_old' | 'all_new' | 'updated_new'; +} + +type GoBatchGetTerminal, ResponseItem> = >(options?: Options) => + Options extends GoBatchGetTerminalOptions + ? 'preserveBatchOrder' extends keyof Options + ? Options['preserveBatchOrder'] extends true + ? Promise<[ + Array>, Array>> + ]> + : Promise<[ + Array>, Array>> + ]> + : Promise<[ + Array>, Array>> + ]> + : 'preserveBatchOrder' extends keyof Options + ? Options['preserveBatchOrder'] extends true + ? [Array>, Array>>] + : [Array>, Array>>] + : [Array>, Array>>] + +type GoGetTerminal, ResponseItem> = >(options?: Options) => + Options extends GoQueryTerminalOptions + ? Promise<{ + [ + Name in keyof ResponseItem as Name extends Attr + ? Name + : never + ]: ResponseItem[Name] + } | null> + : Promise + +export type GoQueryTerminal, Item> = >(options?: Options) => + Options extends GoQueryTerminalOptions + ? Promise> + : Promise> + +export type EntityParseSingleItem, ResponseItem> = + >(item: ParseSingleInput, options?: Options) => + Options extends ParseOptions + ? { + [ + Name in keyof ResponseItem as Name extends Attr + ? Name + : never + ]: ResponseItem[Name] + } | null + : ResponseItem | null + +export type EntityParseMultipleItems, ResponseItem> = + >(item: ParseMultiInput, options?: Options) => + Options extends ParseOptions + ? Array<{ + [ + Name in keyof ResponseItem as Name extends Attr + ? Name + : never + ]: ResponseItem[Name] + }> + : Array + + +export type PageQueryTerminal, Item, CompositeAttributes> = >(page?: (CompositeAttributes & OptionalDefaultEntityIdentifiers) | null, options?: Options) => + Options extends GoQueryTerminalOptions + ? Promise<[ + (CompositeAttributes & OptionalDefaultEntityIdentifiers) | null, + Array<{ + [ + Name in keyof Item as Name extends Attr + ? Name + : never + ]: Item[Name] + }> + ]> + : Promise<[ + (CompositeAttributes & OptionalDefaultEntityIdentifiers) | null, + Array + ]>; + +export type ParamTerminal, ResponseItem> =

= ParamTerminalOptions>(options?: Options) => P; + export type GoRecord = (options?: Options) => Promise; export type BatchGoRecord = (options?: O) => @@ -1037,18 +1207,6 @@ export type ItemAttribute = : never : never -type FormattedPutMapAttributes = { - [P in keyof A["properties"]]: A["properties"][P] extends infer M - ? M extends HiddenAttribute - ? false - : M extends DefaultedAttribute - ? false - : M extends RequiredAttribute - ? true - : false - : false -} - export type ReturnedAttribute = A["type"] extends infer R ? R extends "string" ? string @@ -1079,27 +1237,6 @@ export type ReturnedAttribute = : never : never } - // SkipKeys<{ - // [P in keyof A["properties"]]: A["properties"][P] extends infer M - // ? M extends Attribute - // ? M extends HiddenAttribute - // ? SkipValue - // : M extends RequiredAttribute - // ? ReturnedAttribute - // : SkipValue - // : never - // : never - // }> & SkipKeys<{ - // [P in keyof A["properties"]]?: A["properties"][P] extends infer M - // ? M extends Attribute - // ? M extends HiddenAttribute - // ? SkipValue - // : M extends RequiredAttribute - // ? SkipValue - // : ReturnedAttribute | undefined - // : never - // : never - // }> : never : R extends "list" ? "items" extends keyof A @@ -1151,27 +1288,6 @@ export type CreatedAttribute = : never : never } - // ? SkipKeys<{ - // [P in keyof A["properties"]]: A["properties"][P] extends infer M - // ? M extends Attribute - // ? M extends HiddenAttribute - // ? SkipValue - // : M extends DefaultedAttribute - // ? SkipValue - // : M extends RequiredAttribute - // ? CreatedAttribute - // : SkipValue - // : never - // : never - // }> & SkipKeys<{ - // [P in keyof A["properties"]]?: A["properties"][P] extends infer M - // ? M extends Attribute - // ? M extends HiddenAttribute - // ? SkipValue - // : CreatedAttribute | undefined - // : never - // : never - // }> : never : R extends "list" ? "items" extends keyof A @@ -1632,9 +1748,8 @@ export class Entity): SingleRecordOperationOptions | null>; - get(key: AllTableIndexCompositeAttributes[]): BulkRecordOperationOptions>>, Array>>], [Array> | null>, Array>>]>; - + get(key: AllTableIndexCompositeAttributes): SingleRecordOperationOptions>; + get(key: AllTableIndexCompositeAttributes[]): BatchGetRecordOperationOptions>; delete(key: AllTableIndexCompositeAttributes): DeleteRecordOperationOptions>; delete(key: AllTableIndexCompositeAttributes[]): BulkRecordOperationOptions[], AllTableIndexCompositeAttributes[]>; @@ -1671,8 +1786,26 @@ export class Entity[], TableIndexCompositeAttributes>; query: Queries; - parse(item: ParseSingleInput, options?: ParseOptions): ResponseItem | null; - parse(item: ParseMultiInput, options?: ParseOptions): ResponseItem[]; + parse>>(item: ParseSingleInput, options?: Options): + Options extends ParseOptions + ? { + [ + Name in keyof ResponseItem as Name extends Attr + ? Name + : never + ]: ResponseItem[Name] + } | null + : ResponseItem | null + parse>>(item: ParseMultiInput, options?: Options): + Options extends ParseOptions + ? Array<{ + [ + Name in keyof ResponseItem as Name extends Attr + ? Name + : never + ]: ResponseItem[Name] + }> + : Array> setIdentifier(type: "entity" | "version", value: string): void; client: any; } diff --git a/index.test-d.ts b/index.test-d.ts index 504a4df4..a08cd711 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,4 +1,4 @@ -import { WhereAttributeSymbol, UpdateEntityItem, Schema, EntityItem, Entity, Service } from './'; +import {WhereAttributeSymbol, UpdateEntityItem, Schema, EntityItem, Entity, Service, ResponseItem} from './'; import {expectType, expectError, expectAssignable, expectNotAssignable, expectNotType} from 'tsd'; import * as tests from './test/tests.test-d'; @@ -330,7 +330,7 @@ type Item = { attr1?: string; attr2: string; attr3?: "123" | "def" | "ghi" | undefined; - attr4: string; + attr4: 'abc' | 'ghi'; attr5?: string; attr6?: number; attr7?: any; @@ -343,7 +343,7 @@ type ItemWithoutSK = { attr1?: string; attr2?: string; attr3?: "123" | "def" | "ghi" | undefined; - attr4: string; + attr4: 'abc' | 'def'; attr5?: string; attr6?: number; attr7?: any; @@ -355,7 +355,7 @@ const item: Item = { attr1: "attr1", attr2: "attr2", attr3: "def", - attr4: "attr4", + attr4: "abc", attr5: "attr5", attr6: 123, attr7: "attr7", @@ -437,9 +437,9 @@ let getKeys = ((val) => {}) as GetKeys; expectAssignable(getSingleFinishersWithoutSK); expectAssignable(getBatchFinishersWithoutSK); entityWithSK.get([{attr1: "adg", attr2: "ada"}]).go({concurrency: 24, preserveBatchOrder: true}); - entityWithSK.get([{attr1: "adg", attr2: "ada"}]).params({concurrency: 24, preserveBatchOrder: true}); + expectError(entityWithSK.get([{attr1: "adg", attr2: "ada"}]).params({concurrency: 24, preserveBatchOrder: true})); entityWithoutSK.get([{attr1: "adg"}]).go({concurrency: 24, preserveBatchOrder: true}); - entityWithoutSK.get([{attr1: "adg"}]).params({concurrency: 24, preserveBatchOrder: true}); + expectError(entityWithoutSK.get([{attr1: "adg"}]).params({concurrency: 24, preserveBatchOrder: true})); let getSingleGoWithSK = entityWithSK.get({attr1: "adg", attr2: "ada"}).go; let getSingleGoWithoutSK = entityWithoutSK.get({attr1: "adg"}).go; @@ -465,11 +465,11 @@ let getKeys = ((val) => {}) as GetKeys; type GetBatchParamsParamsWithSK = Parameter; type GetBatchParamsParamsWithoutSK = Parameter; - expectAssignable({includeKeys: true, originalErr: true, params: {}, raw: true, table: "abc"}); - expectAssignable({includeKeys: true, originalErr: true, params: {}, raw: true, table: "abc"}); + expectAssignable({originalErr: true, params: {}, table: "abc"}); + expectAssignable({originalErr: true, params: {}, table: "abc"}); - expectAssignable({includeKeys: true, originalErr: true, params: {}, raw: true, table: "abc"}); - expectAssignable({includeKeys: true, originalErr: true, params: {}, raw: true, table: "abc"}); + expectAssignable({originalErr: true, params: {}, table: "abc"}); + expectAssignable({originalErr: true, params: {}, table: "abc"}); expectError({concurrency: 10, unprocessed: "raw", preserveBatchOrder: true}); expectError({concurrency: 10, unprocessed: "raw", preserveBatchOrder: true}); @@ -480,12 +480,16 @@ let getKeys = ((val) => {}) as GetKeys; expectAssignable({includeKeys: true, originalErr: true, params: {}, raw: true, table: "abc", concurrency: 10, unprocessed: "raw"}); expectAssignable({includeKeys: true, originalErr: true, params: {}, raw: true, table: "abc", concurrency: 10, unprocessed: "raw"}); - expectAssignable({includeKeys: true, originalErr: true, params: {}, raw: true, table: "abc", concurrency: 10, unprocessed: "raw"}); - expectAssignable({includeKeys: true, originalErr: true, params: {}, raw: true, table: "abc", concurrency: 10, unprocessed: "raw"}); + expectAssignable({originalErr: true, params: {}, table: "abc", attributes: ['attr1']}); + expectAssignable({originalErr: true, params: {}, table: "abc", attributes: ['attr1']}); - // Results - expectAssignable>(entityWithSK.get({attr1: "abc", attr2: "def"}).go()); - expectAssignable>(entityWithoutSK.get({attr1: "abc"}).go()); + expectNotAssignable({includeKeys: true, originalErr: true, params: {}, raw: true, table: "abc", concurrency: 10, unprocessed: "raw", attributes: ['attrz1']}); + expectNotAssignable({includeKeys: true, originalErr: true, params: {}, raw: true, table: "abc", concurrency: 10, unprocessed: "raw", attributes: ['attrz1']}); + expectNotAssignable({attributes: ['attrz1']}); + expectNotAssignable({attributes: ['attrz1']}); + + expectAssignable>(entityWithSK.get({attr1: "abc", attr2: "def"}).go()); + expectAssignable>(entityWithoutSK.get({attr1: "abc"}).go()); expectAssignable<"paramtest">(entityWithSK.get({attr1: "abc", attr2: "def"}).params<"paramtest">()); expectAssignable<"paramtest">(entityWithoutSK.get({attr1: "abc"}).params<"paramtest">()); entityWithSK.get([{attr1: "abc", attr2: "def"}]).go().then(results => { @@ -3088,6 +3092,32 @@ entityWithComplexShapes.query ) }) +expectError(() => { + entityWithSK.parse({Items: []}, { + attributes: ['attr1', 'oops'] + }); +}) +const parsedWithAttributes = entityWithSK.parse({Items: []}, { + attributes: ['attr1', 'attr2', 'attr3'] +}); + +const parsedWithAttributesSingle = entityWithSK.parse({Item: {}}, { + attributes: ['attr1', 'attr2', 'attr3'] +}); + +expectType<{ + attr1: string; + attr2: string; + attr3?: '123' | 'def' | 'ghi' | undefined; +}[]>(magnify(parsedWithAttributes)); + +expectType<{ + attr1: string; + attr2: string; + attr3?: '123' | 'def' | 'ghi' | undefined; +} | null>(magnify(parsedWithAttributesSingle)); + + entityWithComplexShapes.get({prop1: "abc", prop2: "def"}).go().then(data => { expectType(data); if (data === null) { diff --git a/package-lock.json b/package-lock.json index fec763db..79917814 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "electrodb", - "version": "1.9.0", + "version": "1.10.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "electrodb", - "version": "1.9.0", + "version": "1.10.2", "license": "ISC", "dependencies": { "@aws-sdk/lib-dynamodb": "^3.54.1", @@ -31,7 +31,7 @@ "moment": "2.24.0", "nyc": "^15.1.0", "source-map-support": "^0.5.19", - "ts-node": "^10.8.1", + "ts-node": "^10.8.2", "tsd": "^0.21.0", "typescript": "^4.7.4", "uuid": "7.0.1" @@ -11671,9 +11671,9 @@ } }, "node_modules/ts-node": { - "version": "10.8.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.1.tgz", - "integrity": "sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==", + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.2.tgz", + "integrity": "sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -22057,9 +22057,9 @@ "dev": true }, "ts-node": { - "version": "10.8.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.1.tgz", - "integrity": "sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==", + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.2.tgz", + "integrity": "sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA==", "dev": true, "requires": { "@cspotcode/source-map-support": "^0.8.0", diff --git a/package.json b/package.json index bc25ca2f..6a99306a 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,14 @@ { "name": "electrodb", - "version": "1.10.2", + "version": "1.11.0", "description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb", "main": "index.js", "scripts": { - "test": "mocha ./test/offline**.spec.js", - "test-ts": "mocha -r ts-node/register ./test/**.spec.ts", - "test-all": "mocha ./test/**.spec.js", - "test-all-local": "LOCAL_DYNAMO_ENDPOINT=http://localhost:8000 node ./test/init.js && LOCAL_DYNAMO_ENDPOINT=http://localhost:8000 mocha ./test/**.spec.js && LOCAL_DYNAMO_ENDPOINT=http://localhost:8000 npm run test-ts && npm run test-types", + "test": "npm run test-types && LOCAL_DYNAMO_ENDPOINT=http://localhost:8000 node ./test/init.js && LOCAL_DYNAMO_ENDPOINT=http://localhost:8000 mocha -r ts-node/register ./test/**.spec.*", "test-types": "tsd", - "coverage": "nyc npm run test-all && nyc report --reporter=text-lcov | coveralls", - "coverage-coveralls-local": "nyc npm run test-all-local && nyc report --reporter=text-lcov | coveralls", - "coverage-html-local": "nyc npm run test-all-local && nyc report --reporter=html", + "coverage": "nyc npm test && nyc report --reporter=text-lcov | coveralls", + "coverage-coveralls-local": "nyc npm test && nyc report --reporter=text-lcov | coveralls", + "coverage-html-local": "nyc npm test && nyc report --reporter=html", "build:browser": "browserify playground/browser.js -o playground/bundle.js" }, "repository": { @@ -43,7 +40,7 @@ "moment": "2.24.0", "nyc": "^15.1.0", "source-map-support": "^0.5.19", - "ts-node": "^10.8.1", + "ts-node": "^10.8.2", "tsd": "^0.21.0", "typescript": "^4.7.4", "uuid": "7.0.1" diff --git a/playground/browser.js b/playground/browser.js index d5342a98..10b43d0a 100644 --- a/playground/browser.js +++ b/playground/browser.js @@ -1,3 +1,4 @@ +/* istanbul ignore file */ const ElectroDB = require("../index"); window.Prism = window.Prism || {}; window.electroParams = window.electroParams || []; diff --git a/src/clauses.js b/src/clauses.js index 96c45e89..f63be5b7 100644 --- a/src/clauses.js +++ b/src/clauses.js @@ -1,4 +1,4 @@ -const { QueryTypes, MethodTypes, ItemOperations, ExpressionTypes, TableIndex } = require("./types"); +const { QueryTypes, MethodTypes, ItemOperations, ExpressionTypes, TableIndex, TerminalOperation } = require("./types"); const {AttributeOperationProxy, UpdateOperations, FilterOperationNames} = require("./operations"); const {UpdateExpression} = require("./update"); const {FilterExpression} = require("./where"); @@ -579,6 +579,7 @@ let clauses = { if (entity.client === undefined) { throw new e.ElectroError(e.ErrorCodes.NoClientDefined, "No client defined on model"); } + options.terminalOperation = TerminalOperation.go; let params = clauses.params.action(entity, state, options); let {config} = entity._applyParameterOptions({}, state.getOptions(), options); return entity.go(state.getMethod(), params, config); @@ -595,11 +596,12 @@ let clauses = { return Promise.reject(state.error); } try { - options.page = page; - options._isPagination = true; if (entity.client === undefined) { throw new e.ElectroError(e.ErrorCodes.NoClientDefined, "No client defined on model"); } + options.page = page; + options._isPagination = true; + options.terminalOperation = TerminalOperation.page; let params = clauses.params.action(entity, state, options); let {config} = entity._applyParameterOptions({}, state.getOptions(), options); return entity.go(state.getMethod(), params, config); diff --git a/src/entity.js b/src/entity.js index 313202d0..79233367 100644 --- a/src/entity.js +++ b/src/entity.js @@ -1,6 +1,6 @@ "use strict"; const { Schema } = require("./schema"); -const { KeyCasing, TableIndex, FormatToReturnValues, ReturnValues, EntityVersions, ItemOperations, UnprocessedTypes, Pager, ElectroInstance, KeyTypes, QueryTypes, MethodTypes, Comparisons, ExpressionTypes, ModelVersions, ElectroInstanceTypes, MaxBatchItems } = require("./types"); +const { KeyCasing, TableIndex, FormatToReturnValues, ReturnValues, EntityVersions, ItemOperations, UnprocessedTypes, Pager, ElectroInstance, KeyTypes, QueryTypes, MethodTypes, Comparisons, ExpressionTypes, ModelVersions, ElectroInstanceTypes, MaxBatchItems, TerminalOperation } = require("./types"); const { FilterFactory } = require("./filters"); const { FilterOperations } = require("./operations"); const { WhereFactory } = require("./where"); @@ -84,13 +84,14 @@ class Entity { return pkMatch; } - ownsPager(pager, index) { + ownsPager(pager, index = TableIndex) { if (pager === null) { return false; } - let tableIndex = TableIndex; - let tableIndexFacets = this.model.facets.byIndex[tableIndex]; - let indexFacets = this.model.facets.byIndex[tableIndex]; + let tableIndexFacets = this.model.facets.byIndex[TableIndex]; + // todo: is the fact it doesn't use the provided index a bug? + // feels like collections may have played a roll into why this is this way + let indexFacets = this.model.facets.byIndex[TableIndex]; // Unknown index if (tableIndexFacets === undefined || indexFacets === undefined) { @@ -260,6 +261,7 @@ class Entity { } async _exec(method, params, config = {}) { + const entity = this; const notifyQuery = () => { this.eventManager.trigger({ type: "query", @@ -285,6 +287,7 @@ class Entity { return results; }) .catch(err => { + notifyQuery(); notifyResults(err, false); err.__isAWSError = true; throw err; @@ -822,6 +825,8 @@ class Entity { pages: undefined, listeners: [], preserveBatchOrder: false, + attributes: [], + terminalOperation: undefined, }; config = options.reduce((config, option) => { @@ -834,6 +839,14 @@ class Entity { config.params.ReturnValues = FormatToReturnValues[format]; } + if (option.terminalOperation in TerminalOperation) { + config.terminalOperation = TerminalOperation[option.terminalOperation]; + } + + if (Array.isArray(option.attributes)) { + config.attributes = config.attributes.concat(option.attributes); + } + if (option.preserveBatchOrder === true) { config.preserveBatchOrder = true; } @@ -1019,7 +1032,103 @@ class Entity { throw new Error(`Invalid method: ${method}`); } let applied = this._applyParameterOptions(params, options, config); - return this._applyParameterExpressionTypes(applied.parameters, filter); + return this._applyParameterExpressions(method, applied.parameters, applied.config, filter); + } + + _applyParameterExpressions(method, parameters, config, filter) { + if (method !== MethodTypes.get) { + return this._applyParameterExpressionTypes(parameters, filter); + } else { + parameters = this._applyProjectionExpressions({parameters, config}); + return this._applyParameterExpressionTypes(parameters, filter); + } + + } + + _applyProjectionExpressions({parameters = {}, config = {}} = {}) { + const attributes = config.attributes || []; + if (attributes.length === 0) { + return parameters; + } + + const requiresRawResponse = !!config.raw; + const enforcesOwnership = !config.ignoreOwnership; + const requiresUserInvolvedPagination = TerminalOperation[config.terminalOperation] === TerminalOperation.page; + const isServerBound = TerminalOperation[config.terminalOperation] === TerminalOperation.go || + TerminalOperation[config.terminalOperation] === TerminalOperation.page; + + // 1. Take stock of invalid attributes, if there are any this should be considered + // unintentional and should throw to prevent unintended results + // 2. Convert all attribute names to their respective "field" names + const unknownAttributes = []; + let attributeFields = new Set(); + for (const attributeName of attributes) { + const fieldName = this.model.schema.getFieldName(attributeName); + if (typeof fieldName !== "string") { + unknownAttributes.push(attributeName); + } else { + attributeFields.add(fieldName); + } + } + + // Stop doing work, prepare error message and throw + if (attributeFields.size === 0 || unknownAttributes.length > 0) { + let message = 'Unknown attributes provided in query options'; + if (unknownAttributes.length) { + message += `: ${u.commaSeparatedString(unknownAttributes)}`; + } + throw new e.ElectroError(e.ErrorCodes.InvalidOptions, message); + } + + // add ExpressionAttributeNames if it doesn't exist already + parameters.ExpressionAttributeNames = parameters.ExpressionAttributeNames || {}; + + if ( + // The response you're returning: + // 1. is not expected to be raw + !requiresRawResponse + // 2. is making a request to the server + && isServerBound + // 3. will expect entity identifiers down stream + && enforcesOwnership + + ) { + // add entity identifiers to so items can be identified + attributeFields.add(this.identifiers.entity); + attributeFields.add(this.identifiers.version); + + // if pagination is required you may enter into a scenario where + // the LastEvaluatedKey doesn't belong to entity and one must be formed. + // We must add the attributes necessary to make that key to not break + // pagination. This stinks. + if ( + requiresUserInvolvedPagination + && config.pager !== Pager.raw + ) { + // LastEvaluatedKeys return the TableIndex keys and the keys for the SecondaryIndex + let tableIndexFacets = this.model.facets.byIndex[TableIndex]; + let indexFacets = this.model.facets.byIndex[parameters.IndexName] || { all: [] }; + + for (const attribute of [...tableIndexFacets.all, ...indexFacets.all]) { + const fieldName = this.model.schema.getFieldName(attribute.name); + attributeFields.add(fieldName); + } + } + } + + for (const attributeField of attributeFields) { + // prefix the ExpressionAttributeNames because some prefixes are not allowed + parameters.ExpressionAttributeNames['#' + attributeField] = attributeField; + } + + // if there is already a ProjectionExpression (e.g. config "params"), merge it + if (typeof parameters.ProjectionExpression === 'string') { + parameters.ProjectionExpression = [parameters.ProjectionExpression, ...Object.keys([parameters.ExpressionAttributeNames])].join(', '); + } else { + parameters.ProjectionExpression = Object.keys(parameters.ExpressionAttributeNames).join(', '); + } + + return parameters; } _batchGetParams(state, config = {}) { @@ -1411,7 +1520,7 @@ class Entity { throw new Error(`Invalid query type: ${state.query.type}`); } let applied = this._applyParameterOptions(parameters, state.query.options, options); - return applied.parameters; + return this._applyProjectionExpressions(applied); } _makeBetweenQueryParams(index, filter, pk, ...sk) { diff --git a/src/parse.js b/src/parse.js deleted file mode 100644 index 75838e2d..00000000 --- a/src/parse.js +++ /dev/null @@ -1,45 +0,0 @@ -function getPartDetail(part = "") { - let detail = { - expression: "", - name: "", - value: "", - }; - if (part.includes("[")) { - if (!part.match(/\[\d\]/gi)) { - throw new Error(`Invalid path part "${part}" has bracket containing non-numeric characters.`); - } - let [name] = part.match(/.*(?=\[)/gi); - detail.name = `#${name}`; - detail.value = name; - } else { - detail.name = `#${part}`; - detail.value = part; - } - detail.expression = `#${part}`; - return detail; -} - -function parse(path = "") { - if (typeof path !== "string" || !path.length) { - throw new Error("Path must be a string with a non-zero length"); - } - let parts = path.split(/\./gi); - let attr = getPartDetail(parts[0]).value; - let target = getPartDetail(parts[parts.length-1]); - if (target.expression.includes("[")) { - - } - let names = {}; - let expressions = []; - for (let part of parts) { - let detail = getPartDetail(part); - names[detail.name] = detail.value; - expressions.push(detail.expression); - } - return {attr, path, names, target: target.value, expression: expressions.join(".")}; -} - -module.exports = { - parse, - getPartDetail -}; diff --git a/src/schema.js b/src/schema.js index 4ff174bc..5c30d981 100644 --- a/src/schema.js +++ b/src/schema.js @@ -1,4 +1,4 @@ -const { CastTypes, ValueTypes, KeyCasing, AttributeTypes, AttributeMutationMethods, AttributeWildCard, PathTypes, TraverserIndexes } = require("./types"); +const { CastTypes, ValueTypes, KeyCasing, AttributeTypes, AttributeMutationMethods, AttributeWildCard, PathTypes } = require("./types"); const AttributeTypeNames = Object.keys(AttributeTypes); const ValidFacetTypes = [AttributeTypes.string, AttributeTypes.number, AttributeTypes.boolean, AttributeTypes.enum]; const e = require("./errors"); @@ -1263,7 +1263,7 @@ class Schema { translateToFields(payload = {}) { let record = {}; for (let [name, value] of Object.entries(payload)) { - let field = this.translationForTable[name]; + let field = this.getFieldName(name); if (value !== undefined) { record[field] = value; } @@ -1271,6 +1271,12 @@ class Schema { return record; } + getFieldName(name) { + if (typeof name === 'string') { + return this.translationForTable[name]; + } + } + checkCreate(payload = {}) { let record = {}; for (let attribute of Object.values(this.attributes)) { @@ -1320,13 +1326,18 @@ class Schema { } formatItemForRetrieval(item, config) { + let returnAttributes = new Set(config.attributes || []); + let hasUserSpecifiedReturnAttributes = returnAttributes.size > 0; let remapped = this.translateFromFields(item, config); let data = this._fulfillAttributeMutationMethod("get", remapped); - if (this.hiddenAttributes.size > 0) { + if (this.hiddenAttributes.size > 0 || hasUserSpecifiedReturnAttributes) { for (let attribute of Object.keys(data)) { if (this.hiddenAttributes.has(attribute)) { delete data[attribute]; } + if (hasUserSpecifiedReturnAttributes && !returnAttributes.has(attribute)) { + delete data[attribute]; + } } } return data; diff --git a/src/types.js b/src/types.js index 9804a708..b4a4d497 100644 --- a/src/types.js +++ b/src/types.js @@ -186,6 +186,11 @@ const EventSubscriptionTypes = [ "results" ]; +const TerminalOperation = { + go: 'go', + page: 'page', +} + module.exports = { Pager, KeyTypes, @@ -210,6 +215,7 @@ module.exports = { TraverserIndexes, UnprocessedTypes, AttributeWildCard, + TerminalOperation, FormatToReturnValues, AttributeProxySymbol, ElectroInstanceTypes, diff --git a/test/queries.test-d.ts b/test/queries.test-d.ts new file mode 100644 index 00000000..a33f410f --- /dev/null +++ b/test/queries.test-d.ts @@ -0,0 +1,559 @@ +import { + Entity, + Schema, + Resolve, + ResponseItem, + GoQueryTerminal, + PageQueryTerminal, + Queries +} from '../'; +import { expectType, expectError, expectNotType } from 'tsd'; + +interface QueryGoOptions { + raw?: boolean; + table?: string; + limit?: number; + params?: object; + includeKeys?: boolean; + originalErr?: boolean; + ignoreOwnership?: boolean; + pages?: number; + attributes?: ReadonlyArray +} + +const troubleshoot = (fn: (...params: Params) => Response, response: Response) => {}; +const magnify = (value: T): Resolve => { return {} as Resolve }; +const keys = (value: T): keyof T => { return {} as keyof T }; + +class MockEntity> { + readonly schema: S; + + constructor(schema: S) { + this.schema = schema; + }; + + getGoQueryTerminal(): GoQueryTerminal> { + return {} as GoQueryTerminal>; + } + + getPageQueryTerminal(): PageQueryTerminal, {abc: string}> { + return {} as PageQueryTerminal, {abc: string}>; + } + + getQueries(): Queries { + return {} as Queries; + } + + getKeyofQueries(): keyof Queries { + return {} as keyof Queries; + } +} + +const entityWithSK = new MockEntity({ + model: { + entity: "abc", + service: "myservice", + version: "myversion" + }, + attributes: { + attr1: { + type: "string", + default: "abc", + get: (val) => val + 123, + set: (val) => (val ?? "") + 456, + validate: (val) => !!val, + }, + attr2: { + type: "string", + // default: () => "sfg", + // required: false, + validate: (val) => val.length > 0 + }, + attr3: { + type: ["123", "def", "ghi"] as const, + default: "def" + }, + attr4: { + type: ["abc", "ghi"] as const, + required: true + }, + attr5: { + type: "string" + }, + attr6: { + type: "number", + default: () => 100, + get: (val) => val + 5, + set: (val) => (val ?? 0) + 5, + validate: (val) => true, + }, + attr7: { + type: "any", + default: () => false, + get: (val) => ({key: "value"}), + set: (val) => (val ?? 0) + 5, + validate: (val) => true, + }, + attr8: { + type: "boolean", + required: true, + get: (val) => !!val, + set: (val) => !!val, + validate: (val) => !!val, + }, + attr9: { + type: "number" + }, + attr10: { + type: "boolean" + } + }, + indexes: { + myIndex: { + collection: "mycollection2", + pk: { + field: "pk", + composite: ["attr1"] + }, + sk: { + field: "sk", + composite: ["attr2"] + } + }, + myIndex2: { + collection: "mycollection1", + index: "gsi1", + pk: { + field: "gsipk1", + composite: ["attr6", "attr9"] + }, + sk: { + field: "gsisk1", + composite: ["attr4", "attr5"] + } + }, + myIndex3: { + collection: "mycollection", + index: "gsi2", + pk: { + field: "gsipk2", + composite: ["attr5"] + }, + sk: { + field: "gsisk2", + composite: ["attr4", "attr3", "attr9"] + } + } + } +}); + +const entityWithoutSK = new MockEntity({ + model: { + entity: "abc", + service: "myservice", + version: "myversion" + }, + attributes: { + attr1: { + type: "string", + // default: "abc", + get: (val) => val + 123, + set: (val) => (val ?? "0") + 456, + validate: (val) => !!val, + }, + attr2: { + type: "string", + // default: () => "sfg", + // required: false, + validate: (val) => val.length > 0 + }, + attr3: { + type: ["123", "def", "ghi"] as const, + default: "def" + }, + attr4: { + type: ["abc", "def"] as const, + required: true + }, + attr5: { + type: "string" + }, + attr6: { + type: "number", + default: () => 100, + get: (val) => val + 5, + set: (val) => (val ?? 0) + 5, + validate: (val) => true, + }, + attr7: { + type: "any", + default: () => false, + get: (val) => ({key: "value"}), + set: (val) => (val ?? 0) + 5, + validate: (val) => true, + }, + attr8: { + type: "boolean", + required: true, + default: () => false, + get: (val) => !!val, + set: (val) => !!val, + validate: (val) => !!val, + }, + attr9: { + type: "number" + } + }, + indexes: { + myIndex: { + pk: { + field: "pk", + composite: ["attr1"] + } + }, + myIndex2: { + index: "gsi1", + collection: "mycollection1", + pk: { + field: "gsipk1", + composite: ["attr6", "attr9"] + }, + sk: { + field: "gsisk1", + composite: [] + } + }, + myIndex3: { + collection: "mycollection", + index: "gsi2", + pk: { + field: "gsipk2", + composite: ["attr5"] + }, + sk: { + field: "gsisk2", + composite: [] + } + } + } +}); + +const entityWithSKE = new Entity({ + model: { + entity: "abc", + service: "myservice", + version: "myversion" + }, + attributes: { + attr1: { + type: "string", + default: "abc", + get: (val) => val + 123, + set: (val) => (val ?? "") + 456, + validate: (val) => !!val, + }, + attr2: { + type: "string", + // default: () => "sfg", + // required: false, + validate: (val) => val.length > 0 + }, + attr3: { + type: ["123", "def", "ghi"] as const, + default: "def" + }, + attr4: { + type: ["abc", "ghi"] as const, + required: true + }, + attr5: { + type: "string" + }, + attr6: { + type: "number", + default: () => 100, + get: (val) => val + 5, + set: (val) => (val ?? 0) + 5, + validate: (val) => true, + }, + attr7: { + type: "any", + default: () => false, + get: (val) => ({key: "value"}), + set: (val) => (val ?? 0) + 5, + validate: (val) => true, + }, + attr8: { + type: "boolean", + required: true, + get: (val) => !!val, + set: (val) => !!val, + validate: (val) => !!val, + }, + attr9: { + type: "number" + }, + attr10: { + type: "boolean" + } + }, + indexes: { + myIndex: { + collection: "mycollection2", + pk: { + field: "pk", + composite: ["attr1"] + }, + sk: { + field: "sk", + composite: ["attr2"] + } + }, + myIndex2: { + collection: "mycollection1", + index: "gsi1", + pk: { + field: "gsipk1", + composite: ["attr6", "attr9"] + }, + sk: { + field: "gsisk1", + composite: ["attr4", "attr5"] + } + }, + myIndex3: { + collection: "mycollection", + index: "gsi2", + pk: { + field: "gsipk2", + composite: ["attr5"] + }, + sk: { + field: "gsisk2", + composite: ["attr4", "attr3", "attr9"] + } + } + } +}); + +const entityWithoutSKE = new Entity({ + model: { + entity: "abc", + service: "myservice", + version: "myversion" + }, + attributes: { + attr1: { + type: "string", + // default: "abc", + get: (val) => val + 123, + set: (val) => (val ?? "0") + 456, + validate: (val) => !!val, + }, + attr2: { + type: "string", + // default: () => "sfg", + // required: false, + validate: (val) => val.length > 0 + }, + attr3: { + type: ["123", "def", "ghi"] as const, + default: "def" + }, + attr4: { + type: ["abc", "def"] as const, + required: true + }, + attr5: { + type: "string" + }, + attr6: { + type: "number", + default: () => 100, + get: (val) => val + 5, + set: (val) => (val ?? 0) + 5, + validate: (val) => true, + }, + attr7: { + type: "any", + default: () => false, + get: (val) => ({key: "value"}), + set: (val) => (val ?? 0) + 5, + validate: (val) => true, + }, + attr8: { + type: "boolean", + required: true, + default: () => false, + get: (val) => !!val, + set: (val) => !!val, + validate: (val) => !!val, + }, + attr9: { + type: "number" + } + }, + indexes: { + myIndex: { + pk: { + field: "pk", + composite: ["attr1"] + } + }, + myIndex2: { + index: "gsi1", + collection: "mycollection1", + pk: { + field: "gsipk1", + composite: ["attr6", "attr9"] + }, + sk: { + field: "gsisk1", + composite: [] + } + }, + myIndex3: { + collection: "mycollection", + index: "gsi2", + pk: { + field: "gsipk2", + composite: ["attr5"] + }, + sk: { + field: "gsisk2", + composite: [] + } + } + } +}); + +const entityWithSKGo = entityWithSK.getGoQueryTerminal(); +entityWithSKGo({attributes: ['attr2', 'attr3', 'attr4', 'attr6', 'attr8']}).then(results => { + expectType<{ + attr2: string; + attr3?: '123' | 'def' | 'ghi' | undefined; + attr4: 'abc' | 'ghi'; + attr6?: number | undefined; + attr8: boolean; + }[]>(results); +}); + +const entityWithSKPage = entityWithSK.getPageQueryTerminal(); +entityWithSKPage(null, {attributes: ['attr2', 'attr3', 'attr4', 'attr6', 'attr8']}).then(data => { + const [page, results] = data; + expectType<{ + attr2: string; + attr3?: '123' | 'def' | 'ghi' | undefined; + attr4: 'abc' | 'ghi'; + attr6?: number | undefined; + attr8: boolean; + }[]>(results); + expectType<{ + __edb_e__?: string | undefined; + __edb_v__?: string | undefined; + abc: string; + } | null>(magnify(page)); +}); + +const entityWithoutSKGo = entityWithoutSK.getGoQueryTerminal(); +entityWithoutSKGo({attributes: ['attr2', 'attr3', 'attr4', 'attr6', 'attr8']}).then(results => { + expectType<{ + attr2?: string | undefined; + attr3?: '123' | 'def' | 'ghi' | undefined; + attr4: 'abc' | 'def'; + attr6?: number | undefined; + attr8: boolean; + }[]>(magnify(results)); +}); + +const entityWithoutSKPage = entityWithoutSK.getPageQueryTerminal(); +entityWithoutSKPage(null, {attributes: ['attr2', 'attr3', 'attr4', 'attr6', 'attr8']}).then(data => { + const [page, results] = data; + expectType<{ + attr2?: string | undefined; + attr3?: '123' | 'def' | 'ghi' | undefined; + attr4: 'abc' | 'def'; + attr6?: number | undefined; + attr8: boolean; + }[]>(magnify(results)); + expectType<{ + __edb_e__?: string | undefined; + __edb_v__?: string | undefined; + abc: string; + } | null>(magnify(page)); +}); + +expectType<'myIndex' | 'myIndex2' | 'myIndex3'>(entityWithSK.getKeyofQueries()); +expectType<'myIndex' | 'myIndex2' | 'myIndex3'>(entityWithoutSK.getKeyofQueries()); + +const entityWithSKQueries = entityWithSK.getQueries(); + +const entityWithSKMyIndex = entityWithSKQueries.myIndex; +const entityWithSKMyIndexOptions = {} as Parameters[0]; +expectType<{ + attr1: string; + attr2?: string | undefined; +}>(magnify(entityWithSKMyIndexOptions)); + +const entityWithSKMyIndexSKOperations = entityWithSKQueries.myIndex2({attr6: 10, attr9: 5, attr4: 'abc'}); + +type EntityWithSKMyIndexOperationsTerminals = Pick; +const afterWhere = entityWithSKMyIndexSKOperations.where((attr, op) => op.eq(attr.attr4, 'zz')); +expectType(afterWhere); + +const entityWithSKMyIndexSKOperationsKeys = {} as keyof typeof entityWithSKMyIndexSKOperations; +expectType<'go' | 'params' | 'page' | 'where' | 'begins' | 'between' | 'gt' | 'gte' | 'lt' | 'lte'>(entityWithSKMyIndexSKOperationsKeys); + +const entityWithSKMyIndexSKOperationsBegins = {} as Parameters[0]; +expectType<{ + attr5?: string | undefined; +}>(magnify(entityWithSKMyIndexSKOperationsBegins)); + +const entityWithSKMyIndexSKOperationsBetween0 = {} as Parameters[0]; +const entityWithSKMyIndexSKOperationsBetween1 = {} as Parameters[1]; +expectType<{ + attr5?: string | undefined; +}>(magnify(entityWithSKMyIndexSKOperationsBetween0)); +expectType<{ + attr5?: string | undefined; +}>(magnify(entityWithSKMyIndexSKOperationsBetween1)); + +const entityWithSKMyIndexSKOperationsGT = {} as Parameters[0]; +expectType<{ + attr5?: string | undefined; +}>(magnify(entityWithSKMyIndexSKOperationsGT)); + +const entityWithSKMyIndexSKOperationsGTE = {} as Parameters[0]; +expectType<{ + attr5?: string | undefined; +}>(magnify(entityWithSKMyIndexSKOperationsGTE)); + +const entityWithSKMyIndexSKOperationsLT = {} as Parameters[0]; +expectType<{ + attr5?: string | undefined; +}>(magnify(entityWithSKMyIndexSKOperationsLT)); + +const entityWithSKMyIndexSKOperationsLTE = {} as Parameters[0]; +expectType<{ + attr5?: string | undefined; +}>(magnify(entityWithSKMyIndexSKOperationsLTE)); + +const entityWithoutSKQueries = entityWithoutSK.getQueries(); +const entityWithoutSKMyIndex = entityWithoutSKQueries.myIndex; +const entityWithoutSKMyIndexOptions = {} as Parameters[0]; +expectType<{ + attr1: string; +}>(magnify(entityWithoutSKMyIndexOptions)); + +const entityWithoutSKMyIndex2SKOperations = entityWithoutSKQueries.myIndex2({attr6: 10, attr9: 5}); +const entityWithoutSKMyIndex2SKOperationsKeys = {} as keyof typeof entityWithoutSKMyIndex2SKOperations; +expectType<'go' | 'params' | 'page' | 'where'>(entityWithoutSKMyIndex2SKOperationsKeys); + + +const afterGetOperations = entityWithSKE.get({attr1: 'abc', attr2: 'def'}) +const afterGetFilterOperations = entityWithSKE.get({attr1: 'abc', attr2: 'def'}).where((attr, op) => { + return op.eq(attr.attr4, 'zzz'); +}); +expectType(afterGetFilterOperations); + + diff --git a/test/tests.test-d.ts b/test/tests.test-d.ts index 27021b8e..5a6ed431 100644 --- a/test/tests.test-d.ts +++ b/test/tests.test-d.ts @@ -1,4 +1,5 @@ +export * from './types.test-d'; +export * from './where.test-d'; export * from './entity.test-d'; export * from './schema.test-d'; -export * from './types.test-d'; -export * from './where.test-d'; \ No newline at end of file +export * from './queries.test-d'; \ No newline at end of file diff --git a/test/ts_connected.crud.spec.ts b/test/ts_connected.crud.spec.ts index d0970f7b..8a05c96d 100644 --- a/test/ts_connected.crud.spec.ts +++ b/test/ts_connected.crud.spec.ts @@ -1,5 +1,5 @@ process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = "1"; -import {CreateEntityItem, Entity} from "../index"; +import {CreateEntityItem, Entity, EntityItem} from "../index"; import { expect } from "chai"; import {v4 as uuid} from "uuid"; import moment from "moment"; @@ -2143,6 +2143,8 @@ describe("Entity", async () => { const itemFromDocClient = await client.get(params).promise(); const parsed = entity.parse(itemFromDocClient); expect(parsed).to.deep.equal({prop1, prop2, prop3}); + const parsedTrimmed = entity.parse(itemFromDocClient, {attributes: ['prop1', 'prop3']}); + expect(parsedTrimmed).to.deep.equal({prop1, prop3}); }); it("should parse the response from an query that lacks identifiers", async () => { @@ -2171,6 +2173,8 @@ describe("Entity", async () => { const itemFromDocClient = await client.query(params).promise(); const parsed = entity.parse(itemFromDocClient); expect(parsed).to.deep.equal([{prop1, prop2, prop3}]); + const parseTrimmed = entity.parse(itemFromDocClient, {attributes: ['prop1', 'prop3']}); + expect(parseTrimmed).to.deep.equal([{prop1, prop3}]); }); it("should parse the response from an update", async () => { @@ -2202,6 +2206,8 @@ describe("Entity", async () => { const results = await client.update(params).promise(); const parsed = entity.parse(results); expect(parsed).to.deep.equal({prop3: prop3b}); + const parseTrimmed = entity.parse(results, {attributes: ['prop3']}); + expect(parseTrimmed).to.deep.equal({prop3: prop3b}); }); it("should parse the response from a complex update", async () => { @@ -2254,9 +2260,15 @@ describe("Entity", async () => { ReturnValues: 'UPDATED_NEW' } - const results2 = await client.update(params).promise(); + const results2 = await client.update(params2).promise(); const parsed2 = entity.parse(results2); - expect(parsed2).to.be.null; + expect(parsed2).to.deep.equal({ + prop4: { + nested: { + prop6: 'xyz' + } + } + }); }); it("should parse the response from a delete", async () => { @@ -2284,6 +2296,8 @@ describe("Entity", async () => { const results = await client.delete(params).promise(); const parsed = entity.parse(results); expect(parsed).to.deep.equal({prop1, prop2, prop3}); + const parseTrimmed = entity.parse(results, {attributes: ['prop1', 'prop3']}); + expect(parseTrimmed).to.deep.equal({prop1, prop3}); }); it("should parse the response from a put", async () => { @@ -2321,6 +2335,11 @@ describe("Entity", async () => { prop2: prop2a, prop3: prop3a, }); + const parseTrimmed = entity.parse(results, {attributes: ['prop1', 'prop3']}); + expect(parseTrimmed).to.deep.equal({ + prop1: prop1a, + prop3: prop3a, + }); }); it("should parse the response from a scan", async () => { @@ -2342,6 +2361,8 @@ describe("Entity", async () => { }; const parsed = entity.parse(scanResponse); expect(parsed).to.deep.equal([{prop1, prop2, prop3}]); + const parseTrimmed = entity.parse(scanResponse, {attributes: ['prop1', 'prop3']}); + expect(parseTrimmed).to.deep.equal([{prop1, prop3}]); }).timeout(10000); }); describe("Key fields that match Attribute fields", () => { @@ -3047,4 +3068,445 @@ describe('pagination order', () => { } } }); +}); + +describe('attributes query option', () => { + const entityWithSK = new Entity({ + model: { + entity: "abc", + service: "myservice", + version: "myversion" + }, + attributes: { + attr1: { + type: "string", + default: "abc", + }, + attr2: { + type: "string", + }, + attr3: { + type: ["123", "def", "ghi"] as const, + default: "def" + }, + attr4: { + type: ["abc", "ghi"] as const, + required: true + }, + attr5: { + type: "string" + }, + attr6: { + type: "number", + }, + attr7: { + type: "any", + }, + attr8: { + type: "boolean", + required: true, + }, + attr9: { + type: "number", + field: 'prop9', + }, + attr10: { + type: "boolean" + } + }, + indexes: { + myIndex: { + collection: "mycollection2", + pk: { + field: "pk", + composite: ["attr1"] + }, + sk: { + field: "sk", + composite: ["attr2"] + } + }, + myIndex2: { + collection: "mycollection1", + index: "gsi1pk-gsi1sk-index", + pk: { + field: "gsi1pk", + composite: ["attr6", "attr9"] + }, + sk: { + field: "gsi1sk", + composite: ["attr4", "attr5"] + } + } + } + }, {table, client}); + + it('should return only the attributes specified in query options', async () => { + const item: EntityItem = { + attr1: uuid(), + attr2: "attr2", + attr9: 9, + attr6: 6, + attr4: 'abc', + attr8: true, + attr5: 'attr5', + attr3: '123', + attr7: 'attr7', + attr10: false, + }; + await entityWithSK.put(item).go(); + const getItem = await entityWithSK + .get({ + attr1: item.attr1, + attr2: item.attr2, + }).go({ + attributes: ['attr2', 'attr9', 'attr5', 'attr10'] + }); + + expect(getItem).to.deep.equal({ + attr2: item.attr2, + attr9: item.attr9, + attr5: item.attr5, + attr10: item.attr10, + }); + + const queryItem = await entityWithSK.query + .myIndex({ + attr1: item.attr1, + attr2: item.attr2, + }).go({ + attributes: ['attr2', 'attr9', 'attr5', 'attr10'] + }); + + expect(queryItem).to.deep.equal([{ + attr2: item.attr2, + attr9: item.attr9, + attr5: item.attr5, + attr10: item.attr10, + }]); + }); + + it('should not add entity identifiers', async () => { + const item: EntityItem = { + attr1: uuid(), + attr2: "attr2", + attr9: 9, + attr6: 6, + attr4: 'abc', + attr8: true, + attr5: 'attr5', + attr3: '123', + attr7: 'attr7', + attr10: false, + }; + await entityWithSK.put(item).go(); + + // params + const getParams = entityWithSK.get({ + attr1: item.attr1, + attr2: item.attr2, + }).params({attributes: ['attr2', 'attr9', 'attr5', 'attr10']}); + expect(getParams.ExpressionAttributeNames).to.deep.equal({ + "#attr2": "attr2", + "#prop9": "prop9", // should convert attribute names to field names when specifying attributes + "#attr5": "attr5", + "#attr10": "attr10" + }); + expect(getParams.ProjectionExpression).to.equal("#attr2, #prop9, #attr5, #attr10"); + const queryParams = entityWithSK.query.myIndex({ + attr1: item.attr1, + attr2: item.attr2, + }).params({attributes: ['attr2', 'attr9', 'attr5', 'attr10']}); + expect(queryParams.ExpressionAttributeNames).to.deep.equal({ + "#pk": "pk", + "#sk1": "sk", + "#attr2": "attr2", + "#prop9": "prop9", // should convert attribute names to field names when specifying attributes + "#attr5": "attr5", + "#attr10": "attr10" + }); + expect(queryParams.ProjectionExpression).to.equal("#pk, #sk1, #attr2, #prop9, #attr5, #attr10"); + + // raw + const getRaw = await entityWithSK.get({ + attr1: item.attr1, + attr2: item.attr2, + }).go({raw: true, attributes: ['attr2', 'attr9', 'attr5', 'attr10']}); + expect(getRaw).to.deep.equal({ + "Item": { + "attr5": item.attr5, + "prop9": item.attr9, // should convert attribute names to field names when specifying attributes + "attr2": item.attr2, + "attr10": item.attr10 + } + }); + const queryRawGo = await entityWithSK.query.myIndex({ + attr1: item.attr1, + attr2: item.attr2, + }).go({raw: true, attributes: ['attr2', 'attr9', 'attr5', 'attr10']}); + expect(queryRawGo).to.deep.equal({ + "Items": [ + { + "sk": `$mycollection2#abc_myversion#attr2_${item.attr2}`, + "attr5": item.attr5, + "prop9": item.attr9, // should convert attribute names to field names when specifying attributes + "pk": `$myservice#attr1_${item.attr1}`, + "attr2": item.attr2, + "attr10": item.attr10 + } + ], + "Count": 1, + "ScannedCount": 1 + }); + const queryRawPage = await entityWithSK.query.myIndex({ + attr1: item.attr1, + attr2: item.attr2, + }).page(null, {raw: true, attributes: ['attr2', 'attr9', 'attr5', 'attr10']}); + expect(queryRawPage).to.deep.equal([ + null, + { + "Items": [ + { + "sk": `$mycollection2#abc_myversion#attr2_${item.attr2}`, + "attr5": item.attr5, + "prop9": item.attr9, // should convert attribute names to field names when specifying attributes + "pk": `$myservice#attr1_${item.attr1}`, + "attr2": item.attr2, + "attr10": item.attr10 + } + ], + "Count": 1, + "ScannedCount": 1 + } + ]); + + // pagerRaw + const queryRawPager = await entityWithSK.query.myIndex({ + attr1: item.attr1, + attr2: item.attr2, + }).page(null, {pager: 'raw', attributes: ['attr2', 'attr9', 'attr5', 'attr10']}); + expect(queryRawPager).to.deep.equal([ + null, + [ + { + "attr5": item.attr5, + "attr9": item.attr9, + "attr2": item.attr2, + "attr10": item.attr10, + } + ] + ]); + // ignoreOwnership + let getIgnoreOwnershipParams: any; + const getIgnoreOwnership = await entityWithSK.get({ + attr1: item.attr1, + attr2: item.attr2, + }).go({ + logger: (event) => { + if (event.type === 'query') { + getIgnoreOwnershipParams = event.params; + } + }, + ignoreOwnership: true, + attributes: ['attr2', 'attr9', 'attr5', 'attr10'] + }); + expect(getIgnoreOwnership).to.deep.equal({ + "attr5": item.attr5, + "attr9": item.attr9, + "attr2": item.attr2, + "attr10": item.attr10 + }) + expect(getIgnoreOwnershipParams.ExpressionAttributeNames).to.deep.equal({ + "#attr2": "attr2", + "#prop9": "prop9", // should convert attribute names to field names when specifying attributes + "#attr5": "attr5", + "#attr10": "attr10", + }); + expect(getIgnoreOwnershipParams.ProjectionExpression).to.equal("#attr2, #prop9, #attr5, #attr10"); + + const queryIgnoreOwnershipGo = await entityWithSK.query.myIndex({ + attr1: item.attr1, + attr2: item.attr2, + }).go({ + ignoreOwnership: true, + attributes: ['attr2', 'attr9', 'attr5', 'attr10'], + logger: (event) => { + if (event.type === 'query') { + getIgnoreOwnershipParams = event.params; + } + }, + }); + expect(queryIgnoreOwnershipGo).to.deep.equal([ + { + "attr5": item.attr5, + "attr9": item.attr9, + "attr2": item.attr2, + "attr10": item.attr10, + } + ]); + expect(getIgnoreOwnershipParams.ExpressionAttributeNames).to.deep.equal({ + "#pk": "pk", + "#sk1": "sk", + "#attr2": "attr2", + "#prop9": "prop9", // should convert attribute names to field names when specifying attributes + "#attr5": "attr5", + "#attr10": "attr10", + }); + expect(getIgnoreOwnershipParams.ProjectionExpression).to.equal("#pk, #sk1, #attr2, #prop9, #attr5, #attr10"); + + const queryIgnoreOwnershipPage = await entityWithSK.query.myIndex({ + attr1: item.attr1, + attr2: item.attr2, + }).page(null, { + ignoreOwnership: true, + attributes: ['attr2', 'attr9', 'attr5', 'attr10'], + logger: (event) => { + if (event.type === 'query') { + getIgnoreOwnershipParams = event.params; + } + }, + }); + + expect(queryIgnoreOwnershipPage).to.deep.equal([ + null, + [ + { + "attr5": item.attr5, + "attr9": item.attr9, + "attr2": item.attr2, + "attr10": item.attr10, + } + ] + ]) + expect(getIgnoreOwnershipParams.ExpressionAttributeNames).to.deep.equal({ + "#pk": "pk", + "#sk1": "sk", + "#attr2": "attr2", + "#prop9": "prop9", // should convert attribute names to field names when specifying attributes + "#attr5": "attr5", + "#attr10": "attr10", + }); + expect(getIgnoreOwnershipParams.ProjectionExpression).to.equal("#pk, #sk1, #attr2, #prop9, #attr5, #attr10"); + }); + + it('should return all values if attributes is empty array', async () => { + const item: EntityItem = { + attr1: uuid(), + attr2: "attr2", + attr9: 9, + attr6: 6, + attr4: 'abc', + attr8: true, + attr5: 'attr5', + attr3: '123', + attr7: 'attr7', + attr10: false, + }; + await entityWithSK.put(item).go(); + + // params + const getParams = await entityWithSK.get({ + attr1: item.attr1, + attr2: item.attr2, + }).go({attributes: []}); + + expect(getParams).to.deep.equal(item); + }); + + it('should include index composite attributes on automatically but not on the response', async () => { + const item: EntityItem = { + attr1: uuid(), + attr2: "attr2", + attr9: 9, + attr6: 6, + attr4: 'abc', + attr8: true, + attr5: uuid(), + attr3: '123', + attr7: 'attr7', + attr10: false, + }; + await entityWithSK.put(item).go(); + let params: any; + const [, results] = await entityWithSK.query.myIndex2({ + attr5: item.attr5, + attr4: item.attr4, + attr6: item.attr6!, + attr9: item.attr9!, + }).page(null, { + logger: (event) => { + if (event.type === 'query') { + params = event.params; + } + }, + attributes: ['attr2', 'attr9', 'attr5', 'attr10'] + }); + expect(results).to.deep.equal([{ + attr2: item.attr2, + attr9: item.attr9, + attr5: item.attr5, + attr10: item.attr10, + }]); + expect(params.ProjectionExpression).to.equal("#pk, #sk1, #attr2, #prop9, #attr5, #attr10, #__edb_e__, #__edb_v__, #attr1, #attr6, #attr4"); + }); + + it('should not include index composite attributes on automatically when pager is raw', async () => { + const item: EntityItem = { + attr1: uuid(), + attr2: "attr2", + attr9: 9, + attr6: 6, + attr4: 'abc', + attr8: true, + attr5: uuid(), + attr3: '123', + attr7: 'attr7', + attr10: false, + }; + await entityWithSK.put(item).go(); + let params: any; + const [, results] = await entityWithSK.query.myIndex2({ + attr5: item.attr5, + attr4: item.attr4, + attr6: item.attr6!, + attr9: item.attr9!, + }).page(null, { + logger: (event) => { + if (event.type === 'query') { + params = event.params; + } + }, + pager: 'raw', + attributes: ['attr2', 'attr9', 'attr5', 'attr10'] + }); + expect(results).to.deep.equal([{ + attr2: item.attr2, + attr9: item.attr9, + attr5: item.attr5, + attr10: item.attr10, + }]); + expect(params.ProjectionExpression).to.equal("#pk, #sk1, #attr2, #prop9, #attr5, #attr10, #__edb_e__, #__edb_v__"); + }); + + it('should throw when unknown attribute names are provided', () => { + const attr1 = 'attr1'; + const attr2 = 'attr2'; + const getParams = () => entityWithSK + .get({attr1, attr2}) + .params({ + // @ts-ignore + attributes: ['prop1'] + }); + expect(getParams).to.throw(`Unknown attributes provided in query options: "prop1"`); + }); + + it('should throw when non-string attributes are provided', () => { + const attr1 = 'attr1'; + const attr2 = 'attr2'; + const getParams = () => entityWithSK + .get({attr1, attr2}) + // @ts-ignore + .params({attributes: [123, {abc: 'def'}]}); + expect(getParams).to.throw(`Unknown attributes provided in query options: "123", "[object Object]"`); + }); }); \ No newline at end of file diff --git a/test/ts_connected.logger.spec.ts b/test/ts_connected.logger.spec.ts index e50b8cf9..29fc735a 100644 --- a/test/ts_connected.logger.spec.ts +++ b/test/ts_connected.logger.spec.ts @@ -365,7 +365,9 @@ describe("listener functions", async () => { }; }) return { - query: entity.get(keys), + query: { + go: async (options) => entity.get(keys).go(options), + }, test: (events) => equalCallCount(callCount, events) }; }); @@ -469,6 +471,8 @@ describe("listener functions", async () => { TableName: 'electro' }, config: { + terminalOperation: "go", + attributes: [], includeKeys: false, originalErr: false, raw: false, @@ -495,6 +499,8 @@ describe("listener functions", async () => { config: { includeKeys: false, originalErr: false, + terminalOperation: "go", + attributes: [], raw: false, params: {}, page: {}, @@ -541,6 +547,8 @@ describe("listener functions", async () => { config: { includeKeys: false, originalErr: false, + terminalOperation: "go", + attributes: [], raw: false, params: {}, page: {}, @@ -563,6 +571,8 @@ describe("listener functions", async () => { type: 'results', method: 'get', config: { + terminalOperation: "go", + attributes: [], includeKeys: false, originalErr: false, raw: false, diff --git a/tslocaltestgrep.sh b/tslocaltestgrep.sh index b078a18d..083f6a85 100644 --- a/tslocaltestgrep.sh +++ b/tslocaltestgrep.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash pattern=$@ -LOCAL_DYNAMO_ENDPOINT=http://localhost:8000 node ./test/init.js && LOCAL_DYNAMO_ENDPOINT=http://localhost:8000 mocha -r ts-node/register ./test/**.spec.ts --grep "$pattern" \ No newline at end of file +LOCAL_DYNAMO_ENDPOINT=http://localhost:8000 node ./test/init.js && LOCAL_DYNAMO_ENDPOINT=http://localhost:8000 npm run test-ts -- --grep "$pattern" \ No newline at end of file