/*jslint adsafe: false, bitwise: true, browser: true, cap: false, css: false,
debug: false, devel: true, eqeqeq: true, es5: false, evil: false,
forin: false, fragment: false, immed: true, laxbreak: false, newcap: true,
nomen: false, on: false, onevar: true, passfail: false, plusplus: true,
regexp: false, rhino: true, safe: false, strict: false, sub: false,
undef: true, white: false, widget: false, windows: false */
/*global jQuery: false, window: false */
"use strict";
/*
* (c) 2010 Carlo Zottmann
* http://github.com/carlo/simplenote-js
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* SimpleNote API wrapper module.
*
* @module simplenote-js
*/
/**
* SimpleNote API wrapper class.
*
* Please note that the SimpleNote API expects everything to be UTF-8, and so
* does simplenote-js.
*
* @class SimpleNote
* @requires jquery
* @constructor
*/
function SimpleNote() {
var $ = window.jQuery,
/**
* The simplenote-js version number.
*
* @property _version
* @type String
* @final
* @private
*/
_version = "1.0.1",
/**
* After a successful login, this variable holds the account email address
* required for all subsequent API requests.
*
* @property _email
* @type String
* @private
*/
_email = "",
/**
* After a successful login, this variable holds the auth token required for
* all subsequent API requests.
*
* @property _token
* @type String
* @private
*/
_token = "",
/**
* Contains the base URL of the API.
*
* @property _baseURL
* @type String
* @final
* @private
*/
_baseURL = "https://simple-note.appspot.com/api",
/**
* Contains the OpenData table used for all YQL calls.
*
* @property _yqlTableURL
* @type String
* @default http://github.com/carlo/simplenote-js/raw/master/src/yql_simplenote.xml
* @private
*/
_yqlTableURL = "http://github.com/carlo/simplenote-js/raw/master/src/yql_simplenote.xml",
/**
* Contains the table name used in YQL query.
*
* @property _yqlTableName
* @type String
* @final
* @private
*/
_yqlTableName = "snjs" + ( new Date() ).getTime(),
/**
* Enables console output of debugging messages.
*
* @property _debugEnabled
* @type Boolean
* @default false
* @private
*/
_debugEnabled = false;
if ( !$.base64 ) {
throw "FATAL ERROR: jQuery.base64 is not available!";
}
function log() {
log.history = log.history || []; // store logs to an array for reference
log.history.push( arguments );
if ( window.console && _debugEnabled ) {
console.log( Array.prototype.slice.call( arguments ) );
}
}
/**
* Deletes both `_email` and `_token` variables.
*
* @method _clearCredentials
* @private
*/
function _clearCredentials() {
_email = "";
_token = "";
}
/**
* Returns a boolean showing whether the user is currently logged in or not.
*
* @method _isLoggedIn
* @return {Boolean}
* @private
*/
function _isLoggedIn() {
return ( !!_email && !!_token );
}
/**
* Throws an exception if either the internal email or token aren't set
* (which means the user's not logged in).
*
* @method _throwUnlessLoggedIn
* @private
*/
function _throwUnlessLoggedIn() {
if ( !_isLoggedIn() ) {
throw "AuthError";
}
}
/**
* Accepts a YQL query and returns the related YQL URL.
*
* @method _getYQLURL
* @param query {String} YQL query
* @private
*/
function _getYQLURL( query ) {
return [
"https://query.yahooapis.com/v1/public/yql?q=",
encodeURIComponent( query ),
"&diagnostics=true",
"&format=json",
"&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys"
].join( "" );
}
/**
* Returns an error code for a passed HTTP status.
*
* @method _getErrorCode
* @param status {String} The HTTP status code returned by YQL.
* @private
*/
function _getErrorCode( status ) {
var codes = {
"400": "bad_request",
"401": "unauthorized",
"403": "forbidden",
"404": "not_found",
"500": "server_error"
};
status = String( status );
return codes[ status ] || "unknown_error";
} // _getErrorCode
/**
* Proxy method abstracting most YQL calls.
*
* @method _queryYQL
* @param caller {String} Name of the calling method. Used for log
* output.
* @param query {String} The YQL query.
* @param cbSuccess {Function} The on-success callback.
* @param cbError {Function} The on-error callback.
* @param context {Object} The context of the callbacks mentioned above.
* @private
*/
function _queryYQL( caller, query, cbSuccess, cbError, context ) {
$.ajax({
url: _getYQLURL( query ),
context: context,
success: function( data, status, req ) {
var yqlStatus,
yqlStatusCode,
res;
if ( !data || !data.query || !data.query.results || !data.query.results.result ) {
log( caller + " error #1", data );
cbError( "yql_error" );
return;
}
yqlStatus = data.query.results.result.status;
if ( yqlStatus !== "200" ) {
yqlStatusCode = _getErrorCode( yqlStatus );
if ( yqlStatus === "401" ) {
_clearCredentials();
}
log( caller + " error #2", yqlStatusCode, data );
cbError( yqlStatusCode );
return;
}
log( caller + " success", data );
res = data.query.results.result;
if ( typeof res.response === "string" ) {
res.response = /^[\[\{]/.test( res.response ) ? $.parseJSON( res.response ) : res.response;
}
cbSuccess( res );
},
error: function( req, status, error ) {
log( caller + " error #3", req, status, error );
cbError( "unknown" );
},
dataType: "jsonp",
scriptCharset: "utf-8"
});
}
/**
* Authenticates the client. The request is made asynchronously via YQL.
* Throws an exception if one of the arguments is missing or empty.
*
* @method _authenticate
* @param config.email {String} SimpleNote account email address
* @param config.password {String} SimpleNote account password
* @param config.success {Function} callback function to be called on
* successful authentication (optional)
* @param config.error {Function} callback function to be called on
* failure, is passed a plain text error string (optional)
* @private
*/
function _authenticate( obj ) {
if ( !obj || !obj.email || !obj.password ) {
throw "ArgumentError: email and password required";
}
var query,
config = $.extend({
success: function() {
alert( "SimpleNote auth success" );
},
error: function( errorCode ) {
alert( "SimpleNote auth error: " + errorCode );
}
}, obj );
query = [
"USE '", _yqlTableURL, "' AS ", _yqlTableName, "; ",
"SELECT * FROM ", _yqlTableName, " ",
"WHERE path='/login' ",
"AND method='post' ",
"AND data='",
$.base64.encode(
$.param({ email: config.email, password: config.password })
),
"'"
].join( "" );
log( "_authenticate", query );
function __cbSuccess( result ) {
_email = config.email;
_token = $.trim( result.response );
config.success();
}
function __cbError( code ) {
_clearCredentials();
config.error( code );
}
_queryYQL( "_authenticate", query, __cbSuccess, __cbError, this );
} // _authenticate
/**
* Checks if the passed argument is an object and has `error` and `success`
* keys which are functions. Throws an `ArgumentError` exception on failure.
*
* @method _validateRetrievalConfig
* @param obj {Object} The object to validate.
* @private
*/
function _validateRetrievalConfig( obj ) {
if ( !$.isPlainObject( obj ) ) {
throw "ArgumentError: argument must be object";
}
if ( !$.isFunction( obj.success ) || !$.isFunction( obj.error ) ) {
throw "ArgumentError: callbacks missing";
}
}
/**
* Returns an index of all notes. This method will return a JSON object with
* three main properties for each note: `key`, `modify`, and `deleted`. Some
* notes may be marked `deleted`; these notes will be removed permanently
* the next time the client synchronizes with the server.
*
* Throws an exception if one of the arguments is missing or empty.
*
* The index will be returned as an array containing hashes. The hashes
* contain the following keys:
*
* * `deleted`: true | false
* * `key`: the note ID
* * `modify`: a string containing the last-modified date
*
* @method _retrieveIndex
* @method retrieveIndex
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the array containing the
* notes index
* @param config.error {Function} callback function to be called on
* failure, is passed a plain text error string
* @private
*/
function _retrieveIndex( obj ) {
_throwUnlessLoggedIn();
_validateRetrievalConfig( obj );
var query,
config = $.extend({
success: function( json ) {},
error: function( errorString ) {}
}, obj );
query = [
"USE '", _yqlTableURL, "' AS ", _yqlTableName, "; ",
"SELECT * FROM ", _yqlTableName, " ",
"WHERE path='/index' ",
"AND data='", $.param({ email: _email, auth: _token }), "'"
].join( "" );
log( "_retrieveIndex", query );
function __cbSuccess( result ) {
config.success( result.response );
}
_queryYQL( "_retrieveIndex", query, __cbSuccess, config.error, this );
} // _retrieveIndex
/**
* Returns notes containing a particular term. Throws an exception if one of
* the arguments is missing or empty.
*
* This method will return a JSON object with two keys, `notes` (containing
* an array with found notes) and `totalRecords` (showing the total number of
* search results).
*
* Each note is represented as an object containing two keys, `key` (the note
* ID) and `body` (the note string).
*
* @method _searchNotes
* @param config.query {String} search string
* @param config.maxResults {Int} (optional): max number of results
* (default: 10)
* @param config.offset {Int} (optional) index offset
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the array containing the
* notes index
* @param config.error {Function} callback function to be called on
* failure, is passed a clear text error string.
* @private
*/
function _searchNotes( obj ) {
_throwUnlessLoggedIn();
_validateRetrievalConfig( obj );
if ( !obj.query ) {
throw "ArgumentError: query is missing";
}
var query,
config = $.extend({
success: function( json ) {},
error: function( errorString ) {}
}, obj );
config.query = $.trim( config.query );
if ( typeof config.maxResults !== "number" ) {
config.maxResults = 10;
}
if ( typeof config.offset !== "number" ) {
config.offset = 0;
}
query = [
"USE '", _yqlTableURL, "' AS ", _yqlTableName, "; ",
"SELECT * FROM ", _yqlTableName, " ",
"WHERE path='/search' ",
"AND data='",
$.param({
email: _email,
auth: _token,
query: config.query,
results: config.maxResults,
offset: config.offset
}), "'"
].join( "" );
log( "_searchNotes", query );
function __cbSuccess( result ) {
var res = result.response.Response,
hash = {
totalRecords: Number( res.totalRecords ),
notes: []
},
numResults = ( hash.totalRecords > 0 ) ? res.Results.length : 0,
i;
for ( i = 0; i < numResults; i += 1 ) {
hash.notes.push({
key: res.Results[ i ].key,
body: res.Results[ i ].content
});
}
config.success( hash );
}
_queryYQL( "_searchNotes", query, __cbSuccess, config.error, this );
}
/**
* Retrieves and returns a single note as a hash in the following form:
*
* {
* body: "my example note",
* key: "agtzaW1wbG0LCxIETm90ZRjoBAw",
* modifydate: "2008-12-18 04:04:20.554442",
* createdate: "2008-12-18 04:04:20.554442",
* deleted: false
* }
*
* Throws an exception if one of the arguments is missing or empty.
*
* @method _retrieveNote
* @param config.key {String} the note ID
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the note hash
* @param config.error {Function} callback function to be called on
* failure, is passed a clear text error string.
* @private
*/
function _retrieveNote( obj ) {
_throwUnlessLoggedIn();
_validateRetrievalConfig( obj );
if ( !obj.key ) {
throw "ArgumentError: key is missing";
}
var query,
config = $.extend({
success: function( json ) {},
error: function( errorString ) {}
}, obj );
query = [
"USE '", _yqlTableURL, "' AS ", _yqlTableName, "; ",
"SELECT * FROM ", _yqlTableName, " ",
"WHERE path='/note' ",
"AND data='", $.param({ email: _email, auth: _token, key: config.key }), "'"
].join( "" );
log( "_retrieveNote", query );
function __cbSuccess( result ) {
config.success({
body: $.trim( result.response ),
key: result.headers[ "note-key" ],
modifydate: result.headers[ "note-modifydate" ],
createdate: result.headers[ "note-createdate" ],
deleted: ( result.headers[ "note-deleted" ].toLowerCase() === "true" )
});
}
_queryYQL( "_retrieveNote", query, __cbSuccess, config.error, this );
} // _retrieveNote
/**
* Creates a new note. Returns the new note ID. Throws an exception if one
* of the arguments is missing or empty.
*
* @method _createNote
* @param config.body {String} the note body
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the note ID string
* @param config.error {Function} callback function to be called on
* failure, is passed a clear text error string.
* @private
*/
function _createNote( obj ) {
_throwUnlessLoggedIn();
_validateRetrievalConfig( obj );
if ( !obj.body ) {
throw "ArgumentError: body is missing";
}
var query,
config = $.extend({
success: function( json ) {},
error: function( errorString ) {}
}, obj );
config.body = $.trim( config.body );
query = [
"USE '", _yqlTableURL, "' AS ", _yqlTableName, "; ",
"SELECT * FROM ", _yqlTableName, " ",
"WHERE path='/note?", $.param({ email: _email, auth: _token }), "' ",
"AND data='", $.base64.encode( config.body ), "' ",
"AND method='post'"
].join( "" );
log( "_createNote", query );
function __cbSuccess( result ) {
config.success( $.trim( result.response ) );
}
_queryYQL( "_createNote", query, __cbSuccess, config.error, this );
} // _createNote
/**
* Updates an existing note. Returns the note ID on success. Throws an
* exception if one of the arguments is missing or empty.
*
* @method _updateNote
* @param config.key {String} the ID of the note to update
* @param config.body {String} the note body
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the note ID string
* @param config.error {Function} callback function to be called on
* failure, is passed a clear text error string.
* @private
*/
function _updateNote( obj ) {
_throwUnlessLoggedIn();
_validateRetrievalConfig( obj );
if ( !obj.body ) {
throw "ArgumentError: body is missing";
}
if ( !obj.key ) {
throw "ArgumentError: key is missing";
}
var query,
config = $.extend({
success: function( json ) {},
error: function( errorString ) {}
}, obj );
config.body = $.trim( config.body );
query = [
"USE '", _yqlTableURL, "' AS ", _yqlTableName, "; ",
"SELECT * FROM ", _yqlTableName, " ",
"WHERE path='/note?", $.param({ email: _email, auth: _token, key: config.key }), "' ",
"AND data='", $.base64.encode( config.body ), "' ",
"AND method='post'"
].join( "" );
log( "_updateNote", query );
function __cbSuccess( result ) {
config.success( $.trim( result.response ) );
}
_queryYQL( "_updateNote", query, __cbSuccess, config.error, this );
} // _updateNote
/**
* Deletes an existing note. Throws an exception if one of the arguments is
* missing or empty.
*
* @method _deleteNote
* @param config.key {String} the ID of the note to delete
* @param config.permanently {Boolean} set to `true` if you want to not
* only mark the note as deleted on the server but to delete it
* right away (see SN API docs)
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the note ID string
* @param config.error {Function} callback function to be called on
* failure, is passed a clear text error string.
* @private
*/
function _deleteNote( obj ) {
_throwUnlessLoggedIn();
_validateRetrievalConfig( obj );
if ( !obj.key ) {
throw "ArgumentError: key is missing";
}
var query,
config = $.extend({
success: function( json ) {},
error: function( errorString ) {}
}, obj );
query = [
"USE '", _yqlTableURL, "' AS ", _yqlTableName, "; ",
"SELECT * FROM ", _yqlTableName, " ",
"WHERE path='/delete' ",
"AND data='",
$.param({
email: _email,
auth: _token,
key: config.key,
dead: ( config.permanently === true ) ? "1" : ""
}),
"' "
].join( "" );
log( "_deleteNote", query );
function __cbSuccess( result ) {
config.success( $.trim( result.response ) );
}
_queryYQL( "_deleteNote", query, __cbSuccess, config.error, this );
} // _deleteNote
// ============ PUBLIC METHODS & PROPERTIES ================================
/**
* Authenticates the client. The request is made asynchronously via YQL.
* Throws an exception if one of the arguments is missing or empty.
*
* @method auth
* @param config.email {String} SimpleNote account email address
* @param config.password {String} SimpleNote account password
* @param config.success {Function} callback function to be called on
* successful authentication (optional)
* @param config.error {Function} callback function to be called on
* failure, is passed a plain text error string (optional)
*/
this.auth = function( config ) {
_authenticate( config );
};
/**
* Returns an index of all notes. This method will return a JSON object with
* three main properties for each note: `key`, `modify`, and `deleted`. Some
* notes may be marked `deleted`; these notes will be removed permanently
* the next time the client synchronizes with the server.
*
* Throws an exception if one of the arguments is missing or empty.
*
* @method retrieveIndex
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the array containing the
* notes index
* @param config.error {Function} callback function to be called on
* failure, is passed a plain text error string
*/
this.retrieveIndex = function( obj ) {
_retrieveIndex( obj );
};
/**
* Returns notes containing a particular term. Throws an exception if one of
* the arguments is missing or empty.
*
* This method will return a JSON object with two keys, `notes` (containing
* an array with found notes) and `totalRecords` (showing the total number of
* search results).
*
* Each note is represented as an object containing two keys, `key` (the note
* ID) and `body` (the note string).
*
* @method searchNotes
* @param config.query {String} search string
* @param config.maxResults {Int} (optional): max number of results
* (default: 10)
* @param config.offset {Int} (optional) index offset
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the array containing the
* notes index
* @param config.error {Function} callback function to be called on
* failure, is passed a clear text error string.
*/
this.searchNotes = function( obj ) {
_searchNotes( obj );
};
/**
* Retrieves and returns a single note as a hash in the following form:
*
* {
* body: "my example note",
* key: "agtzaW1wbG0LCxIETm90ZRjoBAw",
* modifydate: "2008-12-18 04:04:20.554442",
* createdate: "2008-12-18 04:04:20.554442",
* deleted: false
* }
*
* Throws an exception if one of the arguments is missing or empty.
*
* @method retrieveNote
* @param config.key {String} the note ID
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the note hash
* @param config.error {Function} callback function to be called on
* failure, is passed a clear text error string.
*/
this.retrieveNote = function( obj ) {
_retrieveNote( obj );
};
/**
* Creates a new note. Returns the new note ID. Throws an exception if one
* of the arguments is missing or empty.
*
* @method createNote
* @param config.body {String} the note body
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the note ID string
* @param config.error {Function} callback function to be called on
* failure, is passed a clear text error string.
*/
this.createNote = function( obj ) {
_createNote( obj );
};
/**
* Updates an existing note. Returns the note ID on success. Throws an
* exception if one of the arguments is missing or empty.
*
* @method updateNote
* @param config.key {String} the ID of the note to update
* @param config.body {String} the note body
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the note ID string
* @param config.error {Function} callback function to be called on
* failure, is passed a clear text error string.
*/
this.updateNote = function( obj ) {
_updateNote( obj );
};
/**
* Deletes an existing note. Throws an exception if one of the arguments is
* missing or empty.
*
* @method deleteNote
* @param config.key {String} the ID of the note to delete
* @param config.permanently {Boolean} set to `true` if you want to not
* only mark the note as deleted on the server but to delete it
* right away (see SN API docs)
* @param config.success {Function} callback function to be called on
* success; the callback will be passed the note ID string
* @param config.error {Function} callback function to be called on
* failure, is passed a clear text error string.
*/
this.deleteNote = function( obj ) {
_deleteNote( obj );
};
/**
* Returns a boolean showing whether the user is currently logged in or not.
*
* @method isLoggedIn
* @return {Boolean}
*/
this.isLoggedIn = function() {
return _isLoggedIn();
};
/**
* Returns auth details, i.e. an object containing the current email address
* and auth token returned by the API after a successful login.
*
* @method getAuthDetails
* @return {Object} Auth info.
*/
this.getAuthDetails = function() {
return {
token: _token,
email: _email
};
};
/**
* Sets the Open Data table used in all YQL requests. Usually, you'll want
* to put the Open Data table XML file (see `yql_simplenote.xml`) on a server
* controlled by you so you have full control over it. If you do that, you
* can tell `SimpleNote` to use it by setting the new URL with this method.
*
* @method setOpenDataTable
*/
this.setOpenDataTable = function( url ) {
_yqlTableURL = url;
};
/**
* Returns the Open Data table used in all YQL requests.
*
* @method getOpenDataTable
* @return {String} Open Data Table URL.
*/
this.getOpenDataTable = function() {
return _yqlTableURL;
};
/**
* Enables console output of debugging messages.
*
* @method enableDebug
* @param bool {Boolean} Enable/disable debugging.
*/
this.enableDebug = function( bool ) {
_debugEnabled = !!bool;
};
/**
* Returns the simplenote-js version number.
*
* @property VERSION
* @type String
* @final
*/
this.VERSION = _version;
/*
this.debug = {
showToken: function() {
return $.param({
auth: _token,
email: _email
});
}
};
*/
}