roble 1.12.0
roble: ^1.12.0 copied to clipboard
Cliente Flutter para la plataforma ROBLE de Uninorte OpenLab. Autenticación y CRUD sobre bases de datos PostgreSQL.
Changelog #
1.12.0 #
Añadido #
-
Sesión de invitado. Alguien puede escribir antes de tener cuenta, y sin dejar de ser dueño de lo que escribe.
if (!db.isLoggedIn) await db.signInAnonymously(); await db.create('carrito', {'producto': id}); // suyo, y de nadie másUn invitado es un usuario de verdad: tiene
userIdy cada fila que inserta queda a su nombre, así queisMine()y el alcanceownfuncionan igual que con una cuenta normal. Lo que no tiene es credenciales.db.isAnonymousresponde desde el token, sin ir al servidor, para decidir si la pantalla ofrece «guarda tu cuenta».Su
emailes una dirección sintéticaanon_…@anonymous.invalidque no existe y no puede recibir correo: no la muestres.El proyecto tiene que tenerlo habilitado y aplicar propiedad por fila en alguna tabla. Si no, sale
RobleAnonymousAuthExceptiony sucodedice cuál de las dos cosas falta (ANON_AUTH_DISABLEDoANON_REQUIRES_ROW_OWNERSHIP). Que el servidor se niegue en el segundo caso es a propósito: un invitado sin propiedad por fila escribe filas que puede borrar cualquier otro invitado. -
Ascender al invitado a una cuenta, conservando lo suyo.
await db.upgradeAccount(email: email, password: password);No crea un usuario nuevo: muta el que ya hay. El
userIdno cambia, así que cada fila que escribió sigue siendo suya sin mover un dato. Si el correo ya tiene cuenta, saleRobleAnonUpgradeEmailTakenExceptiony no se toca nada: Roble no fusiona dos cuentas a escondidas, porque así es como se pierden datos.linkIdentity(provider: 'google')hace lo mismo con un proveedor: devuelve laurla la que mandar a la persona, y al volver la identidad queda unida a esta cuenta en vez de crear otra. -
Clave publicable: escribir sin sesión ninguna.
final buzon = RobleApiDataBase( config: RobleApiConfig.fromContract( baseUrl: baseUrl, contractId: contractId, anonKey: 'roble_anon_…', ), ); await buzon.create('sugerencias', {'texto': texto});Para un formulario de contacto, un buzón, una encuesta: gente que no se va a registrar. La clave va dentro de la app y es pública por diseño — quien desensamble el binario la va a ver, y eso no es una filtración. Lo que la hace segura es que no puede leer nada.
Un cliente en este modo sólo inserta, y sólo en las tablas que el proyecto marque. Todo lo demás falla en el cliente con
RobleAnonKeyScopeException, sin salir a la red, para que el error aparezca en la línea que lo causó en vez de llegar como un401que parece una sesión caducada.db.isAnonKeyModelo dice, para no ofrecer en pantalla lo que no se va a poder hacer.Las filas que escribe no tienen dueño. Nadie con alcance
ownpodrá editarlas ni borrarlas después, así que no sirve para datos que la persona tenga que poder volver a tocar: para eso está la sesión de invitado.Pasar un
roble_pat_comoanonKeyfalla al construir la configuración: ese es un secreto de servidor y no va en una app. -
RobleUser.isAnonymous.
Corregido #
- Los errores del servidor que traen
codeen el cuerpo ahora lo conservan. Antes se perdía, y dos409que se arreglan en sitios distintos llegaban indistinguibles.
1.11.0 #
Añadido #
-
Roles y permisos por tabla. Un rol puede leer una tabla y no poder borrarla: los permisos dejaron de ser de todo el proyecto y pasaron a ser por tabla y por acción. Cuando el rol no alcanza, la llamada responde
403y ahora sale con su propio tipo.try { await db.delete('Product', id); } on RobleApiForbiddenException { mostrar('Tu cuenta no puede borrar'); }Con esto también salen tipados los rechazos de la política de envío de notificaciones (
NOTIFICATIONS_SEND_FORBIDDEN,NOTIFICATIONS_CONSOLE_ONLY), que ya respondían403y llegaban como un error HTTP cualquiera.RobleRoletrae los roles que crea un proyecto nuevo —admin,user,editoryeditor_own—.editor_ownes el que suele hacer falta: cada quien edita lo suyo, sin que promoverlo le deje vaciar la tabla. El rol de la sesión viene enRobleUser.role. -
_owner: de quién es cada fila. Las tablas nuevas traen una columna con el usuario que insertó el registro. La sella el servidor con el token, así que no se puede falsear.final mias = (await db.read('Product')).where(db.isMine).toList();isMine(fila)ycurrentUserIdresponden sin ir al servidor: salen del token que ya está en memoria. Es para la pantalla —quien decide qué puedes tocar es el servidor—. También estárobleOwnerOf(fila)y la constanterobleOwnerColumn.Una tabla puede ocultar la columna desde la consola, que es lo que quiere una tabla anónima: ahí no viene en las lecturas, filtrar por ella da error, e
isMinerespondefalseporque no hay forma de saberlo. -
RobleApiNotFoundExceptionpara el404. Con la propiedad activada en una tabla, tocar la fila de otra persona responde lo mismo que si no existiera. Es a propósito: si respondiera distinto, probar identificadores diría cuáles existen y de quién son. -
REALTIME_FORBIDDENsale comoRobleApiForbiddenException. Suscribirse a una colección que el rol no puede leer parecía un problema de sesión, y volver a entrar no lo arreglaba nunca.Mismas funciones que el paquete de JS (3.10.0).
Cambiado #
_ownerse quita de lo que envías, igual que ya pasaba con_id: el servidor lo asigna él y rechaza el que mandes. Sin esto, leer una fila, cambiarle un campo y volver a escribirla acababa en un400.
Ojo #
-
Borrar dos veces el mismo
_idya no responde200, responde404. Antes un borrado que no encontraba nada decía que sí; con la propiedad por fila eso significaba que borrar la fila de otra persona reportaba éxito sin borrar nada. Si tu app reintenta borrados, trata ese404como éxito. -
json.remove('mensajes')—la colección entera— es cosa de administradores. A un usuario normal le responde403. Borra las ramas concretas (json.remove('mensajes/$id')) y no hace falta ningún rol.
1.10.0 #
Añadido #
-
db.notifications: avisos que se guardan y llegan al momento. Función aparte del árbol JSON: no hay colección que crear ni ruta que elegir, el destinatario es un usuario del proyecto y cada uno lleva su propio estado de leído.db.notifications.watch().listen((e) => mostrar(e.notification.title)); await db.notifications.send(to: otroUsuarioId, title: 'Te toca');send(to: robleNotificationEveryone)va a todo el proyecto, y que una persona la lea no la marca para las demás.list(),unreadCount(),markRead(),markAllRead()yremove()para lo que ya estaba ahí —el stream solo trae lo que llegue a partir de ahora—, ynotifications.unreadCountChangespara el globito, que el servidor manda al conectar sin que haya que pedirlo.Va por su propio socket, contra el namespace
/notifications: una app puede usar notificaciones sin usar tiempo real, y al revés. Cerrar sesión lo cierra, igual que ya hacía con el de tiempo real.Mismas funciones que
db.notificationsdel paquete de JS (3.9.0). -
Canales: avisar a un grupo. Cuando el aviso no es para una persona ni para todo el proyecto —los de un curso, los que siguen un tema—, se envía una vez y lo reciben quienes estén dentro.
await db.notifications.subscribe('curso-101'); await db.notifications.send(channel: 'curso-101', title: 'Examen el viernes');Un canal existe con solo usarlo. Desde la consola se elige quién puede entrar: cualquiera, ciertos roles, o solo el servidor —para una matrícula, que no la decide el estudiante—. Al entrar no recibes lo anterior, y salirte siempre puedes.
sendyschedulepasan a aceptartoochannel; mandar los dos es unArgumentError, porque quien estuviera en las dos listas la recibiría dos veces. -
schedule(...): enviar más tarde, o todos los días. Lo manda el servidor cuando llegue la hora, aunque nadie tenga la app abierta.await db.notifications.schedule( to: usuarioId, title: 'Tu cita es en una hora', at: DateTime.now().add(const Duration(hours: 1)), );Con
RobleRepeat.dailyoweeklyse repite, que es como se hace un «buenos días» sin montar un cron. La fecha se convierte a UTC antes de enviarla, así que una hora local no acaba programada para la hora equivocada.scheduled()lista los tuyos ycancelScheduled(id)cancela uno pendiente. -
registerDevice(token, platform): notificaciones con la app cerrada. El token lo dafirebase_messaging, que va en tu app y no en este paquete: así una app que no quiera push no carga Firebase.final token = await FirebaseMessaging.instance.getToken(); if (token != null) { await db.notifications.registerDevice(token, RobleDevicePlatform.android); }Hacen falta tus credenciales de Firebase, subidas en la consola de Roble: un token de FCM está atado al proyecto de Firebase con el que registraste la app, así que Roble no puede enviarle push con las suyas.
unregisterDevice(token)antes de cerrar sesión; si no, ese aparato sigue recibiendo los avisos de esa cuenta.
1.9.0 #
Añadido #
-
RobleUser: el perfil con tipos. Lo mismo que devuelvecurrentUser(), pero convertido:userId,email,name,role,extray las fechas ya comoDateTime.Está aquí y no en cada app porque el
Mapviene siempre igual: si cada proyecto lo convierte por su cuenta, cada proyecto se equivoca por su cuenta con los campos que pueden faltar —roleno existía antes de la v1.7.8 del backend— y con los nombres que el servidor cambió por el camino. Lo que el paquete todavía no conozca sigue estando enraw.Es el mismo tipo que el paquete de JS ya tenía.
Cambiado #
-
RobleAuthState.userpasa deMap<String, dynamic>?aRobleUser?. Quien escucheauthStateChangesrecibe el perfil ya convertido, así que una app no necesita traducirlo en su capa de datos para no meter mapas sueltos en la interfaz.currentUser()sigue devolviendo elMaptal cual, así que nada de lo que ya funcionaba deja de hacerlo.
1.8.0 #
Añadido #
-
db.authStateChanges: la sesión como un flujo. Emite al entrar, al recuperar una sesión guardada, al salir y cuando se cae sola. Quien se suscribe recibe primero el estado actual, así que una pantalla puede pintarse desde aquí sin preguntar nada aparte.StreamBuilder<RobleAuthState>( stream: db.authStateChanges, builder: (_, snap) => snap.data?.isSignedIn ?? false ? const Inicio() : const Login(), );Cada estado dice por qué cambió (
RobleAuthReason), que es lo que unUser?a secas no cuenta:signedOutyexpireddejan los dos sin sesión, pero solo uno merece un «tu sesión caducó».restoredse distingue designedInporque recuperar una sesión guardada no es que alguien acabe de entrar.db.authStateda el estado de ahora mismo sin esperar al siguiente cambio.
Cambiado #
-
onSessionExpiredpasa a ser un filtro deauthStateChanges, no otro mecanismo. Mismo comportamiento que en 1.7.0 —avisa una sola vez por caída, se rearma al entrar, calla enlogout()—, y sigue sin repetir el estado actual al suscribirse: es un aviso de lo que pase a partir de ahora, no algo que se reparta a quien llega tarde. -
restoreSession()pide el perfil al comprobar que la sesión sigue viva, para poder emitirlo con el estado. Una app que ya lo pedía por su cuenta al arrancar puede dejar de hacerlo.
1.7.0 #
Añadido #
-
db.onSessionExpired: aviso cuando la sesión se cae sola. Emite cuando el servidor rechaza el access token y el refresh token tampoco vale, que es el punto en el que ya no hay forma de seguir.db.onSessionExpired.listen((_) => irALogin());Antes esto solo se podía deducir cazando
RobleApiAuthExceptionen la app, y únicamente si alguien hacía una llamada y la capturaba en el sitio correcto: una sesión caducada se quedaba enseñando el mensaje de error en cada pantalla mientras la app seguía creyéndose dentro. El paquete es quien primero lo sabe, porque es el código al que le acaba de fallar el refresco.La sesión ya está descartada cuando emite —
isLoggedInesfalse—, así que quien escuche solo tiene que llevar a la persona de vuelta a la entrada. Emite una sola vez por sesión caída, aunque fallen a la vez varias llamadas, y no emite enlogout(): cerrar sesión a propósito no es que se te caiga.
1.6.0 #
Añadido #
-
db.files: archivos en el bucket del proyecto.upload,list,getDownloadUrl,downloadyremove.Los bytes no pasan por Roble: el paquete pide una URL firmada y sube o baja directo contra el bucket S3 del proyecto. Por eso no hay límite de tamaño impuesto por el paquete —el que manda es el del bucket— y el archivo no consume el ancho de banda del servidor.
final fileId = await db.files.upload(fileName: 'foto.png', data: bytes); final bytes = await db.files.download(fileId);Si el
PUTal bucket falla, el archivo queda registrado comoPENDINGy no aparece enlist: no hay fichas apuntando a algo que no llegó.El proyecto necesita un bucket conectado desde la consola, en Configuración → Almacenamiento. Sin él, el servidor responde diciendo eso mismo y dónde hacerlo.
Requiere
app-roblev1.9.1 o superior ydb-service-roblev1.8.0 o superior.
1.5.1 #
Documentación #
- El aviso de obsolescencia de
watchTableywatchRecorddecía «se retira en 2.0.0». Este repositorio ya usó y descartó esa numeración —el commitfffd13c, «Release 1.4.0, not 2.2.0», la bajó de vuelta—, y quedaba un tagv2.0.0colgando de trabajo que nunca se publicó. Citar ese número invitaba a confundir una cosa con la otra, así que ahora dice «una versión mayor futura». El tag se borró.
1.5.0 #
Cambiado #
-
El tiempo real escucha colecciones del árbol JSON, no tablas SQL. El servidor dejó de replicar tablas: emitía a cualquiera con sesión sin pasar por los permisos por rol, y compartía espacio de nombres con las colecciones, así que una tabla homónima se entregaba a quien escuchaba la colección. Requiere
realtimev0.10.1.El cambio es del servidor, así que quedarse en 1.4.0 no lo evita: quien no actualice tendrá
watchTablefallando igual, pero sin el aviso del analizador.
Obsoleto #
-
watchTableywatchRecord. El servidor rechaza esas suscripciones conREALTIME_UNKNOWN_COLLECTIONy ya no entregan nada. Usadb.json.watchsobre la colección correspondiente. Se retiran en una versión mayor futura.No se borran ahora: dejarlas hace que el error del servidor llegue por el stream explicando qué usar, mientras que quitarlas rompería la compilación sin decir por dónde seguir.
1.4.0 #
Todo lo que sigue es aditivo respecto a 1.3.0: no desaparece ningún método ni cambia lo que devuelve ninguno. Actualizar no debería obligarte a tocar nada.
Añadido #
Inicio de sesión social
signInWithGoogle(): una sola llamada. Usa el SDK nativo donde lo haya y la ventana de navegador donde no; el paquete elige solo.signInWithProvider(provider): el flujo de ventana, con PKCE por dentro.signInWithIdToken(...): canjea un token que ya obtuvo un SDK nativo.listProviders()yproviderClientId(nombre): los proveedores configurados, para pintar solo los botones que funcionan y para que la app no lleve una segunda copia del Client ID.startSocialLogin()+exchangeSocialCode()eisSocialCallback(), para quien quiera conducir el flujo a mano.robleNativeOpener(esquema): abre el navegador del sistema en móvil.RobleApiConflictException: el409de cuando el correo ya tiene cuenta con otro proveedor.RoblePkceyRobleApiDataBase.newNonce(), sueltos.
Tiempo real
watchTable(tabla)ywatchRecord(tabla, id): unStream<RobleChange>con cada fila insertada, modificada o borrada. La suscripción se pide cuando alguien empieza a escuchar y se cancela sola al cerrar elStreamSubscription.- Filtros y selección de operaciones que evalúa el servidor, así que lo que no interesa ni viaja.
- Políticas:
realtimePolicies(),realtimePolicy(),setRealtimePolicy()ydisableRealtime(), para decidir qué tablas emiten.
Base de datos JSON
db.json: un árbol por proyecto, al estilo de Firebase Realtime Database, concollections,read(conshallow),write,update,push,removeywatch. No hay esquema que declarar —la estructura nace al escribir— y el árbol vive fuera del esquema del proyecto, así que no aparece entre sus tablas.RobleChange.path: la ruta que cambió dentro del árbol. En una tabla SQL llega vacía, porque ahí la fila la identificaprimaryKey.
Datos
- El perfil trae
role. Llega en cualquier forma de entrar —contraseña o social— porque todas devuelven el mismo perfil. Requiereauth-servicev1.7.8. Esnullsi a esa persona no se le asignó rol. executeQueryByName(nombre): ejecuta una consulta guardada por su nombre. El nombre se lee en la consola y sobrevive a recrear la consulta; el UUID no.
Cambiado #
- El paquete depende ahora de
google_sign_inyflutter_web_auth_2. Se inyectaban para no arrastrar plugins nativos, peroflutter_secure_storageya era uno. Los puntos de inyección siguen ahí para quien los necesite.
Corregido #
- La suscripción de tiempo real no se enviaba nunca en web. El id de
petición usaba
1 << 32, que en web vale 0, ynextInt(0)lanza antes de emitir elsubscribe. - Los filtros del servidor coincidían con todo. El cliente los envolvía en
simpley el servidor los lee planos, así que el operador llegaba vacío y todo pasaba el filtro. - El botón de Google moría sin plugin registrado. La comprobación solo
atrapaba
UnimplementedError; sin plugin saltaMissingPluginException, y ahora degrada al flujo de navegador en vez de reventar.
1.3.0 #
Añadido #
createMany(..., strict: true)lanzaRoblePartialInsertExceptionsi el servidor rechaza alguna fila, en vez de confiar en que quien llama reviseskipped. La excepción conserva el resultado completo, así que se sabe qué sí llegó a escribirse.RobleApiConfig.fromContractvalida sus argumentos y lanzaArgumentErrorsibaseUrlno es una URL o si elcontractIdestá vacío o sigue siendo un valor de ejemplo. Antes eso se manifestaba como un500incomprensible en la primera petición.- Pista en el
500de autenticación: es lo que devuelve Roble cuando el contrato no existe, así que ahora el mensaje lo sugiere en lugar de dejar soloError inesperado al autenticar. register(autoLogin: true)inicia sesión al terminar el registro y devuelve el perfil, igual quelogin. Por defecto esfalsey se sigue devolviendo el mensaje del servidor.registerWithVerificationno lo admite: hasta validar el código del correo la cuenta no puede entrar.login(persistSession: false)mantiene la sesión solo en memoria: sirve para todo mientras la app esté abierta, pero no sobrevive al reinicio. Es el "recordarme" de siempre. Ponerfalseborra además la sesión que hubiera guardada, para no dejar una sesión anterior recuperable en el dispositivo. El valor se respeta también en los refrescos automáticos posteriores.
Cambios incompatibles #
-
El servicio Realtime sale de la API pública.
db.realtimey los tiposRobleRealtime*se retiran mientras se estabiliza, junto con la dependenciasocket_io_client. El código sigue en el historial (v1.2.0) para reincorporarlo más adelante. -
Se recorta la superficie de datos a lo esencial. Desaparecen
createTable()ygetTableData()—usaban endpoints que ROBLE no documenta—,createTableFromTemplate()(las tablas se crean en la consola) y los envoltoriosgetAll()ygetWhere(), que eranread()con otro nombre. Se mantienegetById(), que sí aporta: devuelve una fila onull.Antes Ahora getAll(tabla)read(tabla)getWhere(tabla, col, valor)read(tabla, filters: {col: valor}) -
La sesión se persiste sola. El paquete usa
flutter_secure_storage(Keychain / Keystore / almacenamiento cifrado) por defecto, así que ya no hay que implementar ni pasar unRobleTokenStorage. El parámetrostoragesigue existiendo para sustituirlo en pruebas porRobleMemoryStorage.Esto añade la dependencia
flutter_secure_storagey sube el SDK mínimo a Dart 3.3 / Flutter 3.19. -
RobleApiConfigsolo se crea confromContract(). El constructor conauthUrl/dataUrl/realtimeUrlsueltas pasa a ser privado, y desaparecenfromStrings(),copyWith()yvalidate(). Las URLs se componen siempre a partir del host y del identificador del contrato. -
La sesión deja de ser manipulable desde fuera. Se eliminan de la API pública
accessToken,refreshToken,setTokens(),clearTokens()yonTokenUpdate, y elhttp.Clientpasa a ser privado. El paquete guarda los tokens, los adjunta a cada petición, los renueva ante un401y los borra al cerrar sesión; nada de eso necesita intervención de la app.En su lugar hay un único miembro de consulta:
bool get isLoggedInEquivalencias:
db.accessToken != null→db.isLoggedIn; guardar tokens a mano → pasar unstorageal constructor; restaurar sesión →restoreSession(); borrarla →logout(). Elhttp.Clientse sigue pudiendo inyectar por el constructor para pruebas, pero ya no se expone.
Cambiado #
-
restoreSession()ahora comprueba que la sesión siga viva. Además de cargar los tokens guardados, renueva el access token contra el servidor, así que untruesignifica que la sesión sirve de verdad y no solo que había tokens en el almacenamiento. Si el refresh token caducó o fue revocado, limpia la sesión y devuelvefalse.Los fallos de red no borran la sesión: se propagan
RobleApiNetworkExceptionyRobleApiTimeoutExceptionpara poder distinguir "sesión caducada" de "sin conexión".Con
restoreSession(verify: false)se mantiene el comportamiento anterior de solo leer el almacenamiento.
1.2.0 #
Cambios incompatibles #
-
currentUser()ahora devuelve el perfil del usuario, no los datos del token. Pasa deGET /verify-tokenaGET /me, que es lo que realmente interesa a una app:userId,email,name, elextradel registro y las fechas de creación y actualización. Antes devolvía los claims del JWT (sub,role,sessionId), que son detalle interno de la autenticación.Antes ( /verify-token)Ahora ( /me)subuserIdemailemaildbName,role,sessionId— — id,name,extra,createdAt,updatedAtSi leías
user['sub'], usauser['userId']. La librería ya no llama a/verify-token: la validez del token la gestiona ella sola con el refresco automático. -
login()devuelve el perfil del usuario, no los tokens. Tras autenticar pide/mey devuelve el mismo mapa que [currentUser]. Los tokens se guardan internamente y siguen disponibles enaccessTokenyrefreshToken.Si la llamada a
/mefalla, la sesión sigue activa: el error se propaga peroaccessTokenya tiene valor, así que se puede distinguir un fallo de credenciales de uno de perfil y reintentar concurrentUser().
1.1.0 #
Añadido #
- Persistencia de sesión opcional: la interfaz
RobleTokenStorage, el parámetrostoragedel constructor yrestoreSession(). El cliente guarda la sesión en cada login y refresco y la borra al cerrar sesión, así que sobrevive a un reinicio de la app. IncluyeRobleMemoryStoragepara pruebas. Sinstorage, los tokens siguen viviendo solo en memoria, como hasta ahora.
Corregido #
- Si el servidor rotara el refresh token al refrescar, ahora se conserva en
lugar de descartarse. Hoy
/refresh-tokensolo devuelveaccessToken, así que es prevención.
1.0.0 #
Primera versión publicada bajo el nombre roble. Sustituye al paquete
roble_api_database, cuya API se mantiene salvo por los cambios listados abajo.
Añadido #
RobleApiConfig.fromContract({baseUrl, contractId}): compone las rutas/auth/...y/database/...a partir del identificador del contrato.- Getters públicos
accessTokenyrefreshToken. - Callback
onTokenUpdate, invocado en cada cambio del access token. RobleApiConfig.timeoutconfigurable (30 s por defecto).- Documentación de todos los métodos públicos en el README.
- Cobertura completa de la API documentada de ROBLE (19 endpoints):
registerWithVerification(),verifyEmail(),resendCode(),currentUser()(/verify-token, el único endpoint que devuelve la identidad del usuario),forgotPassword(),resetPassword(),deleteAccount(),createMany(),executeQuery(),createTableFromTemplate()ypublicRead(). - Modelos
RobleInsertResult,RobleSkippedRecordyRobleQueryResult. - Servicio Realtime (
db.realtime): árbol JSON por proyecto con API al estilo Firebase —ref(),child(),parent,key,get(shallow:),set(),update(),push(),remove(), máscollections()yhealth(). - Suscripciones en tiempo real:
ref.onValueyref.onEventsobre WebSocket, conRobleRealtimeEvent,status,onStatusChangeyclose(). Un solo socket compartido, resuscripción automática al reconectar y cancelación por colección cuando no quedan escuchas. Añade la dependenciasocket_io_client. register()yregisterWithVerification()aceptan unextraopcional (Map<String, dynamic>) con campos adicionales que el backend guarda junto al usuario. Se envía en el campoextradel cuerpo, y se omite si es nulo.
Eliminado #
authHeadersydataHeadersdeRobleApiConfig, junto conwithBearerToken()y el getter muertodefaultHeaders. La API solo necesitaContent-TypeyAuthorization, y ambos los pone el cliente. Si necesitas cabeceras propias, inyecta unhttp.Clientque las añada.
Corregido #
PATCHse enviaba comoPUT. Elswitchde_makeRequestagrupaba ambos métodos enclient.put(), así querealtime.ref().update()sobrescribía el nodo en lugar de fusionar los campos. Ahora usaclient.patch().create()podía informar éxito sobre una fila rechazada. Enviaba el registro a/insert, que responde200con{inserted: [], skipped: [...]}cuando el servidor lo rechaza; al no haber nada eninserted, el método devolvía ese objeto como si fuera la fila creada, sin_idy sin error. Ahora usa/insert-one, que devuelve la fila directamente y falla con un error HTTP si la rechaza. Para varios registros,createMany()exponeskippeden lugar de descartarlo.
Cambiado #
- Las excepciones que lanza el cliente ahora son las subclases exportadas del
paquete:
RobleApiNetworkException,RobleApiTimeoutException,RobleApiFormatException,RobleApiHttpException(constatusCode) yRobleApiAuthException. Antes se lanzaba una clase interna homónima que nunca coincidía con la exportada, por lo queon RobleApiException catchjamás capturaba nada. logout()ya no recibeaccessToken: usa el token almacenado y limpia la sesión al terminar.- El punto de entrada de la librería pasa a ser
package:roble/roble.dart. - Un error HTTP ya no se envuelve como
Error inesperado: ...; se propaga comoRobleApiHttpExceptioncon el mensaje del servidor. - Tras un
401, solo el fallo del refresco produceRobleApiAuthException; si el reintento falla, se reporta el error real de esa petición.
Eliminado #
refreshAccessToken()yrefreshToken({refreshToken})públicos. El refresco del token es interno y automático ante un401.simulateGet(), que no hacía nada.RobleApiDataBase.timeoutDuration, reemplazado porRobleApiConfig.timeout.