31 mayo, 2009

Wave reinventa la comunicación en la red

Hace unos meses me gustaba seguir a Jon Udell al respecto de posibles herramientas que permitiesen evolucionar el omnipresente correo electrónico martirizado por el spam. Proyectos como Zoë, Yammer o Groove han intentado ya mejorar ese gigante.

Google Wave es una nueva idea experimental y abierta que pretende rediseñar el futuro de la comunicación en tiempo real sobre la web. "Es lo que parecería el correo electrónico si se inventase hoy".

Wave es a partes iguales conversación y documento. Es posible comunicarse y trabajar juntos sobre texto con formato, fotos, vídeos, mapas y, prácticamente, cualquier cosa que pueda representarse sobre un navegador. Un documento wave puede compartirse. Puede responderse desde cualquier lugar en el que el mensaje esté publicado (por ejemplo un artículo en blogger, en el que los comentarios se introducen automáticamente en la conversación), editar su contenido o añadir nuevos participantes en cualquier momento. Y puede reproducirse su evolución para ver cómo ha ido evolucionando el documento a lo largo del tiempo, viendo quién ha dicho qué y cuando. Además permite la transmisión de información en tiempo real, de forma que puede verse cómo los demás teclean, agilizando las conversaciones y facilitando enormemente el trabajo sincronizado.

Wave ha sido diseñado como un estándar similar al e-mail, es abierto, y puede ser usado en cualquier servidor. Y puesto que el estándar define un protocolo, distintos servidores pueden entenderse entre ellos aunque no pertenezcan a Google.

Aunque aún no está disponible, en la presentación se muestra cómo Wave consigue mejorar enormemente el correo electrónico y la mensajería instantánea, los dos pilares básicos sobre los que se asienta. Y construye sobre ellos una plataforma de innovación totalmente abierta en la decenas de detalles e ideas han sido implementados para mejorar la experiencia de los usuarios cuando quieren comunicarse. Por ejemplo, contestar un mensaje es tan simple como escribirlo en el área prevista para ello (no hay que pulsar un botón y esperar que la aplicación cargue un formulario... simplemente "ya está ahí"); o la implementación de múltiples atajos basados en "arrastrar y soltar" convierten la experiencia en mucho más intuitiva. Aunque no deja de recordar a un cliente de correo electrónico tradicional pero optimizado, puede convertirse en un chat de forma transparente si los usuarios están conectados (de hecho los mensajes los ve el otro lado conforme se teclean, aunque esta opción puede desactivarse).

Cosas que pueden hacerse con wave:

  • reproducir la conversación paso a paso para que alguien incorporado pueda entender completamente el tema
  • mensajes privados dentro de la conversación que sólo ven los interesados
  • compartición rápida de archivos/fotos
  • aprovecha Gears si está instalado para permitir algo tan intuitivo como arrastrar y soltar archivos desde el escritorio
  • funcionalidades basadas en contactos/robot como "bloggy" para publicar la conversación, de forma que el mismo artículo del blog se convierte en parte de la conversación: cualquier comentario allí se incorpora automáticamente, convirtiendo la conversación en pública. O un traductor simultáneo.
  • edición y control de cambios al estilo wiki (resaltados como novedad)
  • posibilidad de ver la conversación como un documento, de forma que se pueda aprovechar para la creación de documentos en equipo
  • posibilidad de trabajar con equipos sobre subdocumentos del documento original, de forma que se trate de una parte del documento principal hasta su versión definitiva, que vuelve al original y así puede ser usado o modificado por otros equipos o subdocumentos
  • posibilidad de enlazar desde la actual conversaciones simplemente arrastrando y soltando
  • todo sincronizado en tiempo real, una nueva conversación puede aparecer en los resultados de una búsqueda después de haberla lanzado
  • ortografía basada en diccionarios pero también en contexto. Especialmente espectacular: "icland is an icland" se convierte en "Iceland is an island".
  • sistema de extensiones similar al del propio navegador (instalables)
  • extensiones como realizar votaciones o juegos de red de competición (ajedrez) o colaborativos (sudoku) donde los eventos son reconocidos por wave y por tanto se puede reproducir una partida como si fuese una conversación
  • conversaciones que muestran formularios de configuración con opciones que pueden rellenar más de un usuario a la vez. Es la respuesta a la pregunta de siempre: ¿Cómo creo una encuesta rápidamente?
  • integración de otros sistemas de comunicación (email, twitter) o con sistemas de depuración de errores (bugtrack) para equipos
  • posibilidad de traducción simultánea (al vuelo) de una conversación para usuarios que hablen en distintos idiomas

Otro "detalle" es que se exige un navegador moderno con soporte de HTML5. La consecuencia inmediata es que, aunque aún no está disponible, si los internautas en masa quieren disponer de esta herramienta, deberán decir adiós a Internet Explorer a menos que Microsoft, por fin, se tome en serio su trabajo.

28 mayo, 2009

A partir de JS PlaceMaker que es un script que permite identificar lugares a partir de un texto y que devuelve objetos con su ubicación, he creado el siguiente ejemplo:

http://jsbin.com/abumi/
http://jsbin.com/uharu/

El código es sencillo:

function callback(e) {  
  if (e.match.length) e= e.match; else e=[e.match]; 
  var gmapsApi= "ABQIAAAAGoC4gjFriF0K4FcpSOhWDhTWx8njL9qYLnq46LoOVGM6mIGTuxR2yPgKzvzjHKLSfLHgjUC0vi45-Q"; 
  var imgs=''; 
  console.dir(e); 
  for (loc in e) { 
    center=e[loc].place.centroid.latitude+','+e[loc].place.centroid.longitude; 
    imgs+= '<img src="http://maps.google.com/staticmap?center='+center+'&markers='+center+'&zoom=13&size=350x250&key='+gmapsApi+'" style="float:left;"/>'; 
  } 
  document.getElementById('map').innerHTML=imgs; 
  window.called=false; 
} 
 
