From ba9e8908c7c506d02f76950f4f4e433ba2c92bf0 Mon Sep 17 00:00:00 2001 From: Noah Anabiik Schwarz Date: Tue, 3 Mar 2020 13:29:48 +0100 Subject: [PATCH] fix: runs in strict mode aka deno v0.34.0 (closes #16) --- .github/workflows/cloud_ci.yml | 4 +- .github/workflows/local_ci.yml | 8 +- .gitignore | 4 +- api/api.ts | 14 +- api/collection.ts | 2 + api/mod.ts | 9 +- api/operation.ts | 8 +- api/shape.ts | 134 ++++---- client/aws_signature_v4.ts | 108 +++--- client/base_op.ts | 7 +- client/converter.ts | 602 +++++++++++++++++---------------- client/create_cache.ts | 4 +- client/create_headers.ts | 2 +- client/derive_config.ts | 2 +- client/translator.ts | 14 +- deps.ts | 6 +- test.ts | 490 --------------------------- test/cloud.ts | 20 +- test/local.ts | 22 +- test/signv4.ts | 10 +- util.ts | 6 +- 21 files changed, 503 insertions(+), 973 deletions(-) delete mode 100644 test.ts diff --git a/.github/workflows/cloud_ci.yml b/.github/workflows/cloud_ci.yml index a867ee8..493acc0 100644 --- a/.github/workflows/cloud_ci.yml +++ b/.github/workflows/cloud_ci.yml @@ -17,9 +17,9 @@ jobs: TABLE_NAME: testing_table steps: - name: clone repo - uses: actions/checkout@v1.0.0 + uses: actions/checkout@v2.0.0 - name: install deno - uses: denolib/setup-deno@v1.1.0 + uses: denolib/setup-deno@v1.2.0 - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v1 with: diff --git a/.github/workflows/local_ci.yml b/.github/workflows/local_ci.yml index fcf0a60..1f6fc89 100644 --- a/.github/workflows/local_ci.yml +++ b/.github/workflows/local_ci.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - name: clone repo - uses: actions/checkout@v1.0.0 + uses: actions/checkout@v2.0.0 - name: install deno - uses: denolib/setup-deno@v1.1.0 + uses: denolib/setup-deno@v1.2.0 - name: run tests run: deno run --allow-env ./test/signv4.ts test_local: @@ -20,9 +20,9 @@ jobs: runs-on: ubuntu-latest steps: - name: clone repo - uses: actions/checkout@v1.0.0 + uses: actions/checkout@v2.0.0 - name: install deno - uses: denolib/setup-deno@v1.1.0 + uses: denolib/setup-deno@v1.2.0 - name: start a local dynamodb run: curl -fsSL https://denopkg.com/chiefbiiko/dynamodb@v0.2.0/start_db.sh | bash - name: sleep 2 allow db startup diff --git a/.gitignore b/.gitignore index ad2363e..569a7ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -dynamodb_local_latest.zip -dynamodb_local_latest \ No newline at end of file +dynamodb_local_latest* +shared-local-instance.db* \ No newline at end of file diff --git a/api/api.ts b/api/api.ts index 6835bad..30a62db 100644 --- a/api/api.ts +++ b/api/api.ts @@ -7,10 +7,14 @@ import { property /*, string as stringUtil*/ } from "../util.ts"; +// NOTE: 2 run in ts strict-mode (bypassing TS7009) +const _Collection: any = Collection +const _Operation: any = Operation + // var Paginator = require('./paginator'); // var ResourceWaiter = require('./resource_waiter'); -export function Api(api: Doc = {}, options: Doc = {}) { +export function Api(this: any, api: Doc = {}, options: Doc = {}) { const self: any = this; // api = api || {}; // options = options || {}; @@ -38,7 +42,7 @@ export function Api(api: Doc = {}, options: Doc = {}) { api.metadata.serviceAbbreviation || api.metadata.serviceFullName; if (!name) { - return null; + return ""; } name = name.replace(/^Amazon|AWS\s*|\(.*|\s+|\W+/g, ""); @@ -59,11 +63,11 @@ export function Api(api: Doc = {}, options: Doc = {}) { property( this, "operations", - new Collection( + new _Collection( api.operations, options, function(name: string, operation: Doc): any { - return new Operation(name, operation, options); + return new _Operation(name, operation, options); } /*, stringUtil.lowerFirst*/, addEndpointOperation ) @@ -72,7 +76,7 @@ export function Api(api: Doc = {}, options: Doc = {}) { property( this, "shapes", - new Collection(api.shapes, options, function( + new _Collection(api.shapes, options, function( name: string, shape: Doc ): any { diff --git a/api/collection.ts b/api/collection.ts index c624231..9902d90 100644 --- a/api/collection.ts +++ b/api/collection.ts @@ -3,6 +3,7 @@ import { Doc, memoizedProperty, noop } from "../util.ts"; function memoize( + this: any, name: string, value: any, factory: ( @@ -16,6 +17,7 @@ function memoize( } export function Collection( + this: any, iterable: any, options: Doc, factory: ( diff --git a/api/mod.ts b/api/mod.ts index 0ef6d5e..0d2a408 100644 --- a/api/mod.ts +++ b/api/mod.ts @@ -1,7 +1,8 @@ -import { Doc } from "../util.ts"; import { Api } from "./api.ts"; -const spec: Doc = JSON.parse(` +const _Api: any = Api; + +export const API: any = new _Api(JSON.parse(` { "version": "2.0", "metadata": { @@ -2228,6 +2229,4 @@ const spec: Doc = JSON.parse(` } } } -`); - -export const API: any = new Api(spec); +`)); diff --git a/api/operation.ts b/api/operation.ts index 9b4970f..738ad44 100644 --- a/api/operation.ts +++ b/api/operation.ts @@ -6,7 +6,7 @@ import { Shape } from "./shape.ts"; import { Doc, memoizedProperty, property } from "../util.ts"; // import { Doc} from "../types.ts" -export function Operation(name: string, operation: Doc, options: Doc = {}) { +export function Operation(this: any, name: string, operation: Doc, options: Doc = {}) { const self: any = this; // options = options || {}; @@ -47,7 +47,7 @@ export function Operation(name: string, operation: Doc, options: Doc = {}) { memoizedProperty(this, "errors", function(): any[] { if (!operation.errors) { - return null; + return []; } return operation.errors.map((error: any): any => @@ -83,8 +83,8 @@ export function Operation(name: string, operation: Doc, options: Doc = {}) { } return Object.entries(self.input.members) - .filter(([_, value]: [string, Doc]): boolean => value.isIdempotent) - .map(([key, _]: [string, Doc]): string => key); + .filter(([_, value]: [string, any]): boolean => value.isIdempotent) + .map(([key, _]: [string, any]): string => key); // for (const name in members) { // if (!members.hasOwnProperty(name)) { diff --git a/api/shape.ts b/api/shape.ts index c66e6d1..beb7fb1 100644 --- a/api/shape.ts +++ b/api/shape.ts @@ -8,7 +8,10 @@ import { property as utilProperty } from "../util.ts"; +const _Collection: any = Collection + function property( + this: any, obj: any, name: string, value: any, @@ -16,22 +19,23 @@ function property( isValue?: boolean ): void { if (value !== null && value !== undefined) { - utilProperty.apply(this, arguments); + utilProperty.apply(this, arguments as any); } } function memoizedProperty( + this: any, obj: any, name: string, get: () => any, enumerable?: boolean ): void { if (!obj.constructor.prototype[name]) { - utilMemoizedProperty.apply(this, arguments); + utilMemoizedProperty.apply(this, arguments as any); } } -export function Shape(shape: Doc, options: Doc = {}, memberName: string) { +export function Shape(this: any, shape: Doc, options: Doc = {}, memberName: string) { property(this, "shape", shape.shape); property(this, "api", options.api, false); property(this, "type", shape.type); @@ -135,7 +139,7 @@ Shape.types = { binary: BinaryShape }; -Shape.resolve = function resolve(shape: Doc, options: Doc = {}): Doc { +Shape.resolve = function resolve(shape: Doc, options: Doc = {}): Doc |null { if (shape.shape) { const refShape: Doc = options.api.shapes[shape.shape]; @@ -152,13 +156,13 @@ Shape.resolve = function resolve(shape: Doc, options: Doc = {}): Doc { Shape.create = function create( shape: Doc, options: Doc = {}, - memberName: string = undefined + memberName: string = "" ): any { if (shape.isShape) { return shape; } - const refShape: Doc = Shape.resolve(shape, options); + const refShape: Doc | null = Shape.resolve(shape, options); if (refShape) { let filteredKeys: string[] = Object.keys(shape); @@ -170,7 +174,7 @@ Shape.create = function create( } // create an inline shape with extra members - const InlineShape: any = function(): void { + const InlineShape: any = function(this: any): void { refShape.constructor.call(this, shape, options, memberName); }; @@ -193,20 +197,20 @@ Shape.create = function create( // normalize types const origType: string = shape.type; - if (Shape.normalizedTypes[shape.type]) { - shape.type = Shape.normalizedTypes[shape.type]; + if ((Shape.normalizedTypes as any)[shape.type]) { + shape.type = (Shape.normalizedTypes as any)[shape.type]; } - if (Shape.types[shape.type]) { - return new Shape.types[shape.type](shape, options, memberName); + if ((Shape.types as any)[shape.type]) { + return new (Shape.types as any)[shape.type](shape, options, memberName); } else { throw new Error("Unrecognized shape type: " + origType); } } }; -function CompositeShape(shape: Doc) { - Shape.apply(this, arguments); +function CompositeShape(this: any, shape: Doc) { + Shape.apply(this, arguments as any); property(this, "isComposite", true); if (shape.flattened) { @@ -214,12 +218,12 @@ function CompositeShape(shape: Doc) { } } -function StructureShape(shape: Doc, options: Doc = {}) { +function StructureShape(this: any, shape: Doc, options: Doc = {}) { const self: any = this; - let requiredMap: Doc = null; + // let requiredMap: null | Doc = null; const firstInit: boolean = !this.isShape; - CompositeShape.apply(this, arguments); + CompositeShape.apply(this, arguments as any); if (firstInit) { property(this, "defaultValue", function() { @@ -237,7 +241,7 @@ function StructureShape(shape: Doc, options: Doc = {}) { property( this, "members", - new Collection(shape.members, options, function( + new _Collection(shape.members, options, function( name: string, member: Doc ): any { @@ -260,6 +264,8 @@ function StructureShape(shape: Doc, options: Doc = {}) { return memberNames[i]; } } + + return ""; }); memoizedProperty(this, "eventHeaderMemberNames", function(): string[] { @@ -281,22 +287,27 @@ function StructureShape(shape: Doc, options: Doc = {}) { if (shape.required) { property(this, "required", shape.required); + + const requiredMap = shape.required.reduce((acc: Doc, req: string): Doc => { + acc[req] = true; + return acc; + }, {}) property( this, "isRequired", function(name: string): boolean { - if (!requiredMap) { - // requiredMap = {}; - // - // for (let i:number = 0; i < shape.required.length; i++) { - // requiredMap[shape.required[i]] = true; - // } - requiredMap = shape.required.reduce((acc: Doc, req: string): Doc => { - acc[req] = true; - return acc; - }, {}); - } + // if (!requiredMap) { + // // requiredMap = {}; + // // + // // for (let i:number = 0; i < shape.required.length; i++) { + // // requiredMap[shape.required[i]] = true; + // // } + // requiredMap = shape.required.reduce((acc: Doc, req: string): Doc => { + // acc[req] = true; + // return acc; + // }, {}); + // } return requiredMap[name]; }, @@ -319,11 +330,11 @@ function StructureShape(shape: Doc, options: Doc = {}) { } } -function ListShape(shape: Doc, options: Doc = {}) { +function ListShape(this: any, shape: Doc, options: Doc = {}) { const self: any = this; const firstInit: boolean = !this.isShape; - CompositeShape.apply(this, arguments); + CompositeShape.apply(this, arguments as any); if (firstInit) { property(this, "defaultValue", function(): any[] { @@ -346,10 +357,10 @@ function ListShape(shape: Doc, options: Doc = {}) { } } -function MapShape(shape: Doc, options: Doc = {}) { +function MapShape(this: any,shape: Doc, options: Doc = {}) { const firstInit: boolean = !this.isShape; - CompositeShape.apply(this, arguments); + CompositeShape.apply(this, arguments as any); if (firstInit) { property(this, "defaultValue", function(): Doc { @@ -372,10 +383,10 @@ function MapShape(shape: Doc, options: Doc = {}) { } } -function TimestampShape(shape: Doc) { +function TimestampShape(this: any, shape: Doc) { const self: any = this; - Shape.apply(this, arguments); + Shape.apply(this, arguments as any); if (shape.timestampFormat) { property(this, "timestampFormat", shape.timestampFormat); @@ -399,18 +410,23 @@ function TimestampShape(shape: Doc) { } } - this.toType = function(value: any): Date { + this.toType = function(value: any): undefined |Date { if (value === null || value === undefined) { - return null; + return undefined; } - if (typeof value.toUTCString === "function") { - return value; + if (typeof value.toISOString === "function") { + return value as Date; + } + + if ( typeof value === "string" || typeof value === "number") { + return date.parseTimestamp(value) } - return typeof value === "string" || typeof value === "number" - ? date.parseTimestamp(value) - : null; + return undefined + // return typeof value === "string" || typeof value === "number" + // ? date.parseTimestamp(value) + // : null; }; this.toWireFormat = function(value: any): string { @@ -418,8 +434,8 @@ function TimestampShape(shape: Doc) { }; } -function StringShape() { - Shape.apply(this, arguments); +function StringShape(this: any) { + Shape.apply(this, arguments as any); const nullLessProtocols: string[] = ["rest-xml", "query", "ec2"]; @@ -443,12 +459,12 @@ function StringShape() { }; } -function FloatShape() { - Shape.apply(this, arguments); +function FloatShape(this: any) { + Shape.apply(this, arguments as any); - this.toType = function(value: any): number { + this.toType = function(value: any): undefined|number { if (value === null || value === undefined) { - return null; + return undefined; } return parseFloat(value); @@ -457,12 +473,12 @@ function FloatShape() { this.toWireFormat = this.toType; } -function IntegerShape() { - Shape.apply(this, arguments); +function IntegerShape(this:any) { + Shape.apply(this, arguments as any); - this.toType = function(value: any): number { + this.toType = function(value: any): undefined|number { if (value === null || value === undefined) { - return null; + return undefined; } return parseInt(value, 10); @@ -471,28 +487,28 @@ function IntegerShape() { this.toWireFormat = this.toType; } -function BinaryShape() { - Shape.apply(this, arguments); +function BinaryShape(this:any) { + Shape.apply(this, arguments as any); this.toType = base64ToUint8Array; this.toWireFormat = base64FromUint8Array; } -function Base64Shape() { - BinaryShape.apply(this, arguments); +function Base64Shape(this: any) { + BinaryShape.apply(this, arguments as any); } -function BooleanShape() { - Shape.apply(this, arguments); +function BooleanShape(this: any) { + Shape.apply(this, arguments as any); - this.toType = function(value: any): boolean { + this.toType = function(value: any): undefined|boolean { if (typeof value === "boolean") { return value; } if (value === null || value === undefined) { - return null; + return undefined; } return value === "true"; diff --git a/client/aws_signature_v4.ts b/client/aws_signature_v4.ts index 35bdcda..6791e9e 100644 --- a/client/aws_signature_v4.ts +++ b/client/aws_signature_v4.ts @@ -1,54 +1,54 @@ -import { encode, decode, hmac } from "../deps.ts"; -import { date } from "../util.ts"; - -/** Some magic bytes. */ -const AWS4: Uint8Array = encode("AWS4", "utf8"); - -/** Creates a HMAC-SHA256 mac.*/ -export function awsSignatureV4( - key: Uint8Array, - msg: Uint8Array, - outputEncoding?: string -): string | Uint8Array { - return hmac("sha256", key, msg, null, outputEncoding); -} - -/** Creates a key for generating an aws signature version 4. */ -export function kdf( - key: string | Uint8Array, - dateStamp: Date | string, - region: string, - service: string, - keyInputEncoding?: string, - outputEncoding?: string -): string | Uint8Array { - if (typeof key === "string") { - key = encode(key, keyInputEncoding) as Uint8Array; - } - - if (typeof dateStamp !== "string") { - dateStamp = date.format(dateStamp, "dateStamp"); - } else if (!date.DATE_STAMP_REGEX.test(dateStamp)) { - throw new TypeError("date stamp format must be yyyymmdd"); - } - - const paddedKey: Uint8Array = new Uint8Array(4 + key.byteLength); - - paddedKey.set(AWS4, 0); - paddedKey.set(key, 4); - - let mac: Uint8Array = hmac( - "sha256", - paddedKey, - dateStamp as string, - "utf8" - ) as Uint8Array; - - mac = hmac("sha256", mac, region, "utf8") as Uint8Array; - - mac = hmac("sha256", mac, service, "utf8") as Uint8Array; - - mac = hmac("sha256", mac, "aws4_request", "utf8") as Uint8Array; - - return outputEncoding ? decode(mac, outputEncoding) : mac; -} +import { encode, decode, hmac } from "../deps.ts"; +import { date } from "../util.ts"; + +/** Some magic bytes. */ +const AWS4: Uint8Array = encode("AWS4", "utf8"); + +/** Creates a HMAC-SHA256 mac.*/ +export function awsSignatureV4( + key: Uint8Array, + msg: Uint8Array, + outputEncoding?: string +): string | Uint8Array { + return hmac("sha256", key, msg, undefined, outputEncoding); +} + +/** Creates a key for generating an aws signature version 4. */ +export function kdf( + key: string | Uint8Array, + dateStamp: Date | string, + region: string, + service: string, + keyInputEncoding?: string, + outputEncoding?: string +): string | Uint8Array { + if (typeof key === "string") { + key = encode(key, keyInputEncoding) as Uint8Array; + } + + if (typeof dateStamp !== "string") { + dateStamp = date.format(dateStamp, "dateStamp"); + } else if (!date.DATE_STAMP_REGEX.test(dateStamp)) { + throw new TypeError("date stamp format must be yyyymmdd"); + } + + const paddedKey: Uint8Array = new Uint8Array(4 + key.byteLength); + + paddedKey.set(AWS4, 0); + paddedKey.set(key, 4); + + let mac: Uint8Array = hmac( + "sha256", + paddedKey, + dateStamp as string, + "utf8" + ) as Uint8Array; + + mac = hmac("sha256", mac, region, "utf8") as Uint8Array; + + mac = hmac("sha256", mac, service, "utf8") as Uint8Array; + + mac = hmac("sha256", mac, "aws4_request", "utf8") as Uint8Array; + + return outputEncoding ? decode(mac, outputEncoding) : mac; +} diff --git a/client/base_op.ts b/client/base_op.ts index 92a287c..f7e29bb 100644 --- a/client/base_op.ts +++ b/client/base_op.ts @@ -3,6 +3,9 @@ import { API } from "../api/mod.ts"; import { Translator } from "./translator.ts"; import { Doc } from "../util.ts"; +// ts strict food +const _Translator: any = Translator; + /** Op options. */ export interface OpOptions { wrapNumbers?: boolean; // wrap numbers to a special number value type? [false] @@ -38,7 +41,7 @@ export async function baseOp( let outputShape: any; if (translateJSON) { - translator = new Translator({ + translator = new _Translator({ wrapNumbers, convertEmptyValues, attrValue: ATTR_VALUE @@ -59,7 +62,7 @@ export async function baseOp( return { [Symbol.asyncIterator](): AsyncIterableIterator { - return this; + return this as AsyncIterableIterator; }, async next(): Promise> { if (!lastEvaluatedKey) { diff --git a/client/converter.ts b/client/converter.ts index 716568e..e7d47a6 100644 --- a/client/converter.ts +++ b/client/converter.ts @@ -1,300 +1,302 @@ -// import DynamoDB = require('../../clients/dynamodb'); -import { base64ToUint8Array, base64FromUint8Array } from "../deps.ts"; -import { Doc, DynamoDBSet, DynamoDBNumberValue, typeOf } from "../util.ts"; - -/** Formats a list. */ -function formatList(data: any[], options: Doc = {}): Doc { - const list: Doc = { L: [] }; - - for (let i: number = 0; i < data.length; i++) { - list["L"].push(Converter.input(data[i], options)); - } - - return list; -} - -/** Converts a number. */ -function convertNumber(value: string, wrapNumbers: boolean = false): any { - return wrapNumbers ? new DynamoDBNumberValue(value) : Number(value); -} - -/** Formats a map. */ -function formatMap(data: Doc, options: Doc = {}): Doc { - const map: Doc = { M: {} }; - - for (const key in data) { - const formatted: Doc = Converter.input(data[key], options); - - if (formatted !== void 0) { - map["M"][key] = formatted; - } - } - - return map; -} - -/** Formats a set. */ -function formatSet(data: Doc, options: Doc = {}): Doc { - let values: any[] = data.values; - - if (options.convertEmptyValues) { - values = filterEmptySetValues(data); - - if (values.length === 0) { - return Converter.input(null); - } - } - - const map: Doc = {}; - - switch (data.type) { - case "String": - map["SS"] = values; - break; - case "Binary": - map["BS"] = values; - break; - case "Number": - map["NS"] = values.map(function(value) { - return value.toString(); - }); - } - - return map; -} - -/** Filters empty set values. */ -function filterEmptySetValues(set: Doc): any[] { - const nonEmptyValues: any[] = []; - - const potentiallyEmptyTypes: Doc = { - String: true, - Binary: true, - Number: false - }; - - if (potentiallyEmptyTypes[set.type]) { - for (let i: number = 0; i < set.values.length; i++) { - if (set.values[i].length === 0) { - continue; - } - - nonEmptyValues.push(set.values[i]); - } - - return nonEmptyValues; - } - - return set.values; -} - -/** aws DynamoDB req/res document converter. */ -export class Converter { - /** - * Convert a JavaScript value to its equivalent DynamoDB AttributeValue type - * - * @param data [any] The data to convert to a DynamoDB AttributeValue - * @param options [map] - * @option options convertEmptyValues [Boolean] Whether to automatically - * convert empty strings, blobs, - * and sets to `null` - * @option options wrapNumbers [Boolean] Whether to return numbers as a - * NumberValue object instead of - * converting them to native JavaScript - * numbers. This allows for the safe - * round-trip transport of numbers of - * arbitrary size. - * @return [map] An object in the Amazon DynamoDB AttributeValue format - * - * @see AWS.DynamoDB.Converter.marshall AWS.DynamoDB.Converter.marshall to - * convert entire records (rather than individual attributes) - */ - static input(data: any, options: Doc = {}): Doc { - const type: string = typeOf(data); - - if (type === "Object") { - return formatMap(data, options); - } else if (type === "Array") { - return formatList(data, options); - } else if (type === "Set") { - return formatSet(data, options); - } else if (type === "String") { - if (data.length === 0 && options.convertEmptyValues) { - return Converter.input(null); - } - return { S: data }; - } else if (type === "Number" || type === "NumberValue") { - return { N: data.toString() }; - } else if (type === "Binary") { - if (data.length === 0 && options.convertEmptyValues) { - return Converter.input(null); - } - // return { B: data }; - return { B: base64FromUint8Array(data) }; - } else if (type === "Boolean") { - return { BOOL: data }; - } else if (type === "null") { - return { NULL: true }; - } else if (type !== "undefined" && type !== "Function") { - // this value has a custom constructor - return formatMap(data, options); - } - } - - /** - * Convert a JavaScript object into a DynamoDB record. - * - * @param data [any] The data to convert to a DynamoDB record - * @param options [map] - * @option options convertEmptyValues [Boolean] Whether to automatically - * convert empty strings, blobs, - * and sets to `null` - * @option options wrapNumbers [Boolean] Whether to return numbers as a - * NumberValue object instead of - * converting them to native JavaScript - * numbers. This allows for the safe - * round-trip transport of numbers of - * arbitrary size. - * - * @return [map] An object in the DynamoDB record format. - * - * @example Convert a JavaScript object into a DynamoDB record - * var marshalled = AWS.DynamoDB.Converter.marshall({ - * string: 'foo', - * list: ['fizz', 'buzz', 'pop'], - * map: { - * nestedMap: { - * key: 'value', - * } - * }, - * number: 123, - * nullValue: null, - * boolValue: true, - * stringSet: new DynamoDBSet(['foo', 'bar', 'baz']) - * }); - */ - static marshall(data: Doc, options?: Doc): Doc { - return Converter.input(data, options).M; - } - - /** - * Convert a DynamoDB AttributeValue object to its equivalent JavaScript type. - * - * @param data [map] An object in the Amazon DynamoDB AttributeValue format - * @param options [map] - * @option options convertEmptyValues [Boolean] Whether to automatically - * convert empty strings, blobs, - * and sets to `null` - * @option options wrapNumbers [Boolean] Whether to return numbers as a - * NumberValue object instead of - * converting them to native JavaScript - * numbers. This allows for the safe - * round-trip transport of numbers of - * arbitrary size. - * - * @return [Object|Array|String|Number|Boolean|null] - * - * @see AWS.DynamoDB.Converter.unmarshall AWS.DynamoDB.Converter.unmarshall to - * convert entire records (rather than individual attributes) - */ - static output(data: Doc, options: Doc = {}): any { - for (const type in data) { - const values: any = data[type]; - - if (type === "M") { - const map: Doc = {}; - - for (const key in values) { - map[key] = Converter.output(values[key], options); - } - - return map; - } else if (type === "L") { - // list = []; - // for (i = 0; i < values.length; i++) { - // list.push(Converter.output(values[i], options)); - // } - // return list; - return values.map((value: any): any => - Converter.output(value, options) - ); - } else if (type === "SS") { - // list = []; - // for (i = 0; i < values.length; i++) { - // list.push(values[i] + ''); - // } - // return new DynamoDBSet(list); - return new DynamoDBSet(values.map(String)); - } else if (type === "NS") { - // list = []; - // for (i = 0; i < values.length; i++) { - // list.push(convertNumber(values[i], options.wrapNumbers)); - // } - // return new DynamoDBSet(list); - return new DynamoDBSet( - values.map((value: any): number => - convertNumber(value, options.wrapNumbers) - ) - ); - } else if (type === "BS") { - // list = []; - // for (i = 0; i < values.length; i++) { - // list.push(base64ToUint8Array(values[i])); - // } - // return new DynamoDBSet(list); - return new DynamoDBSet(values.map(base64ToUint8Array)); - } else if (type === "S") { - return String(values); - } else if (type === "N") { - return convertNumber(values, options.wrapNumbers); - } else if (type === "B") { - return base64ToUint8Array(values); - } else if (type === "BOOL") { - return values === "true" || values === "TRUE" || values === true; - } else if (type === "NULL") { - return null; - } - } - } - - /** - * Convert a DynamoDB record into a JavaScript object. - * - * @param data [any] The DynamoDB record - * @param options [map] - * @option options convertEmptyValues [Boolean] Whether to automatically - * convert empty strings, blobs, - * and sets to `null` - * @option options wrapNumbers [Boolean] Whether to return numbers as a - * NumberValue object instead of - * converting them to native JavaScript - * numbers. This allows for the safe - * round-trip transport of numbers of - * arbitrary size. - * - * @return [map] An object whose properties have been converted from - * DynamoDB's AttributeValue format into their corresponding native - * JavaScript types. - * - * @example Convert a record received from a DynamoDB stream - * var unmarshalled = AWS.DynamoDB.Converter.unmarshall({ - * string: {S: 'foo'}, - * list: {L: [{S: 'fizz'}, {S: 'buzz'}, {S: 'pop'}]}, - * map: { - * M: { - * nestedMap: { - * M: { - * key: {S: 'value'} - * } - * } - * } - * }, - * number: {N: '123'}, - * nullValue: {NULL: true}, - * boolValue: {BOOL: true} - * }); - */ - static unmarshall(data: Doc, options?: Doc): Doc { - return Converter.output({ M: data }, options); - } -} +// import DynamoDB = require('../../clients/dynamodb'); +import { base64ToUint8Array, base64FromUint8Array } from "../deps.ts"; +import { Doc, DynamoDBSet, DynamoDBNumberValue, typeOf } from "../util.ts"; + +/** Formats a list. */ +function formatList(data: any[], options: Doc = {}): Doc { + const list: Doc = { L: [] }; + + for (let i: number = 0; i < data.length; i++) { + list["L"].push(Converter.input(data[i], options)); + } + + return list; +} + +/** Converts a number. */ +function convertNumber(value: string, wrapNumbers: boolean = false): any { + return wrapNumbers ? new DynamoDBNumberValue(value) : Number(value); +} + +/** Formats a map. */ +function formatMap(data: Doc, options: Doc = {}): Doc { + const map: Doc = { M: {} }; + + for (const key in data) { + const formatted: Doc = Converter.input(data[key], options); + + if (formatted !== void 0) { + map["M"][key] = formatted; + } + } + + return map; +} + +/** Formats a set. */ +function formatSet(data: Doc, options: Doc = {}): Doc { + let values: any[] = data.values; + + if (options.convertEmptyValues) { + values = filterEmptySetValues(data); + + if (values.length === 0) { + return Converter.input(null); + } + } + + const map: Doc = {}; + + switch (data.type) { + case "String": + map["SS"] = values; + break; + case "Binary": + map["BS"] = values; + break; + case "Number": + map["NS"] = values.map(function(value) { + return value.toString(); + }); + } + + return map; +} + +/** Filters empty set values. */ +function filterEmptySetValues(set: Doc): any[] { + const nonEmptyValues: any[] = []; + + const potentiallyEmptyTypes: Doc = { + String: true, + Binary: true, + Number: false + }; + + if (potentiallyEmptyTypes[set.type]) { + for (let i: number = 0; i < set.values.length; i++) { + if (set.values[i].length === 0) { + continue; + } + + nonEmptyValues.push(set.values[i]); + } + + return nonEmptyValues; + } + + return set.values; +} + +/** aws DynamoDB req/res document converter. */ +export class Converter { + /** + * Convert a JavaScript value to its equivalent DynamoDB AttributeValue type + * + * @param data [any] The data to convert to a DynamoDB AttributeValue + * @param options [map] + * @option options convertEmptyValues [Boolean] Whether to automatically + * convert empty strings, blobs, + * and sets to `null` + * @option options wrapNumbers [Boolean] Whether to return numbers as a + * NumberValue object instead of + * converting them to native JavaScript + * numbers. This allows for the safe + * round-trip transport of numbers of + * arbitrary size. + * @return [map] An object in the Amazon DynamoDB AttributeValue format + * + * @see AWS.DynamoDB.Converter.marshall AWS.DynamoDB.Converter.marshall to + * convert entire records (rather than individual attributes) + */ + static input(data: any, options: Doc = {}): Doc { + const type: string = typeOf(data); + + if (type === "Object") { + return formatMap(data, options); + } else if (type === "Array") { + return formatList(data, options); + } else if (type === "Set") { + return formatSet(data, options); + } else if (type === "String") { + if (data.length === 0 && options.convertEmptyValues) { + return Converter.input(null); + } + return { S: data }; + } else if (type === "Number" || type === "NumberValue") { + return { N: data.toString() }; + } else if (type === "Binary") { + if (data.length === 0 && options.convertEmptyValues) { + return Converter.input(null); + } + // return { B: data }; + return { B: base64FromUint8Array(data) }; + } else if (type === "Boolean") { + return { BOOL: data }; + } else if (type === "null") { + return { NULL: true }; + } else if (type !== "undefined" && type !== "Function") { + // this value has a custom constructor + return formatMap(data, options); + } + + return {} + } + + /** + * Convert a JavaScript object into a DynamoDB record. + * + * @param data [any] The data to convert to a DynamoDB record + * @param options [map] + * @option options convertEmptyValues [Boolean] Whether to automatically + * convert empty strings, blobs, + * and sets to `null` + * @option options wrapNumbers [Boolean] Whether to return numbers as a + * NumberValue object instead of + * converting them to native JavaScript + * numbers. This allows for the safe + * round-trip transport of numbers of + * arbitrary size. + * + * @return [map] An object in the DynamoDB record format. + * + * @example Convert a JavaScript object into a DynamoDB record + * var marshalled = AWS.DynamoDB.Converter.marshall({ + * string: 'foo', + * list: ['fizz', 'buzz', 'pop'], + * map: { + * nestedMap: { + * key: 'value', + * } + * }, + * number: 123, + * nullValue: null, + * boolValue: true, + * stringSet: new DynamoDBSet(['foo', 'bar', 'baz']) + * }); + */ + static marshall(data: Doc, options?: Doc): Doc { + return Converter.input(data, options).M; + } + + /** + * Convert a DynamoDB AttributeValue object to its equivalent JavaScript type. + * + * @param data [map] An object in the Amazon DynamoDB AttributeValue format + * @param options [map] + * @option options convertEmptyValues [Boolean] Whether to automatically + * convert empty strings, blobs, + * and sets to `null` + * @option options wrapNumbers [Boolean] Whether to return numbers as a + * NumberValue object instead of + * converting them to native JavaScript + * numbers. This allows for the safe + * round-trip transport of numbers of + * arbitrary size. + * + * @return [Object|Array|String|Number|Boolean|null] + * + * @see AWS.DynamoDB.Converter.unmarshall AWS.DynamoDB.Converter.unmarshall to + * convert entire records (rather than individual attributes) + */ + static output(data: Doc, options: Doc = {}): any { + for (const type in data) { + const values: any = data[type]; + + if (type === "M") { + const map: Doc = {}; + + for (const key in values) { + map[key] = Converter.output(values[key], options); + } + + return map; + } else if (type === "L") { + // list = []; + // for (i = 0; i < values.length; i++) { + // list.push(Converter.output(values[i], options)); + // } + // return list; + return values.map((value: any): any => + Converter.output(value, options) + ); + } else if (type === "SS") { + // list = []; + // for (i = 0; i < values.length; i++) { + // list.push(values[i] + ''); + // } + // return new DynamoDBSet(list); + return new DynamoDBSet(values.map(String)); + } else if (type === "NS") { + // list = []; + // for (i = 0; i < values.length; i++) { + // list.push(convertNumber(values[i], options.wrapNumbers)); + // } + // return new DynamoDBSet(list); + return new DynamoDBSet( + values.map((value: any): number => + convertNumber(value, options.wrapNumbers) + ) + ); + } else if (type === "BS") { + // list = []; + // for (i = 0; i < values.length; i++) { + // list.push(base64ToUint8Array(values[i])); + // } + // return new DynamoDBSet(list); + return new DynamoDBSet(values.map(base64ToUint8Array)); + } else if (type === "S") { + return String(values); + } else if (type === "N") { + return convertNumber(values, options.wrapNumbers); + } else if (type === "B") { + return base64ToUint8Array(values); + } else if (type === "BOOL") { + return values === "true" || values === "TRUE" || values === true; + } else if (type === "NULL") { + return null; + } + } + } + + /** + * Convert a DynamoDB record into a JavaScript object. + * + * @param data [any] The DynamoDB record + * @param options [map] + * @option options convertEmptyValues [Boolean] Whether to automatically + * convert empty strings, blobs, + * and sets to `null` + * @option options wrapNumbers [Boolean] Whether to return numbers as a + * NumberValue object instead of + * converting them to native JavaScript + * numbers. This allows for the safe + * round-trip transport of numbers of + * arbitrary size. + * + * @return [map] An object whose properties have been converted from + * DynamoDB's AttributeValue format into their corresponding native + * JavaScript types. + * + * @example Convert a record received from a DynamoDB stream + * var unmarshalled = AWS.DynamoDB.Converter.unmarshall({ + * string: {S: 'foo'}, + * list: {L: [{S: 'fizz'}, {S: 'buzz'}, {S: 'pop'}]}, + * map: { + * M: { + * nestedMap: { + * M: { + * key: {S: 'value'} + * } + * } + * } + * }, + * number: {N: '123'}, + * nullValue: {NULL: true}, + * boolValue: {BOOL: true} + * }); + */ + static unmarshall(data: Doc, options?: Doc): Doc { + return Converter.output({ M: data }, options); + } +} diff --git a/client/create_cache.ts b/client/create_cache.ts index 94bc6e2..13b7f0d 100644 --- a/client/create_cache.ts +++ b/client/create_cache.ts @@ -20,13 +20,13 @@ export function createCache(conf: ClientConfig): Doc { if (typeof conf.credentials === "function") { credentials = await conf.credentials(); } else { - credentials = conf.credentials; + credentials = conf.credentials!; } this._signingKey = kdf( credentials.secretAccessKey, dateStamp, - conf.region, + conf.region!, SERVICE ) as Uint8Array; diff --git a/client/create_headers.ts b/client/create_headers.ts index 9467342..637d863 100644 --- a/client/create_headers.ts +++ b/client/create_headers.ts @@ -38,7 +38,7 @@ export async function createHeaders( const signedHeaders: string = "content-type;host;x-amz-date;x-amz-target"; - const payloadHash: string = sha256(payload, null, "hex") as string; + const payloadHash: string = sha256(payload, undefined, "hex") as string; const canonicalRequest: string = `${conf.method}\n${canonicalUri}\n\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`; diff --git a/client/derive_config.ts b/client/derive_config.ts index adf3a48..1738679 100644 --- a/client/derive_config.ts +++ b/client/derive_config.ts @@ -60,6 +60,6 @@ export function deriveConfig(conf: ClientConfig = {}): Doc { ..._conf, cache: createCache(_conf), method: "POST", - ...deriveHostEndpoint(_conf.region, _conf.port) + ...deriveHostEndpoint(_conf.region!, _conf.port!) }; } diff --git a/client/translator.ts b/client/translator.ts index 9f54744..24fd8f8 100644 --- a/client/translator.ts +++ b/client/translator.ts @@ -3,7 +3,9 @@ import { Doc } from "../util.ts"; import { Converter } from "./converter.ts"; -export function Translator({ +export function Translator( + this: any, + { wrapNumbers, convertEmptyValues, attrValue @@ -32,7 +34,7 @@ Translator.prototype.translate = function(value: any, shape: any): any { } if (shape.shape === self.attrValue) { - return Converter[self.mode](value, { + return (Converter as any)[self.mode](value, { convertEmptyValues: self.convertEmptyValues, wrapNumbers: self.wrapNumbers }); @@ -53,7 +55,7 @@ Translator.prototype.translate = function(value: any, shape: any): any { Translator.prototype.translateStructure = function( structure: any, shape: any -): Doc { +): undefined|Doc { const self: any = this; if (structure == null) { @@ -77,7 +79,7 @@ Translator.prototype.translateStructure = function( return struct; }; -Translator.prototype.translateList = function(list: any[], shape: any): any[] { +Translator.prototype.translateList = function(list: any[], shape: any): undefined | any[] { const self: any = this; if (list == null) { @@ -104,10 +106,10 @@ Translator.prototype.translateList = function(list: any[], shape: any): any[] { // return out; }; -Translator.prototype.translateMap = function(map: Doc, shape: any): Doc { +Translator.prototype.translateMap = function(map: Doc |undefined|null, shape: any): undefined | Doc { const self: any = this; - if (map == null) { + if (!map) { return undefined; } diff --git a/deps.ts b/deps.ts index d597e53..0f911c3 100644 --- a/deps.ts +++ b/deps.ts @@ -2,10 +2,10 @@ export { encode, decode } from "https://denopkg.com/chiefbiiko/std-encoding@v1.0.0/mod.ts"; -export { get } from "https://denopkg.com/chiefbiiko/get-aws-config@v0.3.4/mod.ts"; -export { hmac } from "https://denopkg.com/chiefbiiko/hmac@v1.0.1/mod.ts"; +export { get } from "https://denopkg.com/chiefbiiko/get-aws-config@v0.3.5/mod.ts"; +export { hmac } from "https://denopkg.com/chiefbiiko/hmac@v1.0.2/mod.ts"; export { toUint8Array as base64ToUint8Array, fromUint8Array as base64FromUint8Array } from "https://deno.land/x/base64@v0.2.0/mod.ts"; -export { sha256 } from "https://denopkg.com/chiefbiiko/sha256@v1.0.0/mod.ts"; +export { sha256 } from "https://denopkg.com/chiefbiiko/sha256@v1.0.2/mod.ts"; diff --git a/test.ts b/test.ts deleted file mode 100644 index f4f6cb2..0000000 --- a/test.ts +++ /dev/null @@ -1,490 +0,0 @@ -import { test, runIfMain } from "https://deno.land/std@v0.26.0/testing/mod.ts"; -import { - assert, - assertEquals, - assertThrowsAsync -} from "https://deno.land/std@v0.26.0/testing/asserts.ts"; -import { - ClientConfig, - Credentials, - DynamoDBClient, - createClient -} from "./mod.ts"; -import { encode } from "./deps.ts"; -import { awsSignatureV4 } from "./client/mod.ts"; -import { kdf } from "./client/aws_signature_v4.ts"; -import { Doc } from "./util.ts"; - -const env: Doc = Deno.env(); - -const CONF: ClientConfig = { - credentials: { - accessKeyId: env.AWS_ACCESS_KEY_ID || "DynamoDBLocal", - secretAccessKey: env.AWS_SECRET_ACCESS_KEY || "DoesNotDoAnyAuth", - sessionToken: env.AWS_SESSION_TOKEN || "preferTemporaryCredentials" - }, - region: "local", - port: 8000 // DynamoDB Local's default port -}; - -test({ - name: "aws signature v4 flow", - fn(): void { - const expectedSignature: string = - "31fac5ed29db737fbcafac527470ca6d9283283197c5e6e94ea40ddcec14a9c1"; - - const key: Uint8Array = kdf( - "secret", - "20310430", - "region", - "dynamodb", - "utf8" - ) as Uint8Array; - - const msg: Uint8Array = encode( - "AWS4-HMAC-SHA256\n20310430T201613Z\n20310430/region/dynamodb/aws4_request\n4be20e7bf75dc6c7e93873b5f49096771729b8a28f0c62010db431fea79220ef", - "utf8" - ); - - const actualSignature: string = awsSignatureV4(key, msg, "hex") as string; - - assertEquals(actualSignature, expectedSignature); - } -}); - -test({ - name: "schema translation enabled by default", - async fn(): Promise { - const dyno: DynamoDBClient = createClient(CONF); - - let result: Doc = await dyno.listTables(); - - if (!result.TableNames.includes("users_b")) { - await dyno.createTable({ - TableName: "users_b", - KeySchema: [{ KeyType: "HASH", AttributeName: "id" }], - AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], - ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1 } - }); - } - - const friends: string[] = ["djb", "devil", "donkey kong"]; - - result = await dyno.putItem({ - TableName: "users_b", - Item: { id: "abc", friends } - }); - - result = await dyno.getItem({ - TableName: "users_b", - Key: { id: "abc" } - }); - - assertEquals(result.Item.friends, friends); - - result = await dyno.deleteTable({ - TableName: "users_b" - }); - - assertEquals(result.TableDescription.TableName, "users_b"); - - result = await dyno.listTables(); - - assert(!result.TableNames.includes("users_b")); - } -}); - -test({ - name: "opt-in raw queries", - async fn(): Promise { - const dyno: DynamoDBClient = createClient(CONF); - - let result: Doc = await dyno.listTables(); - - if (!result.TableNames.includes("users_a")) { - await dyno.createTable( - { - TableName: "users_a", - KeySchema: [{ KeyType: "HASH", AttributeName: "id" }], - AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], - ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1 } - }, - { translateJSON: false } - ); - } - - result = await dyno.putItem( - { - TableName: "users_a", - Item: { id: { S: "abc" }, role: { S: "admin" } } - }, - { translateJSON: false } - ); - - assertEquals(result, {}); - - result = await dyno.getItem( - { - TableName: "users_a", - Key: { id: { S: "abc" } } - }, - { translateJSON: false } - ); - - assertEquals(result.Item.role.S, "admin"); - - result = await dyno.deleteItem( - { - TableName: "users_a", - Key: { id: { S: "abc" } } - }, - { translateJSON: false } - ); - - assertEquals(result, {}); - - result = await dyno.deleteTable( - { - TableName: "users_a" - }, - { translateJSON: false } - ); - - assertEquals(result.TableDescription.TableName, "users_a"); - - result = await dyno.listTables(); - - assert(!result.TableNames.includes("users_a")); - } -}); - -test({ - name: "batch write items", - async fn(): Promise { - const dyno: DynamoDBClient = createClient(CONF); - - let result: Doc = await dyno.listTables(); - - if (!result.TableNames.includes("users_c")) { - await dyno.createTable({ - TableName: "users_c", - KeySchema: [{ KeyType: "HASH", AttributeName: "id" }], - AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], - ProvisionedThroughput: { ReadCapacityUnits: 10, WriteCapacityUnits: 10 } - }); - } - - const N: number = 25; - - const params: Doc = { - RequestItems: { users_c: new Array(N) } - }; - - for (let i: number = 0; i < N; ++i) { - params.RequestItems.users_c[i] = { - PutRequest: { - Item: { - id: String(i) - } - } - }; - } - - result = await dyno.batchWriteItem(params); - - assertEquals(Object.keys(result.UnprocessedItems).length, 0); - - result = await dyno.scan({ - TableName: "users_c", - Select: "COUNT" - }); - - assertEquals(result.Count, N); - - result = await dyno.deleteTable({ - TableName: "users_c" - }); - - assertEquals(result.TableDescription.TableName, "users_c"); - - result = await dyno.listTables(); - - assert(!result.TableNames.includes("users_c")); - } -}); - -test({ - name: "storing a binary value", - async fn(): Promise { - const dyno: DynamoDBClient = createClient(CONF); - - let result: Doc = await dyno.listTables(); - - if (!result.TableNames.includes("users_d")) { - await dyno.createTable({ - TableName: "users_d", - KeySchema: [{ KeyType: "HASH", AttributeName: "id" }], - AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], - ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1 } - }); - } - - const buf: Uint8Array = new TextEncoder().encode("deadbeefdeadbeef"); - - result = await dyno.putItem({ - TableName: "users_d", - Item: { id: "abc", buf } - }); - - assertEquals(result, {}); - - result = await dyno.getItem({ - TableName: "users_d", - Key: { id: "abc" } - }); - - assertEquals(result.Item.buf, buf); - - result = await dyno.deleteItem({ - TableName: "users_d", - Key: { id: "abc" } - }); - - assertEquals(result, {}); - - result = await dyno.deleteTable({ - TableName: "users_d" - }); - - assertEquals(result.TableDescription.TableName, "users_d"); - - result = await dyno.listTables(); - - assert(!result.TableNames.includes("users_d")); - } -}); - -test({ - name: "ops that receive paged results return an async iterator by default", - async fn(): Promise { - const dyno: DynamoDBClient = createClient(CONF); - - let result: Doc = await dyno.listTables(); - - if (!result.TableNames.includes("users_e")) { - await dyno.createTable({ - TableName: "users_e", - KeySchema: [{ KeyType: "HASH", AttributeName: "id" }], - AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], - ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1 } - }); - } - - const n: number = 25; - const N: number = 20 * n; - - function batch(_: null, i: number): Promise { - const trash: Uint8Array = new Uint8Array(4096); - - const params: Doc = { - RequestItems: { users_e: new Array(n) } - }; - - for (let j: number = 0; j < n; ++j) { - params.RequestItems.users_e[j] = { - PutRequest: { - Item: { - id: `batch${i} item${j}`, - trash - } - } - }; - } - - return dyno.batchWriteItem(params); - } - - // 20 * n items each gt 4096 bytes - const batches: Promise[] = new Array(20).fill(null).map(batch); - - const results: Doc[] = await Promise.all(batches); - - const unprocessed: number = results.reduce( - (acc: number, result: Doc): number => - acc + Object.keys(result.UnprocessedItems).length, - 0 - ); - - assertEquals(unprocessed, 0); - - const ait: any = await dyno.scan({ TableName: "users_e" }); - - let pages: number = 0; - let items: number = 0; - - for await (const page of ait) { - assert(Array.isArray(page.Items)); - assert(page.Items.length > 0); - - ++pages; - items += page.Count; - } - - assertEquals(pages, 2); - - assertEquals(items, N); - - result = await dyno.deleteTable({ - TableName: "users_e" - }); - - assertEquals(result.TableDescription.TableName, "users_e"); - - result = await dyno.listTables(); - - assert(!result.TableNames.includes("users_e")); - } -}); - -test({ - name: "handling pagination manually", - async fn(): Promise { - const dyno: DynamoDBClient = createClient(CONF); - - let result: Doc = await dyno.listTables(); - - if (!result.TableNames.includes("users_f")) { - await dyno.createTable({ - TableName: "users_f", - KeySchema: [{ KeyType: "HASH", AttributeName: "id" }], - AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }], - ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1 } - }); - } - - const n: number = 25; - - function batch(_: null, i: number): Promise { - const trash: Uint8Array = new Uint8Array(4096); - - const params: Doc = { - RequestItems: { users_f: new Array(n) } - }; - - for (let j: number = 0; j < n; ++j) { - params.RequestItems.users_f[j] = { - PutRequest: { - Item: { - id: `batch${i} item${j}`, - trash - } - } - }; - } - - return dyno.batchWriteItem(params); - } - - // 20 * n items each gt 4096 bytes - const batches: Promise[] = new Array(20).fill(null).map(batch); - - const results: Doc[] = await Promise.all(batches); - - const unprocessed: number = results.reduce( - (acc: number, result: Doc): number => - acc + Object.keys(result.UnprocessedItems).length, - 0 - ); - - assertEquals(unprocessed, 0); - - // only fetching 1 page - not async iterating - result = await dyno.scan({ TableName: "users_f" }, { iteratePages: false }); - - assert(Array.isArray(result.Items)); - assert(result.Items.length > 0); - assert(!!result.LastEvaluatedKey); - - result = await dyno.deleteTable({ - TableName: "users_f" - }); - - assertEquals(result.TableDescription.TableName, "users_f"); - - result = await dyno.listTables(); - - assert(!result.TableNames.includes("users_f")); - } -}); - -test({ - name: "missing table throws a readable error", - async fn(): Promise { - const dyno: DynamoDBClient = createClient(CONF); - - let result: Doc = await dyno.listTables(); - - if (result.TableNames.includes("nonexistent_table")) { - await dyno.deleteTable({ - TableName: "nonexistent_table" - }); - } - - async function fn(): Promise { - await dyno.scan({ TableName: "nonexistent_table" }); - } - - assertThrowsAsync( - fn, - Error, - "Cannot do operations on a non-existent table" - ); - } -}); - -test({ - name: "passing temporary credentials including a session token", - async fn(): Promise { - // currently there's no way to test the session token is appended to the - // header but we include it in the ClientConfig - const conf: ClientConfig = { - credentials: { - accessKeyId: "freshAccessKeyId", - secretAccessKey: "freshAccessKey", - sessionToken: "freshSessionToken" - }, - region: "local", - port: 8000 // DynamoDB Local's default port - }; - - const dyno: DynamoDBClient = createClient(conf); - - await dyno.listTables(); - } -}); - -test({ - name: "having temporary credentials refreshed", - async fn(): Promise { - // 2 have temp credentials refreshed pass a (n async) credentials func - // this will refresh creds after recv a 403 and then retry once - const conf: ClientConfig = { - async credentials(): Promise { - // call STS AssumeRole GetSessionToken or similar... - return { - accessKeyId: "freshAccessKeyId", - secretAccessKey: "freshAccessKey", - sessionToken: "freshSessionToken" - }; - }, - region: "local", - port: 8000 // DynamoDB Local's default port - }; - - const dyno: DynamoDBClient = createClient(conf); - - await dyno.listTables(); - } -}); - -runIfMain(import.meta); diff --git a/test/cloud.ts b/test/cloud.ts index 5199984..5ed2ce0 100644 --- a/test/cloud.ts +++ b/test/cloud.ts @@ -1,9 +1,7 @@ import { assertEquals, assertThrowsAsync, - test, - runIfMain -} from "https://deno.land/std/testing/mod.ts"; +} from "https://deno.land/std@v0.34.0/testing/asserts.ts"; import { ClientConfig, @@ -22,7 +20,7 @@ const TABLE_NAME: string = Deno.env().TABLE_NAME; const dyno: DynamoDBClient = createClient(); -test({ +Deno.test({ name: "schema translation enabled by default", async fn(): Promise { const id: string = "abc"; @@ -45,7 +43,7 @@ test({ } }); -test({ +Deno.test({ name: "opt-in raw queries", async fn(): Promise { const id: string = "def"; @@ -72,7 +70,7 @@ test({ } }); -test({ +Deno.test({ name: "batch write items", async fn(): Promise { const N: number = 25; @@ -97,7 +95,7 @@ test({ } }); -test({ +Deno.test({ name: "storing a binary value", async fn(): Promise { const id: string = "ghi"; @@ -120,7 +118,7 @@ test({ } }); -test({ +Deno.test({ name: "deleting an item", async fn(): Promise { const id: string = "jkl"; @@ -141,7 +139,7 @@ test({ } }); -test({ +Deno.test({ name: "missing table throws a readable error", async fn(): Promise { assertThrowsAsync(async (): Promise => { @@ -150,7 +148,7 @@ test({ } }); -test({ +Deno.test({ name: "TODO: having temporary credentials refreshed", async fn(): Promise { // 2 have temp credentials refreshed pass a (n async) credentials func @@ -172,4 +170,4 @@ test({ } }); -runIfMain(import.meta, { skip: /TODO/ }); +Deno.runTests({ skip: /TODO/ }); \ No newline at end of file diff --git a/test/local.ts b/test/local.ts index 67ab218..06fe48d 100644 --- a/test/local.ts +++ b/test/local.ts @@ -2,9 +2,7 @@ import { assert, assertEquals, assertThrowsAsync, - test, - runIfMain -} from "https://deno.land/std/testing/mod.ts"; +} from "https://deno.land/std@v0.34.0/testing/asserts.ts"; import { ClientConfig, DynamoDBClient, createClient } from "../mod.ts"; @@ -31,7 +29,7 @@ await dyno.createTable({ ProvisionedThroughput: { ReadCapacityUnits: 10, WriteCapacityUnits: 10 } }); -test({ +Deno.test({ name: "schema translation enabled by default", async fn(): Promise { const id: string = "abc"; @@ -54,7 +52,7 @@ test({ } }); -test({ +Deno.test({ name: "opt-in raw queries", async fn(): Promise { const id: string = "def"; @@ -81,7 +79,7 @@ test({ } }); -test({ +Deno.test({ name: "batch write items", async fn(): Promise { const N: number = 25; @@ -106,7 +104,7 @@ test({ } }); -test({ +Deno.test({ name: "storing a binary value", async fn(): Promise { const id: string = "ghi"; @@ -129,7 +127,7 @@ test({ } }); -test({ +Deno.test({ name: "deleting an item", async fn(): Promise { const id: string = "jkl"; @@ -150,7 +148,7 @@ test({ } }); -test({ +Deno.test({ name: "missing table throws a readable error", async fn(): Promise { assertThrowsAsync( @@ -163,7 +161,7 @@ test({ } }); -test({ +Deno.test({ name: "ops that receive paged results return an async iterator by default", async fn(): Promise { const n: number = 25; @@ -222,7 +220,7 @@ test({ } }); -test({ +Deno.test({ name: "handling pagination manually", async fn(): Promise { // only fetching 1 page - not async iterating @@ -237,4 +235,4 @@ test({ } }); -runIfMain(import.meta); +Deno.runTests(); \ No newline at end of file diff --git a/test/signv4.ts b/test/signv4.ts index 50c9b2e..78facb7 100644 --- a/test/signv4.ts +++ b/test/signv4.ts @@ -1,13 +1,9 @@ -import { - assertEquals, - test, - runIfMain -} from "https://deno.land/std/testing/mod.ts"; +import { assertEquals } from "https://deno.land/std@v0.34.0/testing/asserts.ts"; import { encode } from "../deps.ts"; import { awsSignatureV4, kdf } from "../client/aws_signature_v4.ts"; -test({ +Deno.test({ name: "aws signature v4 flow", fn(): void { const expectedSignature: string = @@ -32,4 +28,4 @@ test({ } }); -runIfMain(import.meta); +Deno.runTests(); diff --git a/util.ts b/util.ts index 68294b7..0bb39e8 100644 --- a/util.ts +++ b/util.ts @@ -139,12 +139,12 @@ const memberTypeToSetType: Doc = { /** DynamoDB set type. */ export class DynamoDBSet { readonly wrappername: string = "Set"; - readonly values: any[]; - readonly type: string; + readonly values: any[] = []; + readonly type: string = ""; /** Creates a dynamodb set. */ constructor(list: any[] = [], options: Doc = {}) { - this.values = [].concat(list); + Array.prototype.push.apply(this.values, list); this.type = memberTypeToSetType[typeOf(this.values[0])];