diff --git a/types/dynamodb/Callback.d.ts b/types/dynamodb/Callback.d.ts new file mode 100644 index 0000000000..af3583c3a6 --- /dev/null +++ b/types/dynamodb/Callback.d.ts @@ -0,0 +1 @@ +export type Callback = (err: any, result: any) => void; diff --git a/types/dynamodb/DynamoDB.d.ts b/types/dynamodb/DynamoDB.d.ts new file mode 100644 index 0000000000..892a5cbc1a --- /dev/null +++ b/types/dynamodb/DynamoDB.d.ts @@ -0,0 +1,55 @@ +export interface Projection { + ProjectionType?: 'ALL' | 'KEYS_ONLY' | 'INCLUDE' | string; + NonKeyAttributes?: string[]; +} + +import * as stream from 'stream'; + +export type DynamoDB = any; + +export type DocumentClient = any; + +export type binaryType = + | Buffer + | {} + | ArrayBuffer + | DataView + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | stream.Stream; + +interface StringSet { + type: 'String'; + values: string[]; +} + +interface NumberSet { + type: 'Number'; + values: number[]; +} + +interface BinarySet { + type: 'Binary'; + values: binaryType[]; +} + +export type DynamoDbSet = StringSet | NumberSet | BinarySet; + +export interface GetItemInput { + TableName: string; + Key: object; + AttributesToGet?: string[]; + ConsistentRead?: boolean; + ReturnConsumedCapacity?: string; + ProjectionExpression?: string; + ExpressionAttributeNames?: { [key: string]: string }; +} + +export {}; diff --git a/types/dynamodb/ExecuteFilter.d.ts b/types/dynamodb/ExecuteFilter.d.ts new file mode 100644 index 0000000000..48a48f55f9 --- /dev/null +++ b/types/dynamodb/ExecuteFilter.d.ts @@ -0,0 +1,8 @@ +import { Readable } from 'stream'; + +import { Callback } from './Callback'; + +export interface ExecuteFilter { + (callback: Callback): void; + (): Readable; +} diff --git a/types/dynamodb/Model.d.ts b/types/dynamodb/Model.d.ts new file mode 100644 index 0000000000..0b8b1f65ba --- /dev/null +++ b/types/dynamodb/Model.d.ts @@ -0,0 +1,147 @@ +import { AnySchema } from 'joi'; +import * as bunyan from 'bunyan'; +import { GetItemInput, DynamoDB } from './DynamoDB'; + +import { Callback } from './Callback'; +import { Query } from './Query'; +import { Scan, ParallelScan } from './Scan'; +import { EventEmitter } from 'events'; + +export class Item extends EventEmitter { + constructor(attrs: object, table: any); + get(name: string): any; + set(params: any): this; + save(callback: Callback): void; + save(): Promise; + update(options: any, callback: Callback): void; + update(options: any): Promise; + destroy(options: any, callback: Callback): void; + destroy(options: any): Promise; + toJSON(): object; +} + +export class Model extends Item { + constructor(attrs: any); +} + +export namespace Model { + const get: GetOperation; + const create: CreateOperation; + const update: UpdateOperation; + const destroy: DestroyOperation; + function query(hashKey: string): Query; + function scan(): Scan; + function parallelScan(totalSegments: number): ParallelScan; + const getItems: GetItemsOperation; + function batchGetItems( + hashKey: string, + rangeKey: string, + options: any, + callback?: Callback, + ): Promise | void; + function createTable( + hashKey: string, + rangeKey: string, + options: any, + callback?: Callback, + ): Promise | void; + function updateTable( + hashKey: string, + rangeKey: string, + options: any, + callback?: Callback, + ): Promise | void; + function describeTable( + hashKey: string, + rangeKey: string, + options: any, + callback?: Callback, + ): Promise | void; + function deleteTable(callback: Callback): void; + function deleteTable(): Promise; + function tableName( + hashKey: string, + rangeKey: string, + options: any, + callback: Callback, + ): void; + function tableName( + hashKey: string, + rangeKey: string, + options: any, + ): Promise; + const itemFactory: typeof Model; + const log: bunyan; + const after: any; + const before: any; + function config(config: { dynamodb?: DynamoDB; tableName?: string }): any; + + interface OperationResult { + get(name: string): any; + } + + interface OperationOptions { + UpdateExpression?: any; + ConditionExpression?: any; + ExpressionAttributeValues?: any; + ExpressionAttributeNames?: any; + expected?: any; + overwrite?: boolean; + ReturnValues?: string | boolean; + } + + interface UpdateOperation { + (item: any, options: OperationOptions, callback: Callback): void; + (item: any, callback: Callback): void; + (item: any, options?: OperationOptions): Promise; + } + + interface GetOperation { + ( + hashKey: string, + rangeKey: string, + options: object, + callback: Callback, + ): void; + ( + data: object | string, + options: object | string, + callback: Callback, + ): void; + (data: object | string, callback: Callback): void; + (hashKey: string, rangeKey: string, options: object): Promise; + (hashKey: string, options?: object | string): Promise; + } + + interface CreateOperation { + (doc: any, params: OperationOptions, callback: Callback): void; + (doc: any, callback: Callback): void; + (doc: any, params?: OperationOptions): Promise; + } + + interface DestroyOperation { + ( + hashKey: string, + rangeKey: string, + options: OperationOptions, + callback: Callback, + ): void; + (hashKey: string, rangeKey: string, callback: Callback): void; + ( + data: { [key: string]: any } | string, + options: OperationOptions, + callback: Callback, + ): void; + (data: { [key: string]: any } | string, callback: Callback): void; + (hashKey: string, rangeKey: string, options: OperationOptions): Promise< + any + >; + (hashKey: string, options?: OperationOptions | string): Promise; + } + + interface GetItemsOperation { + (keys: ReadonlyArray, options: any, callback: Callback): void; + (keys: ReadonlyArray, callback: Callback): void; + (keys: ReadonlyArray, options?: any): Promise; + } +} diff --git a/types/dynamodb/Query.d.ts b/types/dynamodb/Query.d.ts new file mode 100644 index 0000000000..a0449e2396 --- /dev/null +++ b/types/dynamodb/Query.d.ts @@ -0,0 +1,42 @@ +import { ExecuteFilter } from './ExecuteFilter'; + +export interface Query { + (hashKey: string, table: any, serializer: any): void; + limit(num: number): Query; + filterExpression(expression: any): Query; + expressionAttributeValues(data: any): Query; + expressionAttributeNames(data: any): Query; + projectionExpression(data: any): Query; + usingIndex(name: string): Query; + consistentRead(read: boolean): Query; + addKeyCondition(condition: { + attributeNames: any; + attributeValues: any; + }): Query; + addFilterCondition(condition: { + attributeNames: any; + attributeValues: any; + }): Query; + startKey(hashKey: string, rangeKey: string): Query; + attributes(attrs: ReadonlyArray | string): Query; + ascending(): Query; + descending(): Query; + select(value: string): Query; + returnConsumedCapacity(value?: string): Query; + loadAll(): Query; + where(keyName: string): Query; + contains(name: string): Query; + notContains(name: string): Query; + filter(keyName: string): Query; + exec: ExecuteFilter; + buildKey(): string; + buildRequest(): any; + equals: (...args: any[]) => Query; + eq: (...args: any[]) => Query; + lte: (...args: any[]) => Query; + lt: (...args: any[]) => Query; + gte: (...args: any[]) => Query; + gt: (...args: any[]) => Query; + beginsWith: (...args: any[]) => Query; + between: (...args: any[]) => Query; +} diff --git a/types/dynamodb/Scan.d.ts b/types/dynamodb/Scan.d.ts new file mode 100644 index 0000000000..c612de11ed --- /dev/null +++ b/types/dynamodb/Scan.d.ts @@ -0,0 +1,38 @@ +import { ExecuteFilter } from './ExecuteFilter'; + +export interface Scan { + limit(num: number): Scan; + addFilterCondition(condition: { + attributeNames: any; + attributeValues: any; + }): Scan; + startKey(hashKey: string, rangeKey?: string): Scan; + attributes(attrs: ReadonlyArray | string): Scan; + select(value: string): Scan; + returnConsumedCapacity(value?: string): Scan; + segments(segment: any, totalSegments: any): Scan; + where(keyName: string): Scan; + filterExpression(expression: any): Scan; + expressionAttributeValues(data: any): Scan; + expressionAttributeNames(data: any): Scan; + projectionExpression(data: any): Scan; + loadAll(): Scan; + notNull(): Scan; + null(): Scan; + contains(name: string): Scan; + beginsWith(name: string): Scan; + between(start: string, end: string): Scan; + notContains(name: string): Scan; + equals: (...args: any[]) => Scan; + in: (...args: any[]) => Scan; + ne: (...args: any[]) => Scan; + eq: (...args: any[]) => Scan; + lte: (...args: any[]) => Scan; + lt: (...args: any[]) => Scan; + gte: (...args: any[]) => Scan; + gt: (...args: any[]) => Scan; + exec: ExecuteFilter; + buildRequest(): any; +} + +export type ParallelScan = Scan; diff --git a/types/dynamodb/dynamodb-tests.ts b/types/dynamodb/dynamodb-tests.ts new file mode 100644 index 0000000000..97e5b2afa2 --- /dev/null +++ b/types/dynamodb/dynamodb-tests.ts @@ -0,0 +1,822 @@ +import * as dynamo from 'dynamodb'; +import * as Joi from 'joi'; + +dynamo.AWS.config.loadFromPath('credentials.json'); +dynamo.AWS.config.update({ region: 'REGION' }); +dynamo.AWS.config.update({ + accessKeyId: 'AKID', + secretAccessKey: 'SECRET', + region: 'REGION', +}); + +let Account = dynamo.define('Account', { + hashKey: 'email', + + // add the timestamp attributes (updatedAt, createdAt) + timestamps: true, + + schema: { + email: Joi.string().email(), + name: Joi.string(), + age: Joi.number(), + roles: dynamo.types.stringSet(), + settings: { + nickname: Joi.string(), + acceptedTerms: Joi.boolean().default(false), + }, + }, +}); + +let BlogPost = dynamo.define('BlogPost', { + hashKey: 'email', + rangeKey: 'title', + schema: { + email: Joi.string().email(), + title: Joi.string(), + content: Joi.binary(), + tags: dynamo.types.stringSet(), + }, +}); + +dynamo.createTables(err => { + if (err) { + console.log('Error creating tables: ', err); + } else { + console.log('Tables has been created'); + } +}); + +dynamo.createTables( + { + BlogPost: { readCapacity: 5, writeCapacity: 10 }, + Account: { readCapacity: 20, writeCapacity: 4 }, + }, + err => { + if (err) { + console.log('Error creating tables: ', err); + } else { + console.log('Tables has been created'); + } + }, +); + +BlogPost.deleteTable(err => { + if (err) { + console.log('Error deleting table: ', err); + } else { + console.log('Table has been deleted'); + } +}); + +const Tweet = dynamo.define('Tweet', { + hashKey: 'TweetID', + timestamps: true, + schema: { + TweetID: dynamo.types.uuid(), + content: Joi.string(), + }, +}); + +Account = dynamo.define('Account', { + hashKey: 'email', + + // add the timestamp attributes (updatedAt, createdAt) + timestamps: true, + + schema: { + email: Joi.string().email(), + }, +}); + +Account = dynamo.define('Account', { + hashKey: 'email', + + // enable timestamps support + timestamps: true, + + // I don't want createdAt + createdAt: false, + + // I want updatedAt to actually be called updateTimestamp + updatedAt: 'updateTimestamp', + + schema: { + email: Joi.string().email(), + }, +}); + +let Event = dynamo.define('Event', { + hashKey: 'name', + schema: { + name: Joi.string(), + total: Joi.number(), + }, + + tableName: 'deviceEvents', +}); + +Event = dynamo.define('Event', { + hashKey: 'name', + schema: { + name: Joi.string(), + total: Joi.number(), + }, + + // store monthly event data + tableName: () => { + const d = new Date(); + return ['events', d.getFullYear(), d.getMonth() + 1].join('_'); + }, +}); + +Account.config({ tableName: 'AccountsTable' }); + +Account.create( + { email: 'foo@example.com', name: 'Foo Bar', age: 21 }, + (err, acc) => { + console.log('created account in DynamoDB', acc.get('email')); + }, +); + +const acc = new Account({ email: 'test@example.com', name: 'Test Example' }); + +acc.save(err => { + console.log('created account in DynamoDB', acc.get('email')); +}); + +BlogPost.create( + { + email: 'werner@example.com', + title: 'Expanding the Cloud', + content: 'Today, we are excited to announce the limited preview...', + }, + (err, post) => { + console.log('created blog post', post.get('title')); + }, +); + +const item1 = { email: 'foo1@example.com', name: 'Foo 1', age: 10 }; +const item2 = { email: 'foo2@example.com', name: 'Foo 2', age: 20 }; +const item3 = { email: 'foo3@example.com', name: 'Foo 3', age: 30 }; + +Account.create([item1, item2, item3], (err, accounts) => { + console.log('created 3 accounts in DynamoDB', accounts); +}); + +let params: dynamo.Model.OperationOptions = {}; +params.ConditionExpression = '#i <> :x'; +params.ExpressionAttributeNames = { '#i': 'id' }; +params.ExpressionAttributeValues = { ':x': 123 }; + +Account.create({ id: 123, name: 'Kurt Warner' }, params, (error, acc) => { + // ... +}); + +Account.create( + { id: 123, name: 'Kurt Warner' }, + { overwrite: false }, + (error, acc) => { + // ... + }, +); + +Account.update({ email: 'foo@example.com', name: 'Bar Tester' }, (err, acc) => { + console.log('update account', acc.get('name')); +}); + +Account.update( + { email: 'foo@example.com', name: 'Bar Tester' }, + { ReturnValues: 'ALL_OLD' }, + (err, acc) => { + console.log('update account', acc.get('name')); // prints the old account name + }, +); + +// Only update the account if the current age of the account is 21 +Account.update( + { email: 'foo@example.com', name: 'Bar Tester' }, + { expected: { age: 22 } }, + (err, acc) => { + console.log('update account', acc.get('name')); + }, +); + +// setting an attribute to null will delete the attribute from DynamoDB +Account.update({ email: 'foo@example.com', age: null }, (err, acc) => { + console.log('update account', acc.get('age')); // prints null +}); + +Account.update({ email: 'foo@example.com', age: { $add: 1 } }, (err, acc) => { + console.log('incremented age by 1', acc.get('age')); +}); + +BlogPost.update( + { + email: 'werner@example.com', + title: 'Expanding the Cloud', + tags: { $add: 'cloud' }, + }, + (err, post) => { + console.log('added single tag to blog post', post.get('tags')); + }, +); + +BlogPost.update( + { + email: 'werner@example.com', + title: 'Expanding the Cloud', + tags: { $add: ['cloud', 'dynamodb'] }, + }, + (err, post) => { + console.log('added tags to blog post', post.get('tags')); + }, +); + +BlogPost.update( + { + email: 'werner@example.com', + title: 'Expanding the Cloud', + tags: { $del: 'cloud' }, + }, + (err, post) => { + console.log('removed cloud tag from blog post', post.get('tags')); + }, +); + +BlogPost.update( + { + email: 'werner@example.com', + title: 'Expanding the Cloud', + tags: { $del: ['aws', 'node'] }, + }, + (err, post) => { + console.log('removed multiple tags', post.get('tags')); + }, +); + +params = {}; +params.UpdateExpression = + 'SET #year = #year + :inc, #dir.titles = list_append(#dir.titles, :title), #act[0].firstName = :firstName ADD tags :tag'; +params.ConditionExpression = '#year = :current'; +params.ExpressionAttributeNames = { + '#year': 'releaseYear', + '#dir': 'director', + '#act': 'actors', +}; + +params.ExpressionAttributeValues = { + ':inc': 1, + ':current': 2001, + ':title': ['The Man'], + ':firstName': 'Rob', + ':tag': dynamo.Set(['Sports', 'Horror'], 'S'), +}; + +const Movie = dynamo.define('Movie', { + hashKey: 'foo', + schema: {}, +}); + +Movie.update( + { title: 'Movie 0', description: 'This is a description' }, + params, + (err, mov) => {}, +); + +Account.destroy('foo@example.com', err => { + console.log('account deleted'); +}); + +// Destroy model using hash and range key +BlogPost.destroy('foo@example.com', 'Hello World!', err => { + console.log('post deleted'); +}); + +BlogPost.destroy({ email: 'foo@example.com', title: 'Another Post' }, err => { + console.log('another post deleted'); +}); + +Account.destroy('foo@example.com', { expected: { age: 22 } }, err => { + console.log('account deleted if the age was 22'); +}); + +Account.destroy('foo@example.com', { ReturnValues: true }, (err, acc) => { + console.log('account deleted'); + console.log('deleted account name', acc.get('name')); +}); + +params = {}; +params.ConditionExpression = '#v = :x'; +params.ExpressionAttributeNames = { '#v': 'version' }; +params.ExpressionAttributeValues = { ':x': '2' }; + +Account.destroy({ id: 123 }, params, (err, acc) => {}); + +Account.get('test@example.com', (err, acc) => { + console.log('got account', acc.get('email')); +}); + +Account.get('test@example.com', { ConsistentRead: true }, (err, acc) => { + console.log('got account', acc.get('email')); +}); + +Account.get( + 'test@example.com', + { ConsistentRead: true, AttributesToGet: ['name', 'age'] }, + (err, acc) => { + console.log('got account', acc.get('email')); + console.log(acc.get('name')); + console.log(acc.get('age')); + console.log(acc.get('email')); // prints null + }, +); + +BlogPost.get( + 'werner@example.com', + 'dynamodb-keeps-getting-better-and-cheaper', + (err, post) => { + console.log('loaded post by range and hash key', post.get('content')); + }, +); + +BlogPost.get( + { email: 'werner@example.com', title: 'Expanding the Cloud' }, + (err, post) => { + console.log('loded post', post.get('content')); + }, +); + +Account.get( + { id: '123456789' }, + { ProjectionExpression: 'email, age, settings.nickname' }, + (err, acc) => {}, +); + +const callback = () => {}; + +// query for blog posts by werner@example.com +BlogPost.query('werner@example.com').exec(callback); + +// same as above, but load all results +BlogPost.query('werner@example.com') + .loadAll() + .exec(callback); + +// only load the first 5 posts by werner +BlogPost.query('werner@example.com') + .limit(5) + .exec(callback); + +// query for posts by werner where the tile begins with 'Expanding' +BlogPost.query('werner@example.com') + .where('title') + .beginsWith('Expanding') + .exec(callback); + +// return only the count of documents that begin with the title Expanding +BlogPost.query('werner@example.com') + .where('title') + .beginsWith('Expanding') + .select('COUNT') + .exec(callback); + +// only return title and content attributes of 10 blog posts +// that begin with the title Expanding +BlogPost.query('werner@example.com') + .where('title') + .beginsWith('Expanding') + .attributes(['title', 'content']) + .limit(10) + .exec(callback); + +// sorting by title ascending +BlogPost.query('werner@example.com') + .ascending() + .exec(callback); + +// sorting by title descending +BlogPost.query('werner@example.com') + .descending() + .exec(callback); + +// All query options are chainable +BlogPost.query('werner@example.com') + .where('title') + .gt('Expanding') + .attributes(['title', 'content']) + .limit(10) + .ascending() + .loadAll() + .exec(callback); + +BlogPost.query('werner@example.com') + .where('title') + .equals('Expanding') + .exec(callback); + +// less than equals +BlogPost.query('werner@example.com') + .where('title') + .lte('Expanding') + .exec(callback); + +// less than +BlogPost.query('werner@example.com') + .where('title') + .lt('Expanding') + .exec(callback); + +// greater than +BlogPost.query('werner@example.com') + .where('title') + .gt('Expanding') + .exec(callback); + +// greater than equals +BlogPost.query('werner@example.com') + .where('title') + .gte('Expanding') + .exec(callback); + +BlogPost.query('werner@example.com') + .where('title') + .beginsWith('Expanding') + .exec(callback); + +BlogPost.query('werner@example.com') + .where('title') + .between('foo@example.com', 'test@example.com') + .exec(callback); + +BlogPost.query('werner@example.com') + .where('title') + .equals('Expanding') + .filter('tags') + .contains('cloud') + .exec(callback); + +BlogPost.query('werner@example.com') + .filterExpression('#title < :t') + .expressionAttributeValues({ ':t': 'Expanding' }) + .expressionAttributeNames({ '#title': 'title' }) + .projectionExpression('#title, tag') + .exec(callback); + +let GameScore = dynamo.define('GameScore', { + hashKey: 'userId', + rangeKey: 'gameTitle', + schema: { + userId: Joi.string(), + gameTitle: Joi.string(), + topScore: Joi.number(), + topScoreDateTime: Joi.date(), + wins: Joi.number(), + losses: Joi.number(), + }, + indexes: [ + { + hashKey: 'gameTitle', + rangeKey: 'topScore', + name: 'GameTitleIndex', + type: 'global', + }, + ], +}); + +GameScore.query('Galaxy Invaders') + .usingIndex('GameTitleIndex') + .descending() + .exec(callback); + +GameScore = dynamo.define('GameScore', { + hashKey: 'userId', + rangeKey: 'gameTitle', + schema: { + userId: Joi.string(), + gameTitle: Joi.string(), + topScore: Joi.number(), + topScoreDateTime: Joi.date(), + wins: Joi.number(), + losses: Joi.number(), + }, + indexes: [ + { + hashKey: 'gameTitle', + rangeKey: 'topScore', + name: 'GameTitleIndex', + type: 'global', + projection: { + NonKeyAttributes: ['wins'], + ProjectionType: 'INCLUDE', + }, // optional, defaults to ALL + }, + ], +}); + +GameScore.query('Galaxy Invaders') + .usingIndex('GameTitleIndex') + .where('topScore') + .gt(1000) + .descending() + .exec((err, data) => { + console.log(data.Items); + }); + +BlogPost = dynamo.define('Account', { + hashKey: 'email', + rangeKey: 'title', + schema: { + email: Joi.string().email(), + title: Joi.string(), + content: Joi.binary(), + PublishedDateTime: Joi.date(), + }, + + indexes: [ + { + hashKey: 'email', + rangeKey: 'PublishedDateTime', + type: 'local', + name: 'PublishedIndex', + }, + ], +}); + +BlogPost.query('werner@example.com') + .usingIndex('PublishedIndex') + .descending() + .exec(callback); + +BlogPost.query('werner@example.com') + .usingIndex('PublishedIndex') + .ascending() + .exec(callback); + +BlogPost.query('werner@example.com') + .usingIndex('PublishedIndex') + .descending() + .loadAll() + .exec(callback); + +// scan all accounts, returning the first page or results +Account.scan().exec(callback); + +// scan all accounts, this time loading all results +// note this will potentially make several calls to DynamoDB +// in order to load all results +Account.scan() + .loadAll() + .exec(callback); + +// Load 20 accounts +Account.scan() + .limit(20) + .exec(callback); + +// Load All accounts, 20 at a time per request +Account.scan() + .limit(20) + .loadAll() + .exec(callback); + +// Load accounts which match a filter +// only return email and created attributes +// and return back the consumed capacity the request took +Account.scan() + .where('email') + .gte('f@example.com') + .attributes(['email', 'created']) + .returnConsumedCapacity() + .exec(callback); + +// Returns number of matching accounts, rather than the matching accounts themselves +Account.scan() + .where('age') + .gte(21) + .select('COUNT') + .exec(callback); + +// Start scan using start key +Account.scan() + .where('age') + .notNull() + .startKey('foo@example.com') + .exec(callback); + +// equals +Account.scan() + .where('name') + .equals('Werner') + .exec(callback); + +// not equals +Account.scan() + .where('name') + .ne('Werner') + .exec(callback); + +// less than equals +Account.scan() + .where('name') + .lte('Werner') + .exec(callback); + +// less than +Account.scan() + .where('name') + .lt('Werner') + .exec(callback); + +// greater than equals +Account.scan() + .where('name') + .gte('Werner') + .exec(callback); + +// greater than +Account.scan() + .where('name') + .gt('Werner') + .exec(callback); + +// name attribute doesn't exist +Account.scan() + .where('name') + .null() + .exec(callback); + +// name attribute exists +Account.scan() + .where('name') + .notNull() + .exec(callback); + +// contains +Account.scan() + .where('name') + .contains('ner') + .exec(callback); + +// not contains +Account.scan() + .where('name') + .notContains('ner') + .exec(callback); + +// in +Account.scan() + .where('name') + .in(['foo@example.com', 'bar@example.com']) + .exec(callback); + +// begins with +Account.scan() + .where('name') + .beginsWith('Werner') + .exec(callback); + +// between +Account.scan() + .where('name') + .between('Bar', 'Foo') + .exec(callback); + +// multiple filters +Account.scan() + .where('name') + .equals('Werner') + .where('age') + .notNull() + .exec(callback); + +Account.scan() + .filterExpression('#age BETWEEN :low AND :high AND begins_with(#email, :e)') + .expressionAttributeValues({ ':low': 18, ':high': 22, ':e': 'test1' }) + .expressionAttributeNames({ '#age': 'age', '#email': 'email' }) + .projectionExpression('#age, #email') + .exec(callback); + +const totalSegments = 8; + +Account.parallelScan(totalSegments) + .where('age') + .gte(18) + .attributes('age') + .exec(callback); + +// Load All accounts +Account.parallelScan(totalSegments).exec(); + +Account.getItems( + ['foo@example.com', 'bar@example.com', 'test@example.com'], + (err, accounts) => { + console.log(`loaded ${accounts.length} accounts`); // prints loaded 3 accounts + }, +); + +// For models with range keys you must pass in objects of hash and range key attributes +const postKey1 = { email: 'test@example.com', title: 'Hello World!' }; +const postKey2 = { email: 'test@example.com', title: 'Another Post' }; + +BlogPost.getItems([postKey1, postKey2], (err, posts) => { + console.log('loaded posts'); +}); + +Account.getItems( + ['foo@example.com', 'bar@example.com'], + { ConsistentRead: true }, + (err, accounts) => { + console.log(`loaded ${accounts.length} accounts`); // prints loaded 2 accounts + }, +); + +let stream = Account.parallelScan(4).exec(); + +stream.on('readable', () => { + console.log('single parallel scan response', stream.read()); +}); + +stream.on('end', () => { + console.log('Parallel scan of accounts finished'); +}); + +let querystream = BlogPost.query('werner@dynamo.com') + .loadAll() + .exec(); + +querystream.on('readable', () => { + console.log('single query response', stream.read()); +}); + +querystream.on('end', () => { + console.log('query for blog posts finished'); +}); + +stream = Account.parallelScan(4).exec(); + +stream.on('readable', () => { + console.log('single parallel scan response', stream.read()); +}); + +stream.on('end', () => { + console.log('Parallel scan of accounts finished'); +}); + +querystream = BlogPost.query('werner@dynamo.com') + .loadAll() + .exec(); + +querystream.on('readable', () => { + console.log('single query response', stream.read()); +}); + +querystream.on('end', () => { + console.log('query for blog posts finished'); +}); + +Event = dynamo.define('Event', { + hashKey: 'name', + schema: { + name: Joi.string(), + total: Joi.number(), + }, + + // store monthly event data + tableName: () => { + const d = new Date(); + return ['events', d.getFullYear(), d.getMonth() + 1].join('_'); + }, +}); + +dynamo.log.level('info'); // enabled INFO log level + +Account = dynamo.define('Account', { hashKey: 'email' }); +Event = dynamo.define('Account', { hashKey: 'name' }); + +Account.log.level('warn'); // enable WARN log level for Account model operations + +Account = dynamo.define('Account', { + hashKey: 'email', + + // add the timestamp attributes (updatedAt, createdAt) + timestamps: true, + + schema: { + email: Joi.string().email(), + name: Joi.string().required(), + age: Joi.number(), + }, +}); + +Account.create( + { email: 'test@example.com', name: 'Test Account' }, + (err, acc) => { + console.log('created account at', acc.get('created')); // prints created Date + + acc.set({ age: 22 }); + + acc.update((err: any) => { + console.log('updated account age'); + }); + }, +); diff --git a/types/dynamodb/index.d.ts b/types/dynamodb/index.d.ts new file mode 100644 index 0000000000..cfe55ee23f --- /dev/null +++ b/types/dynamodb/index.d.ts @@ -0,0 +1,69 @@ +// Type definitions for dynamodb 1.2 +// Project: https://github.com/baseprime/dynamodb#readme +// Definitions by: katsanva +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.4 + +import { AnySchema } from 'joi'; +import * as bunyan from 'bunyan'; +import { callbackify } from 'util'; +import { Readable } from 'stream'; + +import { Callback } from './Callback'; +import { Model } from './Model'; +import { DynamoDB, Projection, DocumentClient, DynamoDbSet } from './DynamoDB'; + +interface CreateTablesOptions { + [key: string]: { readCapacity: number; writeCapacity: number }; +} + +interface CreateTables { + (options?: CreateTablesOptions): Promise; + (options: CreateTablesOptions, callback: Callback): void; + (callback: Callback): void; +} + +interface IndexDefinition { + hashKey: string; + rangeKey?: string; + name: string; + type: 'local' | 'global'; + projection?: Projection; +} + +export interface DefineConfig { + hashKey: string; + rangeKey?: string; + timestamps?: boolean; + createdAt?: boolean | string; + updatedAt?: boolean | string; + tableName?: string | (() => string); + indexes?: ReadonlyArray; + schema?: { + [key: string]: AnySchema | { [key: string]: AnySchema }; + }; +} + +export const log: bunyan; +export function dynamoDriver(driver?: DynamoDB): DynamoDB; +export function documentClient(docClient?: DocumentClient): DocumentClient; +export function reset(): void; +export function Set(data: ReadonlyArray, type: string): DynamoDbSet; +export function define(name: string, config: DefineConfig): typeof Model; +export function model(name: string, model?: Model): typeof Model; +export const createTables: CreateTables; +export const types: { + stringSet: () => AnySchema; + numberSet: () => AnySchema; + binarySet: () => AnySchema; + uuid: () => AnySchema; + timeUUID: () => AnySchema; +}; + +export const models: { + [key: string]: typeof Model; +}; + +export const AWS: any; + +export { Model }; diff --git a/types/dynamodb/tsconfig.json b/types/dynamodb/tsconfig.json new file mode 100644 index 0000000000..3b3bd3a779 --- /dev/null +++ b/types/dynamodb/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": ["es6"], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "baseUrl": "../", + "typeRoots": ["../"], + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "strictFunctionTypes": true + }, + "files": ["index.d.ts", "dynamodb-tests.ts"] +} diff --git a/types/dynamodb/tslint.json b/types/dynamodb/tslint.json new file mode 100644 index 0000000000..3db14f85ea --- /dev/null +++ b/types/dynamodb/tslint.json @@ -0,0 +1 @@ +{ "extends": "dtslint/dt.json" }