window.called=false; 
function changed() { 
  if (window.called) return; 
  setTimeout(function(){ 
    Placemaker.config.appID = "jPkyQyTV34Fi4qlev38PC_eXLDQYqVPkQ5N1aZDxVUSpw3MKOUWD9AHzcVkLFkM-"; 
    Placemaker.getPlaces(document.getElementById('text').value,callback,document.getElementById('locale').value); 
    window.called=true; 
    }, 
  1000); 
}

Puedes probar con el texto: "I'm Jack London and I live in Barcelona but I work at Amsterdam and my parents live in Oslo", (nótese que detecta que la palabra London no se está usando como ubicación) aunque hay un selector para indicar el lenguaje del texto introducido. La detección se produce al salir del campo de texto, aunque también se puede forzar con el botón. Las claves son para el dominio del contenedor de código jsbin.com

26 mayo, 2009

Compresión JSON con JSON.hpack

JSON.hpack es un compresor de conjuntos de datos JSON. Es capaz de reducir hasta un 70% el número de bytes necesarios para representar una colección genérica y homogénea (entendiendo por homogénea la que suele ser producida por una consulta a una base de datos relacional en la que se repite una serie de filas con el mismo número de campos).

Así como un resultado con XML como el siguiente...

  <result>
    <item>
      <name>Andrea</name>
      <age>31</age>
      <gender>Male</gender>
      <skilled>true</skilled>
    </item>
    <item>
      <name>Eva</name>
      <age>27</age>
      <gender>Female</gender>
      <skilled>true</skilled>
    </item>
    <item>
      <name>Daniele</name>
      <age>26</age>
      <gender>Male</gender>
      <skilled>false</skilled>
    </item>
  </result>
  <!-- 286 caracteres (sin contar espacios) -->

... puede ser compactado con JSON a la cadena siguiente...

[
  {
    "name":"Andrea",
    "age":31,
    "gender":"Male",
    "skilled":true
  },
  {
    "name":"Eva",
    "age":27,
    "gender":"Female",
    "skilled":true
  },
  {
    "name":"Daniele",
    "age":26,
    "gender":"Male",
    "skilled":false
  }
]
// 177 caracteres (sin contar espacios)

... podemos ver fácilmente que JSON admite una compresión aún mayor, puesto que cada registro repite los nombres de los campos.

El nivel 0 de compresión con JSON.hpack propone reunir los nombres de los campos en un primer registro:

[["name","age","gender","skilled"],["Andrea",31,"Male",true],["Eva",27,"Female",true],["Daniele",26,"Male",false]]
// 115 caracteres

Aún así, se repiten valores (como true, false, male y female) que podrían ser sustituidos por índices para ahorrar aún más espacio (aunque en este ejemplo con 3 registros el resultado no es mejor que el anterior):

[["name",["Andrea","Eva","Daniele"],"age","gender",["Male","Female"],"skilled",[true,false]],[0,31,0,0],[1,27,1,0],[2,26,0,1]]
// 167 caracteres

Como se puede ver en este nivel 1 de compresión, en el primer registro se indican los nombres de los campos y los posibles valores que pueden adoptar siempre que no sean numéricos (el autor cree que no vale la pena realizar la conversión de unos números por otros (índices)). Por ejemplo, Male o True se sustituyen por 0 y Female o False por 1.

Esto aún puede optimizarse más con el nivel 2 de compresión, comparando si vale la pena enumerar los valores en la cabecera y sustituirlos en los datos por índices o si es mejor mantener los datos (en el siguiente caso, los nombres no se enumeran):

[["name","age","gender",["Male","Female"],"skilled",[true,false]],["Andrea",31,0,0],["Eva",27,1,0],["Daniele",26,0,1]]
// 119 caracteres

Esta comprobación se realiza a nivel de la colección entera. Es posible realizarla a nivel de cada campo, comparando la longitud de la enumeración más los índices que serán necesarios con los de indicar directamente los valores:

// Comparando "gender":
  ["Male","Female",0,1,0] < ["Male","Female","Male"] (23 caracteres es menos que 24)
// comparando "skilled" :
  [true,false,0,0,1]      > [true,true,false]        (18 caracteres es menos que 17)

Según esto, el resultado de aplicar el nivel 3 de compresión será:

[["name","age","gender",["Male","Female"],"skilled"],["Andrea",31,0,true],["Eva",27,1,true],["Daniele",26,0,false]]
// 116 caracteres

Y aún se puede extraer un último nivel 4 de compresión comparando todos los anteriores y seleccionando el que menos ocupe. Es posible hacer pruebas en la página de ejemplos.

Aunque parece que la compresión es importante, hay que tener en cuenta que si el contenido se está comprimiendo con gzip desde el servidor, la reducción real en información enviada será mucho menor. Para conjuntos grandes de datos, sí que puede haber una diferencia importante, aunque se pague el precio del procesamiento necesario para comprimir y descomprimir los datos.
Existen librerías para JavaScript, PHP y C#, aunque según el wiki del proyecto, es posible que se amplíe a Python, Ruby o plugin PHP mediante C.

En concreto, la versión JavaScript cuenta con los siguientes métodos:

  • hpack( Array[, Int[0-4]] ):HArray, convierte una colección homogénea a otra comprimida con hpack
  • hunpack( HArray ):Array, convierte una colección hpack a su original (no es necesario indicar qué compresión se usó)
  • hbest( Array ):Int[0-4], devuelve el nivel de compresión ideal para la colección homogénea especificada

21 mayo, 2009

Mozilla Jetpack


Pese a la dificultad de escribir extensiones para Firefox y que su uso aún requiere reiniciar el navegador, lo cierto es que el sistema de extensiones ha supuesto un antes y un después en la historia de los navegadores.

Google Chrome promete cambiar de golpe ambos problemas (la dificultad de creación y el engorro de reiniciar el navegador) mediante un sistema que se basa en las herramientas que ya están disponibles en el navegador: HTML, Javascript, CSS...

