      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