1 var comb = require("comb"); 2 var moose, adapter; 3 var convert = function(op) { 4 return function() { 5 var columns = this.columns; 6 var args = Array.prototype.slice.call(arguments); 7 if (args.length > 1) { 8 var columnName = args[0], val = args[1]; 9 this.validate(columnName, val); 10 return columns[columnName][op](val); 11 } else { 12 var object = args[0]; 13 if (op == "toSql") this.validate(object); 14 var ret = {}; 15 for (var i in object) { 16 if (i in columns) { 17 ret[i] = columns[i][op](object[i]); 18 } else { 19 ret[i] = object[i]; 20 } 21 } 22 return ret; 23 24 } 25 }; 26 }; 27 28 29 /** 30 * @class Represents a table in a Database. This class is used to update, create, alter... tables. 31 * The Class is typically used in migrations, or wrapped by a {@link moose.Model}. 32 * 33 * @example 34 * 35 * //To load preexisting Table use moose.loadSchema 36 * 37 * moose.loadSchema("employee").then(function(employee){}); 38 * 39 * //Table creation example 40 * 41 * var types = moose.adapters.mysql.types; 42 * 43 * var employee = new moose.Table("employee", { 44 * id : types.INT({allowNull : false, autoIncrement : true}), 45 * firstname : types.VARCHAR({length : 20, allowNull : false}), 46 * lastname : types.VARCHAR({length : 20, allowNull : false}), 47 * midinitial : types.CHAR({length : 1}), 48 * gender : types.ENUM({enums : ["M", "F"], allowNull : false}), 49 * street : types.VARCHAR({length : 50, allowNull : false}), 50 * city : types.VARCHAR({length : 20, allowNull : false}), 51 * primaryKey : "id" 52 * }); 53 * 54 * //or use through migration 55 * 56 *moose.createTable("employee", function(table) { 57 * table.column("id", types.INT({allowNull : false, autoIncrement : true})); 58 * table.column("firstname", types.VARCHAR({length : 20, allowNull : false})); 59 * table.column("lastname", types.VARCHAR({length : 20, allowNull : false})); 60 * table.column("midinitial", types.CHAR({length : 1})); 61 * table.column("gender", types.ENUM({enums : ["M", "F"], allowNull : false})); 62 * table.column("street", types.VARCHAR({length : 50, allowNull : false})); 63 * table.column("city", types.VARCHAR({length : 20, allowNull : false})); 64 * table.primaryKey("id"); 65 *}); 66 * 67 * //alter table examples 68 * 69 * employee.rename("employeeTwo"); 70 * employee.addColumn("age", types.INT()); 71 * employee.addUnique(["firstname", "midinitial"]); 72 * 73 * //use through migration 74 * 75 * moose.alterTable("company", function(table) { 76 * table.rename("companyNew"); 77 * table.addUnique("companyName"); 78 * table.addColumn("employeeCount", types.INT()); 79 * table.addUnique(["companyName", "employeeCount"]); 80 *}); 81 * 82 * //to drop a table use moose.dropTable 83 * 84 * moose.dropTable("company"); 85 * moose.dropTable("employee"); 86 * 87 * 88 * @param {String} tableName the name of the table 89 * @param {Object} properties properties to describe the table, all properties excpet for type, and primaryKey will be interpreted as columns 90 * each property should be a type described by the particular adapter (i.e {@link moose.adapters.mysql.types}). 91 * @param {String} [properties.type="mysql"] the type of database the table resides in. 92 * @param {String|Array} properties.primaryKey the primary key of the table. 93 * 94 * @property {Object} columns the columns contained in this table. 95 * @property {String} createTableSql valid create table SQL for this table. 96 * @property {String} alterTableSql valid alter table SQL for this table. 97 * @property {String} dropTableSql valid drop table SQL for this table. 98 * @property {String} database the schema of the table resides in. 99 * 100 * 101 * @name Table 102 * @memberOf moose 103 104 */ 105 exports.Table = (comb.define(null, { 106 instance : { 107 108 /**@lends moose.Table.prototype*/ 109 110 tableName : null, 111 112 type : null, 113 114 foreignKeys : null, 115 116 117 constructor : function(tableName, properties) { 118 if (!moose) { 119 moose = require("./index"),adapter = moose.adapter; 120 } 121 if (!tableName) throw new Error("Table name required for schema"); 122 properties = properties || {}; 123 this.foreignKeys = []; 124 this.uniqueSql = []; 125 this.pk = null; 126 this.__alteredColumns = {}; 127 this.__addColumns = {}; 128 this.__dropColumns = []; 129 this.__addColumns = {}; 130 this.__newName = null; 131 // pull it out then define our columns then check the primary key 132 var db = properties.database; 133 delete properties.database; 134 if (db) { 135 this.database = db; 136 } 137 var pk = properties.primaryKey; 138 delete properties.primaryKey; 139 this.primaryKeySql = null; 140 this.tableName = tableName; 141 this.__columns = columns = {}; 142 for (var i in properties) { 143 this.column(i, properties[i]); 144 } 145 if (pk) { 146 this.primaryKey(pk); 147 } 148 }, 149 150 /** 151 * Set the engine that the table should leverage. 152 * <br/> 153 * <b>Not all databases support this property</b> 154 * 155 * @param {String} engine the name of the engine. 156 */ 157 engine : function(engine) { 158 this.__engine = engine; 159 }, 160 161 162 /** 163 * Add a column to this table. 164 * 165 * <p><b>This is only valid on new tables.</b></p> 166 * 167 * @param {String} name the name of the column to be created. 168 * @param {moose.adapters.Type} options a type specific to the adpater that this table uses 169 */ 170 column : function(name, options) { 171 if (adapter.isValidType(options)) { 172 this.__columns[name] = options; 173 } else { 174 throw new Error("When adding a column the type must be a type object"); 175 } 176 }, 177 178 /** 179 * Adds a foreign key to this table, see {@link moose.adapters.mysql.foreignKey} 180 * 181 * <p><b>This is only valid on new tables.</b></p> 182 * 183 * 184 */ 185 foreignKey : function(name, options) { 186 this.foreignKeys.push(adapter.foreignKey(name, options)); 187 }, 188 189 /** 190 * Adds a primary key to this table, see {@link moose.adapters.mysql.primaryKey} 191 * 192 * <p><b>This is only valid on new tables.</b></p> 193 * 194 * @param {Array | String} name the name or names to assign to a primary key. 195 * 196 */ 197 primaryKey : function(name) { 198 var isValid = false; 199 if (name instanceof Array && name.length) 200 if (name.length == 1) { 201 return this.primaryKey(name[0]); 202 } else { 203 isValid = name.every(function(n) { 204 if (this.isInTable(n)) { 205 this.columns[n].primaryKey = true; 206 return true; 207 } else { 208 return false; 209 } 210 }, this); 211 this.pk = name; 212 } 213 else { 214 isValid = this.isInTable(name); 215 isValid && (this.columns[name].primaryKey = true); 216 this.pk = name; 217 } 218 if (isValid) { 219 this.primaryKeySql = adapter.primaryKey(name); 220 } else { 221 throw new Error("Primary key is not in the table"); 222 } 223 }, 224 225 /** 226 * Adds a unique constraint to this table, see {@link moose.adapters.mysql.unique} 227 * 228 * <p><b>This is only valid on new tables.</b></p> 229 * 230 * 231 */ 232 unique : function(name) { 233 var isValid = false; 234 if (name instanceof Array && name.length) 235 if (name.length == 1) { 236 return this.unique(name[0]); 237 } else { 238 isValid = name.every(function(n) { 239 if (this.isInTable(n)) { 240 this.columns[n].unique = true; 241 return true; 242 } else { 243 return false; 244 } 245 }, this); 246 } 247 else { 248 isValid = this.isInTable(name); 249 isValid && (this.columns[name].unique = true); 250 } 251 if (isValid) { 252 this.uniqueSql.push(adapter.unique(name)); 253 } else { 254 throw new Error("Unique key is not in the table"); 255 } 256 }, 257 258 /** 259 * Takes a columnName and determines if it is in the table. 260 * 261 * @param {String} columnName the name of the column. 262 * 263 * @return {Boolean} true if the columnd is in the table. 264 */ 265 isInTable : function(columnName) { 266 return (columnName in this.__columns); 267 }, 268 269 /** 270 * Validate an object or columns and value against the columns in this table. 271 * 272 * @param {String|Object} name If the name is a string it is assumed to be the name of the column. 273 * If the name is an object it is assumed to be a an object consiting of {columnName : value}. 274 * @param {*} value if a string is the first argument to validate is a string then the value is compared against the type of the column contained in this table. 275 * 276 * @return {Boolean} true if the column/s are valid. 277 */ 278 validate : function() { 279 var args = Array.prototype.slice.call(arguments); 280 var columns = this.columns; 281 self = this; 282 function validateValue(columnName, value) { 283 if (!columnName) throw new Error("columnName required"); 284 if (value == "undefined") value = null; 285 if (self.isInTable(columnName)) { 286 return columns[columnName].check(value); 287 } else { 288 throw new Error(columnName + " is not in table"); 289 } 290 } 291 292 if (args.length > 1) { 293 return validateValue(args[0], args[1]); 294 } else { 295 var object = args[0]; 296 if (!comb.isObject(object)) throw new Error("object is required"); 297 for (var i in object) { 298 validateValue(i, object[i]); 299 } 300 return true; 301 } 302 }, 303 304 /** 305 * Adds a column to this table. 306 * 307 * <p><b>This is only valid on previously saved tables.</b></p> 308 * 309 * @param {String} name the name of the column to add. 310 * @param {moose.adapters.mysql.Type} options the type information of the column. 311 */ 312 addColumn : function(name, options) { 313 if (adapter.isValidType(options)) { 314 this.__addColumns[name] = options; 315 } else { 316 throw new Error("When adding a column the type must be a Type object"); 317 } 318 }, 319 320 /** 321 * Drops a column from this table. 322 * 323 * <p><b>This is only valid on previously saved tables.</b></p> 324 * 325 * @param {String} name the name of the column to drop. 326 */ 327 dropColumn : function(name) { 328 if (this.isInTable(name)) { 329 this.__dropColumns.push(name); 330 } else { 331 throw new Error(name + " is not in table " + this.tableName); 332 } 333 }, 334 335 /** 336 * Renames a column contained in this table. 337 * 338 * <p><b>This is only valid on previously saved tables.</b></p> 339 * 340 * @param {String} name the name of the column to rename. 341 * @param {String} newName the new name of the column. 342 */ 343 renameColumn : function(name, newName) { 344 if (this.isInTable(name)) { 345 var column = this.__alteredColumns[name]; 346 if (!column) { 347 column = this.__alteredColumns[name] = {original : this.__columns[name]}; 348 } 349 column.newName = newName; 350 } else { 351 throw new Error(name + " is not in table " + this.tableName); 352 } 353 }, 354 355 /** 356 * Set the default value of a column. 357 * 358 * <p><b>This is only valid on previously saved tables.</b></p> 359 * 360 * @param {String} name the name of the column to alter. 361 * @param {*} defaultvalue the value to set as the default of the column. 362 */ 363 setColumnDefault : function(name, defaultvalue) { 364 if (this.isInTable(name)) { 365 var column = this.__alteredColumns[name]; 366 if (!column) { 367 column = this.__alteredColumns[name] = {original : this.__columns[name]}; 368 } 369 column["default"] = defaultvalue; 370 } else { 371 throw new Error(name + " is not in table " + this.tableName); 372 } 373 }, 374 375 /** 376 * Set a new type on a column contained in this table. 377 * 378 * <p><b>This is only valid on previously saved tables.</b></p> 379 * 380 * @param {String} name the name of the column to alter. 381 * @param {moose.adapters.mysql.Type} options the new type information of the column. 382 */ 383 setColumnType : function(name, type) { 384 if (adapter.isValidType(type) && this.isInTable(name)) { 385 var column = this.__alteredColumns[name]; 386 if (!column) { 387 column = this.__alteredColumns[name] = {original : this.__columns[name]}; 388 } 389 column.type = type; 390 } else { 391 throw new Error(name + " is not in table " + this.tableName); 392 } 393 }, 394 395 /** 396 * Set if a column should allow null. 397 * 398 * <p><b>This is only valid on previously saved tables.</b></p> 399 * 400 * @param {String} name the name of the column to alter. 401 * @param {Boolean} allowNull true if null is allowed, false otherwise. 402 */ 403 setAllowNull : function(name, allowNull) { 404 if (this.isInTable(name)) { 405 var column = this.__alteredColumns[name]; 406 if (!column) { 407 column = this.__alteredColumns[name] = {original : this.__columns[name]}; 408 } 409 column.allowNull = allowNull; 410 } else { 411 throw new Error(name + " is not in table " + this.tableName); 412 } 413 }, 414 415 /** 416 * Replace the current primary key, see {@link moose.adapters.mysql.addPrimaryKey}. 417 * <p><b>This is only valid on previously saved tables.</b></p> 418 */ 419 addPrimaryKey : function() { 420 if (this.pk) { 421 this.dropPrimaryKey(this.pk); 422 } 423 this.addPrimaryKeySql = adapter.addPrimaryKey.apply(adapter, arguments); 424 }, 425 426 /** 427 * Drop current primary key, see {@link moose.adapters.mysql.dropPrimaryKey}. 428 * <p><b>This is only valid on previously saved tables.</b></p> 429 */ 430 dropPrimaryKey : function() { 431 this.pk = null; 432 this.dropPrimaryKeySql = adapter.dropPrimaryKey.apply(adapter, arguments); 433 }, 434 435 /** 436 * Add a foreign key to this table, see {@link moose.adapters.mysql.addForeignKey}. 437 * <p><b>This is only valid on previously saved tables.</b></p> 438 */ 439 440 addForeignKey : function() { 441 this.foreignKeys.push(adapter.addForeignKey.apply(adapter, arguments)); 442 }, 443 444 /** 445 * Drop a foreign key on this table, see {@link moose.adapters.mysql.dropForeignKey}. 446 * <p><b>This is only valid on previously saved tables.</b></p> 447 */ 448 dropForeignKey : function() { 449 this.foreignKeys.push(adapter.dropForeignKey.apply(adapter, arguments)); 450 }, 451 452 /** 453 * Add a unique constraint to this table, see {@link moose.adapters.mysql.addUnique}. 454 * <p><b>This is only valid on previously saved tables.</b></p> 455 */ 456 457 addUnique : function() { 458 this.uniqueSql.push(adapter.addUnique.apply(adapter, arguments)); 459 }, 460 461 /** 462 * Drop a unique constraint on this table, see {@link moose.adapters.mysql.dropUnique}. 463 * <p><b>This is only valid on previously saved tables.</b></p> 464 */ 465 dropUnique : function() { 466 this.uniqueSql.push(adapter.dropUnique.apply(adapter, arguments)); 467 }, 468 469 /** 470 * Rename this table. 471 * <p><b>This is only valid on previously saved tables.</b></p> 472 * @param {String} newName the new table name. 473 */ 474 rename : function(newName) { 475 this.__newName = newName; 476 }, 477 478 /** 479 * 480 * @function 481 * Convert an object or column and value to a valid sql value. 482 * 483 * @param {String|Object} name If the name is a string it is assumed to be the name of the column. 484 * If the name is an object it is assumed to be a an object consisting of {columnName : value}. 485 * @param {*} value if a string is the first argument, then the value is compared against the type of the column contained in this table. 486 * 487 * @return {String|Object} the sql value/s. 488 */ 489 toSql : convert("toSql"), 490 491 /** 492 * Convert an object or column and value from a sql value. 493 * 494 * @function 495 * @param {String|Object} name If the name is a string it is assumed to be the name of the column. 496 * If the name is an object it is assumed to be a an object consisting of {columnName : value}. 497 * @param {*} value if a string is the first argument, then the value is compared against the type of the column contained in this table. 498 * 499 * @return {String|Object} the javascript value/s. 500 */ 501 fromSql : convert("fromSql"), 502 503 setters : { 504 database : function(database) { 505 if (comb.isString(database)) { 506 this.__database = database; 507 } else { 508 throw "moose.Table : when setting a database it must be a string."; 509 } 510 } 511 }, 512 513 getters : { 514 515 database : function() { 516 return this.__database; 517 }, 518 519 columns : function() { 520 return this.__columns; 521 }, 522 523 createTableSql : function() { 524 var sql = "CREATE TABLE " + this.tableName + "("; 525 var columns = this.columns; 526 var columnSql = []; 527 for (var i in columns) { 528 columnSql.push(adapter.column(i, columns[i])); 529 } 530 sql += columnSql.join(","); 531 if (this.primaryKeySql) sql += ", " + this.primaryKeySql; 532 if (this.foreignKeys.length) sql += ", " + this.foreignKeys.join(","); 533 if (this.uniqueSql.length) sql += ", " + this.uniqueSql.join(","),needComma = true; 534 if (this.__engine) { 535 sql += ") ENGINE=" + this.__engine + ";"; 536 } else { 537 sql += ");"; 538 } 539 return sql; 540 }, 541 542 alterTableSql : function() { 543 var sql = "ALTER TABLE " + this.tableName, needComma = false; 544 if (this.__newName) { 545 sql += " RENAME " + this.__newName; 546 needComma = true; 547 } 548 var addColumns = this.__addColumns, addColumnsSql = []; 549 var dropColumns = this.__dropColumns, dropColumnsSql = []; 550 var alteredColumns = this.__alteredColumns, alterColumnsSql = []; 551 for (var i in addColumns) { 552 addColumnsSql.push(adapter.addColumn(i, addColumns[i])); 553 } 554 if (addColumnsSql.length) { 555 sql += (needComma ? " , " : " ") + addColumnsSql.join(" ,"); 556 needComma = true; 557 } 558 if (dropColumns.length) { 559 for (i in dropColumns) { 560 dropColumnsSql.push(adapter.dropColumn(dropColumns[i])); 561 } 562 sql += (needComma ? " , " : " ") + dropColumnsSql.join(" ,"),needComma = true; 563 } 564 for (i in alteredColumns) { 565 alterColumnsSql.push(adapter.alterColumn(i, alteredColumns[i])); 566 } 567 if (alterColumnsSql.length) { 568 sql += (needComma ? " , " : " ") + alterColumnsSql.join(","); 569 needComma = true; 570 } 571 572 if (this.dropPrimaryKeySql) { 573 sql += (needComma ? " , " : " ") + this.dropPrimaryKeySql; 574 needComma = true; 575 } 576 if (this.addPrimaryKeySql) { 577 sql += (needComma ? " , " : " ") + this.addPrimaryKeySql; 578 needComma = true; 579 } 580 if (this.foreignKeys.length) { 581 sql += (needComma ? " , " : " ") + this.foreignKeys.join(","); 582 needComma = true; 583 } 584 if (this.uniqueSql.length) { 585 sql += (needComma ? " , " : " ") + this.uniqueSql.join(","); 586 needComma = true; 587 } 588 sql += ";"; 589 return sql; 590 }, 591 592 dropTableSql : function() { 593 return "DROP TABLE IF EXISTS " + this.tableName; 594 } 595 } 596 597 } 598 })); 599 600 601