Lo cierto es que la idea era demasiado buena para que los responsables de Firefox se quedasen con los brazos cruzados, por lo Mozilla Labs acaba de publicar la primera preview de Jetpack.



Jetpack es un nuevo experimento que utiliza tecnologías abiertas para mejorar el navegador, con la meta de permitir que cualquiera que pueda crear una web pueda participar en hacer que la web sea un un mejor sitio para trabajar, comunicarse y jugar. En breve, Jetpack es una API para facilitar la creación de extensiones de Firefox usando tecnologías con las que ya estás familiarizado.

Los objetivos son los siguientes:

Basado en la Web

  • HTML, Javascript, y CSS serán las únicas herramientas necesarias
  • Tener toda la potencia de la Web abierta: capacidades AJAX y multimedia incluyendo <canvas>, <audio> y <video>
  • Ligero y accesibles mediante una URL, como el resto de la Web
  • Depuración en el navegador sin reiniciarlo y usando herramientas comunes de desarrollo Web como Firebug

Seguro

  • Permite acceso sólo a los privilegios necesarios, sin temas de seguridad presentados de forma social y no técnica
  • Código breve y sencillo de revisar que asegura que los potenciales problemas de seguridad sean superficiales y los tiempos de revisión breves

Robustez

  • APIs versionadas de forma que no sea necesario actualizar y revalidar el código con cada nueva versión de Firefox
  • Ligero pero lleno de características, permitiendo que aplicaciones complejas y simples se sumen a la experiencia Web, igual que las extensiones hoy día

Extensible

  • La arquitectura permitirá la inclusión de toolkits de terceros revisados y versionados (p.e. jQuery, Dojo, etc.) y de librerías API (p.e. Twitter, Delicious, Google Maps, etc.)

Ejemplo de extensión (un notificador de Gmail):
// Inicia el componente de barra de estado

jetpack.statusBar.append({
  html: '<img src="http://mail.google.com/mail/images/favicon.ico"><span id="count"></span>',

  onReady: function(doc) {
    var gmail = new GmailNotifier(doc);
  },

  width: 20
});

// Notificador de Gmail

function GmailNotifier(doc){
  $(doc).click(this.goToInbox);
  this.update(doc);
  setInterval(function() {
    this.update(doc);
  }, 60 * 1000);
}

GmailNotifier.prototype = {

  goToInbox: function() {
    jetpack.tabs.open("http://mail.google.com");
    jetpack.tabs[ jetpack.tabs.length-1 ].focus();
  },

  update: function(doc) {
    var url = "http://mail.google.com/mail/feed/atom";
    doc = $(doc);
    $.get( url, function(xml){
      var el = $(xml).find("fullcount"); // Cuenta de mensajes por leer
      if (el) {
        var count = el.get(0).textContent;
        doc.find("#count").text( count );
      } else {
        doc.find("#count").text( "Login" );
      }
    });
  }
}
Via Ajaxian.

Addendum: Elijah Gray ha recopilado la información sobre la API que no está completa en la página oficial y la ha publicado en su blog.

13 mayo, 2009

BrowserCouch

En Day Dreaming about Web Storage, Mark Finkle trata la naturaleza de la futura API de almacenamiento de datos en el navegador. Sea finalmente SQL, como abogan algunos, o un lenguaje más coherente con la idiosincrasia de Javascript y JSON, como abogan otros, la esperada API deberá ser más bien simple para que permita un ecosistema de librerías que, al igual que los actuales frameworks como MooTools o jQuery, ofrezca diversas opciones que se adapten a todas las necesidades y gustos.

Aunque SQL está mucho más extendido, proyectos como TrimQuery demuestran que no es imprescindible que el navegador incorpore un parseador para un lenguaje con tantas versiones incompatibles entre sí como fabricantes existen.

BrowserCouch es un intento de implementar la tecnología MapReduce en el navegador. Está escrito enteramente con JavaScript con la intención de que funcione con todos los navegadores, aprovechando de forma transparente otras capacidades disponibles.

No es casualidad que esta librería trate de imitar la funcionalidad de CouchDB en el cliente, e incluso podría soportar su integración en el futuro.

Esta librería es una respuesta al artículo de Vladimir Vukicevic HTML5 Web Storage and SQL. Una API al estilo de CouchDB parece una buena solución para el almacenamiento persistente en la Web, ya que mucha de su semántica es delegada al lenguaje JavaScript, lo que lo convierte en potencialmente sencillo de estandarizar. Además, el paradigma MapReduce también saca ventaja natural de los múltiples núcleos de los microprocesadores- algo cada vez más frecuente hoy día.

Tutorial


Ésta es una breve introducción al uso de la API de BrowserCouch y del mecanismo MapReduce. Hay que remarcar que no se trata de software "maduro" ya que aún faltan muchas características de CouchDB por portar y la API no es estable.

Respecto a los ejemplos de código de este tutorial: se ejecutan en el mismo navegador y los resultados se muestran en algunos casos en esta misma página. Esto permite asegurar que el software funciona como se pretende y permite un aprendizaje interactivamente. También conviene avisar que existe la posibilidad de que alguna de los ejemplos falle.

Primeros pasos


Supongamos que queremos añadir soporte sin conexión a un blog. Para obtener una base de datos llamada blog-posts en BrowserCouch se puede usar la siguiente función:

BrowserCouch.get('blog-posts', 
  function onRetrieveCb(db) { 
    blogDb = db; /* Guarda la base de datos para luego. */ 
  }, 
  new FakeStorage()
);
Está claro que el primer parámetro es el nombre de la base de datos; el segundo es la función callback que recibirá la base de datos tras obtenerse.

El tercer parámetro especifica el motor que será usado para almacenar la base de datos entre sesiones de navegación. En este caso se usa FakeStorage, el cual almacena los datos en memoria de forma no persistente, lo que vale como ejemplo. Podríamos igualmente no especificar el tercer parámetro para que BrowserCouch averigüe el mejor motor según las capacidades disponibles.

