├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── _config.yml ├── index.js ├── lib ├── dynamodb.js └── helper.js ├── package.json └── test ├── basic-querying.test.js ├── datatype.test.js ├── dynamo.test.js ├── hooks.test.js ├── init.js └── relations.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dump.rdb 3 | lib-cov 4 | *.log 5 | *.csv 6 | *.out 7 | *.pid 8 | pids 9 | logs 10 | results 11 | node_modules 12 | npm-debug.log 13 | .idea 14 | .DS_Store 15 | log/*.log 16 | .c9revisions 17 | coverage.html 18 | .settings 19 | doc 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 tmpaul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## TESTS 2 | 3 | test: 4 | ./node_modules/.bin/mocha test/*.test.js --timeout 5000 --reporter spec 5 | 6 | .PHONY: test 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "DynamoDB connector" 3 | lang: en 4 | toc: false 5 | keywords: 6 | source: loopback-connector-dynamodb 7 | tags: [community] 8 | sidebar: community_sidebar 9 | permalink: /doc/en/community/dynamo-connector.html 10 | --- 11 | 12 | ## Link 13 | 14 | [https://github.com/mandarzope/loopback-connector-dynamodb](https://github.com/mandarzope/loopback-connector-dynamodb) 15 | 16 | ## Overview 17 | 18 | DynamoDB Connector for loopback (compitable with datasource-juggler) 19 | 20 | ## Features 21 | 22 | In progress 23 | 24 | Provide a list of the features here. 25 | 26 | ## Benefits of the project 27 | 28 | In progress 29 | 30 | Describe how your project benefits the LoopBack developer. 31 | 32 | ## Demo or sample code 33 | 34 | In progress (Please give me some time to write examples) 35 | 36 | Give a short demo of how the project works. 37 | Summarize your demo here and provide more sample code (or link to API docs, etc.) if necessary. -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/dynamodb'); 2 | -------------------------------------------------------------------------------- /lib/dynamodb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var g = require('strong-globalize')(); 3 | var Connector = require('loopback-connector').Connector; 4 | 5 | // It doesn't include sharding or auto partitioning of items above 400kb 6 | 7 | /** 8 | * Module dependencies 9 | */ 10 | var AWS = require('aws-sdk'); 11 | var DocClient = AWS.DynamoDB.DocumentClient; 12 | var colors = require('colors'); 13 | var helper = require('./helper.js'); 14 | var async = require('async'); 15 | var EventEmitter = require('events').EventEmitter; 16 | var winston = require('winston'); 17 | var util = require('util'); 18 | // Winston logger configuration 19 | var logger = new(winston.Logger)({ 20 | transports: [ 21 | new(winston.transports.Console)({ 22 | colorize: true, 23 | }) 24 | /* 25 | new(winston.transports.File)({ 26 | filename: 'logs/dynamodb.log', 27 | maxSize: 1024 * 1024 *5 28 | })*/ 29 | ] 30 | }); 31 | 32 | function countProperties(obj) { 33 | return Object.keys(obj).length; 34 | } 35 | 36 | /** 37 | * The constructor for MongoDB connector 38 | * @param {Object} settings The settings object 39 | * @param {DataSource} dataSource The data source instance 40 | * @constructor 41 | */ 42 | function DynamoDB(s, dataSource) { 43 | if (!AWS) { 44 | throw new Error("AWS SDK not installed. Please run npm install aws-sdk"); 45 | } 46 | var i, n; 47 | this.name = 'dynamodb'; 48 | this._models = {}; 49 | this._tables = {}; 50 | this._attributeSpecs = []; 51 | // Connect to dynamodb server 52 | var dynamodb; 53 | // Try to read accessKeyId and secretAccessKey from environment variables 54 | if ((process.env.AWS_ACCESS_KEY_ID !== undefined) && (process.env.AWS_SECRET_ACCESS_KEY !== undefined)) { 55 | logger.log("debug", "Credentials selected from environment variables"); 56 | AWS.config.update({ 57 | region: s.region, 58 | maxRetries: s.maxRetries 59 | }); 60 | dynamodb = new AWS.DynamoDB(); 61 | } else { 62 | logger.log("warn", "Credentials not found in environment variables"); 63 | try { 64 | AWS.config.loadFromPath('credentials.json'); 65 | logger.log("info", "Loading credentials from file"); 66 | dynamodb = new AWS.DynamoDB(); 67 | } catch (e) { 68 | logger.log("warn", "Cannot find credentials file"); 69 | logger.log("info", "Using settings from schema"); 70 | AWS.config.update({ 71 | accessKeyId: s.accessKeyId, 72 | secretAccessKey: s.secretAccessKey, 73 | region: s.region, 74 | maxRetries: s.maxRetries 75 | }); 76 | dynamodb = new AWS.DynamoDB({ 77 | endpoint: new AWS.Endpoint('http://' + s.host + ':' + s.port) 78 | }); 79 | } 80 | } 81 | 82 | this.client = dynamodb; // Used by instance methods 83 | this.docClient = new DocClient({ 84 | service: dynamodb 85 | }); 86 | 87 | this.emitter = new EventEmitter(); 88 | } 89 | 90 | util.inherits(DynamoDB, Connector); 91 | 92 | 93 | /** 94 | * Initialize the Cloudant connector for the given data source 95 | * 96 | * @param {DataSource} ds The data source instance 97 | * @param {Function} [cb] The cb function 98 | */ 99 | exports.initialize = function initializeSchema(ds, cb) { 100 | // s stores the ds settings 101 | var s = ds.settings; 102 | if (ds.settings) { 103 | s.host = ds.settings.host || "localhost"; 104 | s.port = ds.settings.port || 8000; 105 | s.region = ds.settings.region || "ap-southeast-1"; 106 | s.accessKeyId = ds.settings.accessKeyId || "fake"; 107 | s.secretAccessKey = ds.settings.secretAccessKey || "fake"; 108 | s.maxRetries = ds.settings.maxRetries || 0; 109 | } else { 110 | s.region = "ap-southeast-1"; 111 | } 112 | logger.transports.console.level = ds.settings.logLevel || 'debug'; 113 | logger.info("Initializing dynamodb adapter"); 114 | ds.connector = new DynamoDB(s, ds); 115 | ds.connector.dataSource = ds; 116 | 117 | if (cb) { 118 | cb(); 119 | } 120 | }; 121 | 122 | 123 | /* 124 | Assign Attribute Definitions 125 | and KeySchema based on the keys 126 | */ 127 | function AssignKeys(name, type, settings) { 128 | var attr = {}; 129 | var tempString; 130 | var aType; 131 | 132 | attr.keyType = name.keyType; 133 | tempString = (name.type).toString(); 134 | aType = tempString.match(/\w+(?=\(\))/)[0]; 135 | aType = aType.toLowerCase(); 136 | attr.attributeType = helper.TypeLookup(aType); 137 | return attr; 138 | } 139 | 140 | /** 141 | Record current time in milliseconds 142 | */ 143 | function startTimer() { 144 | let timeNow = new Date().getTime(); 145 | return timeNow; 146 | } 147 | /** 148 | Given start time, return a string containing time difference in ms 149 | */ 150 | function stopTimer(timeStart) { 151 | return "[" + String(new Date().getTime() - timeStart) + " ms]"; 152 | } 153 | /** 154 | * Create a table based on hashkey, rangekey specifications 155 | * @param {object} dynamodb : adapter 156 | * @param {object} tableParams : KeySchema & other attrs 157 | * @param {Boolean} tableStatusWait : If true, wait for table to become active 158 | * @param {Number} timeInterval : Check table status after `timeInterval` milliseconds 159 | * @param {function} callback : Callback function 160 | */ 161 | function createTable(dynamodb, tableParams, tableStatusWait, timeInterval, callback) { 162 | var tableExists = false; 163 | var tableStatusFlag = false; 164 | dynamodb.listTables(function (err, data) { 165 | if (err || !data) { 166 | logger.log("error", "-------Error while fetching tables from server. Please check your connection settings & AWS config--------"); 167 | callback(err, null); 168 | return; 169 | } else { 170 | // Boolean variable to check if table already exists. 171 | var existingTableNames = data.TableNames; 172 | existingTableNames.forEach(function (existingTableName) { 173 | if (tableParams.TableName === existingTableName) { 174 | tableExists = true; 175 | logger.log("info", "TABLE %s FOUND IN DATABASE", existingTableName); 176 | } 177 | }); 178 | // If table exists do not create new table 179 | if (tableExists === false) { 180 | // DynamoDB will throw error saying table does not exist 181 | logger.log("info", "CREATING TABLE: %s IN DYNAMODB", tableParams.TableName); 182 | dynamodb.createTable(tableParams, function (err, data) { 183 | if (err || !data) { 184 | callback(err, null); 185 | return; 186 | } else { 187 | logger.log("info", "TABLE CREATED"); 188 | if (tableStatusWait) { 189 | 190 | async.whilst(function () { 191 | return !tableStatusFlag; 192 | 193 | }, function (innerCallback) { 194 | logger.log("info", "Checking Table Status"); 195 | dynamodb.describeTable({ 196 | TableName: tableParams.TableName 197 | }, function (err, tableData) { 198 | if (err) { 199 | innerCallback(err); 200 | } else if (tableData.Table.TableStatus === "ACTIVE") { 201 | logger.log("info", "Table Status is `ACTIVE`"); 202 | tableStatusFlag = true; 203 | innerCallback(null); 204 | } else { 205 | setTimeout(innerCallback, timeInterval); 206 | } 207 | }); 208 | 209 | }, function (err) { 210 | if (err) { 211 | callback(err, null); 212 | } else { 213 | callback(null, "active"); 214 | } 215 | }.bind(this)); 216 | } 217 | } // successful response 218 | }.bind(this)); 219 | } else { 220 | callback(null, "done"); 221 | } 222 | } 223 | }); 224 | } 225 | 226 | 227 | // Check if object is empty 228 | function isEmpty(obj) { 229 | var hasOwnProperty = Object.prototype.hasOwnProperty; 230 | // null and undefined are "empty" 231 | if (obj === null) return true; 232 | 233 | // Assume if it has a length property with a non-zero value 234 | // that that property is correct. 235 | if (obj.length > 0) return false; 236 | if (obj.length === 0) return true; 237 | 238 | // Otherwise, does it have any properties of its own? 239 | // Note that this doesn't handle 240 | // toString and valueOf enumeration bugs in IE < 9 241 | for (var key in obj) { 242 | if (hasOwnProperty.call(obj, key)) return false; 243 | } 244 | 245 | return true; 246 | } 247 | 248 | 249 | /** 250 | * Define schema and create table with hash and range keys 251 | * @param {object} descr : description specified in the schema 252 | */ 253 | DynamoDB.prototype.define = function (descr) { 254 | var timeStart = startTimer(); 255 | if (!descr.settings) descr.settings = {}; 256 | var modelName = descr.model.modelName; 257 | var emitter = this.emitter; 258 | this._models[modelName] = descr; 259 | // Set Read & Write Capacity Units 260 | this._models[modelName].ReadCapacityUnits = descr.settings.ReadCapacityUnits || 5; 261 | this._models[modelName].WriteCapacityUnits = descr.settings.WriteCapacityUnits || 10; 262 | 263 | this._models[modelName].localIndexes = {}; 264 | this._models[modelName].globalIndexes = {}; 265 | 266 | var timeInterval, tableStatusWait; 267 | // Wait for table to become active? 268 | if (descr.settings.tableStatus) { 269 | tableStatusWait = descr.settings.tableStatus.waitTillActive; 270 | if (tableStatusWait === undefined) { 271 | tableStatusWait = true; 272 | } 273 | timeInterval = descr.settings.tableStatus.timeInterval || 5000; 274 | } else { 275 | tableStatusWait = true; 276 | timeInterval = 5000; 277 | } 278 | 279 | // Create table now with the hash and range index. 280 | var properties = descr.properties; 281 | // Iterate through properties and find index 282 | var tableParams = {}; 283 | tableParams.AttributeDefinitions = []; 284 | tableParams.KeySchema = []; 285 | let LocalSecondaryIndexes = []; 286 | let GlobalSecondaryIndexes = []; 287 | this._attributeSpecs[modelName] = {}; 288 | // Temporary object to store read and write capacity units for breakable attrs 289 | var rcus = {}; 290 | var wcus = {}; 291 | 292 | /* 293 | Build KeySchema for the table based on schema definitions. 294 | */ 295 | for (var key in properties) { 296 | // Assign breakers, limits or whatever other properties 297 | // are specified first 298 | // Store the type of attributes in _attributeSpecs. This is 299 | // quite helpful to do Date & Boolean conversions later 300 | // on. 301 | var tempString = (properties[key].type).toString(); 302 | var aType = tempString.match(/\w+(?=\(\))/)[0]; 303 | aType = aType.toLowerCase(); 304 | this._attributeSpecs[modelName][key] = aType; 305 | 306 | // Check if UUID is set to be true for HASH KEY attribute 307 | if (properties[key].keyType === "hash") { 308 | if (properties[key].uuid === true) { 309 | if (key !== 'id') { 310 | throw new Error("UUID generation is only allowed for attribute name id"); 311 | } else { 312 | this._models[modelName].hashKeyUUID = true; 313 | logger.log("debug", "Hash key UUID generation: TRUE"); 314 | } 315 | 316 | } else { 317 | this._models[modelName].hashKeyUUID = false; 318 | } 319 | } 320 | // Following code is applicable only for keys 321 | if (properties[key].keyType !== undefined) { 322 | var attrs = AssignKeys(properties[key]); 323 | // The keys have come! Add to tableParams 324 | // Add Attribute Definitions 325 | // HASH primary key? 326 | if (attrs.keyType === "hash") { 327 | this._models[modelName].hashKey = key; 328 | logger.log("debug", "HASH KEY:", key); 329 | tableParams.KeySchema.push({ 330 | AttributeName: key, 331 | KeyType: 'HASH' 332 | }); 333 | tableParams.AttributeDefinitions.push({ 334 | AttributeName: key, 335 | AttributeType: attrs.attributeType 336 | }); 337 | } 338 | // Range primary key? 339 | if (attrs.keyType === "range") { 340 | this._models[modelName].rangeKey = key; 341 | logger.log("debug", "RANGE KEY:", key); 342 | tableParams.KeySchema.push({ 343 | AttributeName: key, 344 | KeyType: 'RANGE' 345 | }); 346 | tableParams.AttributeDefinitions.push({ 347 | AttributeName: key, 348 | AttributeType: attrs.attributeType 349 | }); 350 | } 351 | // Composite virtual primary key? 352 | if (attrs.keyType === "pk") { 353 | this._models[modelName].pKey = key; 354 | this._models[modelName].pkSeparator = properties[key].separator || "--x--"; 355 | } 356 | } 357 | 358 | if (properties[key].index !== undefined) { 359 | if (properties[key].index.local !== undefined) { 360 | var attrs = AssignKeys(properties[key]); 361 | let index = properties[key].index.local; 362 | let keyName = key + 'LocalIndex'; 363 | let localIndex = { 364 | IndexName: keyName, 365 | KeySchema: [{ 366 | AttributeName: this._models[modelName].hashKey, 367 | KeyType: 'HASH' 368 | }, { 369 | AttributeName: key, 370 | KeyType: 'RANGE' 371 | }], 372 | Projection: {} 373 | }; 374 | 375 | if (index.project) { 376 | if (util.isArray(index.project)) { 377 | localIndex.Projection = { 378 | ProjectionType: 'INCLUDE', 379 | NonKeyAttributes: index.project 380 | }; 381 | } else { 382 | localIndex.Projection = { 383 | ProjectionType: 'ALL' 384 | }; 385 | } 386 | } else { 387 | localIndex.Projection = { 388 | ProjectionType: 'KEYS_ONLY' 389 | }; 390 | } 391 | LocalSecondaryIndexes.push(localIndex); 392 | tableParams.AttributeDefinitions.push({ 393 | AttributeName: key, 394 | AttributeType: attrs.attributeType 395 | }); 396 | this._models[modelName].localIndexes[key] = { 397 | hash: this._models[modelName].hashKey, 398 | range: key, 399 | IndexName: keyName 400 | }; 401 | } 402 | if (properties[key].index.global !== undefined) { 403 | var attrs = AssignKeys(properties[key]); 404 | let index = properties[key].index.global; 405 | let keyName = key + 'GlobalIndex'; 406 | var globalIndex = { 407 | IndexName: keyName, 408 | KeySchema: [{ 409 | AttributeName: key, 410 | KeyType: 'HASH' 411 | }, 412 | { 413 | AttributeName: index.rangeKey, 414 | KeyType: 'RANGE' 415 | } 416 | ], 417 | ProvisionedThroughput: { 418 | ReadCapacityUnits: index.throughput.read || 5, 419 | WriteCapacityUnits: index.throughput.write || 10 420 | } 421 | }; 422 | 423 | if (index.project) { 424 | if (util.isArray(index.project)) { 425 | globalIndex.Projection = { 426 | ProjectionType: 'INCLUDE', 427 | NonKeyAttributes: index.project 428 | }; 429 | } else { 430 | globalIndex.Projection = { 431 | ProjectionType: 'ALL' 432 | }; 433 | } 434 | } else { 435 | globalIndex.Projection = { 436 | ProjectionType: 'KEYS_ONLY' 437 | }; 438 | } 439 | GlobalSecondaryIndexes.push(localIndex); 440 | tableParams.AttributeDefinitions.push({ 441 | AttributeName: key, 442 | AttributeType: attrs.attributeType 443 | }); 444 | this._models[modelName].globalIndexes[key] = { 445 | hash: key, 446 | range: index.rangeKey, 447 | IndexName: keyName 448 | }; 449 | } 450 | } 451 | } 452 | if (LocalSecondaryIndexes.length) { 453 | tableParams.LocalSecondaryIndexes = LocalSecondaryIndexes; 454 | } 455 | if (GlobalSecondaryIndexes.length) { 456 | tableParams.GlobalSecondaryIndexes = GlobalSecondaryIndexes; 457 | } 458 | 459 | tableParams.ProvisionedThroughput = { 460 | ReadCapacityUnits: this._models[modelName].ReadCapacityUnits, 461 | WriteCapacityUnits: this._models[modelName].WriteCapacityUnits 462 | }; 463 | logger.log("debug", "Read Capacity Units:", tableParams.ProvisionedThroughput.ReadCapacityUnits); 464 | logger.log("debug", "Write Capacity Units:", tableParams.ProvisionedThroughput.WriteCapacityUnits); 465 | 466 | if ((this._models[modelName].rangeKey !== undefined) && (this._models[modelName].pKey !== undefined)) { 467 | if (this._models[modelName].pKey !== 'id') { 468 | throw new Error("Primary Key must be named `id`"); 469 | } 470 | } 471 | if ((this._models[modelName].rangeKey !== undefined) && (this._models[modelName].pKey === undefined)) { 472 | throw new Error("Range key is present, but primary key not specified in schema"); 473 | } 474 | 475 | /* 476 | JugglingDB expects an id attribute in return even if a hash key is not specified. Hence 477 | if hash key is not defined in the schema, create an attribute called id, set it as hashkey. 478 | */ 479 | if ((this._models[modelName].hashKey === undefined) && (properties.id === undefined)) { 480 | this._models[modelName].hashKey = 'id'; 481 | this._models[modelName].hashKeyUUID = true; 482 | this._attributeSpecs[modelName][this._models[modelName].hashKey] = "string"; 483 | tableParams.KeySchema.push({ 484 | AttributeName: 'id', 485 | KeyType: 'HASH' 486 | }); 487 | tableParams.AttributeDefinitions.push({ 488 | AttributeName: 'id', 489 | AttributeType: 'S' 490 | }); 491 | } 492 | 493 | // If there are breakable attrs with sharding set to true, create the 494 | // extra tables now 495 | var _dynamodb = this.client; 496 | var attributeSpecs = this._attributeSpecs[modelName]; 497 | var ReadCapacityUnits = this._models[modelName].ReadCapacityUnits; 498 | var WriteCapacityUnits = this._models[modelName].WriteCapacityUnits; 499 | var hashKey = this._models[modelName].hashKey; 500 | var pKey = this._models[modelName].pKey; 501 | 502 | // Assign table name 503 | tableParams.TableName = descr.settings.table || modelName; 504 | logger.log("debug", "Table Name:", tableParams.TableName); 505 | // Add this to _tables so that instance methods can use it. 506 | this._tables[modelName] = tableParams.TableName; 507 | // Create main table function 508 | createTable(_dynamodb, tableParams, tableStatusWait, timeInterval, function (err, data) { 509 | if (err || !data) { 510 | var tempString = "while creating table: " + tableParams.TableName + " => " + err.message.toString(); 511 | throw new Error(tempString); 512 | } else { 513 | 514 | } 515 | }); 516 | logger.log("info", "Defining model: ", modelName, stopTimer(timeStart).bold.cyan); 517 | }; 518 | /** 519 | * Creates a DynamoDB compatible representation 520 | * of arrays, objects and primitives. 521 | * @param {object} data: Object to be converted 522 | * @return {object} DynamoDB compatible JSON 523 | */ 524 | function DynamoFromJSON(data) { 525 | var obj; 526 | /* 527 | If data is an array, loop through each member 528 | of the array, and call objToDB on the element 529 | e.g ["someword",20] --> [ {'S': 'someword'} , {'N' : '20'}] 530 | */ 531 | if (data instanceof Array) { 532 | obj = []; 533 | data.forEach(function (dataElement) { 534 | // If string is empty, assign it as 535 | // "null". 536 | if (dataElement === "") { 537 | dataElement = "empty"; 538 | } 539 | if (dataElement instanceof Date) { 540 | dataElement = Number(dataElement); 541 | } 542 | obj.push(helper.objToDB(dataElement)); 543 | }); 544 | } 545 | /* 546 | If data is an object, loop through each member 547 | of the object, and call objToDB on the element 548 | e.g { age: 20 } --> { age: {'N' : '20'} } 549 | */ 550 | else if ((data instanceof Object) && (data instanceof Date !== true)) { 551 | obj = {}; 552 | for (var key in data) { 553 | if (data.hasOwnProperty(key)) { 554 | // If string is empty, assign it as 555 | // "null". 556 | if (data[key] === undefined) { 557 | data[key] = "undefined"; 558 | } 559 | if (data[key] === null) { 560 | data[key] = "null"; 561 | } 562 | if (data[key] === "") { 563 | data[key] = "empty"; 564 | } 565 | // If Date convert to number 566 | if (data[key] instanceof Date) { 567 | data[key] = Number(data[key]); 568 | } 569 | obj[key] = helper.objToDB(data[key]); 570 | } 571 | } 572 | /* 573 | If data is a number, or string call objToDB on the element 574 | e.g 20 --> {'N' : '20'} 575 | */ 576 | } else { 577 | 578 | // If string is empty, assign it as 579 | // "empty". 580 | if (data === null) { 581 | data = "null"; 582 | } 583 | if (data === undefined) { 584 | data = "undefined"; 585 | } 586 | if (data === "") { 587 | data = "empty"; 588 | } 589 | // If Date convert to number 590 | if (data instanceof Date) { 591 | data = Number(data); 592 | } 593 | obj = helper.objToDB(data); 594 | } 595 | return obj; 596 | } 597 | 598 | function KeyOperatorLookup(operator) { 599 | let value; 600 | switch (operator) { 601 | case "=": 602 | value = "="; 603 | break; 604 | case "lt": 605 | value = "<"; 606 | break; 607 | case "lte": 608 | value = "<="; 609 | break; 610 | case "gt": 611 | value = ">"; 612 | break; 613 | case "gte": 614 | value = ">="; 615 | break; 616 | case "between": 617 | value = "BETWEEN"; 618 | break; 619 | default: 620 | value = "="; 621 | break; 622 | } 623 | return value; 624 | } 625 | 626 | /** 627 | * Converts jugglingdb operators like 'gt' to DynamoDB form 'GT' 628 | * @param {string} DynamoDB comparison operator 629 | */ 630 | function OperatorLookup(operator) { 631 | if (operator === "inq") { 632 | operator = "in"; 633 | } 634 | return operator.toUpperCase(); 635 | } 636 | 637 | DynamoDB.prototype.defineProperty = function (model, prop, params) { 638 | this._models[model].properties[prop] = params; 639 | }; 640 | 641 | DynamoDB.prototype.tables = function (name) { 642 | if (!this._tables[name]) { 643 | this._tables[name] = name; 644 | } 645 | return this._tables[name]; 646 | }; 647 | /** 648 | * Create a new item or replace/update it if it exists 649 | * @param {object} model 650 | * @param {object} data : key,value pairs of new model object 651 | * @param {Function} callback 652 | */ 653 | DynamoDB.prototype.create = function (model, data, callback) { 654 | var timerStart = startTimer(); 655 | var hashKey = this._models[model].hashKey; 656 | var rangeKey = this._models[model].rangeKey; 657 | var pkSeparator = this._models[model].pkSeparator; 658 | var pKey = this._models[model].pKey; 659 | var err; 660 | // If jugglingdb defined id is undefined, and it is not a 661 | // hashKey or a primary key , then delete it. 662 | if ((data.id === undefined) && (hashKey !== 'id')) { 663 | delete data.id; 664 | } 665 | // If some key is a hashKey, check if uuid is set to true. If yes, call the 666 | // UUID() function and generate a unique id. 667 | if (this._models[model].hashKeyUUID === true) { 668 | data[hashKey] = helper.UUID(); 669 | } 670 | var originalData = {}; 671 | // Copy all attrs from data to originalData 672 | for (var key in data) { 673 | originalData[key] = data[key]; 674 | } 675 | 676 | if (data[hashKey] === undefined) { 677 | err = new Error("Hash Key `" + hashKey + "` is undefined."); 678 | callback(err, null); 679 | return; 680 | } 681 | if (data[hashKey] === null) { 682 | err = new Error("Hash Key `" + hashKey + "` cannot be NULL."); 683 | callback(err, null); 684 | return; 685 | } 686 | // If pKey is defined, range key is also present. 687 | if (pKey !== undefined) { 688 | if ((data[rangeKey] === null) || (data[rangeKey] === undefined)) { 689 | err = new Error("Range Key `" + rangeKey + "` cannot be null or undefined."); 690 | callback(err, null); 691 | return; 692 | } else { 693 | data[pKey] = String(data[hashKey]) + pkSeparator + String(data[rangeKey]); 694 | originalData[pKey] = data[pKey]; 695 | } 696 | } 697 | 698 | var queryString = "CREATE ITEM IN TABLE " + this.tables(model); 699 | var tableParams = {}; 700 | tableParams.TableName = this.tables(model); 701 | tableParams.ReturnConsumedCapacity = "TOTAL"; 702 | 703 | var attributeSpecs = this._attributeSpecs[model]; 704 | var outerCounter = 0; 705 | var chunkedData = {}; 706 | 707 | var tempString = "INSERT ITEM INTO TABLE: " + tableParams.TableName; 708 | logger.log("debug", tempString); 709 | // if (pKey !== undefined) { 710 | // delete data[pKey]; 711 | // } 712 | tableParams.Item = data; 713 | this.docClient.put(tableParams, function (err, res) { 714 | if (err || !res) { 715 | callback(err, null); 716 | return; 717 | } else { 718 | logger.log("info", queryString.blue, stopTimer(timerStart).bold.cyan); 719 | if (pKey !== undefined) { 720 | originalData.id = originalData[pKey]; 721 | callback(null, originalData.id); 722 | return; 723 | } else { 724 | originalData.id = originalData[hashKey]; 725 | callback(null, originalData.id); 726 | return; 727 | } 728 | } 729 | }.bind(this)); 730 | }; 731 | /** 732 | * Function that performs query operation on dynamodb 733 | * @param {object} model 734 | * @param {object} filter : Query filter 735 | * @param {Number/String} hashKey : Hash Key 736 | * @param {object} rangeKey : Range Key 737 | * @param {String} queryString : The query string (used for console logs) 738 | * @param {Number} timeStart : Start time of query operation in milliseconds 739 | * @return {object} : Final query object to be sent to dynamodb 740 | */ 741 | function query(modelName, filter, model, queryString, timeStart) { 742 | // Table parameters to do the query/scan 743 | let hashKey = model.hashKey; 744 | let rangeKey = model.rangeKey; 745 | let localKeys = model.localIndexes; 746 | let globalKeys = model.globalIndexes; 747 | 748 | var tableParams = {}; 749 | // Define the filter if it does not exist 750 | if (!filter) { 751 | filter = {}; 752 | } 753 | // Initialize query as an empty object 754 | var queryObj = {}; 755 | // Construct query for amazon DynamoDB 756 | // Set queryfileter to empty object 757 | tableParams.ExpressionAttributeNames = {}; 758 | let ExpressionAttributeNames = {}; 759 | tableParams.ExpressionAttributeValues = {}; 760 | tableParams.KeyConditionExpression = ""; 761 | 762 | let KeyConditionExpression = []; 763 | let FilterExpression = []; 764 | let ExpressionAttributeValues = {}; 765 | 766 | // If a where clause exists in the query, extract 767 | // the conditions from it. 768 | if (filter.where) { 769 | queryString = queryString + " WHERE "; 770 | for (var key in filter.where) { 771 | var condition = filter.where[key]; 772 | 773 | let keyName = "#" + key.slice(0, 1).toUpperCase(); 774 | if (tableParams.ExpressionAttributeNames[keyName] == undefined) { 775 | tableParams.ExpressionAttributeNames[keyName] = key; 776 | } else { 777 | let i = 1; 778 | while (tableParams.ExpressionAttributeNames[keyName] != undefined) { 779 | keyName = "#" + key.slice(0, i); 780 | i++; 781 | } 782 | keyName = "#" + key.slice(0, i).toUpperCase(); 783 | tableParams.ExpressionAttributeNames[keyName] = key; 784 | } 785 | 786 | ExpressionAttributeNames[key] = keyName; 787 | 788 | var ValueExpression = ":" + key; 789 | var insideKey = null; 790 | 791 | if (key === hashKey || (globalKeys[key] && globalKeys[key].hash === key) || 792 | (localKeys[key] && localKeys[key].hash === key)) { 793 | if (condition && condition.constructor.name === 'Object') {} else if (condition && condition.constructor.name === "Array") {} else { 794 | 795 | KeyConditionExpression[0] = keyName + " = " + ValueExpression; 796 | tableParams.ExpressionAttributeValues[ValueExpression] = condition; 797 | if (globalKeys[key] && globalKeys[key].hash === key) { 798 | tableParams.IndexName = globalKeys[key].IndexName; 799 | } else if (localKeys[key] && localKeys[key].hash === key) { 800 | tableParams.IndexName = localKeys[key].IndexName; 801 | } 802 | } 803 | } else if (key === rangeKey || (globalKeys[key] && globalKeys[key].range === key) || 804 | (localKeys[key] && localKeys[key].range === key)) { 805 | if (condition && condition.constructor.name === 'Object') { 806 | insideKey = Object.keys(condition)[0]; 807 | condition = condition[insideKey]; 808 | let operator = KeyOperatorLookup(insideKey); 809 | if (operator === "BETWEEN") { 810 | tableParams.ExpressionAttributeValues[ValueExpression + "_start"] = condition[0]; 811 | tableParams.ExpressionAttributeValues[ValueExpression + "_end"] = condition[1]; 812 | KeyConditionExpression[1] = keyName + " " + operator + " " + ValueExpression + "_start" + " AND " + ValueExpression + "_end"; 813 | } else { 814 | tableParams.ExpressionAttributeValues[ValueExpression] = condition; 815 | KeyConditionExpression[1] = keyName + " " + operator + " " + ValueExpression; 816 | } 817 | } else if (condition && condition.constructor.name === "Array") { 818 | tableParams.ExpressionAttributeValues[ValueExpression + "_start"] = condition[0]; 819 | tableParams.ExpressionAttributeValues[ValueExpression + "_end"] = condition[1]; 820 | KeyConditionExpression[1] = keyName + " BETWEEN " + ValueExpression + "_start" + " AND " + ValueExpression + "_end"; 821 | } else { 822 | tableParams.ExpressionAttributeValues[ValueExpression] = condition; 823 | KeyConditionExpression[1] = keyName + " = " + ValueExpression; 824 | } 825 | 826 | if (globalKeys[key] && globalKeys[key].range === key) { 827 | tableParams.IndexName = globalKeys[key].IndexName; 828 | } else if (localKeys[key] && localKeys[key].range === key) { 829 | tableParams.IndexName = localKeys[key].IndexName; 830 | } 831 | } else { 832 | if (condition && condition.constructor.name === 'Object') { 833 | insideKey = Object.keys(condition)[0]; 834 | condition = condition[insideKey]; 835 | let operator = KeyOperatorLookup(insideKey); 836 | if (operator === "BETWEEN") { 837 | tableParams.ExpressionAttributeValues[ValueExpression + "_start"] = condition[0]; 838 | tableParams.ExpressionAttributeValues[ValueExpression + "_end"] = condition[1]; 839 | FilterExpression.push(keyName + " " + operator + " " + ValueExpression + "_start" + " AND " + ValueExpression + "_end"); 840 | } else if (operator === "IN") { 841 | tableParams.ExpressionAttributeValues[ValueExpression] = "(" + condition.join(',') + ")"; 842 | FilterExpression.push(keyName + " " + operator + " " + ValueExpression); 843 | } else { 844 | tableParams.ExpressionAttributeValues[ValueExpression] = condition; 845 | FilterExpression.push(keyName + " " + operator + " " + ValueExpression); 846 | } 847 | } else if (condition && condition.constructor.name === "Array") { 848 | tableParams.ExpressionAttributeValues[ValueExpression] = "(" + condition.join(',') + ")"; 849 | FilterExpression.push(keyName + " IN " + ValueExpression); 850 | } else { 851 | tableParams.ExpressionAttributeValues[ValueExpression] = condition; 852 | FilterExpression.push(keyName + " = " + ValueExpression); 853 | } 854 | } 855 | 856 | // If condition is of type object, obtain key 857 | // and the actual condition on the key 858 | // In jugglingdb, `where` can have the following 859 | // forms. 860 | // 1) where : { key: value } 861 | // 2) where : { startTime : { gt : Date.now() } } 862 | // 3) where : { someKey : ["something","nothing"] } 863 | // condition now holds value in case 1), 864 | // { gt: Date.now() } in case 2) 865 | // ["something, "nothing"] in case 3) 866 | /* 867 | If key is of hash or hash & range type, 868 | we can use the query function of dynamodb 869 | to access the table. This saves a lot of time 870 | since it does not have to look at all records 871 | */ 872 | // var insideKey = null; 873 | // if (condition && condition.constructor.name === 'Object') { 874 | // insideKey = Object.keys(condition)[0]; 875 | // condition = condition[insideKey]; 876 | 877 | // logger.log("debug","Condition Type => Object", "Operator", insideKey, "Condition Value:", condition); 878 | // // insideKey now holds gt and condition now holds Date.now() 879 | // queryObj[key] = { 880 | // operator: OperatorLookup(insideKey), 881 | // attrs: condition 882 | // }; 883 | // } else if (condition && condition.constructor.name === "Array") { 884 | // logger.log("debug", "Condition Type => Array", "Opearator", "IN", "Condition Value:", condition); 885 | // queryObj[key] = { 886 | // operator: 'IN', 887 | // attrs: condition 888 | // }; 889 | // } else { 890 | // logger.log("debug", "Condition Type => Equality", "Condition Value:", condition); 891 | // queryObj[key] = { 892 | // operator: 'EQ', 893 | // attrs: condition 894 | // }; 895 | // } 896 | 897 | // if (key === hashKey) { 898 | // // Add hashkey eq condition to keyconditions 899 | // tableParams.KeyConditions[key] = {}; 900 | // tableParams.KeyConditions[key].ComparisonOperator = queryObj[key].operator; 901 | // // For hashKey only 'EQ' operator is allowed. Issue yellow error. DB will 902 | // // throw a red error. 903 | // if (queryObj[key].operator !== 'EQ') { 904 | // var errString = "Warning: Only equality condition is allowed on HASHKEY"; 905 | // logger.log("warn", errString.yellow); 906 | // } 907 | // tableParams.KeyConditions[key].AttributeValueList = []; 908 | // tableParams.KeyConditions[key].AttributeValueList.push(queryObj[key].attrs); // incorporated document client 909 | // //tableParams.KeyConditions[key].AttributeValueList.push(DynamoFromJSON(queryObj[key].attrs)); 910 | // queryString = queryString + " HASHKEY: `" + String(key) + "` " + String(queryObj[key].operator) + " `" + String(queryObj[key].attrs) + "`"; 911 | // } else if (key === rangeKey) { 912 | // // Add hashkey eq condition to keyconditions 913 | // tableParams.KeyConditions[key] = {}; 914 | // tableParams.KeyConditions[key].ComparisonOperator = queryObj[key].operator; 915 | // tableParams.KeyConditions[key].AttributeValueList = []; 916 | 917 | // var attrResult = queryObj[key].attrs; 918 | // //var attrResult = DynamoFromJSON(queryObj[key].attrs); 919 | // if (attrResult instanceof Array) { 920 | // logger.log("debug", "Attribute Value list is an array"); 921 | // tableParams.KeyConditions[key].AttributeValueList = queryObj[key].attrs; // incorporated document client 922 | // //tableParams.KeyConditions[key].AttributeValueList = DynamoFromJSON(queryObj[key].attrs); 923 | // } else { 924 | // tableParams.KeyConditions[key].AttributeValueList.push(queryObj[key].attrs); // incorporated document client 925 | // //tableParams.KeyConditions[key].AttributeValueList.push(DynamoFromJSON(queryObj[key].attrs)); 926 | // } 927 | 928 | // queryString = queryString + "& RANGEKEY: `" + String(key) + "` " + String(queryObj[key].operator) + " `" + String(queryObj[key].attrs) + "`"; 929 | // } else { 930 | // tableParams.QueryFilter[key] = {}; 931 | // tableParams.QueryFilter[key].ComparisonOperator = queryObj[key].operator; 932 | // tableParams.QueryFilter[key].AttributeValueList = []; 933 | 934 | 935 | // var attrResult = queryObj[key].attrs; 936 | // //var attrResult = DynamoFromJSON(queryObj[key].attrs); 937 | // if (attrResult instanceof Array) { 938 | // tableParams.QueryFilter[key].AttributeValueList = queryObj[key].attrs; // incorporated document client 939 | // //tableParams.QueryFilter[key].AttributeValueList = DynamoFromJSON(queryObj[key].attrs); 940 | // } else { 941 | // tableParams.QueryFilter[key].AttributeValueList.push(queryObj[key].attrs); // incorporated document client 942 | // //tableParams.QueryFilter[key].AttributeValueList.push(DynamoFromJSON(queryObj[key].attrs)); 943 | // } 944 | // queryString = queryString + "& `" + String(key) + "` " + String(queryObj[key].operator) + " `" + String(queryObj[key].attrs) + "`"; 945 | // } 946 | } 947 | tableParams.KeyConditionExpression = KeyConditionExpression.join(" AND "); 948 | if (countProperties(tableParams.ExpressionAttributeNames) > countProperties(KeyConditionExpression)) { 949 | //tableParams.FilterExpression = ""; 950 | tableParams.FilterExpression = "" + FilterExpression.join(" AND "); 951 | } 952 | } 953 | queryString = queryString + ' WITH QUERY OPERATION '; 954 | logger.log("info", queryString.blue, stopTimer(timeStart).bold.cyan); 955 | return tableParams; 956 | } 957 | 958 | /** 959 | * Builds table parameters for scan operation 960 | * @param {[type]} model Model object 961 | * @param {[type]} filter Filter 962 | * @param {[type]} queryString String that holds query operation actions 963 | * @param {[type]} timeStart start time of operation 964 | */ 965 | function scan(model, filter, queryString, timeStart) { 966 | // Table parameters to do the query/scan 967 | var tableParams = {}; 968 | // Define the filter if it does not exist 969 | if (!filter) { 970 | filter = {}; 971 | } 972 | // Initialize query as an empty object 973 | var query = {}; 974 | // Set scanfilter to empty object 975 | tableParams.ScanFilter = {}; 976 | // If a where clause exists in the query, extract 977 | // the conditions from it. 978 | if (filter.where) { 979 | queryString = queryString + " WHERE "; 980 | for (var key in filter.where) { 981 | var condition = filter.where[key]; 982 | // If condition is of type object, obtain key 983 | // and the actual condition on the key 984 | // In jugglingdb, `where` can have the following 985 | // forms. 986 | // 1) where : { key: value } 987 | // 2) where : { startTime : { gt : Date.now() } } 988 | // 3) where : { someKey : ["something","nothing"] } 989 | // condition now holds value in case 1), 990 | // { gt: Date.now() } in case 2) 991 | // ["something, "nothing"] in case 3) 992 | var insideKey = null; 993 | if (condition && condition.constructor.name === 'Object') { 994 | logger.log("debug", "Condition Type => Object", "Operator", insideKey, "Condition Value:", condition); 995 | insideKey = Object.keys(condition)[0]; 996 | condition = condition[insideKey]; 997 | // insideKey now holds gt and condition now holds Date.now() 998 | query[key] = { 999 | operator: OperatorLookup(insideKey), 1000 | attrs: condition 1001 | }; 1002 | } else if (condition && condition.constructor.name === "Array") { 1003 | logger.log("debug", "Condition Type => Array", "Operator", insideKey, "Condition Value:", condition); 1004 | query[key] = { 1005 | operator: 'IN', 1006 | attrs: condition 1007 | }; 1008 | } else { 1009 | logger.log("debug", "Condition Type => Equality", "Condition Value:", condition); 1010 | query[key] = { 1011 | operator: 'EQ', 1012 | attrs: condition 1013 | }; 1014 | } 1015 | tableParams.ScanFilter[key] = {}; 1016 | tableParams.ScanFilter[key].ComparisonOperator = query[key].operator; 1017 | tableParams.ScanFilter[key].AttributeValueList = []; 1018 | 1019 | var attrResult = query[key].attrs; 1020 | //var attrResult = DynamoFromJSON(query[key].attrs); 1021 | 1022 | if (attrResult instanceof Array) { 1023 | logger.log("debug", "Attribute Value list is an array"); 1024 | tableParams.ScanFilter[key].AttributeValueList = query[key].attrs; 1025 | //tableParams.ScanFilter[key].AttributeValueList = DynamoFromJSON(query[key].attrs); 1026 | } else { 1027 | tableParams.ScanFilter[key].AttributeValueList.push(query[key].attrs); 1028 | //tableParams.ScanFilter[key].AttributeValueList.push(DynamoFromJSON(query[key].attrs)); 1029 | } 1030 | 1031 | 1032 | queryString = queryString + "`" + String(key) + "` " + String(query[key].operator) + " `" + String(query[key].attrs) + "`"; 1033 | } 1034 | } 1035 | queryString = queryString + ' WITH SCAN OPERATION '; 1036 | logger.log("info", queryString.blue, stopTimer(timeStart).bold.cyan); 1037 | 1038 | return tableParams; 1039 | } 1040 | /** 1041 | * Uses Amazon DynamoDB query/scan function to fetch all 1042 | * matching entries in the table. 1043 | * 1044 | */ 1045 | DynamoDB.prototype.all = function all(model, filter, callback) { 1046 | var timeStart = startTimer(); 1047 | var queryString = "GET ALL ITEMS FROM TABLE "; 1048 | 1049 | // If limit is specified, use it to limit results 1050 | var limitObjects; 1051 | if (filter && filter.limit) { 1052 | if (typeof (filter.limit) !== "number") { 1053 | callback(new Error("Limit must be a number in Model.all function"), null); 1054 | return; 1055 | } 1056 | limitObjects = filter.limit; 1057 | } 1058 | 1059 | 1060 | // Order, default by hash key or id 1061 | var orderByField; 1062 | var args = {}; 1063 | if (this._models[model].rangeKey === undefined) { 1064 | orderByField = this._models[model].hashKey; 1065 | args[orderByField] = 1; 1066 | } else { 1067 | orderByField = 'id'; 1068 | args['id'] = 1; 1069 | } 1070 | // Custom ordering 1071 | if (filter && filter.order) { 1072 | var keys = filter.order; 1073 | if (typeof keys === 'string') { 1074 | keys = keys.split(','); 1075 | } 1076 | 1077 | for (var index in keys) { 1078 | var m = keys[index].match(/\s+(A|DE)SC$/); 1079 | var keyA = keys[index]; 1080 | keyA = keyA.replace(/\s+(A|DE)SC$/, '').trim(); 1081 | orderByField = keyA; 1082 | if (m && m[1] === 'DE') { 1083 | args[keyA] = -1; 1084 | } else { 1085 | args[keyA] = 1; 1086 | } 1087 | } 1088 | 1089 | } 1090 | 1091 | // Skip , Offset 1092 | var offset; 1093 | if (filter && filter.offset) { 1094 | if (typeof (filter.offset) !== "number") { 1095 | callback(new Error("Offset must be a number in Model.all function"), null); 1096 | return; 1097 | } 1098 | offset = filter.offset; 1099 | } else if (filter && filter.skip) { 1100 | if (typeof (filter.skip) !== "number") { 1101 | callback(new Error("Skip must be a number in Model.all function"), null); 1102 | return; 1103 | } 1104 | offset = filter.skip; 1105 | } 1106 | 1107 | 1108 | queryString = queryString + String(this.tables(model)); 1109 | // If hashKey is present in where filter, use query 1110 | var hashKeyFound = false; 1111 | if (filter && filter.where) { 1112 | for (var key in filter.where) { 1113 | console.log(key, this._models[model].hashKey); 1114 | if (key === this._models[model].hashKey) { 1115 | hashKeyFound = true; 1116 | logger.log("debug", "Hash Key Found, QUERY operation will be used"); 1117 | } 1118 | } 1119 | } 1120 | 1121 | // Check if an array of hash key values are provided. If yes, use scan. 1122 | // Otherwise use query. This is because query does not support array of 1123 | // hash key values 1124 | if (hashKeyFound === true) { 1125 | var condition = filter.where[this._models[model].hashKey]; 1126 | var insideKey = null; 1127 | if ((condition && condition.constructor.name === 'Object') || (condition && condition.constructor.name === "Array")) { 1128 | insideKey = Object.keys(condition)[0]; 1129 | condition = condition[insideKey]; 1130 | if (condition instanceof Array) { 1131 | hashKeyFound = false; 1132 | logger.log("debug", "Hash key value is an array. Using SCAN operation instead"); 1133 | } 1134 | } 1135 | } 1136 | 1137 | // If true use query function 1138 | if (hashKeyFound === true) { 1139 | var tableParams = query(model, filter, this._models[model], queryString, timeStart); 1140 | console.log('tableParams', tableParams); 1141 | // Set table name based on model 1142 | tableParams.TableName = this.tables(model); 1143 | tableParams.ReturnConsumedCapacity = "TOTAL"; 1144 | 1145 | var attributeSpecs = this._attributeSpecs[model]; 1146 | var LastEvaluatedKey = "junk"; 1147 | var queryResults = []; 1148 | var finalResult = []; 1149 | var hashKey = this._models[model].hashKey; 1150 | var docClient = this.docClient; 1151 | var pKey = this._models[model].pKey; 1152 | var pkSeparator = this._models[model].pkSeparator; 1153 | var rangeKey = this._models[model].rangeKey; 1154 | tableParams.ExclusiveStartKey = undefined; 1155 | var modelObj = this._models[model]; 1156 | // If KeyConditions exist, then call DynamoDB query function 1157 | if (tableParams.KeyConditionExpression) { 1158 | async.doWhilst(function (queryCallback) { 1159 | logger.log("debug", "Query issued"); 1160 | docClient.query(tableParams, function (err, res) { 1161 | if (err || !res) { 1162 | queryCallback(err); 1163 | } else { 1164 | // Returns an array of objects. Pass each one to 1165 | // JSONFromDynamo and push to empty array 1166 | LastEvaluatedKey = res.LastEvaluatedKey; 1167 | if (LastEvaluatedKey !== undefined) { 1168 | logger.log("debug", "LastEvaluatedKey found. Refetching.."); 1169 | tableParams.ExclusiveStartKey = LastEvaluatedKey; 1170 | } 1171 | 1172 | queryResults = queryResults.concat(res.Items); 1173 | queryCallback(); 1174 | } 1175 | }.bind(this)); 1176 | }, function () { 1177 | return LastEvaluatedKey !== undefined; 1178 | }, function (err) { 1179 | if (err) { 1180 | callback(err, null); 1181 | } else { 1182 | if (offset !== undefined) { 1183 | logger.log("debug", "Offset by", offset); 1184 | queryResults = queryResults.slice(offset, limitObjects + offset); 1185 | } 1186 | if (limitObjects !== undefined) { 1187 | logger.log("debug", "Limit by", limitObjects); 1188 | queryResults = queryResults.slice(0, limitObjects); 1189 | } 1190 | logger.log("debug", "Sort by", orderByField, "Order:", args[orderByField] > 0 ? 'ASC' : 'DESC'); 1191 | queryResults = helper.SortByKey(queryResults, orderByField, args[orderByField]); 1192 | if (filter && filter.include) { 1193 | logger.log("debug", "Model includes", filter.include); 1194 | modelObj.model.include(queryResults, filter.include, callback); 1195 | } else { 1196 | logger.log("debug", "Query results complete"); 1197 | callback(null, queryResults); 1198 | } 1199 | } 1200 | }.bind(this)); 1201 | } 1202 | } 1203 | // else { 1204 | // // Call scan function 1205 | // var tableParams = scan(model, filter, queryString, timeStart); 1206 | // tableParams.TableName = this.tables(model); 1207 | // tableParams.ReturnConsumedCapacity = "TOTAL"; 1208 | // var attributeSpecs = this._attributeSpecs[model]; 1209 | // var finalResult = []; 1210 | // var hashKey = this._models[model].hashKey; 1211 | // var pKey = this._models[model].pKey; 1212 | // var pkSeparator = this._models[model].pkSeparator; 1213 | // var rangeKey = this._models[model].rangeKey; 1214 | // var LastEvaluatedKey = "junk"; 1215 | // var queryResults = []; 1216 | // var docClient = this.docClient; 1217 | // tableParams.ExclusiveStartKey = undefined; 1218 | // var modelObj = this._models[model]; 1219 | // // Scan DynamoDB table 1220 | // async.doWhilst( function(queryCallback) { 1221 | // docClient.scan(tableParams, function(err, res) { 1222 | // if (err || !res) { 1223 | // queryCallback(err); 1224 | // } else { 1225 | // LastEvaluatedKey = res.LastEvaluatedKey; 1226 | // if (LastEvaluatedKey !== undefined) { 1227 | // tableParams.ExclusiveStartKey = LastEvaluatedKey; 1228 | // } 1229 | // queryResults = queryResults.concat(res.Items); 1230 | // queryCallback(); 1231 | // } 1232 | // }.bind(this));}, function() { return LastEvaluatedKey !== undefined; }, function (err){ 1233 | // if (err) { 1234 | // callback(err, null); 1235 | // } else { 1236 | // if (offset !== undefined) { 1237 | // logger.log("debug", "Offset by", offset); 1238 | // queryResults = queryResults.slice(offset, limitObjects + offset); 1239 | // } 1240 | // if (limitObjects !== undefined) { 1241 | // logger.log("debug", "Limit by", limitObjects); 1242 | // queryResults = queryResults.slice(0, limitObjects); 1243 | // } 1244 | // logger.log("debug", "Sort by", orderByField, "Order:", args[orderByField] > 0 ? 'ASC' : 'DESC'); 1245 | // queryResults = helper.SortByKey(queryResults, orderByField, args[orderByField]); 1246 | // if (filter && filter.include) { 1247 | // logger.log("debug", "Model includes", filter.include); 1248 | // modelObj.model.include(queryResults, filter.include, callback); 1249 | // } else { 1250 | // callback(null, queryResults); 1251 | // logger.log("debug", "Query complete"); 1252 | // } 1253 | // } 1254 | // }.bind(this)); 1255 | // } 1256 | }; 1257 | /** 1258 | * Find an item based on hashKey alone 1259 | * @param {object} model [description] 1260 | * @param {object/primitive} pKey : If range key is undefined, 1261 | * this is the same as hash key. If range key is defined, 1262 | * then pKey is hashKey + (Separator) + rangeKey 1263 | * @param {Function} callback 1264 | */ 1265 | DynamoDB.prototype.find = function find(model, pk, callback) { 1266 | var timeStart = startTimer(); 1267 | var queryString = "GET AN ITEM FROM TABLE "; 1268 | var hashKey = this._models[model].hashKey; 1269 | var rangeKey = this._models[model].rangeKey; 1270 | var attributeSpecs = this._attributeSpecs[model]; 1271 | var hk, rk; 1272 | var pKey = this._models[model].pKey; 1273 | var pkSeparator = this._models[model].pkSeparator; 1274 | if (pKey !== undefined) { 1275 | var temp = pk.split(pkSeparator); 1276 | hk = temp[0]; 1277 | rk = temp[1]; 1278 | if (this._attributeSpecs[model][rangeKey] === "number") { 1279 | rk = parseInt(rk); 1280 | } else if (this._attributeSpecs[model][rangeKey] === "date") { 1281 | rk = Number(rk); 1282 | } 1283 | } else { 1284 | hk = pk; 1285 | } 1286 | 1287 | // If hashKey is of type Number use parseInt 1288 | if (this._attributeSpecs[model][hashKey] === "number") { 1289 | hk = parseInt(hk); 1290 | } else if (this._attributeSpecs[model][hashKey] === "date") { 1291 | hk = Number(hk); 1292 | } 1293 | 1294 | var tableParams = {}; 1295 | tableParams.Key = {}; 1296 | tableParams.Key[hashKey] = hk; 1297 | if (pKey !== undefined) { 1298 | tableParams.Key[rangeKey] = rk; 1299 | } 1300 | 1301 | tableParams.TableName = this.tables(model); 1302 | 1303 | tableParams.ReturnConsumedCapacity = "TOTAL"; 1304 | 1305 | if (tableParams.Key) { 1306 | this.docClient.get(tableParams, function (err, res) { 1307 | if (err || !res) { 1308 | callback(err, null); 1309 | } else if (isEmpty(res)) { 1310 | callback(null, null); 1311 | } else { 1312 | var finalResult = []; 1313 | var pKey = this._models[model].pKey; 1314 | var pkSeparator = this._models[model].pkSeparator; 1315 | // Single object - > Array 1316 | callback(null, res.Item); 1317 | logger.log("info", queryString.blue, stopTimer(timeStart).bold.cyan); 1318 | } 1319 | }.bind(this)); 1320 | } 1321 | }; 1322 | 1323 | /** 1324 | * Save an object to the database 1325 | * @param {[type]} model [description] 1326 | * @param {[type]} data [description] 1327 | * @param {Function} callback [description] 1328 | * @return {[type]} [description] 1329 | */ 1330 | DynamoDB.prototype.save = function save(model, data, callback) { 1331 | var timeStart = startTimer(); 1332 | var originalData = {}; 1333 | var hashKey = this._models[model].hashKey; 1334 | var rangeKey = this._models[model].rangeKey; 1335 | var pkSeparator = this._models[model].pkSeparator; 1336 | var pKey = this._models[model].pKey; 1337 | 1338 | /* Data is the original object coming in the body. In the body 1339 | if the data has a key which is breakable, it must be chunked 1340 | into N different attrs. N is specified by the breakValue[key] 1341 | */ 1342 | var attributeSpecs = this._attributeSpecs[model]; 1343 | var outerCounter = 0; 1344 | 1345 | /* 1346 | Checks for hash and range keys 1347 | */ 1348 | if ((data[hashKey] === null) || (data[hashKey] === undefined)) { 1349 | var err = new Error("Hash Key `" + hashKey + "` cannot be null or undefined."); 1350 | callback(err, null); 1351 | return; 1352 | } 1353 | // If pKey is defined, range key is also present. 1354 | if (pKey !== undefined) { 1355 | if ((data[rangeKey] === null) || (data[rangeKey] === undefined)) { 1356 | var err = new Error("Range Key `" + rangeKey + "` cannot be null or undefined."); 1357 | callback(err, null); 1358 | return; 1359 | } else { 1360 | data[pKey] = String(data[hashKey]) + pkSeparator + String(data[rangeKey]); 1361 | originalData[pKey] = data[pKey]; 1362 | } 1363 | } 1364 | 1365 | // Copy all attrs from data to originalData 1366 | for (var key in data) { 1367 | originalData[key] = data[key]; 1368 | } 1369 | 1370 | var queryString = "PUT ITEM IN TABLE "; 1371 | var tableParams = {}; 1372 | tableParams.TableName = this.tables(model); 1373 | tableParams.ReturnConsumedCapacity = "TOTAL"; 1374 | 1375 | if (pKey !== undefined) { 1376 | delete data[pKey]; 1377 | } 1378 | tableParams.Item = data; 1379 | this.docClient.put(tableParams, function (err, res) { 1380 | if (err) { 1381 | callback(err, null); 1382 | } else { 1383 | callback(null, originalData); 1384 | } 1385 | }.bind(this)); 1386 | 1387 | logger.log("info", queryString.blue, stopTimer(timeStart).bold.cyan); 1388 | }; 1389 | 1390 | // function not working 1391 | DynamoDB.prototype.updateAttributes = function (model, pk, data, callback) { 1392 | var timeStart = startTimer(); 1393 | var originalData = {}; 1394 | var hashKey = this._models[model].hashKey; 1395 | var rangeKey = this._models[model].rangeKey; 1396 | var pkSeparator = this._models[model].pkSeparator; 1397 | var pKey = this._models[model].pKey; 1398 | var hk, rk, err, key; 1399 | var tableParams = {}; 1400 | var attributeSpecs = this._attributeSpecs[model]; 1401 | var outerCounter = 0; 1402 | // Copy all attrs from data to originalData 1403 | for (key in data) { 1404 | originalData[key] = data[key]; 1405 | } 1406 | 1407 | // If pKey is defined, range key is also present. 1408 | if (pKey !== undefined) { 1409 | if ((data[rangeKey] === null) || (data[rangeKey] === undefined)) { 1410 | err = new Error("Range Key `" + rangeKey + "` cannot be null or undefined."); 1411 | callback(err, null); 1412 | return; 1413 | } else { 1414 | data[pKey] = String(data[hashKey]) + pkSeparator + String(data[rangeKey]); 1415 | originalData[pKey] = data[pKey]; 1416 | } 1417 | } 1418 | 1419 | // Log queryString 1420 | var queryString = "UPDATE ITEM IN TABLE "; 1421 | 1422 | // Use updateItem function of DynamoDB 1423 | 1424 | // Set table name as usual 1425 | tableParams.TableName = this.tables(model); 1426 | tableParams.Key = {}; 1427 | tableParams.AttributeUpdates = {}; 1428 | tableParams.ReturnConsumedCapacity = "TOTAL"; 1429 | 1430 | // Add hashKey / rangeKey to tableParams 1431 | if (pKey !== undefined) { 1432 | var temp = pk.split(pkSeparator); 1433 | hk = temp[0]; 1434 | rk = temp[1]; 1435 | tableParams.Key[this._models[model].hashKey] = hk; 1436 | tableParams.Key[this._models[model].rangeKey] = rk; 1437 | } else { 1438 | tableParams.Key[this._models[model].hashKey] = pk; 1439 | hk = pk; 1440 | } 1441 | 1442 | if (pKey !== undefined) { 1443 | delete data[pKey]; 1444 | } 1445 | // Add attrs to update 1446 | 1447 | for (var key in data) { 1448 | /*if (data[key] instanceof Date) { 1449 | data[key] = Number(data[key]); 1450 | }*/ 1451 | if (data.hasOwnProperty(key) && data[key] !== null && (key !== hashKey) && (key !== rangeKey)) { 1452 | tableParams.AttributeUpdates[key] = {}; 1453 | tableParams.AttributeUpdates[key].Action = 'PUT'; 1454 | tableParams.AttributeUpdates[key].Value = data[key]; 1455 | 1456 | } 1457 | } 1458 | tableParams.ReturnValues = "ALL_NEW"; 1459 | this.docClient.update(tableParams, function (err, res) { 1460 | if (err) { 1461 | callback(err, null); 1462 | } else if (!res) { 1463 | callback(null, null); 1464 | } else { 1465 | callback(null, res.data); 1466 | } 1467 | }.bind(this)); 1468 | logger.log("info", queryString.blue, stopTimer(timeStart).bold.cyan); 1469 | 1470 | }; 1471 | 1472 | DynamoDB.prototype.destroy = function (model, pk, callback) { 1473 | var timeStart = startTimer(); 1474 | var hashKey = this._models[model].hashKey; 1475 | var rangeKey = this._models[model].rangeKey; 1476 | var hk, rk; 1477 | var pKey = this._models[model].pKey; 1478 | var pkSeparator = this._models[model].pkSeparator; 1479 | 1480 | if (pKey !== undefined) { 1481 | var temp = pk.split(pkSeparator); 1482 | hk = temp[0]; 1483 | rk = temp[1]; 1484 | if (this._attributeSpecs[model][rangeKey] === "number") { 1485 | rk = parseInt(rk); 1486 | } else if (this._attributeSpecs[model][rangeKey] === "date") { 1487 | rk = Number(rk); 1488 | } 1489 | } else { 1490 | hk = pk; 1491 | } 1492 | 1493 | // If hashKey is of type Number use parseInt 1494 | if (this._attributeSpecs[model][hashKey] === "number") { 1495 | hk = parseInt(hk); 1496 | } else if (this._attributeSpecs[model][hashKey] === "date") { 1497 | hk = Number(hk); 1498 | } 1499 | 1500 | // Use updateItem function of DynamoDB 1501 | var tableParams = {}; 1502 | // Set table name as usual 1503 | tableParams.TableName = this.tables(model); 1504 | tableParams.Key = {}; 1505 | // Add hashKey to tableParams 1506 | tableParams.Key[this._models[model].hashKey] = hk; 1507 | 1508 | if (pKey !== undefined) { 1509 | tableParams.Key[this._models[model].rangeKey] = rk; 1510 | } 1511 | 1512 | tableParams.ReturnValues = "ALL_OLD"; 1513 | var attributeSpecs = this._attributeSpecs[model]; 1514 | var outerCounter = 0; 1515 | var chunkedData = {}; 1516 | 1517 | this.docClient.delete(tableParams, function (err, res) { 1518 | if (err) { 1519 | callback(err, null); 1520 | } else if (!res) { 1521 | callback(null, null); 1522 | } else { 1523 | // Attributes is an object 1524 | var tempString = "DELETE ITEM FROM TABLE " + tableParams.TableName + " WHERE " + hashKey + " `EQ` " + String(hk); 1525 | logger.log("info", tempString.blue, stopTimer(timeStart).bold.cyan); 1526 | callback(null, res.Attributes); 1527 | } 1528 | }.bind(this)); 1529 | 1530 | }; 1531 | 1532 | DynamoDB.prototype.defineForeignKey = function (model, key, cb) { 1533 | var hashKey = this._models[model].hashKey; 1534 | var attributeSpec = this._attributeSpecs[model].id || this._attributeSpecs[model][hashKey]; 1535 | if (attributeSpec === "string") { 1536 | cb(null, String); 1537 | } else if (attributeSpec === "number") { 1538 | cb(null, Number); 1539 | } else if (attributeSpec === "date") { 1540 | cb(null, Date); 1541 | } 1542 | }; 1543 | 1544 | /** 1545 | * Destroy all deletes all records from table. 1546 | * @param {[type]} model [description] 1547 | * @param {Function} callback [description] 1548 | */ 1549 | DynamoDB.prototype.destroyAll = function (model, callback) { 1550 | /* 1551 | Note: 1552 | Deleting individual items is extremely expensive. According to 1553 | AWS, a better solution is to destroy the table, and create it back again. 1554 | */ 1555 | var timeStart = startTimer(); 1556 | var t = "DELETE EVERYTHING IN TABLE: " + this.tables(model); 1557 | var hashKey = this._models[model].hashKey; 1558 | var rangeKey = this._models[model].rangeKey; 1559 | var pkSeparator = this._models[model].pkSeparator; 1560 | var attributeSpecs = this._attributeSpecs[model]; 1561 | var hk, rk, pk; 1562 | var docClient = this.docClient; 1563 | 1564 | var self = this; 1565 | var tableParams = {}; 1566 | tableParams.TableName = this.tables(model); 1567 | docClient.scan(tableParams, function (err, res) { 1568 | if (err) { 1569 | callback(err); 1570 | return; 1571 | } else if (res === null) { 1572 | callback(null); 1573 | return; 1574 | } else { 1575 | async.mapSeries(res.Items, function (item, insideCallback) { 1576 | 1577 | if (rangeKey === undefined) { 1578 | hk = item[hashKey]; 1579 | pk = hk; 1580 | } else { 1581 | hk = item[hashKey]; 1582 | rk = item[rangeKey]; 1583 | pk = String(hk) + pkSeparator + String(rk); 1584 | } 1585 | self.destroy(model, pk, insideCallback); 1586 | }, function (err, items) { 1587 | if (err) { 1588 | callback(err); 1589 | } else { 1590 | callback(); 1591 | } 1592 | 1593 | }.bind(this)); 1594 | } 1595 | 1596 | }); 1597 | logger.log("warn", t.bold.red, stopTimer(timeStart).bold.cyan); 1598 | }; 1599 | 1600 | /** 1601 | * Get number of records matching a filter 1602 | * @param {Object} model 1603 | * @param {Function} callback 1604 | * @param {Object} where : Filter 1605 | * @return {Number} : Number of matching records 1606 | */ 1607 | DynamoDB.prototype.count = function count(model, callback, where) { 1608 | var filter = {}; 1609 | filter.where = where; 1610 | this.all(model, filter, function (err, results) { 1611 | if (err || !results) { 1612 | callback(err, null); 1613 | } else { 1614 | callback(null, results.length); 1615 | } 1616 | }); 1617 | }; 1618 | 1619 | /** 1620 | * Check if a given record exists 1621 | * @param {[type]} model [description] 1622 | * @param {[type]} id [description] 1623 | * @param {Function} callback [description] 1624 | * @return {[type]} [description] 1625 | */ 1626 | DynamoDB.prototype.exists = function exists(model, id, callback) { 1627 | this.find(model, id, function (err, record) { 1628 | if (err) { 1629 | callback(err, null); 1630 | } else if (isEmpty(record)) { 1631 | callback(null, false); 1632 | } else { 1633 | callback(null, true); 1634 | } 1635 | }); 1636 | }; -------------------------------------------------------------------------------- /lib/helper.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var colors = require('colors'); 3 | module.exports = { 4 | TypeLookup: function TypeLookup(typestring) { 5 | switch (typestring) { 6 | case "string": 7 | return 'S'; 8 | break; 9 | case "number": 10 | return 'N'; 11 | break; 12 | case "boolean": 13 | return 'S'; 14 | break; 15 | case "date": 16 | return 'N'; 17 | break; 18 | default: 19 | break; 20 | } 21 | }, 22 | 23 | ReverseTypeLookup: function ReverseTypeLookup(typestring) { 24 | switch (typestring) { 25 | case "date": 26 | return 'N'; 27 | break; 28 | default: 29 | break; 30 | } 31 | if (typestring === 'S') { 32 | return "string"; 33 | } else if (typestring === 'N') { 34 | return "number"; 35 | } else { 36 | return "string"; 37 | } 38 | }, 39 | /** 40 | * Helper function to convert a regular model 41 | * object to DynamoDB JSON notation. 42 | * 43 | * e.g 20 will be returned as { 'N': '20' } 44 | * & `foobar` will be returned as { 'S' : 'foobar' } 45 | * 46 | * Usage 47 | * - objToDB(20); 48 | * - objToDB("foobar"); 49 | * ---------------------------------------------- 50 | * 51 | * @param {object} data to be converted 52 | * @return {object} DynamoDB compatible JSON object 53 | */ 54 | objToDB: function objToDB(data) { 55 | var tempObj = {}; 56 | var elementType = this.TypeLookup(typeof (data)); 57 | tempObj[elementType] = data.toString(); 58 | return tempObj; 59 | }, 60 | /** 61 | * Helper function to convert a DynamoDB type 62 | * object into regular model object. 63 | * 64 | * e.g { 'N': '20' } will be returned as 20 65 | * & { 'S' : 'foobar' } will be returned as `foobar` 66 | * 67 | * @param {object} data 68 | * @return {object} 69 | */ 70 | objFromDB: function objFromDB(data) { 71 | var tempObj; 72 | for (var key in data) { 73 | if (data.hasOwnProperty(key)) { 74 | var elementType = this.ReverseTypeLookup(key); 75 | if (elementType === "string") { 76 | tempObj = data[key]; 77 | } else if (elementType === "number") { 78 | tempObj = Number(data[key]); 79 | } else { 80 | tempObj = data[key]; 81 | } 82 | } 83 | } 84 | return tempObj; 85 | }, 86 | /** 87 | * Slice a string into N different strings 88 | * @param {String} str : The string to be chunked 89 | * @param {Number} N : Number of pieces into which the string must be broken 90 | * @return {Array} Array of N strings 91 | */ 92 | splitSlice: function splitSlice(str, N) { 93 | var ret = []; 94 | var strLen = str.length; 95 | if (strLen === 0) { 96 | return ret; 97 | } else { 98 | var len = Math.floor(strLen / N) + 1; 99 | var residue = strLen % len; 100 | var offset = 0; 101 | for (var index = 1; index < N; index++) { 102 | var subString = str.slice(offset, len + offset); 103 | ret.push(subString); 104 | offset = offset + len; 105 | } 106 | ret.push(str.slice(offset, residue + offset)); 107 | return ret; 108 | } 109 | }, 110 | /** 111 | * Chunks data and assigns it to the data object 112 | * @param {Object} data : Complete data object 113 | * @param {String} key : Attribute to be chunked 114 | * @param {Number} N : Number of chunks 115 | */ 116 | ChunkMe: function ChunkMe(data, key, N) { 117 | var counter; 118 | var newData = []; 119 | //Call splitSlice to chunk the data 120 | var chunkedData = this.splitSlice(data[key], N); 121 | //Assign each element in the chunked data 122 | //to data. 123 | for (counter = 1; counter <= N; counter++) { 124 | var tempObj = {}; 125 | var chunkKeyName = key; 126 | // DynamoDB does not allow empty strings. 127 | // So filter out empty strings 128 | if (chunkedData[counter - 1] !== "") { 129 | tempObj[chunkKeyName] = chunkedData[counter - 1]; 130 | newData.push(tempObj); 131 | } 132 | } 133 | delete data[key]; 134 | // Finally delete data[key] 135 | return newData; 136 | }, 137 | /** 138 | * Builds back a chunked object stored in the 139 | * database to its normal form 140 | * @param {Object} data : Object to be rebuilt 141 | * @param {String} key : Name of the field in the object 142 | */ 143 | BuildMeBack: function BuildMeBack(data, breakKeys) { 144 | var counter; 145 | var currentName; 146 | var finalObject; 147 | breakKeys.forEach(function (breakKey) { 148 | counter = 1; 149 | finalObject = ""; 150 | for (var key in data) { 151 | currentName = breakKey + "-" + String(counter); 152 | if (data[currentName]) { 153 | finalObject = finalObject + data[currentName]; 154 | delete data[currentName]; 155 | counter++; 156 | } 157 | } 158 | data[breakKey] = finalObject; 159 | }); 160 | return data; 161 | }, 162 | /* 163 | See http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript 164 | */ 165 | UUID: function UUID() { 166 | var uuid ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 167 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 168 | return v.toString(16); 169 | }); 170 | return uuid; 171 | }, 172 | 173 | GetMyChildrenBack: function GetMyChildrenBack(data, model, pKey, breakables, dynamodb, OuterCallback) { 174 | // Iterate over breakables. Query using data's hashKey 175 | var hashKeyAttribute = model.toLowerCase() + "#" + pKey; 176 | /* 177 | Use async series to fetch each breakable attribute in series. 178 | */ 179 | async.mapSeries(breakables, function (breakable, callback) { 180 | var params = {}; 181 | params.KeyConditions = {}; 182 | params.KeyConditions[hashKeyAttribute] = {}; 183 | params.KeyConditions[hashKeyAttribute].ComparisonOperator = 'EQ'; 184 | params.KeyConditions[hashKeyAttribute].AttributeValueList = []; 185 | params.KeyConditions[hashKeyAttribute].AttributeValueList.push({ 186 | 'S': String(data[pKey]) 187 | }); 188 | params.TableName = model + "_" + breakable; 189 | dynamodb.query(params, function (err, res){ 190 | if (err) { 191 | return callback(err,null); 192 | } else { 193 | var callbackData = ""; 194 | res.Items.forEach(function (item) { 195 | callbackData = callbackData + item[breakable]['S']; 196 | }); 197 | callback(null,callbackData); 198 | } 199 | }.bind(this)); 200 | }, function (err, results) { 201 | if (err) { 202 | OuterCallback(err, null); 203 | } else { 204 | // results array will contain an array of built back attribute values. 205 | for (i = 0; i < results.length; i++) { 206 | data[breakables[i]] = results[i]; 207 | } 208 | OuterCallback(null, data); 209 | } 210 | }.bind(this)); 211 | }, 212 | SortByKey: function SortByKey(array, key, order) { 213 | return array.sort(function(a, b) { 214 | var x = a[key]; 215 | var y = b[key]; 216 | 217 | if (typeof x == "string") 218 | { 219 | x = x.toLowerCase(); 220 | y = y.toLowerCase(); 221 | } 222 | if (order === 1) { 223 | return ((x < y) ? -1 : ((x > y) ? 1 : 0)); 224 | } else { 225 | return ((x < y) ? 1 : ((x > y) ? -1 : 0)); 226 | } 227 | 228 | }); 229 | } 230 | }; 231 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-connector-dynamodb", 3 | "description": "DynamoDB connector for LoopBack", 4 | "version": "0.1.0-5", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test", 8 | "coverage": "istanbul cover ./node_modules/mocha/bin/_mocha -- --ui bdd -R spec -t 5000" 9 | }, 10 | "dependencies": { 11 | "aws-sdk": "latest", 12 | "colors": "latest", 13 | "async": "latest", 14 | "winston": "~0.7.3" 15 | }, 16 | "devDependencies": { 17 | "jugglingdb": ">= 0.1.0", 18 | "should": "latest", 19 | "mocha": "latest" 20 | }, 21 | "repository": "https://github.com/mandarzope/loopback-connector-dynamodb.git", 22 | "author": { 23 | "name": "Mandar Zope", 24 | "email": "mandaranilzope@gmail.com" 25 | }, 26 | "license": "MIT" 27 | } 28 | -------------------------------------------------------------------------------- /test/basic-querying.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | var should = require('./init.js'); 3 | var db, User; 4 | 5 | describe('basic-querying', function() { 6 | 7 | before(function(done) { 8 | db = getSchema(); 9 | 10 | User = db.define('User', { 11 | name: {type: String, sort: true, limit: 100}, 12 | email: {type: String, index: true, limit: 100}, 13 | role: {type: String, index: true, limit: 100}, 14 | order: {type: Number, index: true, sort: true, limit: 100}, 15 | tasks: { type: String, sharding : true, splitter : "10kb" } 16 | }); 17 | 18 | db.adapter.emitter.on("created-user", function(){ 19 | User.destroyAll(done); 20 | }); 21 | 22 | }); 23 | 24 | 25 | describe('find', function() { 26 | 27 | before(function(done) { 28 | done(); 29 | }); 30 | 31 | it('should query by id: not found', function(done) { 32 | User.find("1", function(err, u) { 33 | should.not.exist(u); 34 | should.not.exist(err); 35 | done(); 36 | }); 37 | }); 38 | 39 | it('should query by id: found', function(done) { 40 | User.create(function(err, u) { 41 | should.not.exist(err); 42 | should.exist(u.id); 43 | User.find(u.id, function(err, u) { 44 | should.exist(u); 45 | should.not.exist(err); 46 | u.should.be.an.instanceOf(User); 47 | u.destroy(function(err) { 48 | done(); 49 | }); 50 | 51 | }); 52 | }); 53 | }); 54 | 55 | }); 56 | 57 | describe('all', function() { 58 | 59 | before(seed); 60 | 61 | it('should query collection', function(done) { 62 | User.all(function(err, users) { 63 | should.exists(users); 64 | should.not.exists(err); 65 | users.should.have.lengthOf(6); 66 | done(); 67 | }); 68 | }); 69 | 70 | it('should query limited collection', function(done) { 71 | User.all({limit: 3}, function(err, users) { 72 | should.exists(users); 73 | should.not.exists(err); 74 | users.should.have.lengthOf(3); 75 | done(); 76 | }); 77 | }); 78 | 79 | it('should query offset collection with limit', function(done) { 80 | User.all({skip: 1, limit: 4}, function(err, users) { 81 | should.exists(users); 82 | should.not.exists(err); 83 | users.should.have.lengthOf(4); 84 | done(); 85 | }); 86 | }); 87 | 88 | it('should query filtered collection', function(done) { 89 | User.all({where: {role: 'lead'}}, function(err, users) { 90 | should.exists(users); 91 | should.not.exists(err); 92 | users.should.have.lengthOf(2); 93 | done(); 94 | }); 95 | }); 96 | 97 | it('should query collection sorted by numeric field', function(done) { 98 | User.all({order: 'order'}, function(err, users) { 99 | should.exists(users); 100 | should.not.exists(err); 101 | users.forEach(function(u, i) { 102 | u.order.should.eql(i + 1); 103 | }); 104 | done(); 105 | }); 106 | }); 107 | 108 | it('should query collection desc sorted by numeric field', function(done) { 109 | User.all({order: 'order DESC'}, function(err, users) { 110 | should.exists(users); 111 | should.not.exists(err); 112 | users.forEach(function(u, i) { 113 | u.order.should.eql(users.length - i); 114 | }); 115 | done(); 116 | }); 117 | }); 118 | 119 | it('should query collection sorted by string field', function(done) { 120 | User.all({order: 'name'}, function(err, users) { 121 | should.exists(users); 122 | should.not.exists(err); 123 | users.shift().name.should.equal('George Harrison'); 124 | users.shift().name.should.equal('John Lennon'); 125 | users.pop().name.should.equal('Stuart Sutcliffe'); 126 | done(); 127 | }); 128 | }); 129 | 130 | it('should query collection desc sorted by string field', function(done) { 131 | User.all({order: 'name DESC'}, function(err, users) { 132 | should.exists(users); 133 | should.not.exists(err); 134 | users.pop().name.should.equal('George Harrison'); 135 | users.pop().name.should.equal('John Lennon'); 136 | users.shift().name.should.equal('Stuart Sutcliffe'); 137 | done(); 138 | }); 139 | }); 140 | 141 | }); 142 | 143 | describe('count', function() { 144 | 145 | before(seed); 146 | 147 | it('should query total count', function(done) { 148 | User.count(function(err, n) { 149 | should.not.exist(err); 150 | should.exist(n); 151 | n.should.equal(6); 152 | done(); 153 | }); 154 | }); 155 | 156 | it('should query filtered count', function(done) { 157 | User.count({role: 'lead'}, function(err, n) { 158 | should.not.exist(err); 159 | should.exist(n); 160 | n.should.equal(2); 161 | done(); 162 | }); 163 | }); 164 | }); 165 | 166 | describe('findOne', function() { 167 | 168 | before(seed); 169 | 170 | it('should work even when find by id', function(done) { 171 | User.findOne(function(e, u) { 172 | User.findOne({where: {id: u.id}}, function(err, user) { 173 | should.not.exist(err); 174 | should.exist(user); 175 | done(); 176 | }); 177 | }); 178 | }); 179 | 180 | }); 181 | }); 182 | 183 | 184 | 185 | describe('exists', function() { 186 | 187 | before(seed); 188 | 189 | it('should check whether record exist', function(done) { 190 | User.findOne(function(e, u) { 191 | User.exists(u.id, function(err, exists) { 192 | should.not.exist(err); 193 | should.exist(exists); 194 | exists.should.be.ok; 195 | done(); 196 | }); 197 | }); 198 | }); 199 | 200 | it('should check whether record not exist', function(done) { 201 | User.destroyAll(function() { 202 | User.exists("asdasd", function(err, exists) { 203 | should.not.exist(err); 204 | exists.should.not.be.ok; 205 | done(); 206 | }); 207 | }); 208 | }); 209 | 210 | }); 211 | 212 | function seed(done) { 213 | var count = 0; 214 | var beatles = [ 215 | { 216 | name: 'John Lennon', 217 | mail: 'john@b3atl3s.co.uk', 218 | role: 'lead', 219 | order: 2, 220 | tasks: 'Sing me a song' 221 | }, { 222 | name: 'Paul McCartney', 223 | mail: 'paul@b3atl3s.co.uk', 224 | role: 'lead', 225 | order: 1, 226 | tasks: 'Play me a tune' 227 | }, 228 | {name: 'George Harrison', order: 5}, 229 | {name: 'Ringo Starr', order: 6}, 230 | {name: 'Pete Best', order: 4}, 231 | {name: 'Stuart Sutcliffe', order: 3} 232 | ]; 233 | 234 | User.destroyAll(function() { 235 | beatles.forEach(function(beatle) { 236 | User.create(beatle, ok); 237 | }); 238 | }); 239 | 240 | 241 | function ok() { 242 | if (++count === beatles.length) { 243 | done(); 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /test/datatype.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | var should = require('./init.js'); 3 | 4 | var db, Model; 5 | 6 | describe('datatypes', function() { 7 | 8 | before(function(done){ 9 | db = getSchema(); 10 | Model = db.define('Model', { 11 | str: String, 12 | date: Date, 13 | num: Number, 14 | bool: Boolean 15 | }); 16 | db.adapter.emitter.on("created-model", function() { 17 | Model.destroyAll(done); 18 | }); 19 | }); 20 | 21 | it('should keep types when get read data from db', function(done) { 22 | var d = new Date, id; 23 | 24 | Model.create({ 25 | str: 'hello', date: d, num: '3', bool: 1 26 | }, function(err, m) { 27 | should.not.exist(err); 28 | should.exist(m && m.id); 29 | m.str.should.be.a.String; 30 | m.num.should.be.a.Number; 31 | m.bool.should.be.a.Boolean; 32 | id = m.id; 33 | testFind(testAll); 34 | }); 35 | 36 | function testFind(next) { 37 | Model.find(id, function(err, m) { 38 | should.not.exist(err); 39 | should.exist(m); 40 | m.str.should.be.a.String; 41 | m.num.should.be.a.Number; 42 | m.bool.should.be.a.Boolean; 43 | m.date.should.be.an.instanceOf(Date); 44 | m.date.toString().should.equal(d.toString(), 'Time must match'); 45 | next(); 46 | }); 47 | } 48 | 49 | function testAll() { 50 | Model.findOne(function(err, m) { 51 | should.not.exist(err); 52 | should.exist(m); 53 | m.str.should.be.a.String; 54 | m.num.should.be.a.Number; 55 | m.bool.should.be.a.Boolean; 56 | m.date.should.be.an.instanceOf(Date); 57 | m.date.toString().should.equal(d.toString(), 'Time must match'); 58 | done(); 59 | }); 60 | } 61 | 62 | }); 63 | 64 | it('should convert "false" to false for boolean', function() { 65 | var m = new Model({bool: 'false'}); 66 | m.bool.should.equal(false); 67 | }); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /test/dynamo.test.js: -------------------------------------------------------------------------------- 1 | var should = require('./init.js'); 2 | 3 | var db, User, Book, Cookie, Car; 4 | 5 | describe('dynamodb', function(){ 6 | 7 | before(function(done) { 8 | db = getSchema(); 9 | User = db.define('User', { 10 | id: { type: String, keyType: "hash"}, 11 | name: { type: String }, 12 | email: { type: String }, 13 | age: {type: Number}, 14 | tasks: { type: String, sharding : true, splitter : "10kb"} 15 | }); 16 | 17 | Book = db.define('Book', { 18 | id : { type: String, keyType: "pk", separator: "--oo--"}, 19 | ida : { type: String, keyType: "hash"}, 20 | subject : { type: String, keyType: "range"}, 21 | essay : { type: String, sharding : true } 22 | }, { 23 | table: "book_test" 24 | }); 25 | 26 | Cookie = db.define('Cookie', { 27 | color: {type: String}, 28 | recipe: {type: String, sharding : true } 29 | }); 30 | 31 | Car = db.define('Car', { 32 | doors: {type: Number}, 33 | licensePlate: { type: String, keyType: "hash"} 34 | }); 35 | 36 | var modelCount = 0; 37 | db.adapter.emitter.on("created", function(){ 38 | modelCount++; 39 | // Tables for both models created in database. 40 | if (modelCount === 4) { 41 | Book.destroyAll(function(){ 42 | Car.destroyAll(function(){ 43 | Cookie.destroyAll(function(){ 44 | User.destroyAll(done); 45 | }); 46 | }); 47 | }); 48 | } 49 | }); 50 | }); 51 | 52 | beforeEach(function(done){ 53 | Book.destroyAll(function(){ 54 | Car.destroyAll(function(){ 55 | Cookie.destroyAll(function(){ 56 | User.destroyAll(done); 57 | }); 58 | }); 59 | }); 60 | }); 61 | 62 | describe('sharding', function() { 63 | 64 | // It should create table on model definition 65 | it("should create sharded table for User", function() { 66 | db.adapter.client.listTables(function (err, data){ 67 | var existingTableNames = data.TableNames; 68 | var tableExists = false; 69 | // Table user_test and book_test are present. Check for sharded table 70 | existingTableNames.forEach(function (existingTableName) { 71 | if (existingTableName === 'User_tasks') { 72 | tableExists = true; 73 | } 74 | }); 75 | tableExists.should.be.true; 76 | }); 77 | }); 78 | 79 | it("should have sharded table with hash key and range key", function(done){ 80 | db.adapter.client.describeTable({TableName: "User_tasks"}, function (err, data){ 81 | data.Table.AttributeDefinitions[0].AttributeName.should.eql("user#id"); 82 | data.Table.AttributeDefinitions[1].AttributeName.should.eql("tasks#ID"); 83 | done(); 84 | }); 85 | }); 86 | 87 | it("should not create sharded table if sharding property is not set", function(done){ 88 | db.adapter.client.describeTable({ TableName: "Book_subject"}, function(err, data){ 89 | (data === null).should.be.true; 90 | done(); 91 | }); 92 | }); 93 | 94 | it("should split by the size specified during sharding", function(done){ 95 | db.adapter._models['User'].splitSizes[0].should.eql(10); 96 | done(); 97 | }); 98 | 99 | it("should split by default size of 63 kb if splitter is not specified", function(done){ 100 | db.adapter._models['Cookie'].splitSizes[0].should.eql(63); 101 | done(); 102 | }); 103 | 104 | it('should write data to sharded table on save', function(done){ 105 | var tempUser = new User({ 106 | id: "1", 107 | name: "John Doe", 108 | email: "john@doe.com", 109 | age: 20, 110 | tasks: "Blah blah blah" 111 | }); 112 | User.create(tempUser, function (err, user) { 113 | should.not.exist(err); 114 | user.tasks = "Plim Plum Pooh Popo Dara Dum Dee Dum"; 115 | user.save(function(err, savedUser){ 116 | should.not.exist(err); 117 | User.find("1", function(err, fetchedUser){ 118 | fetchedUser.should.have.property('tasks', 'Plim Plum Pooh Popo Dara Dum Dee Dum'); 119 | done(); 120 | }); 121 | }); 122 | }); 123 | }); 124 | 125 | it('should handle empty values for breakable attribute', function(done){ 126 | var tempUser = new User({ 127 | id: "2", 128 | name: "John Doe", 129 | email: "john@doe.com", 130 | age: 20, 131 | tasks: "" 132 | }); 133 | User.create(tempUser, function (err, user) { 134 | should.not.exist(err); 135 | (user.tasks === "").should.be.true; 136 | done(); 137 | }); 138 | }); 139 | 140 | it('should handle null value for breakable attribute', function(done){ 141 | var tempUser = new User({ 142 | id: "2", 143 | name: "John Doe", 144 | email: "john@doe.com", 145 | age: 20, 146 | tasks: null 147 | }); 148 | User.create(tempUser, function (err, user) { 149 | should.not.exist(err); 150 | (user.tasks === null).should.be.true; 151 | done(); 152 | }); 153 | }); 154 | 155 | it('should handle undefined value for breakable attribute', function(done){ 156 | var tempUser = new User({ 157 | id: "2", 158 | name: "John Doe", 159 | email: "john@doe.com", 160 | age: 20 161 | }); 162 | User.create(tempUser, function (err, user) { 163 | should.not.exist(err); 164 | (user.tasks === undefined).should.be.true; 165 | done(); 166 | }); 167 | }); 168 | 169 | it('should write data to sharded table on updateAttributes', function(done){ 170 | var tempUser = new User({ 171 | id: "2", 172 | name: "John Doe", 173 | email: "john@doe.com", 174 | age: 20, 175 | tasks: "Blah blah blah" 176 | }); 177 | User.create(tempUser, function (err, user) { 178 | user.updateAttributes({tasks: "Plim Plum Pooh Popo Dara Dum Dee Dum"}, function(err){ 179 | should.not.exist(err); 180 | User.find("2", function(err, fetchedUser){ 181 | fetchedUser.should.have.property('tasks', 'Plim Plum Pooh Popo Dara Dum Dee Dum'); 182 | done(); 183 | }); 184 | }); 185 | }); 186 | }); 187 | 188 | it('should destroy sharded table data on destruction of parent table data', function(done){ 189 | var tempUser = new User({ 190 | id: "2", 191 | name: "John Doe", 192 | email: "john@doe.com", 193 | age: 20, 194 | tasks: "Blah blah blah" 195 | }); 196 | User.create(tempUser, function (err, user) { 197 | user.destroy(function(err){ 198 | db.adapter.client.scan({ TableName: "User_tasks"}, function(err, data){ 199 | (data.Items).should.have.lengthOf(0); 200 | done(); 201 | }); 202 | }); 203 | }); 204 | }); 205 | }); 206 | 207 | 208 | /* 209 | ONLY HASH KEYS 210 | */ 211 | 212 | describe('if model only has a hash key', function() { 213 | 214 | it('should assign a hash key if not specified', function(done){ 215 | Cookie.create({ color: "brown", recipe: "Bake it nice n soft" }, function(err, cookie){ 216 | should.not.exist(err); 217 | cookie.should.have.property('id'); 218 | db.adapter._models['Cookie'].hashKey.should.eql('id'); 219 | db.adapter._models['Cookie'].hashKeyUUID.should.be.true; 220 | done(); 221 | }); 222 | }); 223 | 224 | it('should throw error if uuid is true and attribute name is not id', function(done){ 225 | (function() { 226 | db.define('Model', { 227 | attribute1 : { type: String, keyType: "hash", uuid: true}, 228 | }); 229 | }).should.throw(); 230 | done(); 231 | }); 232 | 233 | it('should should fetch based on hash key', function(done){ 234 | User.find("1", function(err, user){ 235 | should.not.exist(err); 236 | should.exist.user; 237 | done(); 238 | }); 239 | }); 240 | 241 | it('should assign same value as hash key to id attribute', function(done){ 242 | Car.create({licensePlate: "XXYY-112", doors: 4}, function(err, car){ 243 | should.not.exist(err); 244 | should.exist(car); 245 | car.should.have.property('id','XXYY-112'); 246 | done(); 247 | }); 248 | }); 249 | 250 | it('should create user with given hash key', function (done) { 251 | var tempUser = new User({ 252 | id: "1", 253 | name: "John Doe", 254 | email: "john@doe.com", 255 | age: 20, 256 | tasks: "Blah blah blah" 257 | }); 258 | User.create(tempUser, function (err, user) { 259 | should.not.exist(err); 260 | user.should.have.property('id'); 261 | user.should.have.property('name', 'John Doe'); 262 | user.should.have.property('tasks'); 263 | done(); 264 | }); 265 | }); 266 | 267 | it('should replace original record if same hash key is provided', function(done){ 268 | var tempUser = new User({ 269 | id: "1", 270 | name: "Johnny Doey", 271 | email: "johnny@doey.com", 272 | age: 21, 273 | tasks: "Blah blah blah" 274 | }); 275 | User.create(tempUser, function (err, user) { 276 | should.not.exist(err); 277 | user.should.have.property('id', '1'); 278 | user.should.have.property('name', 'Johnny Doey'); 279 | user.should.have.property('age', 21); 280 | done(); 281 | }); 282 | }); 283 | 284 | 285 | /* 286 | DynamoDB handles undefined entities by storing them as the string `undefined` and null fields 287 | as the string `null`. Please handle undefined and null fields in your code. Do not expect adapter 288 | to throw an error here. 289 | */ 290 | it('should handle undefined and null attributes and return the same from database', function (done) { 291 | var tempUser = new User({ 292 | id: "2", 293 | email: null, 294 | age: null, 295 | tasks: "Blah blah blah" 296 | }); 297 | User.create(tempUser, function (err, user) { 298 | should.not.exist(err); 299 | (user.dob === undefined).should.be.true; 300 | (user.age === null).should.be.true; 301 | (user.email === null).should.be.true; 302 | done(); 303 | }); 304 | }); 305 | 306 | // Null hash keys are not allowed 307 | it('should return error saying hash key cannot be null', function (done) { 308 | var tempUser = new User({ 309 | id: null, 310 | email: null, 311 | age: null, 312 | tasks: "Blah blah blah" 313 | }); 314 | User.create(tempUser, function (err, user) { 315 | should.exist(err); 316 | done(); 317 | }); 318 | }); 319 | }); 320 | 321 | 322 | /* 323 | BOTH HASH AND RANGE KEYS 324 | */ 325 | 326 | describe('if model has hash and range keys', function(){ 327 | 328 | it('should use separator specified in schema definition', function(done){ 329 | var book = new Book({ 330 | ida: "abcd", 331 | subject: "Nature" 332 | }); 333 | Book.create(book, function (err, _book){ 334 | _book.should.have.property('id','abcd--oo--Nature'); 335 | done(); 336 | }); 337 | }); 338 | 339 | it('should throw error id attribute is missing', function(done){ 340 | (function() { 341 | db.define('Model', { 342 | attribute1 : { type: String, keyType: "hash"}, 343 | attribute2 : { type: Number, keyType: "range"} 344 | }); 345 | }).should.throw(); 346 | done(); 347 | }); 348 | 349 | it('should find objects with id attribute', function(done){ 350 | var book = new Book({ 351 | ida: "bca", 352 | subject: "Wildlife" 353 | }); 354 | Book.create(book, function (e, b){ 355 | Book.find(b.id, function (err, fetchedBook){ 356 | fetchedBook.ida.should.eql("bca"); 357 | fetchedBook.subject.should.eql("Wildlife"); 358 | done(); 359 | }); 360 | }); 361 | }); 362 | 363 | it('should handle breakable attribute for hash and range key combination', function(done){ 364 | var book = new Book({ 365 | ida: "abc", 366 | subject : "Freaky", 367 | essay : "He's dead Jim." 368 | }); 369 | Book.create(book, function(e, b){ 370 | should.not.exist(e); 371 | Book.find(b.id, function(err, fetchedBook){ 372 | fetchedBook.essay.should.eql("He's dead Jim."); 373 | fetchedBook.ida.should.eql("abc"); 374 | fetchedBook.subject.should.eql("Freaky"); 375 | done(); 376 | }); 377 | }); 378 | }); 379 | 380 | // Check if rangekey is supported 381 | it('should create two books for same id but different subjects', function (done) { 382 | var book1 = new Book({ 383 | ida: "abcd", 384 | subject: "Nature" 385 | }); 386 | 387 | var book2 = new Book({ 388 | ida: "abcd", 389 | subject: "Fiction" 390 | }); 391 | 392 | Book.create(book1, function (err, _book1) { 393 | should.not.exist(err); 394 | should.exist(_book1); 395 | _book1.should.have.property('ida', 'abcd'); 396 | _book1.should.have.property('subject', 'Nature'); 397 | 398 | Book.create(book2, function (err, _book2) { 399 | should.not.exist(err); 400 | should.exist(_book2); 401 | _book2.should.have.property('ida', 'abcd'); 402 | _book2.should.have.property('subject', 'Fiction'); 403 | done(); 404 | }); 405 | }); 406 | }); 407 | }); 408 | 409 | after(function(done){ 410 | db.adapter.client.deleteTable({TableName: 'User'}, function(){ 411 | db.adapter.client.deleteTable({TableName: 'Car'}, function(){ 412 | db.adapter.client.deleteTable({TableName: 'book_test'}, function(){ 413 | db.adapter.client.deleteTable({TableName: 'Cookie'}, function(){ 414 | done(); 415 | }); 416 | }); 417 | }); 418 | }); 419 | }); 420 | }); -------------------------------------------------------------------------------- /test/hooks.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | var should = require('./init.js'); 3 | 4 | var j = require('../'), 5 | Schema = j.Schema, 6 | AbstractClass = j.AbstractClass, 7 | Hookable = j.Hookable, 8 | 9 | db, User; 10 | 11 | describe('hooks', function() { 12 | 13 | before(function(done) { 14 | db = getSchema(); 15 | 16 | User = db.define('User', { 17 | email: {type: String, index: true, limit: 100}, 18 | name: String, 19 | password: String, 20 | state: String 21 | }); 22 | 23 | db.adapter.emitter.on("created-user", function(){ 24 | done(); 25 | }); 26 | }); 27 | 28 | describe('behavior', function() { 29 | 30 | it('should allow to break flow in case of error', function(done) { 31 | 32 | var Model = db.define('Model'); 33 | Model.beforeCreate = function(next, data) { 34 | next(new Error('Fail')); 35 | }; 36 | 37 | Model.create(function(err, model) { 38 | should.not.exist(model); 39 | should.exist(err); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | 45 | describe('initialize', function() { 46 | 47 | afterEach(function() { 48 | User.afterInitialize = null; 49 | }); 50 | 51 | it('should be triggered on new', function(done) { 52 | User.afterInitialize = function() { 53 | done(); 54 | }; 55 | new User; 56 | }); 57 | 58 | it('should be triggered on create', function(done) { 59 | var user; 60 | User.afterInitialize = function() { 61 | if (this.name === 'Nickolay') { 62 | this.name += ' Rozental'; 63 | } 64 | }; 65 | User.create({name: 'Nickolay'}, function(err, u) { 66 | u.id.should.be.ok; 67 | u.name.should.equal('Nickolay Rozental'); 68 | done(); 69 | }); 70 | }); 71 | 72 | }); 73 | 74 | describe('create', function() { 75 | 76 | afterEach(removeHooks('Create')); 77 | 78 | it('should be triggered on create', function(done) { 79 | addHooks('Create', done); 80 | User.create(); 81 | }); 82 | 83 | it('should not be triggered on new', function() { 84 | User.beforeCreate = function(next) { 85 | should.fail('This should not be called'); 86 | next(); 87 | }; 88 | var u = new User; 89 | }); 90 | 91 | it('should be triggered on new+save', function(done) { 92 | addHooks('Create', done); 93 | (new User).save(); 94 | }); 95 | 96 | it('afterCreate should not be triggered on failed create', function(done) { 97 | var old = User.schema.adapter.create; 98 | User.schema.adapter.create = function(modelName, id, cb) { 99 | cb(new Error('error')); 100 | } 101 | 102 | User.afterCreate = function() { 103 | throw new Error('shouldn\'t be called') 104 | }; 105 | User.create(function (err, user) { 106 | User.schema.adapter.create = old; 107 | done(); 108 | }); 109 | }); 110 | }); 111 | 112 | describe('save', function() { 113 | afterEach(removeHooks('Save')); 114 | 115 | it('should be triggered on create', function(done) { 116 | addHooks('Save', done); 117 | User.create(); 118 | }); 119 | 120 | it('should be triggered on new+save', function(done) { 121 | addHooks('Save', done); 122 | (new User).save(); 123 | }); 124 | 125 | it('should be triggered on updateAttributes', function(done) { 126 | User.create(function(err, user) { 127 | addHooks('Save', done); 128 | user.updateAttributes({name: 'Anatoliy'}); 129 | }); 130 | }); 131 | 132 | it('should be triggered on save', function(done) { 133 | User.create(function(err, user) { 134 | addHooks('Save', done); 135 | user.name = 'Hamburger'; 136 | user.save(); 137 | }); 138 | }); 139 | 140 | it('should save full object', function(done) { 141 | User.create(function(err, user) { 142 | User.beforeSave = function(next, data) { 143 | data.should.have.keys('id', 'name', 'email', 144 | 'password', 'state') 145 | done(); 146 | }; 147 | user.save(); 148 | }); 149 | }); 150 | 151 | it('should save actual modifications to database', function(done) { 152 | User.beforeSave = function(next, data) { 153 | data.password = 'hash'; 154 | next(); 155 | }; 156 | User.destroyAll(function() { 157 | User.create({ 158 | email: 'james.bond@example.com', 159 | password: '53cr3t' 160 | }, function() { 161 | User.findOne({ 162 | where: {email: 'james.bond@example.com'} 163 | }, function(err, jb) { 164 | jb.password.should.equal('hash'); 165 | done(); 166 | }); 167 | }); 168 | }); 169 | }); 170 | 171 | it('should save actual modifications on updateAttributes', function(done) { 172 | User.beforeSave = function(next, data) { 173 | data.password = 'hash'; 174 | next(); 175 | }; 176 | User.destroyAll(function() { 177 | User.create({ 178 | email: 'james.bond@example.com' 179 | }, function(err, u) { 180 | u.updateAttribute('password', 'new password', function(e, u) { 181 | should.not.exist(e); 182 | should.exist(u); 183 | u.password.should.equal('hash'); 184 | User.findOne({ 185 | where: {email: 'james.bond@example.com'} 186 | }, function(err, jb) { 187 | jb.password.should.equal('hash'); 188 | done(); 189 | }); 190 | }); 191 | }); 192 | }); 193 | }); 194 | 195 | }); 196 | 197 | describe('update', function() { 198 | afterEach(removeHooks('Update')); 199 | 200 | it('should not be triggered on create', function() { 201 | User.beforeUpdate = function(next) { 202 | should.fail('This should not be called'); 203 | next(); 204 | }; 205 | User.create(); 206 | }); 207 | 208 | it('should not be triggered on new+save', function() { 209 | User.beforeUpdate = function(next) { 210 | should.fail('This should not be called'); 211 | next(); 212 | }; 213 | (new User).save(); 214 | }); 215 | 216 | it('should be triggered on updateAttributes', function(done) { 217 | User.create(function (err, user) { 218 | addHooks('Update', done); 219 | user.updateAttributes({name: 'Anatoliy'}); 220 | }); 221 | }); 222 | 223 | it('should be triggered on save', function(done) { 224 | User.create(function (err, user) { 225 | addHooks('Update', done); 226 | user.name = 'Hamburger'; 227 | user.save(); 228 | }); 229 | }); 230 | 231 | it('should update limited set of fields', function(done) { 232 | User.create(function (err, user) { 233 | User.beforeUpdate = function(next, data) { 234 | data.should.have.keys('name', 'email'); 235 | done(); 236 | }; 237 | user.updateAttributes({name: 1, email: 2}); 238 | }); 239 | }); 240 | 241 | it('should not trigger after-hook on failed save', function(done) { 242 | User.afterUpdate = function() { 243 | should.fail('afterUpdate shouldn\'t be called') 244 | }; 245 | User.create(function (err, user) { 246 | var save = User.schema.adapter.save; 247 | User.schema.adapter.save = function(modelName, id, cb) { 248 | User.schema.adapter.save = save; 249 | cb(new Error('Error')); 250 | } 251 | 252 | user.save(function(err) { 253 | done(); 254 | }); 255 | }); 256 | }); 257 | }); 258 | 259 | describe('destroy', function() { 260 | 261 | afterEach(removeHooks('Destroy')); 262 | 263 | it('should be triggered on destroy', function(done) { 264 | var hook = 'not called'; 265 | User.beforeDestroy = function(next) { 266 | hook = 'called'; 267 | next(); 268 | }; 269 | User.afterDestroy = function(next) { 270 | hook.should.eql('called'); 271 | next(); 272 | }; 273 | User.create(function (err, user) { 274 | user.destroy(done); 275 | }); 276 | }); 277 | 278 | it('should not trigger after-hook on failed destroy', function(done) { 279 | var destroy = User.schema.adapter.destroy; 280 | User.schema.adapter.destroy = function(modelName, id, cb) { 281 | cb(new Error('error')); 282 | } 283 | User.afterDestroy = function() { 284 | should.fail('afterDestroy shouldn\'t be called') 285 | }; 286 | User.create(function (err, user) { 287 | user.destroy(function(err) { 288 | User.schema.adapter.destroy = destroy; 289 | done(); 290 | }); 291 | }); 292 | }); 293 | 294 | }); 295 | 296 | describe('lifecycle', function() { 297 | var life = [], user; 298 | before(function(done) { 299 | User.beforeSave = function(d){life.push('beforeSave'); d();}; 300 | User.beforeCreate = function(d){life.push('beforeCreate'); d();}; 301 | User.beforeUpdate = function(d){life.push('beforeUpdate'); d();}; 302 | User.beforeDestroy = function(d){life.push('beforeDestroy');d();}; 303 | User.beforeValidate = function(d){life.push('beforeValidate');d();}; 304 | User.afterInitialize= function( ){life.push('afterInitialize'); }; 305 | User.afterSave = function(d){life.push('afterSave'); d();}; 306 | User.afterCreate = function(d){life.push('afterCreate'); d();}; 307 | User.afterUpdate = function(d){life.push('afterUpdate'); d();}; 308 | User.afterDestroy = function(d){life.push('afterDestroy'); d();}; 309 | User.afterValidate = function(d){life.push('afterValidate');d();}; 310 | User.create(function(e, u) { 311 | user = u; 312 | life = []; 313 | done(); 314 | }); 315 | }); 316 | beforeEach(function() { 317 | life = []; 318 | }); 319 | 320 | it('should describe create sequence', function(done) { 321 | User.create(function() { 322 | life.should.eql([ 323 | 'afterInitialize', 324 | 'beforeValidate', 325 | 'afterValidate', 326 | 'beforeCreate', 327 | 'beforeSave', 328 | 'afterSave', 329 | 'afterCreate' 330 | ]); 331 | done(); 332 | }); 333 | }); 334 | 335 | it('should describe new+save sequence', function(done) { 336 | var u = new User; 337 | u.save(function() { 338 | life.should.eql([ 339 | 'afterInitialize', 340 | 'beforeValidate', 341 | 'afterValidate', 342 | 'beforeCreate', 343 | 'beforeSave', 344 | 'afterSave', 345 | 'afterCreate' 346 | ]); 347 | done(); 348 | }); 349 | }); 350 | 351 | it('should describe updateAttributes sequence', function(done) { 352 | user.updateAttributes({name: 'Antony'}, function() { 353 | life.should.eql([ 354 | 'beforeValidate', 355 | 'afterValidate', 356 | 'beforeSave', 357 | 'beforeUpdate', 358 | 'afterUpdate', 359 | 'afterSave', 360 | ]); 361 | done(); 362 | }); 363 | }); 364 | 365 | it('should describe isValid sequence', function(done) { 366 | should.not.exist( 367 | user.constructor._validations, 368 | 'Expected user to have no validations, but she have'); 369 | user.isValid(function(valid) { 370 | valid.should.be.true; 371 | life.should.eql([ 372 | 'beforeValidate', 373 | 'afterValidate' 374 | ]); 375 | done(); 376 | }); 377 | }); 378 | 379 | it('should describe destroy sequence', function(done) { 380 | user.destroy(function() { 381 | life.should.eql([ 382 | 'beforeDestroy', 383 | 'afterDestroy' 384 | ]); 385 | done(); 386 | }); 387 | }); 388 | 389 | }); 390 | }); 391 | 392 | function addHooks(name, done) { 393 | var called = false, random = String(Math.floor(Math.random() * 1000)); 394 | User['before' + name] = function(next, data) { 395 | called = true; 396 | data.email = random; 397 | next(); 398 | }; 399 | User['after' + name] = function(next) { 400 | (new Boolean(called)).should.equal(true); 401 | this.email.should.equal(random); 402 | done(); 403 | }; 404 | } 405 | 406 | function removeHooks(name) { 407 | return function() { 408 | User['after' + name] = null; 409 | User['before' + name] = null; 410 | }; 411 | } 412 | -------------------------------------------------------------------------------- /test/init.js: -------------------------------------------------------------------------------- 1 | module.exports = require('should'); 2 | 3 | var Schema = require('jugglingdb').Schema; 4 | 5 | global.getSchema = function() { 6 | var db = new Schema(require('../'), { 7 | host: 'localhost', 8 | port: '8000', 9 | logLevel: 'info' 10 | }); 11 | db.log = function (a) { console.log(a); }; 12 | 13 | return db; 14 | }; 15 | -------------------------------------------------------------------------------- /test/relations.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | var should = require('./init.js'); 3 | 4 | var db, Book, Chapter, Author, Reader; 5 | 6 | describe('relations', function() { 7 | before(function(done) { 8 | db = getSchema(); 9 | Book = db.define('Book', {name: String}); 10 | Chapter = db.define('Chapter', {name: {type: String, index: true, limit: 20}}); 11 | Author = db.define('Author', {name: String}); 12 | Reader = db.define('Reader', {name: String}); 13 | 14 | var modelCount = 0; 15 | db.adapter.emitter.on("created", function () { 16 | modelCount++; 17 | // Tables for both models created in database. 18 | if (modelCount === 4) { 19 | Book.destroyAll(function(){ 20 | Chapter.destroyAll(function(){ 21 | Author.destroyAll(function(){ 22 | Reader.destroyAll(done); 23 | }); 24 | }); 25 | }); 26 | } 27 | }); 28 | 29 | 30 | 31 | }); 32 | 33 | after(function() { 34 | db.disconnect(); 35 | }); 36 | 37 | describe('hasMany', function() { 38 | it('can be declared in different ways', function(done) { 39 | Book.hasMany(Chapter); 40 | Book.hasMany(Reader, {as: 'users'}); 41 | Book.hasMany(Author, {foreignKey: 'projectId'}); 42 | var b = new Book; 43 | b.chapters.should.be.an.instanceOf(Function); 44 | b.users.should.be.an.instanceOf(Function); 45 | b.authors.should.be.an.instanceOf(Function); 46 | (new Chapter).toObject().should.have.property('bookId'); 47 | (new Author).toObject().should.have.property('projectId'); 48 | db.automigrate(done); 49 | }); 50 | 51 | it('can be declared in short form', function(done) { 52 | Author.hasMany('readers'); 53 | (new Author).readers.should.be.an.instanceOf(Function); 54 | (new Reader).toObject().should.have.property('authorId'); 55 | 56 | db.autoupdate(done); 57 | }); 58 | 59 | it('should build record on scope', function(done) { 60 | Book.create(function(err, book) { 61 | var c = book.chapters.build(); 62 | c.bookId.should.equal(book.id); 63 | c.save(done); 64 | }); 65 | }); 66 | 67 | it('should create record on scope', function(done) { 68 | Book.create(function(err, book) { 69 | book.chapters.create(function(err, c) { 70 | should.not.exist(err); 71 | should.exist(c); 72 | c.bookId.should.equal(book.id); 73 | done(); 74 | }); 75 | }); 76 | }); 77 | 78 | it('should find scoped record', function(done) { 79 | var id; 80 | Book.create(function(err, book) { 81 | book.chapters.create({name: 'a'}, function(err, ch) { 82 | id = ch.id; 83 | book.chapters.create({name: 'z'}, function() { 84 | book.chapters.create({name: 'c'}, function() { 85 | fetch(book); 86 | }); 87 | }); 88 | }); 89 | }); 90 | 91 | function fetch(book) { 92 | book.chapters.find(id, function(err, ch) { 93 | should.not.exist(err); 94 | should.exist(ch); 95 | ch.id.should.equal(id); 96 | done(); 97 | }); 98 | } 99 | }); 100 | 101 | it('should destroy scoped record', function(done) { 102 | Book.create(function(err, book) { 103 | book.chapters.create({name: 'a'}, function(err, ch) { 104 | book.chapters.destroy(ch.id, function(err) { 105 | should.not.exist(err); 106 | book.chapters.find(ch.id, function(err, ch) { 107 | should.exist(err); 108 | err.message.should.equal('Not found'); 109 | should.not.exist(ch); 110 | done(); 111 | }); 112 | }); 113 | }); 114 | }); 115 | }); 116 | 117 | it('should not allow destroy not scoped records', function(done) { 118 | Book.create(function(err, book1) { 119 | book1.chapters.create({name: 'a'}, function(err, ch) { 120 | var id = ch.id 121 | Book.create(function(err, book2) { 122 | book2.chapters.destroy(ch.id, function(err) { 123 | should.exist(err); 124 | err.message.should.equal('Permission denied'); 125 | book1.chapters.find(ch.id, function(err, ch) { 126 | should.not.exist(err); 127 | should.exist(ch); 128 | ch.id.should.equal(id); 129 | done(); 130 | }); 131 | }); 132 | }); 133 | }); 134 | }); 135 | }); 136 | 137 | }); 138 | 139 | describe('belongsTo', function() { 140 | var List, Item, Fear, Mind; 141 | 142 | before(function(done) { 143 | var modelCount = 0; 144 | List = db.define('List', {name: String}); 145 | Item = db.define('Item', {name: String}); 146 | Fear = db.define('Fear'); 147 | Mind = db.define('Mind'); 148 | 149 | // syntax 1 (old) 150 | Item.belongsTo(List); 151 | (new Item).toObject().should.have.property('listId'); 152 | (new Item).list.should.be.an.instanceOf(Function); 153 | 154 | // syntax 2 (new) 155 | Fear.belongsTo('mind'); 156 | (new Fear).toObject().should.have.property('mindId'); 157 | (new Fear).mind.should.be.an.instanceOf(Function); 158 | // (new Fear).mind.build().should.be.an.instanceOf(Mind); 159 | 160 | 161 | db.adapter.emitter.on("created", function () { 162 | modelCount++; 163 | // Tables for both models created in database. 164 | if (modelCount === 4) { 165 | List.destroyAll(function(){ 166 | Item.destroyAll(function(){ 167 | Fear.destroyAll(function(){ 168 | Mind.destroyAll(done); 169 | }); 170 | }); 171 | }); 172 | } 173 | }); 174 | 175 | }); 176 | 177 | it('can be used to query data', function(done) { 178 | List.hasMany('todos', {model: Item}); 179 | db.automigrate(function() { 180 | List.create(function(e, list) { 181 | should.not.exist(e); 182 | should.exist(list); 183 | list.todos.create(function(err, todo) { 184 | todo.list(function(e, l) { 185 | should.not.exist(e); 186 | should.exist(l); 187 | l.should.be.an.instanceOf(List); 188 | todo.list().should.equal(l.id); 189 | done(); 190 | }); 191 | }); 192 | }); 193 | }); 194 | }); 195 | 196 | it('could accept objects when creating on scope', function(done) { 197 | List.create(function(e, list) { 198 | should.not.exist(e); 199 | should.exist(list); 200 | Item.create({list: list}, function(err, item) { 201 | should.not.exist(err); 202 | should.exist(item); 203 | should.exist(item.listId); 204 | item.listId.should.equal(list.id); 205 | item.__cachedRelations.list.should.equal(list); 206 | done(); 207 | }); 208 | }); 209 | }); 210 | 211 | }); 212 | 213 | describe('hasAndBelongsToMany', function() { 214 | var Article, Tag, ArticleTag; 215 | before(function(done) { 216 | var modelCount = 0; 217 | Article = db.define('Article', {title: String}); 218 | Tag = db.define('Tag', {name: String}); 219 | Article.hasAndBelongsToMany('tags'); 220 | ArticleTag = db.models.ArticleTag; 221 | db.adapter.emitter.on("created", function () { 222 | modelCount++; 223 | // Tables for both models created in database. 224 | if (modelCount === 3) { 225 | Article.destroyAll(function(){ 226 | Tag.destroyAll(function(){ 227 | ArticleTag.destroyAll(function(){ 228 | done(); 229 | }); 230 | }); 231 | }); 232 | } 233 | }); 234 | }); 235 | 236 | it('should allow to create instances on scope', function(done) { 237 | Article.create(function(e, article) { 238 | article.tags.create({name: 'popular'}, function(e, t) { 239 | t.should.be.an.instanceOf(Tag); 240 | ArticleTag.findOne(function(e, at) { 241 | should.exist(at); 242 | at.tagId.toString().should.equal(t.id.toString()); 243 | at.articleId.toString().should.equal(article.id.toString()); 244 | done(); 245 | }); 246 | }); 247 | }); 248 | }); 249 | 250 | it('should allow to fetch scoped instances', function(done) { 251 | Article.findOne(function(e, article) { 252 | article.tags(function(e, tags) { 253 | should.not.exist(e); 254 | should.exist(tags); 255 | done(); 256 | }); 257 | }); 258 | }); 259 | 260 | it('should allow to add connection with instance', function(done) { 261 | Article.findOne(function(e, article) { 262 | Tag.create({name: 'awesome'}, function(e, tag) { 263 | article.tags.add(tag, function(e, at) { 264 | should.not.exist(e); 265 | should.exist(at); 266 | at.should.be.an.instanceOf(ArticleTag); 267 | at.tagId.should.equal(tag.id); 268 | at.articleId.should.equal(article.id); 269 | done(); 270 | }); 271 | }); 272 | }); 273 | }); 274 | 275 | it('should allow to remove connection with instance', function(done) { 276 | Article.findOne(function(e, article) { 277 | article.tags(function(e, tags) { 278 | var len = tags.length; 279 | tags.should.not.be.empty; 280 | should.exist(tags[0]); 281 | article.tags.remove(tags[0], function(e) { 282 | should.not.exist(e); 283 | article.tags(true, function(e, tags) { 284 | tags.should.have.lengthOf(len - 1); 285 | done(); 286 | }); 287 | }); 288 | }); 289 | }); 290 | }); 291 | 292 | it('should remove the correct connection', function(done) { 293 | Article.create({title: 'Article 1'}, function(e, article1) { 294 | Article.create({title: 'Article 2'}, function(e, article2) { 295 | Tag.create({name: 'correct'}, function(e, tag) { 296 | article1.tags.add(tag, function(e, at) { 297 | article2.tags.add(tag, function(e, at) { 298 | article2.tags.remove(tag, function(e) { 299 | article2.tags(true, function(e, tags) { 300 | tags.should.have.lengthOf(0); 301 | article1.tags(true, function(e, tags) { 302 | tags.should.have.lengthOf(1); 303 | done(); 304 | }); 305 | }); 306 | }); 307 | }); 308 | }); 309 | }); 310 | }); 311 | }); 312 | }); 313 | 314 | }); 315 | 316 | }); 317 | --------------------------------------------------------------------------------