Nuestra Comunidad Artículo invitado proviene de Joel Andrews. Joel es un desarrollador políglota que vive en la lluviosa costa oeste de Canadá. Desempeña diversas funciones en Kashoo, como desarrollador backend, administrador de bases de datos, DevOps, propietario de producto y, ocasionalmente, desarrollador frontend web y Android. Siente un amor profundo y duradero por Couchbase (especialmente Couchbase Móvil), lo que le hace totalmente imparcial a la hora de debatir los pros y los contras de cualquier solución de almacenamiento de datos.
Al construir cualquier sistema con componentes de cliente y servidor, es fundamental validar los datos que el cliente intenta almacenar en el servidor y asegurarse de que sólo los autorizados pueden verlos o modificarlos. De lo contrario, un cliente malintencionado puede aprovecharse de su sistema para hacer todo tipo de cosas desagradables. Por eso, cuando en Kashoo empezamos a usar Couchbase Mobile y Sync Gateway para sincronizar datos entre múltiples dispositivos móviles y nuestra aplicación web, construimos nuestras funciones de sincronización para realizar una validación exhaustiva de todas las propiedades de los documentos y los permisos (es decir, canales) que serían necesarios para leerlos, crearlos, reemplazarlos y eliminarlos.
La flexibilidad de API de la función de sincronización de Sync Gateway puede ser tanto una bendición como una maldición. Nos dimos cuenta de que, cuando empezábamos con un cubo sencillo que sólo tenía unos pocos tipos de documentos, no era demasiado difícil mantener las cosas organizadas y fáciles de mantener a la vez que se realizaba una buena cantidad de validación. Sin embargo, a medida que añadíamos más y más tipos de documentos, el tamaño y la complejidad de nuestra función de sincronización se descontrolaban rápidamente. Escribir una función de sincronización que realice una validación exhaustiva se vuelve cada vez más inmanejable cuantos más tipos de documentos añadas... es decir, a menos que empieces a aplicar alguna estructura al código.
Esto nos llevó a dividir la función de sincronización de nuestra base de datos Sync Gateway en cuatro fases principales:
-
Averiguar el tipo de documento
-
Compruebe que el usuario pertenece a uno de los canales necesarios para realizar la operación
-
Validar cada una de las propiedades del documento
-
Asignar canales al documento
Esto funcionó bien hasta que introdujimos una segunda base de datos Sync Gateway. Dado que las funciones de sincronización se definen como un único bloque de código sin capacidad para cargar dinámicamente bibliotecas externas, duplicamos la estructura básica que habíamos creado para la función de sincronización de la primera base de datos en la función de sincronización de la segunda base de datos. Sin embargo, como desarrolladores, esta duplicación de código no nos gustó porque significaba que, a medida que mejorábamos una función de sincronización, teníamos que trasladar manualmente esos cambios a la otra o bien se separaban gradualmente en términos de funcionalidad.
Por lo tanto, decidimos evolucionar el marco en bruto para convertirlo en una herramienta independiente que pueda compartirse entre bases de datos de Sync Gateway sin necesidad de duplicar el código. Dado que la API de la función de sincronización de Sync Gateway no permite el uso de bibliotecas externas, adoptamos el enfoque de la generación de código en lugar de la vinculación dinámica: usted escribe sus definiciones de tipo de documento como objetos JavaScript y luego utiliza la utilidad para importar esas definiciones en una plantilla pregenerada para crear una función de sincronización cohesiva y autónoma.
Ahora disponíamos de una forma de definir nuestros tipos de documento (o "esquema") de una manera muy declarativa que se asemejaba más a escribir una configuración que a escribir código, y en la que las definiciones de los tipos de documento podían mantenerse con total independencia del código que realmente realizaba la autorización, la validación y la asignación de canales. Esto eliminó gran parte de la duplicación de código que se requería anteriormente. Y nos funcionó muy bien. De hecho, funcionó tan bien que decidimos liberar y apoyar la utilidad como software de código abierto para que otros no tengan que duplicar nuestros esfuerzos. La utilidad se llama synctos, y ya está disponible para su uso bajo la muy permisiva licencia MIT.
Desde su lanzamiento inicial también hemos ampliado sus capacidades, de modo que ahora ejecuta cinco fases en lugar de las cuatro originales y cada fase es significativamente más robusta:
-
Averiguar el tipo de documento
-
Autorizar al usuario determinando si
-
el usuario pertenece a uno de los canales necesarios para realizar la operación
-
el usuario pertenece a uno de los roles necesarios para realizar la operación
-
el usuario tenía permiso explícito para realizar la operación
-
-
Validar cada una de las propiedades del documento
-
Asignar acceso:
-
añadir usuarios y/o roles a los canales
-
añadir usuarios a roles
-
-
Asignar canales al documento
He aquí un ejemplo de definición de tipo de documento para que se haga una idea de lo que es capaz synctos:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
{ // A request/requisition for payment of an invoice paymentRequisition: { // Determine whether the document is of this type typeFilter: function(doc, oldDoc) { return doc.type === 'paymentRequisition' || oldDoc.type === 'paymentRequisition'; }, // The channels that the document will be assigned to. // In order for a user to perform each operation, it must belong to at least one of the listed channels for that operation type. channels: { add: [ 'create-payment-requisition' ], replace: [ 'replace-payment-requisition' ], remove: [ 'remove-payment-requisition' ] }, // Define what is expected of each of the properties that belong to this document type propertyValidators: { type: { // The document's type type: 'string', required: true, immutable: true }, businessId: { // The ID of the business with which the payment requisition is associated type: 'integer', required: true, immutable: true, minimumValue: 1 }, invoiceRecordId: { // The ID of the invoice with which the payment requisition is associated type: 'integer', required: true, immutable: true, minimumValue: 1 }, issuedAt: { // When the payment requisition was sent/issued type: 'datetime', immutable: true }, issuedByUserId: { // The ID of the user that issued the payment requisition type: 'string', immutable: true, mustNotBeEmpty: true }, invoiceRecipients: { // Who received the payment requisition type: 'string' } } } } |
Es fácil empezar a utilizar synctos con npm (el gestor de paquetes para Node.js). Ejecute lo siguiente en un directorio vacío:
|
1 |
npm install synctos |
Esto descargará la última versión de synctos en el directorio local node_modules. Y, ahora que synctos está descargado, si guardas el ejemplo de definiciones de documentos anterior en un archivo llamado "ejemplo-doc-definiciones.js", puedes usar synctos para generar una función sync a partir de él:
node_modules/synctos/make-sync-function ./ejemplo-doc-definitions.js ./ejemplo-generated-sync-function.js
Eso debería haber creado una función de sincronización totalmente autónoma combinando la plantilla predefinida de synctos con las definiciones de documentos que nos proporcionó, como encontrará si abre "example-generated-sync-function.js" en un editor de texto. La función de sincronización generada puede insertarse textualmente en un archivo de configuración de Sync Gateway.
A continuación se muestra un ejemplo sencillo de configuración de Sync Gateway que utiliza una instancia en memoria de la aplicación Morsa para que pueda probar la función de sincronización sin tener que configurar un bucket de Couchbase Server. Sustituye la cadena "FUNCIÓN_SINCRONIZACIÓN_GENERADA%" con la función de sincronización que se generó en el paso anterior. Tenga en cuenta que las comillas (`) que rodean la función de sincronización son un añadido personalizado al formato JSON que permite cadenas de varias líneas en una pasarela de sincronización archivo de configuración.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "databases": { "synctos-test": { "server": "walrus:", "users": { "my-user": { "admin_channels": [ "create-payment-requisition", "replace-payment-requisition" ], "password": "password" } }, "sync": `%GENERATED_SYNC_FUNCTION%` } } } |
Guarde la configuración de Sync Gateway anterior como "example-sync-gateway-config.json" y, a continuación, inicie Sync Gateway con la nueva configuración:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
/path/to/sync_gateway ./example-sync-gateway-config.json Now use a tool like curl to create a new instance of the "paymentRequisition" document type. For example: curl -X PUT -u "my-user:password" -H "Content-Type: application/json" -d '{ "type": "paymentRequisition", "businessId": 15, "invoiceRecordId": 79, "issuedAt": "2017-01-11T18:21:18-0800", "issuedByUserId": "my-user", "invoiceRecipients": "foo@example.com,bar@example.com" }' "https://localhost:4984/synctos-test/myDoc1" The response will indicate success: {"id":"myDoc1","ok":true,"rev":"1-ed7b6831f4339188eee412082d44ca32"} |
Crear un documento válido está muy bien, pero ¿qué ocurre si intentamos sustituir ese documento por otro que viole la definición del documento? Observa que, en el siguiente ejemplo, el valor de la propiedad "businessId" ha sido modificado desde su valor original de 15; esto es un problema porque la propiedad fue marcada como "inmutable" en la definición del documento.
|
1 2 3 4 5 6 7 8 |
curl -X PUT -u "my-user:password" -H "Content-Type: application/json" -d '{ "type": "paymentRequisition", "businessId": 99999, "invoiceRecordId": 79, "issuedAt": "2017-01-11T18:21:18-0800", "issuedByUserId": "my-user", "invoiceRecipients": "new-email@example.com" }' "https://localhost:4984/synctos-test/myDoc1?rev=1-ed7b6831f4339188eee412082d44ca32" |
La respuesta será un error:
|
1 |
{"error":"Forbidden","reason":"Invalid paymentRequisition document: value of item "businessId" may not be modified"} |
Como puede ver, synctos genera mensajes de error detallados que facilitan la determinación de lo que salió mal al intentar escribir una revisión del documento. Este es un ejemplo sencillo con un solo error de validación, pero si el documento contuviera varios errores, cada error adicional se enumeraría en el mensaje de error de la respuesta.
¿Y si intentamos realizar una operación que requiere un canal al que nuestro usuario no pertenece? Recuerda que el usuario que definimos en la configuración de Sync Gateway no pertenece al canal "eliminar-pago-requisición", por lo que no se le debería permitir eliminar el documento que creamos anteriormente.
|
1 2 3 |
curl -X DELETE -u "my-user:password" "https://localhost:4984/synctos-test/myDoc1?rev=1-ed7b6831f4339188eee412082d44ca32" And indeed the response indicates that the user does not belong to the necessary channel: {"error":"Forbidden","reason":"missing channel access"} |
Hay mucho más en synctos (por ejemplo, la asignación de acceso a usuarios y roles, la ejecución de acciones personalizadas después de cada fase de la función de sincronización y la validación de las funciones de sincronización generadas utilizando el módulo de prueba incorporado), pero esperamos que esto le haya dado una idea de lo que es capaz de hacer y cómo puede ayudarle a escribir funciones de sincronización de Sync Gateway que validen exhaustivamente sus documentos. Consulte la documentación del proyecto en GitHub o npm para más detalles.