Si la base de datos no existe, se creará una nueva. Poner nuevos artículos en la base de datos se consigue con el método put():

blogDb.put( [
  {id: 0, author: 'Myk', title: 'Burritos', content: 'Burritos are yum.'}, 
  {id: 1, author: 'Thunder', title: 'Bacon', content: 'I like bacon.'}, 
  {id: 2, author: 'Thunder', title: 'Beer', content: 'Beer is good too.'}], 
  function onDone() { 
    /* Código función... */ 
  }
);
Cada elemento que guardemos en la base de datos necesita un atributo identificador, pero a parte de eso, el elemento puede contener cualquier dato codificable como JSON.

Vistas

Ahora que tenemos datos, podemos jugar con la generación de vistas sobre los datos usando el mecanismo MapReduce. Por ejemplo, a continuación se define una vista definiendo sólo la fase de mapeo que organiza todos los títulos de artículos por autor:

blogDb.view({ 
  map: function(doc, emit) { 
    emit(doc.author, doc.title); 
  }, 
  finished: function(result) { 
    displayInElement(result, 'author-keyed-view'); 
  } 
});
El método view() tiene muchos argumentos opcionales, y esa es la razón por la que estamos pasando un único objeto con las claves que se corresponden con los nombres de los argumentos. El argumento map es la función usada en la fase de mapeo, y el argumento finished es la función callback que recibirá los resultados cuando termine el proceso.

El resultado obtenido en el elemento author-keyed-view será:

{"rows":[
  {"id":0,"key":"Myk","value":"Burritos"},
  {"id":1,"key":"Thunder","value":"Bacon"},
  {"id":2,"key":"Thunder","value":"Beer"}
]}

Como se puede ver, BrowserCouch esencialmente itera sobre todos los objetos de artículo, pasándole cada uno a la función map() junto con una función arbitraria llamada emit(). La función map() entonces decide si el par clave-valor debe aceptarse pasándolo a la función emit(). map() puede hacer tantas llamadas a emit() como desee: cada llamada generará una nueva fila en la vista.

En este punto quizás resulte interesante saltar a la sección de pruebas para jugar definiendo una función map() personalizada. Edita el código y al salir del campo de texto se actualizará el resultado.

La fase de reducción de una vista es totalmente opcional y un poco confusa. Intentemos añadir una función reduce() a nuestra vista para agrupar juntos los títulos de artículos con los autores:

blogDb.view({ 
  map: function(doc, emit) { 
    emit(doc.author, doc.title); 
  }, 
  reduce: function(keys, values) { 
    return values;
  }, 
  finished: function(result) { 
    authors = result; /* Guarda los resultados para más tarde. */
    displayInElement(authors, 'author-titles-view');
  } 
});
lo que generará el siguiente resultado:

{"rows":[
  {"key":"Myk","value":["Burritos"]},
  {"key":"Thunder","value":["Bacon","Beer"]}
]}
BrowserCouch tomará todas las filas generadas por map() y genera una nueva lista de filas clave-valor, donde el valor de cada fila es la lista de todos los valores que coinciden con la clave de la fila. Esto explica el significado del argumento values pasado a reduce().

El argumento keys es una lista de tuplas de dos elementos, el primera de los cuales es la clave y el segundo es el identificador del documento que emitió la clave durante la fase de mapeo.

La función reduce() es invocada por cada clave única, y su valor de retoro es el valor de su clave en la vista final.

Una vez que se tiene la vista, se puede usar el método findRow() de la vista para encontrar la primera fila cuya clave coincida con (o sea la más parecida a) la provista. Por ejemplo:

var rowIndex = authors.findRow('Thunder'); displayInElement(authors.rows[rowIndex], 'author-find-row-view');
Y el resultado es:
{"key":"Thunder","value":["Bacon","Beer"]}

Prueba tú

Si tienes los ojos cruzados, no te preocupes - a mucha gente le lleva bastante tiempo comprender cómo funciona MapReduce. Dicho eso, la forma más rápida de comprender su funcionamiento es jugar creando tu propia vista.

En la página del proyecto tienes un campo de texto para hacerlo. Pulsa la tecla tabuladora cuando hayas terminado de hacer cambios para lanzar la consulta y ver los resultados.

12 mayo, 2009

TaffyDB

Siguiendo el debate sobre el mecanismo a introducir en los navegadores para implementar bases de datos locales, describo aquí las características y el funcionamiento de TaffyDB: TaffyDB es una librería JavaScript open source que actua como una delgada capa para aplicaciones web 2.0 y AJaX. Sus características son:

  • Tamaño de 10KB
  • Sintaxis simple centrada en JavaScript
  • Rápida
  • Fácil de incluir en cualquier aplicación web
  • Compatible con las librerías más comunes: YUI, jQuery, MooTools, Dojo, Prototype, EXT, ...
  • Interfície CRUD (Crear,Leer,Actualizar,Eliminar)
  • Ordenación
  • Bucles
  • Consultas avanzadas

Introducción rápida

Creando colecciones

Para crear una colección Taffy hay que pasar un array de objetos similares como el que devuelve un servicio web JSON, por ejemplo cuatro amigos:
var amigos = new TAFFY(
[
{nombre:"Pepe",
  genero:"H",
  casado:"No",
  edad:25,
  ciudad:"MAD",
  comidas_preferidas:["pizza","tacos"]},
 {nombre:"Eva",
  genero:"M",
  casado:"No",
  edad:29,
  ciudad:"BCN",
  comidas_preferidas:["ensalada","palitos de queso"]},
 {nombre:"Paco",
  genero:"H",
  casado:"No",
  edad:29,
  ciudad:"VLC",
  comidas_preferidas:["pizza","hamburguesas","BLTs"]},
 {nombre:"Sara",
  genero:"M",
  casado:"No",
  edad:21,
  ciudad:"ZAZ",
  comidas_preferidas:["pizza","sushi"]}
  ]
)
También acepta una cadena de texto JSON por evaluar. Siempre devolverá una colección de métodos para trabajar sobre el conjunto recibido.

Encontrar

Para ver los amigos que tienen más de 22 años:
amigos.find({edad:{greaterthan:22}});
Para realizar una consulta se debe llamar al método find con un objeto como parámetro para indicar las condiciones de filtrado a aplicar. Es similar a la cláusula where de SQL. Devuelve un array de índices a los objetos que cumplan las condiciones. Para encontrar a los amigos que vivan en Valencia, Madrid o Zaragoza:
amigos.find({ciudad:["VLC","MAD","ZAZ"]});
Que sería similar al operador IN de SQL.

Actualizar

Cómo actualizar la colección para reflejar que Pepe ahora vive en Bilbao y se ha casado:
amigos.update(
 {
 ciudad:"BIO",
 casado:"Yes"
 },
 {
 nombre:"Pepe"
 }
);
El método update recibe un objeto con los datos que hay que cambiar y otro objeto (cláusula WHERE) con las condiciones de filtrado para encontrar los objetos sobre los que hay que aplicar los cambios. También es posible indicar el índice del objeto a modificar o directamente el resultado de una búsqueda (si no se indica un segundo parámetro, las modificaciones afectarán a todos los objetos de la colección):
amigos.update({ciudad:"BIO",casado:"Yes"},1);
amigos.update(
 {
 ciudad:"BIO",
 casado:"Yes"
 },
 amigos.find(
  {nombre:"Pepe"}
  )
 );

Insertar

El método insert acepta un objeto a insertar o un array de objetos:
amigos.insert(
 {nombre:"Brian",
 genero:"H",
 casado:"No",
 edad:52,
 ciudad:"IBZ",
 comidas_preferidas:["fruta","chuleta"]
 });

Eliminar

El método para eliminar registros se llama remove dado que delete es una palabra reservada. Acepta como parámetro un objeto con las condiciones de filtrado para seleccionar los objetos a borrar:
amigos.remove({nombre:"Brian"});

Ordenar

Para ordenar hay que indicar un array con los campos de ordenación, indicando el nombre del campo si es ascendente o con la expresión {"campo":"desc"} si es descendente. Por ejemplo, para ordenar por edad ascendente y por nombre alfabético pero descendente:
amigos.orderBy(["edad",{"nombre":"desc"}]);
También es posible ordenar de forma lógica indicando como parámetro "logical" o "logicaldesc" de la misma forma como se indica "desc" para los descendentes. El método orderBy también acepta un segundo parámetro opcional con una función JavaScript como la que acepta la array.sort().

ForEach

Es un método que permite aplicar una función a cada elemento de una colección:
amigos.forEach(function (f,n) {alert(f.nombre)});
El primer parámetro de la función es el objeto y el segundo el índice del objeto dentro de la colección. Opcionalmente se pueden indicar condicionantes de filtrado para reducir el conjunto de objetos sobre el que se aplica la función:
amigos.forEach(
 function (f,n) {alert(f.nombre);},
 {comidas_preferidas:{has:"pizza"}}
);
Es posible modificar un registro, devolviendo una copia del objeto recibido desde la función. Si no se devuelve nada, el registro no se modifica. Por ejemplo, para incrementar la edad de todos los amigos:
amigos.forEach(
    function (f,n) {f.edad = f.edad+1; return f;}
);

Get/First/Last

Para obtener un conjunto de objetos que cumplan una condición de filtrado, se usará get(); first() para obtener el primer objeto que cumpla la/s condición/es, y last() para obtener el último. Si no se especifica el parámetro, las mismas funciones devolverán todos los objetos, o el primero y último de la base de datos.

amigos.get({nombre:"Pepe"});
amigos.first({nombre:"Pepe"});
amigos.last({nombre:"Pepe"});

Stringify

Funciona como get() pero devuelve el resultado como una cadena de texto JSON preparada para ser usada en servicios web / AJaX.

Templates (patrones)

Se pueden usar patrones para añadir valores por defecto a una colección y minimizar el código escrito. Un patrón es un objeto que será usado como base para nuevas inserciones. A menos que el nuevo registro sobreescriba los valores indicados en el patrón, TaffyDB usara los valores del patrón para rellenar el registro.

Para preparar un patrón llamar a collection.config.set("template",{}). Para eliminarlo se llamará a collection.config.set("template",null). Un patrón se aplicará automáticamente a cada nuevo registro de la colección.

Este ejemplo añade un patrón a la colección de amigos y establece unos valores por defecto para el teléfono y el correo electrónico. Cualquier amigo que ya tuviese definidos dichos valores no se vería afectado por el patrón:

amigos.config.set("template",
 {email:"none",
 telefono:"none"}
);
También se puede usar collection.applyTemplate para aplicar un patrón a un subconjunto de los registros. El patrón se aplicará en este caso a los registros actuales, pero no a las futuras inserciones o cambios aunque cumplan la/s condición/es indicadas:

amigos.applyTemplate(
 {"conyugue":"desconocida"},
 {casado:"Yes"}
);
Nota: es posible recuperar el patrón impuesto a una colección mediante collection.config.get("template").

Eventos

Hay tres eventos que pueden usarse:
  • onInsert(nuevoObj) - se dispara cada vez que se inserta un nuevo registro.
  • onUpdate(modificadoObj,originalObj) - se dispara cada vez que un registro es actualizado.
  • onRemove(eliminadoObj) - se dispara cuando un nuevo registro es eliminado.

Ejemplo:
amigos.onRemove = function (r) {
 alert(r.nombre + " ha sido eliminado");
};
amigos.remove();

Consultas avanzadas

Ya que casi todos los métodos de TaffyDB incluyen condiciones de filtrado, conviene comprender sus posibilidades:

Usando el método de búsqueda detallado arriba, es posible pasar varios filtros para reducir los resultados:

amigos.find({ciudad:["VLC","BCN","MAD"],
  edad:{greaterthan:22}}); //mayorque
Aquí se recibirán los amigos de las tres ciudades que tengan más de 22 años. Agregar filtros a una consulta es equivalente a usar un AND en un WHERE de SQL.

Nota: es posible invertir la lógica de filtrado añadiendo el signo ! antes del nombre, que sería similar al operador != en el SQL estándar. Esto se aplica a todos los tipos de filtrado.
Ejemplo (encontrar amigos que no sean de Madrid):
amigos.find({ciudad:{"!is":"MAD"});
Hay 17 formas de filtrar una colección:
  • equal (default) (equivalente: is) // Colección que coincida con un texto o número indicado.
  • startswith (equivalente: starts) // Colección que empieza por el texto indicado.
  • endswith (equivalente: ends) // Colección que termina con el texto indicado.
  • greaterthan (equivalente: gt) // Colección mayor que el número indicado.
  • lessthan (equivalente: lt) // Colección menor que el número indicado.
  • has // Colección que contiene el objeto, texto, o clave indicado.
  • hasAll // Colección que contiene todos los objetos, textos o claves definidas en el array indicado.
  • regexppass (equivalente: regex) // Colección que cumple una expresión regular.
  • like // Colección que incluye el texto indicado.
  • notlike // Colección que no incluye el texto indicado.
  • isSameObject // El valor del objeto colección coincide con el objeto indicado.
  • isSameArray // El valor de array de la colección coincide con el array indicado.
  • length // La longitud del valor de la colección coincide con el número indicado (leer a continuación).

Usando los filtros de longitud

Usando el filtro de longitud es posible encontrar cadenas y arrays basándose en su longitud. Las forma simple es con una coincidencia de uno a uno:

friends.find({state:{length:15});

La otra forma es utilizar uno de los métodos de arriba. Es una buena forma de encontrar longitudes por encima o por debajo de un valor:

friends.find({state:{length:{gt:10}});
Consultas basadas en tipos

La version 1.4 de TaffyDB introdujo un número de métodos para filtrar que se basan en el tipo. Ya que TaffyDB permite almacenar cualquier tipo de datos en cualquier columna, los métodos pueden usarse para filtrar por una columna multi-tipo. Las opciones para filtrar por tipo son:

  • isString // La colección es una cadena.
  • isNumber // La colección es un número.
  • isArray // La colección es un array.
  • isObject // La colección es un objeto.
  • isBoolean // La colección es a boolean (true/false).
  • isFunction // La colección es function.
  • isNull // La colección es null.
  • isUndefined // La colección es "undefined".
  • isNumeric // La colección contiene sólo números.
  • isTAFFY // La columna de colección es una colección de TaffyDB.

Para usar estos filtros se necesitará pasar un valor "true" o "false" contra el que comparar:

amigos.find({ciudad:{isArray:true}});
Construyendo consultas eficientes

Además de poder filtrar de distintas formas y combinar los filtros para llevar a cabo consultas más complejas, también se pueden optimizar las consultas pasando al método de consulta una lista con los índices contra los que ejecutar la consulta en lugar de tener que hacerlo sobre la colección entera:

amigos.find({ciudad:["MAD","BCN","VLC"],
  edad:{lt:50}},
  [0,2]);
Cada llamada al método de consulta requerirá que cada registro de la colección sea comprobado al menos una vez. Para consultas complejas y colecciones grandes, esto puede requerir mucho trabajo con bucles. TaffyDB reduce automáticamente los resultados conforme va filtrando (con el segundo filtro siendo aplicado sólo a aquellos registros que han pasado el primero, y así sucesivamente). Pero a veces eso no es suficiente. En el ejemplo anterior ya limitamos los resultados indicando un array de índices. Esta es una forma de evitar que TaffyDB recorra la colección completa si ya se conocen los registros sobre los que se va a actuar.

07 mayo, 2009

jLinq, el LINQ de Javascript

jLinq es una versión Javascript del popular lenguaje LINQ (Language Integrated Query) de Microsoft que permite consultas nativas semejantes a las de SQL en los lenguajes .NET.

En este caso, jLinq ataca a arrays de objetos Javascript en memoria y permite realizar consultas sobre ellos con una sintaxis basada en funciones encadenadas (como jQuery), pero que contiene todos los componentes para seleccionar y filtrar que ofrece SQL. Por ejemplo:

var results = jLinq.from(data.users)
    .startsWith("first", "a")
    .or("j").or("m")
    .orEndsWith("y")
    .orderBy("admin","age")
    .select();
Es fácil identificar la función from() (indica el array de objetos sobre el que se realizará la consulta), la función orderBy() (ordena los resultados a partir de los campos indicados) y la función select() (devuelve los datos filtrados hasta el momento). El resto de funciones emulan las posibilidades que SQL ofrece en la cláusula "where".

En concreto, el ejemplo anterior obtiene los usuarios cuyo nombre(first) empiece por "a", "j" o "m" o que termine por "y", y los ordena por los campos administrador(admin) y edad(age).

La demo de jLinq incluye un completo tutorial para aprender sus posibilidades paso a paso, permitiendo probar online los resultados de las consultas de ejemplo. Algunos apuntes:

  • Por defecto se entiende que las funciones de filtrado se unen por ANDs (y lógico) a menos que se especifique expresamente la función or().
  • Si queremos obtener una lista de los usuarios que son administradores junto con los que no lo son pero tienen permiso de borrado, NO podemos hacer lo siguiente ya que todo se evalua a la vez y la consulta no devolvería nada:
var results = jLinq.from(data.users) 
.is("admin") 
.isNot("admin") 
.contains("permissions", "delete") 
.select(); 
La forma correcta es:
var results = jLinq.from(data.users)  
.is("admin") 
.orCombine(function(q) { 
q.isNot("admin") 
.contains("permissions", "delete"); 
}) 
.orderBy("admin") 
.select();
  • Una forma de obtener los usuarios que empiecen por a,b,c,d,e y ordenarlos inversamente:
jLinq.from(data.users).startsWith('first', ['a','b','c','d','e']).orderBy("-first").select();
  • Para el típico join de dos tablas (aquí data.users y data.locations):
var results = jLinq.from(data.users) 
.join(data.locations, //el array fuente 
"location", //el alias a usar (cuando se haga el join) 
"locationId", // el id de localización del user 
"id" // el id de localización 
) 
.select(function(r) { 
return { 
fullname:r.first + " " + r.last, 
city:r.location.city, 
state:r.location.state 
}; 
}); 
Básicamente realiza una consulta en la tabla data.locations uniendo el campo locationId de data.users con el campo id de data.locations. También se puede observar como seleccionar los campos a visualizar dentro de la función que se pasa como parámetro al select).
  • Los comandos disponibles son:
    • especificación de origen: from
    • acciones: and, andNot, combine, debug, ignoreCase, not, or, orCombine, orNot, useCase
    • consultas: con 2 parámetros between, betweenEquals, con 1 parámetro contains, empty, endsWith, equals, greater, greaterEquals, is, isNot, less, lessEquals, match, startsWith, y where.
    • selección: all, any, at, count, distinct, each, first, groupBy, join, last, none, orderBy, select, skipTake, take, toTable
Sin duda, supone una forma programática de conseguir lo mismo que con SQL y evitar el proceso de parseado. Pero el afán por seguir el estándar SQL, aunque hay que decir que está muy extendido, impide ver las posibilidades que el propio Javascript puede ofrecer para este tipo de trabajo. Sigue resultando más claro leer una simple consulta SQL. El objetivo sería conseguir la claridad de SQL aprovechando la flexibilidad que Javascript ofrece. Ahí creo que CouchDB va mejor enfocado, aunque la claridad deja que desear.

04 mayo, 2009

JSONDB

Hace muy poco presenté Interactive CouchDB, un experimento para trabajar con una emulación de CouchDB en el navegador. También propuse crear un sistema alternativo de consulta sobre SQLite que aprovechase esa base de datos pero implementase un lenguaje de consulta más orientado a la idiosincrasia de JavaScript. Si bien, JSONDB no es exactamente una interfície a SQLite, sí que propone un lenguaje que aprovecha JSON y permite aprovechar la flexibilidad de JavaScript. Traduzco:

Por Lloyd Hilaiel

Estado

(En este momento, este documento es un hack, un trabajo en desarrollo, y no debe tomarse en serio. Si sobreestimas esto, dejarás de gustar a tus amigos, tu caballo huirá, y alguien podría robar tus limpiaparabrisas traseros).

Resumen

Conforme las tecnologías de los navegadores maduran y evolucionan, hemos visto una nueva horda de tecnologías y APIs disponibles para el Javascript basado en los navegadores. Un área en particular estado de fluidez es el almacenamiento en el lado del navegador o cliente. Las propuestas en HTML5 van desde el almacenamiento de clave/valor hasta el SQL en el cliente, y mientrastanto, varios fabricantes han empezado ya a implementar distintos mecanismos.

En la actualidad, SQL (y específicamente SQLite) ha sido el ganador de facto del debate sobre el almacenamiento en local. Varios expertos en la comunidad han expresado sus dudas sobre si SQL, aún siendo una elección robusta (dada la maravillosa y gratuita librería SQLite), es necesariamente la elección correcta.

Esta página es una especificación e una API de base de datos JSON para el navegador, que está basada principalmente en el trabajo realizado por el proyecto CouchDB. La meta de esta especificación es explorar una alternativa a SQL en el cliente.

Visión general

JSONDB propone una API asíncrona y un esquema para el almacenamiento orientado a documentos en el cliente. Mientras que JSONDB toma prestadas muchas ideas presentes en CouchDB, se han adaptado al entorno del cliente. Las siguientes características han sido propuestas:
  • Almacenamiento desestructurado, basado en documentos
  • soporte para cualquier número de bases de datos
  • ámbito de la base de datos limitado al dominio (p.e. darkness.com no puede acceder a los datos de light.org)
  • búsqueda de texto completo
  • sistema de vistas ligeras que facilitan un acceso ordenado a los datos

Los "documentos" no son más que objetos jerárquicos en Javascript. Los documentos pueden tener un tamaño y anidamiento arbitrarios. Todas las propiedades de documentos que empiecen con una barra baja _ están reservadas para su uso por la implementación de la base de datos. La propiedad _id de un documento especifica su identificador único. Un cliente podría establecer explícitamente su _id al añadir un documento a la base de datos, y que será utilizado si resulta ser único. En caso de que _id no esté definido, la base de datos asignará automáticamente un identificador a cada documento en cuanto sea guardado.

Las "vistas" son un mecanismo simple que hace posible la ordenación de documentos basada en elementos hijos específicos. Las vistas en JSONDB son mucho más simples (y menos potentes) que las de CouchDB.

JSONDB


JSONDB es un objeto global de javascript. Provee 5 métodos: open, drop, info, list y getView. El método JSONDB.open() devuelve (via una función callback aportada por el cliente) un manejador (handle) de base de datos (referido como Dbh en este documento). Un manejador de base de datos provee 4 métodos: get, put, delete, copy y all.

Métodos de gestión de la base de datos

JSONDB.open() // abre o crea una base de datos
void JSONDB.open ( nombre, fn, [ crear ] )
Parámetros:
string nombre - Nombre de la base de datos a crear
function fn - Función callback a la que se pasará un objeto si la llamada tiene éxito o null si hay algún error.
boolean crear - Si es true la base de datos será creada si no existe

JSONDB.drop() // borra una base de datos o vista
void JSONDB.drop ( nombre, [ fn ] )
Parámetros:
string nombre - El nombre de la base de datos o vista a eliminar
function fn - Función callback opcional a la que se pasará un boolean el resultado de la operación

JSONDB.info() // obtener información sobre una base de datos
void JSONDB.info ( nombre, fn )
Parámetros:
string nombre - El nombre de la base de datos de la que extraer información
function fn - Función callback a la que se pasará el objeto con la información solicitada

JSONDB.list() // muestra una lista con todas las bases de datos y vistas
void JSONDB.list ( fn )
Parámetros:
function fn - Función callback a la que se pasará una lista de cadenas con los nombres de las bases de datos disponibles

JSONDB.getView() // crea o abre una vista de base de datos. La vista existirá en el mismo espacio de nombres que las bases de datos, por lo que un nombre único es necesario. Esta función devolverá, mediante callback, un manejador o handle de sólo lectura (por los que los métodos .all() y .get() estarán disponibles, pero no así .put() ni .delete()) o bien devolverá null en caso de problemas
void JSONDB.getView ( nombre, base, clave, fn )
Parámetros:
string nombre - El nombre de la vista a recuperar o crear
string base - El nombre de la base de datos contra la que se creará esta vista
string clave - Un camino a la entidad usada como clave de ordenación
function fn - Función callback que recibirá una lista de cadenas con los nombres de las bases de datos disponibles

Métodos de gestión de documentos

Dbh.get() // obtiene un documento específico a partir de su identificador
void Dbh.get ( id, fn )
Parámetros:
string id - El identificador del documento a recuperar
function fn - Función ca[]llback

Dbh.put() // inserta o actualiza un documento
void Dbh.put ( obj, fn )

Dbh.delete() // elimina un documento
void Dbh.delete ( [string | obj], fn )

Dbh.all() // lista elementos en la base de datos o vista
void Dbh.all ( obj, fn )

Seguridad

Inicialmente cada dominio será un espacio de nombres (namespace) de forma que cada dominio tenga un conjunto privado de bases de datos en el navegador. Finalmente podría ser deseable relajar esta restricción, permitiendo algún mecanismo para la compartición segura de bases de datos entre dominios. Hay al menos dos casos distintos, primero cuando un sitio está dividido entre varios dominios, y segundo cuando se desee usar el almacenamiento en el cliente como una especie de cache para el intercambio de documentos entre dominios. Me parece curioso el segundo caso, pero no estaría interesado en el último.

Utilización de disco

Actualmente en HTML5 debe haber mecanismos incorporados para evitar un uso excesivo y no monitorizado del disco desde código javascript no verificado, y permitir que una aplicación especifique de antemano una estimación aproximada del espacio necesario para reducir el número de solicitudes de permiso al usuario para aumentos de tamaño.

Créditos

  • Leí por primera vez la idea de una base de datos JSON en el cliente en lugar de SQLite a Douglas Crockford hará un año.
  • La mayoría del diseño de esta especificación está inspirado por CouchDB.

Fuente: JSONDB

02 mayo, 2009

YQL Execute; select a from Internet; response.object=<item>a</item>

YQL es la nueva interfície de servicios web XML/JSONP de Yahoo!. Se parece a SQL y aporta como novedad YQL Execute, que es el soporte de JavaScript como lenguaje de procedimientos almacenados (una elección perfecta) por lo que ahora es posible usar JavaScript y E4X (la extensión del lenguaje para trabajar con XML nativamente) para extraer información de otras páginas web y consultar, filtrar o unir los resultados con YQL.

El lema de YQL de convertir Internet en una base de datos da un paso más hacia convertirse en una herramienta imprescindible capaz de crear con muy poco esfuerzo auténticos mash-ups de información: desde la obtención de datos de varias fuentes hasta una presentación personalizada de los resultados. Su estándar Open Data Tables ofrece unas guías para que se pueda publicar información y que sea fácilmente consultable mediante YQL sin necesidad de procesarla previamente. Pero si los datos que necesitamos no siguen el estándar, con YQL Execute se ofrece una flexibilidad extrema para procesar documentos HTML de Internet con Javascript y obtener justo lo que se necesita.

Los datos pueden ser accedidos de forma segura, combinados, filtrados, convertidos, retorcidos y, finalmente, generar un resultado en el formato óptimo para que las aplicaciones web se encuentren con el trabajo hecho y resulten muy fáciles de escribir.

Ejemplo:

<?xml version="1.0" encoding="UTF-8"?>
<table xmlns="http://query.yahooapis.com/v1/schema/table.xsd">
  <meta>
    <sampleQuery>select * from {table} where a='cat' and b='dog';</sampleQuery>
  </meta>
  <bindings>
    <select itemPath="" produces="XML">
      <urls>
        <url>http://fake.url/{a}</url>
      </urls>
      <inputs>
        <key id='a' type='xs:string' paramType='path' required="true" />
        <key id='b' type='xs:string' paramType='variable' required="true" />
      </inputs>
      <execute><![CDATA[
        // Your server-side javascript goes here
        response.object = <item>
          <url>{request.url}</url>
          <a>{a}</a>
          <b>{b}</b>
        </item>;
      ]]></execute>
    </select>
  </bindings>
</table>

El corazón de este ejemplo es el elemento "execute", que acepta varios valores de entrada del motor YQL (tal y como se indica en los elementos "input") y produce los datos a devolver. En el ejemplo, se crea un documento XML simple usando E4X y devolviéndolo con los valores "a" y "b" a partir de la sentencia YQL.

Para usar una sola tabla open data en YQL es necesario anteceder una sentencia use antes de ejecutar la consulta sobre la tabla. Por tanto, enviar la siguiente sentencia YQL a nuestro servicio importará la tabla de arriba y la ejecutará:

use "http://yqlblog.net/samples/helloworld.xml";

select * from helloworld where a="cat" and b="dog";

Puede verse el resultado aquí. Otros ejemplos en el blog de Yahoo! Developer. Via Ajaxian.

Últimos links en indiza.com