From 72397ca7fc3579325807b0861952818058f6cabb Mon Sep 17 00:00:00 2001 From: Uwe Date: Mon, 3 Feb 2025 10:49:41 +0100 Subject: [PATCH 01/16] Dev docs: update C library update instructions --- dev-doc/updating-c-library.md | 76 ++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/dev-doc/updating-c-library.md b/dev-doc/updating-c-library.md index a08abc9d..f331dc46 100644 --- a/dev-doc/updating-c-library.md +++ b/dev-doc/updating-c-library.md @@ -5,40 +5,96 @@ Dart won't error on C function signature mismatch, leading to obscure memory bug ## C libraries +For each: + +- Update versions below +- Run update script +- Add [CHANGELOG](../objectbox/CHANGELOG.md) entry +- Commit changes + +### Desktop, Scripts + For Dart Native and unit tests ([install.sh](../install.sh)), for the binding update script (see below) and -for Flutter (`flutter_libs` and `sync_flutter_libs` plugins) on Linux and Windows: -``` +for Flutter (`flutter_libs` and `sync_flutter_libs` plugins) on Linux and Windows: + +```bash ./tool/set-c-version.sh 4.0.2 ``` -For the Flutter plugins on Android ([view releases](https://github.com/objectbox/objectbox-java/releases)): +```text +* Flutter for Linux/Windows, Dart Native: update to [objectbox-c 4.0.2](https://github.com/objectbox/objectbox-c/releases/tag/v4.0.2). +``` + +```text +Update C library [4.0.1 -> 4.0.2] ``` + +### Android + +For the Flutter plugins on Android ([view releases](https://github.com/objectbox/objectbox-java/releases)): + +```bash ./tool/set-android-version.sh 4.0.3 ``` -For the Flutter plugins on iOS/macOS ([view releases](https://github.com/objectbox/objectbox-swift/releases)) +```text +* Flutter for Android: update to [objectbox-android 4.0.3](https://github.com/objectbox/objectbox-java/releases/tag/V4.0.3). + If your project is [using Admin](https://docs.objectbox.io/data-browser#admin-for-android), make sure to + update to `io.objectbox:objectbox-android-objectbrowser:4.0.3` in `android/app/build.gradle`. +``` + +```text +Update objectbox-android [4.0.2 -> 4.0.3] + +Bundled with C API 4.0.1 and ObjectBox 4.0.2-2024-10-15 ``` + +Note: the embedded C API and ObjectBox version can be looked up +from the relevant objectbox repository release tag (like `java-4.0.3`). + +### Apple OSs + +For the Flutter plugins on iOS/macOS ([view releases](https://github.com/objectbox/objectbox-swift/releases)) + +```bash ./tool/set-swift-version.sh 4.0.1 ``` -For each, add an entry (see previous releases) to the [CHANGELOG](../objectbox/CHANGELOG.md). +```text +* Flutter for iOS/macOS: update to [objectbox-swift 4.0.1](https://github.com/objectbox/objectbox-swift/releases/tag/v4.0.1). + For existing projects, run `pod repo update` and `pod update ObjectBox` in the `ios` or `macos` directories. +``` + +```text +Update ObjectBox Swift [4.0.0 -> 4.0.1] + +Bundled with C API 4.0.2 and ObjectBox 4.0.2-2024-10-15 +``` + +Note: the embedded C API and ObjectBox version can be looked up +from the objectbox-swift release tag (like `v4.0.1`) and +the objectbox commit it points to (see `external/objectbox`). ## Dart C API bindings + To download the C library header files and generate bindings with ffigen (requires LLVM libraries, see [ffigen docs](https://pub.dev/packages/ffigen#installing-llvm) and the ffigen section in [pubspec.yaml](../objectbox/pubspec.yaml)): -``` + +```bash ./tool/update-c-binding.sh ``` Then manually: + - Copy/update enums that need to be exposed to users from [objectbox_c.dart](../objectbox/lib/src/native/bindings/objectbox_c.dart) to [enums.dart](../objectbox/lib/src/modelinfo/enums.dart). - Check the changed files, make any required changes in the Dart library (like method signature changes). - ⚠️ Update minimum C API and core version and notes as needed in [bindings.dart](../objectbox/lib/src/native/bindings/bindings.dart). - - Note: the embedded C API and core version can be looked up - for Android from the relevant core repository release tag and - for Swift from its repos release tag and the core commit it points to. +- Commit as + +```text +Update C-API [4.0.1 -> 4.0.2] +``` From a9aadcbce58794a42b63bca8195f9361facf40f4 Mon Sep 17 00:00:00 2001 From: Uwe Date: Mon, 3 Feb 2025 10:51:44 +0100 Subject: [PATCH 02/16] Update C library [4.0.2 -> 4.1.0] --- dev-doc/updating-c-library.md | 6 +++--- flutter_libs/linux/CMakeLists.txt | 2 +- flutter_libs/windows/CMakeLists.txt | 2 +- install.sh | 2 +- objectbox/CHANGELOG.md | 2 ++ sync_flutter_libs/linux/CMakeLists.txt | 2 +- sync_flutter_libs/windows/CMakeLists.txt | 2 +- tool/update-c-binding.sh | 2 +- 8 files changed, 11 insertions(+), 9 deletions(-) diff --git a/dev-doc/updating-c-library.md b/dev-doc/updating-c-library.md index f331dc46..2fe99e07 100644 --- a/dev-doc/updating-c-library.md +++ b/dev-doc/updating-c-library.md @@ -19,15 +19,15 @@ for the binding update script (see below) and for Flutter (`flutter_libs` and `sync_flutter_libs` plugins) on Linux and Windows: ```bash -./tool/set-c-version.sh 4.0.2 +./tool/set-c-version.sh 4.1.0 ``` ```text -* Flutter for Linux/Windows, Dart Native: update to [objectbox-c 4.0.2](https://github.com/objectbox/objectbox-c/releases/tag/v4.0.2). +* Flutter for Linux/Windows, Dart Native: update to [objectbox-c 4.1.0](https://github.com/objectbox/objectbox-c/releases/tag/v4.1.0). ``` ```text -Update C library [4.0.1 -> 4.0.2] +Update C library [4.0.2 -> 4.1.0] ``` ### Android diff --git a/flutter_libs/linux/CMakeLists.txt b/flutter_libs/linux/CMakeLists.txt index 6abf6625..aca438e2 100644 --- a/flutter_libs/linux/CMakeLists.txt +++ b/flutter_libs/linux/CMakeLists.txt @@ -44,7 +44,7 @@ target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) # ---------------------------------------------------------------------- # Download and add objectbox-c prebuilt library. -set(OBJECTBOX_VERSION 4.0.2) +set(OBJECTBOX_VERSION 4.1.0) set(OBJECTBOX_ARCH ${CMAKE_SYSTEM_PROCESSOR}) if (${OBJECTBOX_ARCH} MATCHES "x86_64") diff --git a/flutter_libs/windows/CMakeLists.txt b/flutter_libs/windows/CMakeLists.txt index a699fde5..77595b41 100644 --- a/flutter_libs/windows/CMakeLists.txt +++ b/flutter_libs/windows/CMakeLists.txt @@ -50,7 +50,7 @@ set(objectbox_flutter_libs_bundled_libraries # ---------------------------------------------------------------------- # Download and add objectbox-c prebuilt library. -set(OBJECTBOX_VERSION 4.0.2) +set(OBJECTBOX_VERSION 4.1.0) set(OBJECTBOX_ARCH ${CMAKE_SYSTEM_PROCESSOR}) if (${OBJECTBOX_ARCH} MATCHES "AMD64") diff --git a/install.sh b/install.sh index 0feeb1ae..2707c1c6 100755 --- a/install.sh +++ b/install.sh @@ -5,7 +5,7 @@ set -eu # It's important that the generated dart bindings and the c-api library version match. Dart won't error on C function # signature mismatch, leading to obscure memory bugs. # For how to upgrade the version see dev-doc/updating-c-library.md -cLibVersion=4.0.2 +cLibVersion=4.1.0 os=$(uname) cLibArgs="$*" diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index 4bb8b311..a8de18c9 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,5 +1,7 @@ ## latest +* Flutter for Linux/Windows, Dart Native: update to [objectbox-c 4.1.0](https://github.com/objectbox/objectbox-c/releases/tag/v4.1.0). + ## 4.0.3 (2024-10-17) * Generator: replace cryptography library, allows to use newer versions of the transitive `js` dependency. [#638](https://github.com/objectbox/objectbox-dart/issues/638) diff --git a/sync_flutter_libs/linux/CMakeLists.txt b/sync_flutter_libs/linux/CMakeLists.txt index 2738fbb3..4958308b 100644 --- a/sync_flutter_libs/linux/CMakeLists.txt +++ b/sync_flutter_libs/linux/CMakeLists.txt @@ -44,7 +44,7 @@ target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) # ---------------------------------------------------------------------- # Download and add objectbox-c prebuilt library. -set(OBJECTBOX_VERSION 4.0.2) +set(OBJECTBOX_VERSION 4.1.0) set(OBJECTBOX_ARCH ${CMAKE_SYSTEM_PROCESSOR}) if (${OBJECTBOX_ARCH} MATCHES "x86_64") diff --git a/sync_flutter_libs/windows/CMakeLists.txt b/sync_flutter_libs/windows/CMakeLists.txt index fd1cdd64..75e92965 100644 --- a/sync_flutter_libs/windows/CMakeLists.txt +++ b/sync_flutter_libs/windows/CMakeLists.txt @@ -50,7 +50,7 @@ set(objectbox_sync_flutter_libs_bundled_libraries # ---------------------------------------------------------------------- # Download and add objectbox-c prebuilt library. -set(OBJECTBOX_VERSION 4.0.2) +set(OBJECTBOX_VERSION 4.1.0) set(OBJECTBOX_ARCH ${CMAKE_SYSTEM_PROCESSOR}) if (${OBJECTBOX_ARCH} MATCHES "AMD64") diff --git a/tool/update-c-binding.sh b/tool/update-c-binding.sh index 16476af0..c8b3a2bd 100755 --- a/tool/update-c-binding.sh +++ b/tool/update-c-binding.sh @@ -5,7 +5,7 @@ # copies the header files, makes some required modifications # and runs the ffigen binding generator on them. -cLibVersion=4.0.2 +cLibVersion=4.1.0 echo "Downloading C library source files from GitHub..." # Note: the release archives do not contain objectbox-dart.h, so get the full sources. From 1848f6460199b8be36493ca3469b055b979288e5 Mon Sep 17 00:00:00 2001 From: Uwe Date: Mon, 3 Feb 2025 11:01:28 +0100 Subject: [PATCH 03/16] Update objectbox-android [4.0.3 -> 4.1.0] Bundled with C API 4.1.0 and ObjectBox 4.1.0-2025-01-28 --- dev-doc/updating-c-library.md | 12 ++++++------ flutter_libs/android/build.gradle | 2 +- objectbox/CHANGELOG.md | 3 +++ .../android/app/build.gradle | 2 +- sync_flutter_libs/android/build.gradle | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/dev-doc/updating-c-library.md b/dev-doc/updating-c-library.md index 2fe99e07..580fb255 100644 --- a/dev-doc/updating-c-library.md +++ b/dev-doc/updating-c-library.md @@ -35,23 +35,23 @@ Update C library [4.0.2 -> 4.1.0] For the Flutter plugins on Android ([view releases](https://github.com/objectbox/objectbox-java/releases)): ```bash -./tool/set-android-version.sh 4.0.3 +./tool/set-android-version.sh 4.1.0 ``` ```text -* Flutter for Android: update to [objectbox-android 4.0.3](https://github.com/objectbox/objectbox-java/releases/tag/V4.0.3). +* Flutter for Android: update to [objectbox-android 4.1.0](https://github.com/objectbox/objectbox-java/releases/tag/V4.1.0). If your project is [using Admin](https://docs.objectbox.io/data-browser#admin-for-android), make sure to - update to `io.objectbox:objectbox-android-objectbrowser:4.0.3` in `android/app/build.gradle`. + update to `io.objectbox:objectbox-android-objectbrowser:4.1.0` in `android/app/build.gradle`. ``` ```text -Update objectbox-android [4.0.2 -> 4.0.3] +Update objectbox-android [4.0.3 -> 4.1.0] -Bundled with C API 4.0.1 and ObjectBox 4.0.2-2024-10-15 +Bundled with C API 4.1.0 and ObjectBox 4.1.0-2025-01-28 ``` Note: the embedded C API and ObjectBox version can be looked up -from the relevant objectbox repository release tag (like `java-4.0.3`). +from the relevant objectbox repository release tag (like `java-4.1.0`). ### Apple OSs diff --git a/flutter_libs/android/build.gradle b/flutter_libs/android/build.gradle index 160dde47..233ae152 100644 --- a/flutter_libs/android/build.gradle +++ b/flutter_libs/android/build.gradle @@ -52,6 +52,6 @@ android { // ObjectBox Android library that includes an ObjectBox C library version compatible with // the C API binding of the ObjectBox Dart package. // https://central.sonatype.com/search?q=g:io.objectbox%20objectbox-android - implementation "io.objectbox:objectbox-android:4.0.3" + implementation "io.objectbox:objectbox-android:4.1.0" } } diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index a8de18c9..d3062110 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,6 +1,9 @@ ## latest * Flutter for Linux/Windows, Dart Native: update to [objectbox-c 4.1.0](https://github.com/objectbox/objectbox-c/releases/tag/v4.1.0). +* Flutter for Android: update to [objectbox-android 4.1.0](https://github.com/objectbox/objectbox-java/releases/tag/V4.1.0). + If your project is [using Admin](https://docs.objectbox.io/data-browser#admin-for-android), make sure to + update to `io.objectbox:objectbox-android-objectbrowser:4.1.0` in `android/app/build.gradle`. ## 4.0.3 (2024-10-17) diff --git a/objectbox/example/flutter/objectbox_demo_relations/android/app/build.gradle b/objectbox/example/flutter/objectbox_demo_relations/android/app/build.gradle index dfcc7549..f4fc2951 100644 --- a/objectbox/example/flutter/objectbox_demo_relations/android/app/build.gradle +++ b/objectbox/example/flutter/objectbox_demo_relations/android/app/build.gradle @@ -82,5 +82,5 @@ dependencies { // Add objectbox-android-objectbrowser only for debug builds. // Warning: when objectbox_flutter_libs updates check if version // needs update, e.g. check https://github.com/objectbox/objectbox-dart/releases. - debugImplementation("io.objectbox:objectbox-android-objectbrowser:4.0.3") + debugImplementation("io.objectbox:objectbox-android-objectbrowser:4.1.0") } diff --git a/sync_flutter_libs/android/build.gradle b/sync_flutter_libs/android/build.gradle index 024e51c0..95ae6c0f 100644 --- a/sync_flutter_libs/android/build.gradle +++ b/sync_flutter_libs/android/build.gradle @@ -52,6 +52,6 @@ android { // ObjectBox Android library that includes an ObjectBox C library version compatible with // the C API binding of the ObjectBox Dart package. // https://central.sonatype.com/search?q=g:io.objectbox%20objectbox-sync-android - implementation "io.objectbox:objectbox-sync-android:4.0.3" + implementation "io.objectbox:objectbox-sync-android:4.1.0" } } From b922b05b3e31beb0527475d9544e6b35f19088bb Mon Sep 17 00:00:00 2001 From: Uwe Date: Mon, 3 Feb 2025 11:05:50 +0100 Subject: [PATCH 04/16] Update ObjectBox Swift [4.0.0 -> 4.0.1] Bundled with C API 4.1.0 and ObjectBox 4.1.0-2025-01-30 --- dev-doc/updating-c-library.md | 8 ++++---- flutter_libs/ios/objectbox_flutter_libs.podspec | 2 +- flutter_libs/macos/objectbox_flutter_libs.podspec | 2 +- objectbox/CHANGELOG.md | 2 ++ sync_flutter_libs/ios/objectbox_sync_flutter_libs.podspec | 2 +- .../macos/objectbox_sync_flutter_libs.podspec | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/dev-doc/updating-c-library.md b/dev-doc/updating-c-library.md index 580fb255..0b23d8bc 100644 --- a/dev-doc/updating-c-library.md +++ b/dev-doc/updating-c-library.md @@ -58,22 +58,22 @@ from the relevant objectbox repository release tag (like `java-4.1.0`). For the Flutter plugins on iOS/macOS ([view releases](https://github.com/objectbox/objectbox-swift/releases)) ```bash -./tool/set-swift-version.sh 4.0.1 +./tool/set-swift-version.sh 4.1.0 ``` ```text -* Flutter for iOS/macOS: update to [objectbox-swift 4.0.1](https://github.com/objectbox/objectbox-swift/releases/tag/v4.0.1). +* Flutter for iOS/macOS: update to [objectbox-swift 4.1.0](https://github.com/objectbox/objectbox-swift/releases/tag/v4.1.0). For existing projects, run `pod repo update` and `pod update ObjectBox` in the `ios` or `macos` directories. ``` ```text Update ObjectBox Swift [4.0.0 -> 4.0.1] -Bundled with C API 4.0.2 and ObjectBox 4.0.2-2024-10-15 +Bundled with C API 4.1.0 and ObjectBox 4.1.0-2025-01-30 ``` Note: the embedded C API and ObjectBox version can be looked up -from the objectbox-swift release tag (like `v4.0.1`) and +from the objectbox-swift release tag (like `v4.1.0`) and the objectbox commit it points to (see `external/objectbox`). ## Dart C API bindings diff --git a/flutter_libs/ios/objectbox_flutter_libs.podspec b/flutter_libs/ios/objectbox_flutter_libs.podspec index c2243b0d..3a24c744 100644 --- a/flutter_libs/ios/objectbox_flutter_libs.podspec +++ b/flutter_libs/ios/objectbox_flutter_libs.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'ObjectBox', '4.0.1' + s.dependency 'ObjectBox', '4.1.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/flutter_libs/macos/objectbox_flutter_libs.podspec b/flutter_libs/macos/objectbox_flutter_libs.podspec index b3340ff5..5dcbcf81 100644 --- a/flutter_libs/macos/objectbox_flutter_libs.podspec +++ b/flutter_libs/macos/objectbox_flutter_libs.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' - s.dependency 'ObjectBox', '4.0.1' + s.dependency 'ObjectBox', '4.1.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.3' diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index d3062110..c95c8574 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -4,6 +4,8 @@ * Flutter for Android: update to [objectbox-android 4.1.0](https://github.com/objectbox/objectbox-java/releases/tag/V4.1.0). If your project is [using Admin](https://docs.objectbox.io/data-browser#admin-for-android), make sure to update to `io.objectbox:objectbox-android-objectbrowser:4.1.0` in `android/app/build.gradle`. +* Flutter for iOS/macOS: update to [objectbox-swift 4.1.0](https://github.com/objectbox/objectbox-swift/releases/tag/v4.1.0). + For existing projects, run `pod repo update` and `pod update ObjectBox` in the `ios` or `macos` directories. ## 4.0.3 (2024-10-17) diff --git a/sync_flutter_libs/ios/objectbox_sync_flutter_libs.podspec b/sync_flutter_libs/ios/objectbox_sync_flutter_libs.podspec index 777b7b11..d452b7b9 100644 --- a/sync_flutter_libs/ios/objectbox_sync_flutter_libs.podspec +++ b/sync_flutter_libs/ios/objectbox_sync_flutter_libs.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'ObjectBox', '4.0.1-sync' + s.dependency 'ObjectBox', '4.1.0-sync' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/sync_flutter_libs/macos/objectbox_sync_flutter_libs.podspec b/sync_flutter_libs/macos/objectbox_sync_flutter_libs.podspec index f347e954..7c39f43e 100644 --- a/sync_flutter_libs/macos/objectbox_sync_flutter_libs.podspec +++ b/sync_flutter_libs/macos/objectbox_sync_flutter_libs.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' - s.dependency 'ObjectBox', '4.0.1-sync' + s.dependency 'ObjectBox', '4.1.0-sync' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.3' From 427c6f559d78d0688519a1ce1635f12c913d564c Mon Sep 17 00:00:00 2001 From: Uwe Date: Mon, 3 Feb 2025 11:38:47 +0100 Subject: [PATCH 05/16] Update C-API [4.0.2 -> 4.1.0] --- dev-doc/updating-c-library.md | 2 +- .../lib/src/native/bindings/bindings.dart | 15 +- .../lib/src/native/bindings/objectbox-sync.h | 60 +- objectbox/lib/src/native/bindings/objectbox.h | 126 +++- .../lib/src/native/bindings/objectbox_c.dart | 598 +++++++++++++++++- 5 files changed, 766 insertions(+), 35 deletions(-) diff --git a/dev-doc/updating-c-library.md b/dev-doc/updating-c-library.md index 0b23d8bc..f5ae9dcc 100644 --- a/dev-doc/updating-c-library.md +++ b/dev-doc/updating-c-library.md @@ -96,5 +96,5 @@ Then manually: - Commit as ```text -Update C-API [4.0.1 -> 4.0.2] +Update C-API [4.0.2 -> 4.1.0] ``` diff --git a/objectbox/lib/src/native/bindings/bindings.dart b/objectbox/lib/src/native/bindings/bindings.dart index 71861aaa..5f526954 100644 --- a/objectbox/lib/src/native/bindings/bindings.dart +++ b/objectbox/lib/src/native/bindings/bindings.dart @@ -94,15 +94,16 @@ ObjectBoxC? _tryObjectBoxLibFile() { // Require the minimum C API version of all supported platform-specific // libraries. -// Library | C API version | Core version -// objectbox-c | 4.0.2 | 4.0.2-2024-10-15 -// ObjectBox Swift 4.0.1 | 4.0.2 | 4.0.2-2024-10-15 -// objectbox-android 4.0.3 | 4.0.1 | 4.0.2-2024-10-15 +// Library | C API | Core +// ------------------------|-------|----------------- +// objectbox-c | 4.1.0 | 4.1.0-2025-01-28 +// ObjectBox Swift 4.1.0 | 4.1.0 | 4.1.0-2025-01-30 +// objectbox-android 4.1.0 | 4.1.0 | 4.1.0-2025-01-28 var _obxCminMajor = 4; -var _obxCminMinor = 0; -var _obxCminPatch = 1; +var _obxCminMinor = 1; +var _obxCminPatch = 0; // Require minimum core version guaranteeing actual C API availability. -var _obxCoreMinVersion = "4.0.2-2024-10-15"; +var _obxCoreMinVersion = "4.1.0-2025-01-28"; bool _isSupportedVersion(ObjectBoxC obxc) { if (!obxc.version_is_at_least(_obxCminMajor, _obxCminMinor, _obxCminPatch)) { diff --git a/objectbox/lib/src/native/bindings/objectbox-sync.h b/objectbox/lib/src/native/bindings/objectbox-sync.h index bb697092..2a154f39 100644 --- a/objectbox/lib/src/native/bindings/objectbox-sync.h +++ b/objectbox/lib/src/native/bindings/objectbox-sync.h @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 ObjectBox Ltd. All rights reserved. + * Copyright 2018-2025 ObjectBox Ltd. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ #include "objectbox.h" #if defined(static_assert) || defined(__cplusplus) -static_assert(OBX_VERSION_MAJOR == 4 && OBX_VERSION_MINOR == 0 && OBX_VERSION_PATCH == 2, // NOLINT +static_assert(OBX_VERSION_MAJOR == 4 && OBX_VERSION_MINOR == 1 && OBX_VERSION_PATCH == 0, // NOLINT "Versions of objectbox.h and objectbox-sync.h files do not match, please update"); #endif @@ -56,11 +56,19 @@ typedef struct OBX_sync OBX_sync; /// specifies a generic client-side credential type. typedef enum { OBXSyncCredentialsType_NONE = 1, - OBXSyncCredentialsType_SHARED_SECRET = 2, + OBXSyncCredentialsType_SHARED_SECRET = 2, ///< Deprecated, replaced by SHARED_SECRET_SIPPED OBXSyncCredentialsType_GOOGLE_AUTH = 3, - OBXSyncCredentialsType_SHARED_SECRET_SIPPED = 4, - OBXSyncCredentialsType_OBX_ADMIN_USER = 5, - OBXSyncCredentialsType_USER_PASSWORD = 6, + OBXSyncCredentialsType_SHARED_SECRET_SIPPED = 4, ///< Uses shared secret to create a hashed credential. + OBXSyncCredentialsType_OBX_ADMIN_USER = 5, ///< ObjectBox admin users (username/password) + OBXSyncCredentialsType_USER_PASSWORD = 6, ///< Generic credential type suitable for ObjectBox admin + ///< (and possibly others in the future) + OBXSyncCredentialsType_JWT_ID = 7, ///< JSON Web Token (JWT): an ID token that typically provides identity + ///< information about the authenticated user. + OBXSyncCredentialsType_JWT_ACCESS = 8, ///< JSON Web Token (JWT): an access token that is used to access resources. + OBXSyncCredentialsType_JWT_REFRESH = 9, ///< JSON Web Token (JWT): a refresh token that is used to obtain a new + ///< access token. + OBXSyncCredentialsType_JWT_CUSTOM = 10, ///< JSON Web Token (JWT): a token that is neither an ID, access, + ///< nor refresh token. } OBXSyncCredentialsType; // TODO sync prefix @@ -194,8 +202,11 @@ OBX_C_API OBX_sync* obx_sync_urls(OBX_store* store, const char* server_urls[], s OBX_C_API obx_err obx_sync_close(OBX_sync* sync); /// Sets credentials to authenticate the client with the server. -/// See OBXSyncCredentialsType for available options. -/// The accepted OBXSyncCredentials type depends on your sync-server configuration. +/// Any credentials that were set before are replaced; +/// if you want to pass multiple credentials, use obx_sync_credentials_add() instead. +/// If the client was waiting for credentials, this can trigger a reconnection/login attempt. +/// @param type See OBXSyncCredentialsType for available options. +/// The accepted OBXSyncCredentials type depends on your sync-server configuration. /// @param data may be NULL in combination with OBXSyncCredentialsType_NONE OBX_C_API obx_err obx_sync_credentials(OBX_sync* sync, OBXSyncCredentialsType type, const uint8_t* data, size_t size); @@ -207,6 +218,29 @@ OBX_C_API obx_err obx_sync_credentials(OBX_sync* sync, OBXSyncCredentialsType ty OBX_C_API obx_err obx_sync_credentials_user_password(OBX_sync* sync, OBXSyncCredentialsType type, const char* username, const char* password); +/// For authentication with multiple credentials, collect credentials by calling this function multiple times. +/// When adding the last credentials element, the "complete" flag must be set to true. +/// When completed, it will "activate" the collected credentials and replace any previously set credentials and +/// potentially trigger a reconnection/login attempt. +/// @param type See OBXSyncCredentialsType for available options. +/// The accepted OBXSyncCredentials type depends on your sync-server configuration. +/// @param data non-NULL (OBXSyncCredentialsType_NONE is not allowed) +/// @param complete set to true when adding the last credentials element to activate the set of credentials +OBX_C_API obx_err obx_sync_credentials_add(OBX_sync* sync, OBXSyncCredentialsType type, const uint8_t* data, size_t size, + bool complete); + +/// For authentication with multiple credentials, collect credentials by calling this function multiple times. +/// When adding the last credentials element, the "complete" flag must be set to true. +/// When completed, it will "activate" the collected credentials and replace any previously set credentials and +/// potentially trigger a reconnection/login attempt. +/// @param type See OBXSyncCredentialsType for available options. +/// The accepted OBXSyncCredentials type depends on your sync-server configuration. +/// @param username non-NULL +/// @param password non-NULL +/// @param complete set to true when adding the last credentials element to activate the set of credentials +OBX_C_API obx_err obx_sync_credentials_add_user_password(OBX_sync* sync, OBXSyncCredentialsType type, + const char* username, const char* password, bool complete); + /// Configures the maximum number of outgoing TX messages that can be sent without an ACK from the server. /// @returns OBX_ERROR_ILLEGAL_ARGUMENT if value is not in the range 1-20 OBX_C_API obx_err obx_sync_max_messages_in_flight(OBX_sync* sync, int value); @@ -374,9 +408,9 @@ OBX_C_API void obx_sync_listener_msg_objects(OBX_sync* sync, OBX_sync_listener_m void* listener_arg); /// Set or overwrite a previously set 'error' listener - provides information about occurred sync-level errors. -/// @param listener set NULL to reset +/// @param listener The callback to receive sync errors. Set to NULL to reset. /// @param listener_arg is a pass-through argument passed to the listener -OBX_C_API void obx_sync_listener_error(OBX_sync* sync, OBX_sync_listener_error* error, void* listener_arg); +OBX_C_API void obx_sync_listener_error(OBX_sync* sync, OBX_sync_listener_error* listener, void* listener_arg); //---------------------------------------------- // Sync Stats @@ -702,7 +736,7 @@ typedef enum { /// Get u64 value for sync server statistics. /// @param counter_type the counter value to be read (make sure to choose a uint64_t (u64) metric value type). -/// @param out_count receives the counter value. +/// @param out_value receives the counter value. /// @return OBX_SUCCESS if the counter has been successfully retrieved. /// @return OBX_ERROR_ILLEGAL_ARGUMENT if counter_type is undefined (this also happens if the wrong type is requested) /// @return OBX_ERROR_ILLEGAL_STATE if the server is not started. @@ -711,7 +745,7 @@ OBX_C_API obx_err obx_sync_server_stats_u64(OBX_sync_server* server, OBXSyncServ /// Get double value for sync server statistics. /// @param counter_type the counter value to be read (make sure to use a double (f64) metric value type). -/// @param out_count receives the counter value. +/// @param out_value receives the counter value. /// @return OBX_SUCCESS if the counter has been successfully retrieved. /// @return OBX_ERROR_ILLEGAL_ARGUMENT if counter_type is undefined (this also happens if the wrong type is requested) /// @return OBX_ERROR_ILLEGAL_STATE if the server is not started. @@ -785,7 +819,7 @@ typedef void OBX_custom_msg_server_func_client_connection_close(void* server_use /// Callback to shutdown and free all resources associated with the sync client connection to the custom server. /// Note that the custom server may already have been shutdown at this point (e.g. no server user data is supplied). /// Must be provided to implement a custom server. See notes on OBX_custom_msg_server_functions for more details. -/// @param server_user_data User supplied data returned by the function that created the server +/// @param connection_user_data User supplied data returned by the function that created the server typedef void OBX_custom_msg_server_func_client_connection_shutdown(void* connection_user_data); /// Struct of the custom server function callbacks. In order to implement the custom server, you must provide diff --git a/objectbox/lib/src/native/bindings/objectbox.h b/objectbox/lib/src/native/bindings/objectbox.h index c859454b..6973625c 100644 --- a/objectbox/lib/src/native/bindings/objectbox.h +++ b/objectbox/lib/src/native/bindings/objectbox.h @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 ObjectBox Ltd. All rights reserved. + * Copyright 2018-2025 ObjectBox Ltd. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,8 +52,8 @@ extern "C" { /// When using ObjectBox as a dynamic library, you should verify that a compatible version was linked using /// obx_version() or obx_version_is_at_least(). #define OBX_VERSION_MAJOR 4 -#define OBX_VERSION_MINOR 0 -#define OBX_VERSION_PATCH 2 // values >= 100 are reserved for dev releases leading to the next minor/major increase +#define OBX_VERSION_MINOR 1 +#define OBX_VERSION_PATCH 0 // values >= 100 are reserved for dev releases leading to the next minor/major increase //---------------------------------------------- // Common types @@ -173,6 +173,8 @@ typedef enum { /// Sync connector to integrate MongoDB with SyncServer. OBXFeature_SyncMongoDb = 16, + /// Enables additional authentication/authorization methods for sync login, e.g. JWT based methods. + OBXFeature_Auth = 17, } OBXFeature; @@ -480,6 +482,11 @@ typedef enum { OBXVectorDistanceType_Manhattan = 4, OBXVectorDistanceType_Hamming = 5, + /// For geospatial coordinates aka latitude/longitude pairs. + /// Note, that the vector dimension must be 2, with the latitude being the first element and longitude the second. + /// Internally, this uses haversine distance. + OBXVectorDistanceType_Geo = 6, + /// A custom dot product similarity measure that does not require the vectors to be normalized. /// Note: this is no replacement for cosine similarity (like DotProduct for normalized vectors is). /// The non-linear conversion provides a high precision over the entire float range (for the raw dot product). @@ -487,6 +494,7 @@ typedef enum { /// The more negative the dot product, the higher the distance is (the farther the vectors are). /// Value range: 0.0 - 2.0 (nonlinear; 0.0: nearest, 1.0: orthogonal, 2.0: farthest) OBXVectorDistanceType_DotProductNonNormalized = 10, + } OBXVectorDistanceType; /// Utility function to calculate the distance of two given vectors. @@ -1857,10 +1865,120 @@ OBX_C_API obx_qb_cond obx_qb_contains_string(OBX_query_builder* builder, obx_sch OBX_C_API obx_qb_cond obx_qb_contains_element_string(OBX_query_builder* builder, obx_schema_id property_id, const char* value, bool case_sensitive); -/// For flex properties that have a map as root value, this looks for matching key/value pair. +/// @Deprecated use obx_qb_equals_key_value_string instead OBX_C_API obx_qb_cond obx_qb_contains_key_value_string(OBX_query_builder* builder, obx_schema_id property_id, const char* key, const char* value, bool case_sensitive); +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value equal to the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must equal this one. +/// @param case_sensitive if true, the value's match is case-sensitive, otherwise case-insensitive. +OBX_C_API obx_qb_cond obx_qb_equals_key_value_string(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, const char* value, bool case_sensitive); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value equal to the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must equal this one. +OBX_C_API obx_qb_cond obx_qb_equals_key_value_int(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, int64_t value); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value equal to the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must equal this one. +OBX_C_API obx_qb_cond obx_qb_equals_key_value_double(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, double value); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being greater than the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be greater than this one. +/// @param case_sensitive if true, the value's match is case-sensitive, otherwise case-insensitive. +OBX_C_API obx_qb_cond obx_qb_greater_key_value_string(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, const char* value, bool case_sensitive); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being greater than the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be greater than this one. +OBX_C_API obx_qb_cond obx_qb_greater_key_value_int(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, int64_t value); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being greater than the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be greater than this one. +OBX_C_API obx_qb_cond obx_qb_greater_key_value_double(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, double value); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being greater than or equal to the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be greater than or equal to this one. +/// @param case_sensitive if true, the value's match is case-sensitive, otherwise case-insensitive. +OBX_C_API obx_qb_cond obx_qb_greater_or_equal_key_value_string(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, const char* value, bool case_sensitive); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being greater than or equal to the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be greater than or equal to this one. +OBX_C_API obx_qb_cond obx_qb_greater_or_equal_key_value_int(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, int64_t value); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being greater than or equal to the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be greater than or equal to this one. +OBX_C_API obx_qb_cond obx_qb_greater_or_equal_key_value_double(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, double value); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being lesser than the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be lesser than this one. +/// @param case_sensitive if true, the value's match is case-sensitive, otherwise case-insensitive. +OBX_C_API obx_qb_cond obx_qb_less_than_key_value_string(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, const char* value, bool case_sensitive); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being lesser than the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be lesser than this one. +OBX_C_API obx_qb_cond obx_qb_less_than_key_value_int(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, int64_t value); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being lesser than the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be lesser than this one. +OBX_C_API obx_qb_cond obx_qb_less_than_key_value_double(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, double value); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being lesser than or equal to the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be lesser than or equal to this one. +/// @param case_sensitive if true, the value's match is case-sensitive, otherwise case-insensitive. +OBX_C_API obx_qb_cond obx_qb_less_or_equal_key_value_string(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, const char* value, bool case_sensitive); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being lesser than or equal to the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be lesser than or equal to this one. +OBX_C_API obx_qb_cond obx_qb_less_or_equal_key_value_int(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, int64_t value); + +/// For flex properties that have a map as root value, this looks for a matching key/value pair, +/// with the map value being lesser than or equal to the given one. +/// @param key must be an exact match exactly (case-sensitive) +/// @param value the map's value must be lesser than or equal to this one. +OBX_C_API obx_qb_cond obx_qb_less_or_equal_key_value_double(OBX_query_builder* builder, obx_schema_id property_id, + const char* key, double value); + OBX_C_API obx_qb_cond obx_qb_starts_with_string(OBX_query_builder* builder, obx_schema_id property_id, const char* value, bool case_sensitive); diff --git a/objectbox/lib/src/native/bindings/objectbox_c.dart b/objectbox/lib/src/native/bindings/objectbox_c.dart index 27a08d62..702343fd 100644 --- a/objectbox/lib/src/native/bindings/objectbox_c.dart +++ b/objectbox/lib/src/native/bindings/objectbox_c.dart @@ -4152,7 +4152,7 @@ class ObjectBoxC { int Function(ffi.Pointer, int, ffi.Pointer, bool)>(); - /// For flex properties that have a map as root value, this looks for matching key/value pair. + /// @Deprecated use obx_qb_equals_key_value_string instead int qb_contains_key_value_string( ffi.Pointer builder, int property_id, @@ -4182,6 +4182,475 @@ class ObjectBoxC { int Function(ffi.Pointer, int, ffi.Pointer, ffi.Pointer, bool)>(); + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value equal to the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must equal this one. + /// @param case_sensitive if true, the value's match is case-sensitive, otherwise case-insensitive. + int qb_equals_key_value_string( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + ffi.Pointer value, + bool case_sensitive, + ) { + return _qb_equals_key_value_string( + builder, + property_id, + key, + value, + case_sensitive, + ); + } + + late final _qb_equals_key_value_stringPtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Pointer, + ffi.Bool)>>('obx_qb_equals_key_value_string'); + late final _qb_equals_key_value_string = + _qb_equals_key_value_stringPtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, ffi.Pointer, bool)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value equal to the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must equal this one. + int qb_equals_key_value_int( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + int value, + ) { + return _qb_equals_key_value_int( + builder, + property_id, + key, + value, + ); + } + + late final _qb_equals_key_value_intPtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Int64)>>('obx_qb_equals_key_value_int'); + late final _qb_equals_key_value_int = _qb_equals_key_value_intPtr.asFunction< + int Function( + ffi.Pointer, int, ffi.Pointer, int)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value equal to the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must equal this one. + int qb_equals_key_value_double( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + double value, + ) { + return _qb_equals_key_value_double( + builder, + property_id, + key, + value, + ); + } + + late final _qb_equals_key_value_doublePtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Double)>>('obx_qb_equals_key_value_double'); + late final _qb_equals_key_value_double = + _qb_equals_key_value_doublePtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, double)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being greater than the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be greater than this one. + /// @param case_sensitive if true, the value's match is case-sensitive, otherwise case-insensitive. + int qb_greater_key_value_string( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + ffi.Pointer value, + bool case_sensitive, + ) { + return _qb_greater_key_value_string( + builder, + property_id, + key, + value, + case_sensitive, + ); + } + + late final _qb_greater_key_value_stringPtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Pointer, + ffi.Bool)>>('obx_qb_greater_key_value_string'); + late final _qb_greater_key_value_string = + _qb_greater_key_value_stringPtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, ffi.Pointer, bool)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being greater than the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be greater than this one. + int qb_greater_key_value_int( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + int value, + ) { + return _qb_greater_key_value_int( + builder, + property_id, + key, + value, + ); + } + + late final _qb_greater_key_value_intPtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Int64)>>('obx_qb_greater_key_value_int'); + late final _qb_greater_key_value_int = + _qb_greater_key_value_intPtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, int)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being greater than the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be greater than this one. + int qb_greater_key_value_double( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + double value, + ) { + return _qb_greater_key_value_double( + builder, + property_id, + key, + value, + ); + } + + late final _qb_greater_key_value_doublePtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Double)>>('obx_qb_greater_key_value_double'); + late final _qb_greater_key_value_double = + _qb_greater_key_value_doublePtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, double)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being greater than or equal to the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be greater than or equal to this one. + /// @param case_sensitive if true, the value's match is case-sensitive, otherwise case-insensitive. + int qb_greater_or_equal_key_value_string( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + ffi.Pointer value, + bool case_sensitive, + ) { + return _qb_greater_or_equal_key_value_string( + builder, + property_id, + key, + value, + case_sensitive, + ); + } + + late final _qb_greater_or_equal_key_value_stringPtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Pointer, + ffi.Bool)>>('obx_qb_greater_or_equal_key_value_string'); + late final _qb_greater_or_equal_key_value_string = + _qb_greater_or_equal_key_value_stringPtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, ffi.Pointer, bool)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being greater than or equal to the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be greater than or equal to this one. + int qb_greater_or_equal_key_value_int( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + int value, + ) { + return _qb_greater_or_equal_key_value_int( + builder, + property_id, + key, + value, + ); + } + + late final _qb_greater_or_equal_key_value_intPtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Int64)>>('obx_qb_greater_or_equal_key_value_int'); + late final _qb_greater_or_equal_key_value_int = + _qb_greater_or_equal_key_value_intPtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, int)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being greater than or equal to the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be greater than or equal to this one. + int qb_greater_or_equal_key_value_double( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + double value, + ) { + return _qb_greater_or_equal_key_value_double( + builder, + property_id, + key, + value, + ); + } + + late final _qb_greater_or_equal_key_value_doublePtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Double)>>('obx_qb_greater_or_equal_key_value_double'); + late final _qb_greater_or_equal_key_value_double = + _qb_greater_or_equal_key_value_doublePtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, double)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being lesser than the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be lesser than this one. + /// @param case_sensitive if true, the value's match is case-sensitive, otherwise case-insensitive. + int qb_less_than_key_value_string( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + ffi.Pointer value, + bool case_sensitive, + ) { + return _qb_less_than_key_value_string( + builder, + property_id, + key, + value, + case_sensitive, + ); + } + + late final _qb_less_than_key_value_stringPtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Pointer, + ffi.Bool)>>('obx_qb_less_than_key_value_string'); + late final _qb_less_than_key_value_string = + _qb_less_than_key_value_stringPtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, ffi.Pointer, bool)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being lesser than the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be lesser than this one. + int qb_less_than_key_value_int( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + int value, + ) { + return _qb_less_than_key_value_int( + builder, + property_id, + key, + value, + ); + } + + late final _qb_less_than_key_value_intPtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Int64)>>('obx_qb_less_than_key_value_int'); + late final _qb_less_than_key_value_int = + _qb_less_than_key_value_intPtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, int)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being lesser than the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be lesser than this one. + int qb_less_than_key_value_double( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + double value, + ) { + return _qb_less_than_key_value_double( + builder, + property_id, + key, + value, + ); + } + + late final _qb_less_than_key_value_doublePtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Double)>>('obx_qb_less_than_key_value_double'); + late final _qb_less_than_key_value_double = + _qb_less_than_key_value_doublePtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, double)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being lesser than or equal to the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be lesser than or equal to this one. + /// @param case_sensitive if true, the value's match is case-sensitive, otherwise case-insensitive. + int qb_less_or_equal_key_value_string( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + ffi.Pointer value, + bool case_sensitive, + ) { + return _qb_less_or_equal_key_value_string( + builder, + property_id, + key, + value, + case_sensitive, + ); + } + + late final _qb_less_or_equal_key_value_stringPtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Pointer, + ffi.Bool)>>('obx_qb_less_or_equal_key_value_string'); + late final _qb_less_or_equal_key_value_string = + _qb_less_or_equal_key_value_stringPtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, ffi.Pointer, bool)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being lesser than or equal to the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be lesser than or equal to this one. + int qb_less_or_equal_key_value_int( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + int value, + ) { + return _qb_less_or_equal_key_value_int( + builder, + property_id, + key, + value, + ); + } + + late final _qb_less_or_equal_key_value_intPtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Int64)>>('obx_qb_less_or_equal_key_value_int'); + late final _qb_less_or_equal_key_value_int = + _qb_less_or_equal_key_value_intPtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, int)>(); + + /// For flex properties that have a map as root value, this looks for a matching key/value pair, + /// with the map value being lesser than or equal to the given one. + /// @param key must be an exact match exactly (case-sensitive) + /// @param value the map's value must be lesser than or equal to this one. + int qb_less_or_equal_key_value_double( + ffi.Pointer builder, + int property_id, + ffi.Pointer key, + double value, + ) { + return _qb_less_or_equal_key_value_double( + builder, + property_id, + key, + value, + ); + } + + late final _qb_less_or_equal_key_value_doublePtr = _lookup< + ffi.NativeFunction< + obx_qb_cond Function( + ffi.Pointer, + obx_schema_id, + ffi.Pointer, + ffi.Double)>>('obx_qb_less_or_equal_key_value_double'); + late final _qb_less_or_equal_key_value_double = + _qb_less_or_equal_key_value_doublePtr.asFunction< + int Function(ffi.Pointer, int, + ffi.Pointer, double)>(); + int qb_starts_with_string( ffi.Pointer builder, int property_id, @@ -7812,7 +8281,10 @@ class ObjectBoxC { _sync_closePtr.asFunction)>(); /// Sets credentials to authenticate the client with the server. - /// See OBXSyncCredentialsType for available options. + /// Any credentials that were set before are replaced; + /// if you want to pass multiple credentials, use obx_sync_credentials_add() instead. + /// If the client was waiting for credentials, this can trigger a reconnection/login attempt. + /// @param type See OBXSyncCredentialsType for available options. /// The accepted OBXSyncCredentials type depends on your sync-server configuration. /// @param data may be NULL in combination with OBXSyncCredentialsType_NONE int sync_credentials( @@ -7867,6 +8339,80 @@ class ObjectBoxC { int Function(ffi.Pointer, int, ffi.Pointer, ffi.Pointer)>(); + /// For authentication with multiple credentials, collect credentials by calling this function multiple times. + /// When adding the last credentials element, the "complete" flag must be set to true. + /// When completed, it will "activate" the collected credentials and replace any previously set credentials and + /// potentially trigger a reconnection/login attempt. + /// @param type See OBXSyncCredentialsType for available options. + /// The accepted OBXSyncCredentials type depends on your sync-server configuration. + /// @param data non-NULL (OBXSyncCredentialsType_NONE is not allowed) + /// @param complete set to true when adding the last credentials element to activate the set of credentials + int sync_credentials_add( + ffi.Pointer sync1, + int type, + ffi.Pointer data, + int size, + bool complete, + ) { + return _sync_credentials_add( + sync1, + type, + data, + size, + complete, + ); + } + + late final _sync_credentials_addPtr = _lookup< + ffi.NativeFunction< + obx_err Function( + ffi.Pointer, + ffi.Int32, + ffi.Pointer, + ffi.Size, + ffi.Bool)>>('obx_sync_credentials_add'); + late final _sync_credentials_add = _sync_credentials_addPtr.asFunction< + int Function( + ffi.Pointer, int, ffi.Pointer, int, bool)>(); + + /// For authentication with multiple credentials, collect credentials by calling this function multiple times. + /// When adding the last credentials element, the "complete" flag must be set to true. + /// When completed, it will "activate" the collected credentials and replace any previously set credentials and + /// potentially trigger a reconnection/login attempt. + /// @param type See OBXSyncCredentialsType for available options. + /// The accepted OBXSyncCredentials type depends on your sync-server configuration. + /// @param username non-NULL + /// @param password non-NULL + /// @param complete set to true when adding the last credentials element to activate the set of credentials + int sync_credentials_add_user_password( + ffi.Pointer sync1, + int type, + ffi.Pointer username, + ffi.Pointer password, + bool complete, + ) { + return _sync_credentials_add_user_password( + sync1, + type, + username, + password, + complete, + ); + } + + late final _sync_credentials_add_user_passwordPtr = _lookup< + ffi.NativeFunction< + obx_err Function( + ffi.Pointer, + ffi.Int32, + ffi.Pointer, + ffi.Pointer, + ffi.Bool)>>('obx_sync_credentials_add_user_password'); + late final _sync_credentials_add_user_password = + _sync_credentials_add_user_passwordPtr.asFunction< + int Function(ffi.Pointer, int, ffi.Pointer, + ffi.Pointer, bool)>(); + /// Configures the maximum number of outgoing TX messages that can be sent without an ACK from the server. /// @returns OBX_ERROR_ILLEGAL_ARGUMENT if value is not in the range 1-20 int sync_max_messages_in_flight( @@ -8500,16 +9046,16 @@ class ObjectBoxC { ffi.Pointer)>(); /// Set or overwrite a previously set 'error' listener - provides information about occurred sync-level errors. - /// @param listener set NULL to reset + /// @param listener The callback to receive sync errors. Set to NULL to reset. /// @param listener_arg is a pass-through argument passed to the listener void sync_listener_error( ffi.Pointer sync1, - ffi.Pointer error, + ffi.Pointer listener, ffi.Pointer listener_arg, ) { return _sync_listener_error( sync1, - error, + listener, listener_arg, ); } @@ -8968,7 +9514,7 @@ class ObjectBoxC { /// Get u64 value for sync server statistics. /// @param counter_type the counter value to be read (make sure to choose a uint64_t (u64) metric value type). - /// @param out_count receives the counter value. + /// @param out_value receives the counter value. /// @return OBX_SUCCESS if the counter has been successfully retrieved. /// @return OBX_ERROR_ILLEGAL_ARGUMENT if counter_type is undefined (this also happens if the wrong type is requested) /// @return OBX_ERROR_ILLEGAL_STATE if the server is not started. @@ -8994,7 +9540,7 @@ class ObjectBoxC { /// Get double value for sync server statistics. /// @param counter_type the counter value to be read (make sure to use a double (f64) metric value type). - /// @param out_count receives the counter value. + /// @param out_value receives the counter value. /// @return OBX_SUCCESS if the counter has been successfully retrieved. /// @return OBX_ERROR_ILLEGAL_ARGUMENT if counter_type is undefined (this also happens if the wrong type is requested) /// @return OBX_ERROR_ILLEGAL_STATE if the server is not started. @@ -9729,6 +10275,9 @@ abstract class OBXFeature { /// Sync connector to integrate MongoDB with SyncServer. static const int SyncMongoDb = 16; + + /// Enables additional authentication/authorization methods for sync login, e.g. JWT based methods. + static const int Auth = 17; } /// Log level as passed to obx_log_callback. @@ -9850,6 +10399,11 @@ abstract class OBXVectorDistanceType { static const int Manhattan = 4; static const int Hamming = 5; + /// For geospatial coordinates aka latitude/longitude pairs. + /// Note, that the vector dimension must be 2, with the latitude being the first element and longitude the second. + /// Internally, this uses haversine distance. + static const int Geo = 6; + /// A custom dot product similarity measure that does not require the vectors to be normalized. /// Note: this is no replacement for cosine similarity (like DotProduct for normalized vectors is). /// The non-linear conversion provides a high precision over the entire float range (for the raw dot product). @@ -10333,11 +10887,35 @@ class OBX_sync extends ffi.Opaque {} /// specifies a generic client-side credential type. abstract class OBXSyncCredentialsType { static const int NONE = 1; + + /// < Deprecated, replaced by SHARED_SECRET_SIPPED static const int SHARED_SECRET = 2; static const int GOOGLE_AUTH = 3; + + /// < Uses shared secret to create a hashed credential. static const int SHARED_SECRET_SIPPED = 4; + + /// < ObjectBox admin users (username/password) static const int OBX_ADMIN_USER = 5; + + /// < Generic credential type suitable for ObjectBox admin + /// < (and possibly others in the future) static const int USER_PASSWORD = 6; + + /// < JSON Web Token (JWT): an ID token that typically provides identity + /// < information about the authenticated user. + static const int JWT_ID = 7; + + /// < JSON Web Token (JWT): an access token that is used to access resources. + static const int JWT_ACCESS = 8; + + /// < JSON Web Token (JWT): a refresh token that is used to obtain a new + /// < access token. + static const int JWT_REFRESH = 9; + + /// < JSON Web Token (JWT): a token that is neither an ID, access, + /// < nor refresh token. + static const int JWT_CUSTOM = 10; } abstract class OBXRequestUpdatesMode { @@ -10761,7 +11339,7 @@ typedef OBX_custom_msg_server_func_client_connection_close = ffi.NativeFunction< /// Callback to shutdown and free all resources associated with the sync client connection to the custom server. /// Note that the custom server may already have been shutdown at this point (e.g. no server user data is supplied). /// Must be provided to implement a custom server. See notes on OBX_custom_msg_server_functions for more details. -/// @param server_user_data User supplied data returned by the function that created the server +/// @param connection_user_data User supplied data returned by the function that created the server typedef OBX_custom_msg_server_func_client_connection_shutdown = ffi.NativeFunction< ffi.Void Function(ffi.Pointer connection_user_data)>; @@ -10881,9 +11459,9 @@ typedef obx_dart_closer const int OBX_VERSION_MAJOR = 4; -const int OBX_VERSION_MINOR = 0; +const int OBX_VERSION_MINOR = 1; -const int OBX_VERSION_PATCH = 2; +const int OBX_VERSION_PATCH = 0; const int OBX_ID_NEW = -1; From 799529f2bedb8929983950a2b682efae57d92ead Mon Sep 17 00:00:00 2001 From: Uwe Date: Mon, 3 Feb 2025 12:46:22 +0100 Subject: [PATCH 06/16] Sync credentials: check for error when setting user + password --- objectbox/lib/src/native/sync.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/objectbox/lib/src/native/sync.dart b/objectbox/lib/src/native/sync.dart index e09985aa..b908992a 100644 --- a/objectbox/lib/src/native/sync.dart +++ b/objectbox/lib/src/native/sync.dart @@ -240,8 +240,8 @@ class SyncClient { creds._user, (userCStr) => withNativeString( creds._password, - (passwordCStr) => C.sync_credentials_user_password( - _ptr, creds._type, userCStr, passwordCStr))); + (passwordCStr) => checkObx(C.sync_credentials_user_password( + _ptr, creds._type, userCStr, passwordCStr)))); } else if (creds is SyncCredentialsSecret) { withNativeBytes( creds.data, From 4ef8cd40ba68df623821d6de0283a126349339eb Mon Sep 17 00:00:00 2001 From: Uwe Date: Mon, 3 Feb 2025 13:05:38 +0100 Subject: [PATCH 07/16] Sync credentials: support setting multiple #134 --- objectbox/CHANGELOG.md | 4 ++ objectbox/lib/src/native/sync.dart | 78 ++++++++++++++++++++++++++++-- objectbox_test/test/sync_test.dart | 62 +++++++++++++++++++++++- 3 files changed, 139 insertions(+), 5 deletions(-) diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index c95c8574..a5b32ac4 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -7,6 +7,10 @@ * Flutter for iOS/macOS: update to [objectbox-swift 4.1.0](https://github.com/objectbox/objectbox-swift/releases/tag/v4.1.0). For existing projects, run `pod repo update` and `pod update ObjectBox` in the `ios` or `macos` directories. +### Sync + +* Sync clients can send multiple credentials for login. + ## 4.0.3 (2024-10-17) * Generator: replace cryptography library, allows to use newer versions of the transitive `js` dependency. [#638](https://github.com/objectbox/objectbox-dart/issues/638) diff --git a/objectbox/lib/src/native/sync.dart b/objectbox/lib/src/native/sync.dart index b908992a..0b6f856f 100644 --- a/objectbox/lib/src/native/sync.dart +++ b/objectbox/lib/src/native/sync.dart @@ -169,10 +169,10 @@ class SyncClient { /// Creates a Sync client associated with the given store and options. /// This does not initiate any connection attempts yet: call start() to do so. SyncClient._( - this._store, List serverUrls, SyncCredentials credentials) { + this._store, List serverUrls, List credentials) { if (serverUrls.isEmpty) { throw ArgumentError.value( - serverUrls, "serverUrls", "must contain at least one server URL"); + serverUrls, "serverUrls", "Provide at least one server URL"); } if (!Sync.isAvailable()) { @@ -187,7 +187,12 @@ class SyncClient { C.sync_urls(InternalStoreAccess.ptr(_store), ptr, size), 'failed to create Sync client')); - setCredentials(credentials); + if (credentials.length == 1) { + setCredentials(credentials[0]); + } else { + // also covers the length == 0 case + setMultipleCredentials(credentials); + } } /// Closes and cleans up all resources used by this sync client. @@ -250,6 +255,54 @@ class SyncClient { } } + /// Like [setCredentials], but accepts multiple credentials. + /// + /// However, does **not** support [SyncCredentials.none()]. + void setMultipleCredentials(List credentials) { + if (credentials.isEmpty) { + throw ArgumentError.value( + credentials, "credentials", "Provide at least one credential"); + } + + var length = credentials.length; + for (int i = 0; i < length; i++) { + final isLast = i + 1 == length; + var credential = credentials[i]; + + if (credential is _SyncCredentialsNone) { + throw ArgumentError.value(credentials, "credentials", + "SyncCredentials.none() is not supported, use setCredentials() instead"); + } + + try { + if (credential is _SyncCredentialsUserPassword) { + withNativeString( + credential._user, + (userCStr) => withNativeString( + credential._password, + (passwordCStr) => checkObx( + C.sync_credentials_add_user_password(_ptr, + credential._type, userCStr, passwordCStr, isLast)))); + } else if (credential is SyncCredentialsSecret) { + withNativeBytes( + credential.data, + (Pointer credsPtr, int credsSize) => checkObx( + C.sync_credentials_add( + _ptr, credential._type, credsPtr, credsSize, isLast))); + } + } catch (e) { + // To make exceptions related to a credential easier to attribute, + // wrap in an ArgumentError and give the position in the list. + if (e is StateError) { + // State errors should not be specific to a credential, so rethrow + rethrow; + } else { + throw ArgumentError.value(credential, "credentials[$i]", "$e"); + } + } + } + } + /// Configures how sync updates are received from the server. If automatic /// updates are turned off, they will need to be requested manually. void setRequestUpdatesMode(SyncRequestUpdatesMode mode) { @@ -626,9 +679,26 @@ class Sync { Store store, String serverUrl, SyncCredentials credentials) => clientMultiUrls(store, [serverUrl], credentials); + /// Like [client], but accepts a list of credentials. + /// + /// When passing multiple credentials, does **not** support + /// [SyncCredentials.none()]. + static SyncClient clientMultiCredentials( + Store store, String serverUrl, List credentials) => + clientMultiCredentialsMultiUrls(store, [serverUrl], credentials); + /// Like [client], but accepts a list of URLs to work with multiple servers. static SyncClient clientMultiUrls( - Store store, List serverUrls, SyncCredentials credentials) { + Store store, List serverUrls, SyncCredentials credentials) => + clientMultiCredentialsMultiUrls(store, serverUrls, [credentials]); + + /// Like [client], but accepts a list of credentials and a list of URLs to + /// work with multiple servers. + /// + /// When passing multiple credentials, does **not** support + /// [SyncCredentials.none()]. + static SyncClient clientMultiCredentialsMultiUrls( + Store store, List serverUrls, List credentials) { if (syncClientsStorage.containsKey(store)) { throw StateError('Only one sync client can be active for a store'); } diff --git a/objectbox_test/test/sync_test.dart b/objectbox_test/test/sync_test.dart index b21b0b7b..1d468993 100644 --- a/objectbox_test/test/sync_test.dart +++ b/objectbox_test/test/sync_test.dart @@ -57,10 +57,18 @@ void main() { }); test('Sync.clientMulti throws if empty URL list', () { + // Note: this test works with a library that does not have the Sync + // feature, because the URLs are checked before checking for the feature. expect( () => Sync.clientMultiUrls(store, [], SyncCredentials.none()), throwsA(isArgumentError.having((e) => e.message, 'message', - contains('must contain at least one server URL')))); + contains('Provide at least one server URL')))); + + expect( + () => Sync.clientMultiCredentialsMultiUrls( + store, [], [SyncCredentials.none()]), + throwsA(isArgumentError.having((e) => e.message, 'message', + contains('Provide at least one server URL')))); }); test('SyncCredentials string encoding', () { @@ -114,6 +122,18 @@ void main() { expect(store.syncClient(), isNull); }); + test('Sync.clientMulti throws if empty credential list', () { + expect( + () => Sync.clientMultiCredentials(store, 'test-url', []), + throwsA(isArgumentError.having((e) => e.message, 'message', + contains('Provide at least one credential')))); + + expect( + () => Sync.clientMultiCredentialsMultiUrls(store, ['test-url'], []), + throwsA(isArgumentError.having((e) => e.message, 'message', + contains('Provide at least one credential')))); + }); + test('SyncClient is closed when a store is closed', () { final client = createClient(env2.store); env2.closeAndDelete(); @@ -150,13 +170,30 @@ void main() { expect(() => c.cancelUpdates(), error); expect(() => c.requestUpdates(subscribeForFuturePushes: true), error); expect(() => c.outgoingMessageCount(), error); + expect(() => c.setCredentials(SyncCredentials.none()), error); + expect( + () => c.setCredentials(SyncCredentials.sharedSecretString('secret')), + error); + expect( + () => c + .setCredentials(SyncCredentials.userAndPassword('obx', 'secret')), + error); + + expect( + () => c.setMultipleCredentials([ + SyncCredentials.sharedSecretString('secret'), + SyncCredentials.userAndPassword('obx', 'secret') + ]), + error); + expect(() => c.setRequestUpdatesMode(SyncRequestUpdatesMode.auto), error); }); test('SyncClient simple coverage (no server available)', () { SyncClient c = createClient(store); expect(c.isClosed(), isFalse); + c.setCredentials(SyncCredentials.none()); c.setCredentials(SyncCredentials.googleAuthString('secret')); c.setCredentials(SyncCredentials.sharedSecretString('secret')); @@ -165,6 +202,7 @@ void main() { c.setCredentials(SyncCredentials.sharedSecretUint8List( Uint8List.fromList([13, 0, 25]))); c.setCredentials(SyncCredentials.userAndPassword('obx', 'secret')); + c.setCredentials(SyncCredentials.none()); c.setRequestUpdatesMode(SyncRequestUpdatesMode.manual); c.start(); @@ -176,6 +214,28 @@ void main() { expect(c.state(), equals(SyncState.stopped)); }); + test('SyncClient setMultipleCredentials', () { + SyncClient c = createClient(store); + + expect( + () => c.setMultipleCredentials([]), + throwsA(isA() + .having((e) => e.name, "name", "credentials"))); + + // none() not supported + expect( + () => c.setMultipleCredentials([SyncCredentials.none()]), + throwsA(isA() + .having((e) => e.name, "name", "credentials"))); + + // Not throwing in Dart for any supported type + c.setMultipleCredentials([ + SyncCredentials.googleAuthString('secret'), + SyncCredentials.sharedSecretString('secret'), + SyncCredentials.userAndPassword('obx', 'secret') + ]); + }); + group('Sync tests with server', () { late SyncServer server; From 51f41127e7cf0c9d930b916ed2f6144ccdfac021 Mon Sep 17 00:00:00 2001 From: Uwe Date: Mon, 3 Feb 2025 14:57:40 +0100 Subject: [PATCH 08/16] Sync credentials: add JWT credentials, test JWT auth #134 --- objectbox/CHANGELOG.md | 1 + objectbox/lib/src/native/sync.dart | 22 ++ objectbox_test/test/sync_test.dart | 497 ++++++++++++++++++----------- 3 files changed, 335 insertions(+), 185 deletions(-) diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index a5b32ac4..c7af74b1 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -9,6 +9,7 @@ ### Sync +* Add [JWT authentication](https://sync.objectbox.io/sync-server-configuration/jwt-authentication). * Sync clients can send multiple credentials for login. ## 4.0.3 (2024-10-17) diff --git a/objectbox/lib/src/native/sync.dart b/objectbox/lib/src/native/sync.dart index 0b6f856f..a6ddc0ba 100644 --- a/objectbox/lib/src/native/sync.dart +++ b/objectbox/lib/src/native/sync.dart @@ -44,6 +44,28 @@ class SyncCredentials { static SyncCredentials userAndPassword(String user, String password) => _SyncCredentialsUserPassword._( OBXSyncCredentialsType.USER_PASSWORD, user, password); + + /// JSON Web Token (JWT): an ID token that typically provides identity + /// information about the authenticated user. + static SyncCredentials jwtIdToken(String jwtIdToken) => + SyncCredentialsSecret._encode(OBXSyncCredentialsType.JWT_ID, jwtIdToken); + + /// JSON Web Token (JWT): an access token that is used to access resources. + static SyncCredentials jwtAccessToken(String jwtAccessToken) => + SyncCredentialsSecret._encode( + OBXSyncCredentialsType.JWT_ACCESS, jwtAccessToken); + + /// JSON Web Token (JWT): a refresh token that is used to obtain a new + /// access token. + static SyncCredentials jwtRefreshToken(String jwtRefreshToken) => + SyncCredentialsSecret._encode( + OBXSyncCredentialsType.JWT_REFRESH, jwtRefreshToken); + + /// JSON Web Token (JWT): a token that is neither an ID, access, + /// nor refresh token. + static SyncCredentials jwtCustomToken(String jwtCustomToken) => + SyncCredentialsSecret._encode( + OBXSyncCredentialsType.JWT_CUSTOM, jwtCustomToken); } class _SyncCredentialsNone extends SyncCredentials { diff --git a/objectbox_test/test/sync_test.dart b/objectbox_test/test/sync_test.dart index 1d468993..ae61f447 100644 --- a/objectbox_test/test/sync_test.dart +++ b/objectbox_test/test/sync_test.dart @@ -37,9 +37,13 @@ void main() { expect(waitUntil(() => client.state() == SyncState.loggedIn), isTrue); } - // lambda to easily create clients in the test below + // lambda to easily create clients in the tests below + SyncClient createAuthenticatedClient( + Store s, List credentials) => + Sync.clientMultiCredentials(s, 'ws://127.0.0.1:$serverPort', credentials); + SyncClient createClient(Store s) => - Sync.client(s, 'ws://127.0.0.1:$serverPort', SyncCredentials.none()); + createAuthenticatedClient(s, [SyncCredentials.none()]); // lambda to easily create clients in the test below SyncClient loggedInClient(Store s) { @@ -202,6 +206,10 @@ void main() { c.setCredentials(SyncCredentials.sharedSecretUint8List( Uint8List.fromList([13, 0, 25]))); c.setCredentials(SyncCredentials.userAndPassword('obx', 'secret')); + c.setCredentials(SyncCredentials.jwtIdToken('id-token')); + c.setCredentials(SyncCredentials.jwtAccessToken('access-token')); + c.setCredentials(SyncCredentials.jwtRefreshToken('refresh-token')); + c.setCredentials(SyncCredentials.jwtCustomToken('custom-token')); c.setCredentials(SyncCredentials.none()); c.setRequestUpdatesMode(SyncRequestUpdatesMode.manual); @@ -232,218 +240,329 @@ void main() { c.setMultipleCredentials([ SyncCredentials.googleAuthString('secret'), SyncCredentials.sharedSecretString('secret'), - SyncCredentials.userAndPassword('obx', 'secret') + SyncCredentials.userAndPassword('obx', 'secret'), + SyncCredentials.jwtIdToken('id-token'), + SyncCredentials.jwtAccessToken('access-token'), + SyncCredentials.jwtRefreshToken('refresh-token'), + SyncCredentials.jwtCustomToken('custom-token') ]); }); group('Sync tests with server', () { - late SyncServer server; - - setUp(() async { - server = SyncServer(); - serverPort = await server.start(); - }); + group('Sync tests with server (no auth)', () { + late SyncServer server; - tearDown(() async => await server.stop()); - - test('SyncClient data sync', () async { - await server.online(); - final client1 = loggedInClient(env.store); - final client2 = loggedInClient(env2.store); - - final box = env.store.box(); - final box2 = env2.store.box(); - int id = box.put(TestEntitySynced(value: Random().nextInt(1 << 32))); - expect(waitUntil(() => box2.get(id) != null), isTrue); - - TestEntitySynced? read1 = box.get(id); - TestEntitySynced? read2 = box2.get(id); - expect(read1, isNotNull); - expect(read2, isNotNull); - expect(read1!.id, equals(read2!.id)); - expect(read1.value, equals(read2.value)); - client1.close(); - client2.close(); - }); + setUp(() async { + server = SyncServer(); + serverPort = await server.start(); + }); - test('SyncClient listeners: connection', () async { - final client = createClient(env.store); + tearDown(() async => await server.stop()); + + test('SyncClient data sync', () async { + await server.online(); + final client1 = loggedInClient(env.store); + final client2 = loggedInClient(env2.store); + + final box = env.store.box(); + final box2 = env2.store.box(); + int id = box.put(TestEntitySynced(value: Random().nextInt(1 << 32))); + expect(waitUntil(() => box2.get(id) != null), isTrue); + + TestEntitySynced? read1 = box.get(id); + TestEntitySynced? read2 = box2.get(id); + expect(read1, isNotNull); + expect(read2, isNotNull); + expect(read1!.id, equals(read2!.id)); + expect(read1.value, equals(read2.value)); + client1.close(); + client2.close(); + }); - // collect connection events - final events = []; - final streamSub = client.connectionEvents.listen(events.add); + test('SyncClient listeners: connection', () async { + final client = createClient(env.store); - // multiple subscriptions work as well - final events2 = []; - final streamSub2 = client.connectionEvents.listen(events2.add); + // collect connection events + final events = []; + final streamSub = client.connectionEvents.listen(events.add); - await server.online(); - client.start(); + // multiple subscriptions work as well + final events2 = []; + final streamSub2 = client.connectionEvents.listen(events2.add); - waitUntilLoggedIn(client); - await yieldExecution(); - expect(events, equals([SyncConnectionEvent.connected])); - expect(events2, equals([SyncConnectionEvent.connected])); + await server.online(); + client.start(); - await streamSub2.cancel(); + waitUntilLoggedIn(client); + await yieldExecution(); + expect(events, equals([SyncConnectionEvent.connected])); + expect(events2, equals([SyncConnectionEvent.connected])); - await server.stop(keepDb: true); + await streamSub2.cancel(); - expect( - waitUntil(() => client.state() == SyncState.disconnected), isTrue); - await yieldExecution(); - expect( - events, - equals([ - SyncConnectionEvent.connected, - SyncConnectionEvent.disconnected - ])); + await server.stop(keepDb: true); - await server.start(keepDb: true); - await server.online(); + expect(waitUntil(() => client.state() == SyncState.disconnected), + isTrue); + await yieldExecution(); + expect( + events, + equals([ + SyncConnectionEvent.connected, + SyncConnectionEvent.disconnected + ])); - waitUntilLoggedIn(client); - await yieldExecution(); + await server.start(keepDb: true); + await server.online(); - expect( - events, - equals([ - SyncConnectionEvent.connected, - SyncConnectionEvent.disconnected, - SyncConnectionEvent.connected - ])); - expect(events2, equals([SyncConnectionEvent.connected])); + waitUntilLoggedIn(client); + await yieldExecution(); - await streamSub.cancel(); - client.close(); - }); + expect( + events, + equals([ + SyncConnectionEvent.connected, + SyncConnectionEvent.disconnected, + SyncConnectionEvent.connected + ])); + expect(events2, equals([SyncConnectionEvent.connected])); - test('SyncClient listeners: login', () async { - final client = createClient(env.store); + await streamSub.cancel(); + client.close(); + }); - client.setCredentials(SyncCredentials.sharedSecretString('foo')); + test('SyncClient listeners: login', () async { + final client = createClient(env.store); - // collect login events - final events = []; - client.loginEvents.listen(events.add); + client.setCredentials(SyncCredentials.sharedSecretString('foo')); - await server.online(); - client.start(); + // collect login events + final events = []; + client.loginEvents.listen(events.add); - expect(await client.loginEvents.first.timeout(defaultTimeout), - equals(SyncLoginEvent.credentialsRejected)); + await server.online(); + client.start(); - client.setCredentials(SyncCredentials.none()); + expect(await client.loginEvents.first.timeout(defaultTimeout), + equals(SyncLoginEvent.credentialsRejected)); - waitUntilLoggedIn(client); - await yieldExecution(); - expect( - events, - equals( - [SyncLoginEvent.credentialsRejected, SyncLoginEvent.loggedIn])); + client.setCredentials(SyncCredentials.none()); - client.close(); - }); + waitUntilLoggedIn(client); + await yieldExecution(); + expect( + events, + equals([ + SyncLoginEvent.credentialsRejected, + SyncLoginEvent.loggedIn + ])); - test('SyncClient listeners: completion', () async { - await server.online(); - final client = loggedInClient(store); - addTearDown(() { client.close(); }); - final box = env.store.box(); - final box2 = env2.store.box(); - expect(box.isEmpty(), isTrue); - // Do multiple changes to verify only a single completion event is sent - // after all changes are received. - box.put(TestEntitySynced(value: 1)); - box.put(TestEntitySynced(value: 100)); - - // Note: wait for the client to finish sending to the server. - // There's currently no other way to recognize this. - sleep(const Duration(milliseconds: 100)); - - final client2 = createClient(env2.store); - addTearDown(() { - client2.close(); - }); - final Completer firstEvent = Completer(); - var receivedEvents = 0; - final subscription = client2.completionEvents.listen((event) { - if (!firstEvent.isCompleted) { - firstEvent.complete(); - } - receivedEvents++; + + test('SyncClient listeners: completion', () async { + await server.online(); + final client = loggedInClient(store); + addTearDown(() { + client.close(); + }); + final box = env.store.box(); + final box2 = env2.store.box(); + expect(box.isEmpty(), isTrue); + // Do multiple changes to verify only a single completion event is sent + // after all changes are received. + box.put(TestEntitySynced(value: 1)); + box.put(TestEntitySynced(value: 100)); + + // Note: wait for the client to finish sending to the server. + // There's currently no other way to recognize this. + sleep(const Duration(milliseconds: 100)); + + final client2 = createClient(env2.store); + addTearDown(() { + client2.close(); + }); + final Completer firstEvent = Completer(); + var receivedEvents = 0; + final subscription = client2.completionEvents.listen((event) { + if (!firstEvent.isCompleted) { + firstEvent.complete(); + } + receivedEvents++; + }); + + client2.start(); + waitUntilLoggedIn(client2); + + // Yield and wait for the first event... + await firstEvent.future.timeout(defaultTimeout); + // ...and some more on any additional events (should be none) + await Future.delayed(Duration(milliseconds: 200)); + expect(receivedEvents, 1); + // Note: the ID just happens to be the same as the box was unused + expect(box2.get(2)!.value, 100); + + // Do another change + box.put(TestEntitySynced(value: 200)); + // Yield and wait for event(s) to come in + await Future.delayed(Duration(milliseconds: 200)); + await subscription.cancel(); + expect(receivedEvents, 2); }); - client2.start(); - waitUntilLoggedIn(client2); - - // Yield and wait for the first event... - await firstEvent.future.timeout(defaultTimeout); - // ...and some more on any additional events (should be none) - await Future.delayed(Duration(milliseconds: 200)); - expect(receivedEvents, 1); - // Note: the ID just happens to be the same as the box was unused - expect(box2.get(2)!.value, 100); - - // Do another change - box.put(TestEntitySynced(value: 200)); - // Yield and wait for event(s) to come in - await Future.delayed(Duration(milliseconds: 200)); - await subscription.cancel(); - expect(receivedEvents, 2); - }); + test('SyncClient listeners: changes', () async { + await server.online(); + final client = loggedInClient(store); + final client2 = loggedInClient(env2.store); + + final events = >[]; + client2.changeEvents.listen(events.add); + + expect(env2.store.box().get(1), isNull); + final box = env.store.box(); + final box2 = env2.store.box(); + box.put(TestEntitySynced(value: 10)); + env.store.runInTransaction(TxMode.write, () { + Box(env.store).put(TestEntity()); // not synced + box.put(TestEntitySynced(value: 20)); + box.put(TestEntitySynced(value: 1)); + expect(box.remove(1), isTrue); + }); + + // wait for the data to be transferred + expect(waitUntil(() => box2.count() == 2), isTrue); + + // check the events + await yieldExecution(); + expect(events.length, 2); + + // box.put(TestEntitySynced(value: 10)); + expect(events[0].length, 1); + expect(events[0][0].entity, TestEntitySynced); + expect( + events[0][0].entityId, + InternalStoreAccess.entityDef(store) + .model + .id + .id); + expect(events[0][0].puts, [1]); + expect(events[0][0].removals, isEmpty); + + // env.store.runInTransaction(TxMode.Write, () { + // Box(env.store).put(TestEntity()); // not synced + // box.put(TestEntitySynced(value: 20)); + // box.put(TestEntitySynced(value: 1)); + // expect(box.remove(1), isTrue); + // }); + expect(events[1].length, 1); + expect(events[1][0].entity, TestEntitySynced); + expect( + events[1][0].entityId, + InternalStoreAccess.entityDef(store) + .model + .id + .id); + expect(events[1][0].puts, [2, 3]); + expect(events[1][0].removals, [1]); - test('SyncClient listeners: changes', () async { - await server.online(); - final client = loggedInClient(store); - final client2 = loggedInClient(env2.store); - - final events = >[]; - client2.changeEvents.listen(events.add); - - expect(env2.store.box().get(1), isNull); - final box = env.store.box(); - final box2 = env2.store.box(); - box.put(TestEntitySynced(value: 10)); - env.store.runInTransaction(TxMode.write, () { - Box(env.store).put(TestEntity()); // not synced - box.put(TestEntitySynced(value: 20)); - box.put(TestEntitySynced(value: 1)); - expect(box.remove(1), isTrue); + client.close(); + client2.close(); }); + }); - // wait for the data to be transferred - expect(waitUntil(() => box2.count() == 2), isTrue); - - // check the events - await yieldExecution(); - expect(events.length, 2); - - // box.put(TestEntitySynced(value: 10)); - expect(events[0].length, 1); - expect(events[0][0].entity, TestEntitySynced); - expect(events[0][0].entityId, - InternalStoreAccess.entityDef(store).model.id.id); - expect(events[0][0].puts, [1]); - expect(events[0][0].removals, isEmpty); - - // env.store.runInTransaction(TxMode.Write, () { - // Box(env.store).put(TestEntity()); // not synced - // box.put(TestEntitySynced(value: 20)); - // box.put(TestEntitySynced(value: 1)); - // expect(box.remove(1), isTrue); - // }); - expect(events[1].length, 1); - expect(events[1][0].entity, TestEntitySynced); - expect(events[1][0].entityId, - InternalStoreAccess.entityDef(store).model.id.id); - expect(events[1][0].puts, [2, 3]); - expect(events[1][0].removals, [1]); - - client.close(); - client2.close(); + group('Sync tests with auth', () { + /// The following JWT tokens are generated with https://token.dev. + /// + /// Use the following RSA256 private key to sign the JWTs: + /// + /// -----BEGIN PRIVATE KEY----- + /// MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDpLtqxS7OrlD/d + /// T2tuz4+QNUh2OCa2Bat4bmpY+wL3FdkqIxXUCJX0tfKpCwBikKoQMzddt+ZmoZvj + /// zIuFv9eploqBJhoL+HYOMzuWCshACn33TZGvx9SYs3aK+vm2cvFRQ6cw5zZJC2v1 + /// 2DNM41hblm7c/DK8BaTkPq54hSEu1jOlwH562g10vcivbvjoojL9VSwPAAzt2Gup + /// IrxTbEUIaVq7iKQ5O2/MOjCcAwcyt8TurUHpZlAMBCUGbFFCzIqWfkMiwq/rFq42 + /// wdGAEApy1TFkbwzhAkjHdLoC6CF3dFkLgJrkB7193wvyaU1gEKtCE5nt1LR/hq3h + /// quUtxqO3AgMBAAECggEBANX6C+7EA/TADrbcCT7fMuNnMb5iGovPuiDCWc6bUIZC + /// Q0yac45l7o1nZWzfzpOkIprJFNZoSgIF7NJmQeYTPCjAHwsSVraDYnn3Y4d1D3tM + /// 5XjJcpX2bs1NactxMTLOWUl0JnkGwtbWp1Qq+DBnMw6ghc09lKTbHQvhxSKNL/0U + /// C+YmCYT5ODmxzLBwkzN5RhxQZNqol/4LYVdji9bS7N/UITw5E6LGDOo/hZHWqJsE + /// fgrJTPsuCyrYlwrNkgmV2KpRrGz5MpcRM7XHgnqVym+HyD/r9E7MEFdTLEaiiHcm + /// Ish1usJDEJMFIWkF+rnEoJkQHbqiKlQBcoqSbCmoMWECgYEA/4379mMPF0JJ/EER + /// 4VH7/ZYxjdyphenx2VYCWY/uzT0KbCWQF8KXckuoFrHAIP3EuFn6JNoIbja0NbhI + /// HGrU29BZkATG8h/xjFy/zPBauxTQmM+yS2T37XtMoXNZNS/ubz2lJXMOapQQiXVR + /// l/tzzpyWaCe9j0NT7DAU0ZFmDbECgYEA6ZbjkcOs2jwHsOwwfamFm4VpUFxYtED7 + /// 9vKzq5d7+Ii1kPKHj5fDnYkZd+mNwNZ02O6OGxh40EDML+i6nOABPg/FmXeVCya9 + /// Vump2Yqr2fAK3xm6QY5KxAjWWq2kVqmdRmICSL2Z9rBzpXmD5o06y9viOwd2bhBo + /// 0wB02416GecCgYEA+S/ZoEa3UFazDeXlKXBn5r2tVEb2hj24NdRINkzC7h23K/z0 + /// pDZ6tlhPbtGkJodMavZRk92GmvF8h2VJ62vAYxamPmhqFW5Qei12WL+FuSZywI7F + /// q/6oQkkYT9XKBrLWLGJPxlSKmiIGfgKHrUrjgXPutWEK1ccw7f10T2UXvgECgYEA + /// nXqLa58G7o4gBUgGnQFnwOSdjn7jkoppFCClvp4/BtxrxA+uEsGXMKLYV75OQd6T + /// IhkaFuxVrtiwj/APt2lRjRym9ALpqX3xkiGvz6ismR46xhQbPM0IXMc0dCeyrnZl + /// QKkcrxucK/Lj1IBqy0kVhZB1IaSzVBqeAPrCza3AzqsCgYEAvSiEjDvGLIlqoSvK + /// MHEVe8PBGOZYLcAdq4YiOIBgddoYyRsq5bzHtTQFgYQVK99Cnxo+PQAvzGb+dpjN + /// /LIEAS2LuuWHGtOrZlwef8ZpCQgrtmp/phXfVi6llcZx4mMm7zYmGhh2AsA9yEQc + /// acgc4kgDThAjD7VlXad9UHpNMO8= + /// -----END PRIVATE KEY----- + /// + /// The expired JWT token is obtained by setting `iat` and `exp` to the + /// same value, which is a time since Unix Epoch. + final String testJwtToken = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJzeW5jLXNlcnZlciIsImlzcyI6Im9iamVjdGJveC1hdXRoIn0.YZSt5XIp7KLSIEtYegEGInea2IvyZajEOWEXcH8p0kYTvhU07LFcxbPWxnNeBtQSjkGp0U0XQUQkCaRjRbNDiHKHCtQHOsUtLefAfQc-WENzSSrGqbb7YKw7FHgsGCQX7FRblcdw3ExU9w8NBgt0xQaDqnwBYfltfu6bmJG5QabGnljcmLGB3Q5EcppxBgWZdLzhmVRiqkiIsCp8kBtELz3Lk8a2LIJP80khJWdls1zIK_NR0XtV6Dbbac1fFN0v5F2VN61VjL9HXZWm68zf2ueW_jobN8IBcJkOAfefgsQu_1e5B0iVAxyRki6F99V1B8Ci_5wbTXRs4bob1Nsl2Q"; + final String testExpiredJwtToken = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJzeW5jLXNlcnZlciIsImlzcyI6Im9iamVjdGJveC1hdXRoIiwiZXhwIjoxNzM4MjE1NjAwLCJpYXQiOjE3MzgyMTc0MDN9.3auqtgaSEqpFqXhuCyoDM-LbfTOIEGGF6X0AjCcykJ2Nv1WN6LaVbuMDjMf-tKSLyeqFkzQbIckP4FvLHh7wQJ6rafDiT4H2pb6xhouU1QH3szK2S_7VDl_4BhxRbW5pEUt9086HXaVFHEZVS0417pxomlPHxrc1n4Z_A4QxZM5_xh5xcHV8PiGgXWb6_2basjBj5z6POTrazRs67IOQ-ob6ROIsOUGu3om6b8i0h_QSMmeJbujfr2EZqhYWTKijeyidbjRWZ97NFxtGRYN_jPOvy-T3gANXs2a32Er8XvgZTjr_-O8tl_1fHPo2kDE-UCNdwUfBQFhTokDUdJ81bg"; + final String testJwtPublicKey = ''' + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6S7asUuzq5Q/3U9rbs+P + kDVIdjgmtgWreG5qWPsC9xXZKiMV1AiV9LXyqQsAYpCqEDM3XbfmZqGb48yLhb/X + qZaKgSYaC/h2DjM7lgrIQAp9902Rr8fUmLN2ivr5tnLxUUOnMOc2SQtr9dgzTONY + W5Zu3PwyvAWk5D6ueIUhLtYzpcB+etoNdL3Ir2746KIy/VUsDwAM7dhrqSK8U2xF + CGlau4ikOTtvzDownAMHMrfE7q1B6WZQDAQlBmxRQsyKln5DIsKv6xauNsHRgBAK + ctUxZG8M4QJIx3S6Aughd3RZC4Ca5Ae9fd8L8mlNYBCrQhOZ7dS0f4at4arlLcaj + twIDAQAB + -----END PUBLIC KEY----- + '''; + + test('Auth with JSON Web Token (JWT)', () async { + // Note: the objectbox project covers all cases, this test just + // ensures the Dart parts work as expected. + final server = SyncServer(); + final publicKeyString = testJwtPublicKey.replaceAll(" ", ""); + serverPort = await server.start(authArguments: [ + "--jwt-public-key $publicKeyString", + "--jwt-claim-aud sync-server", + "--jwt-claim-iss objectbox-auth" + ]); + await server.online(); + addTearDown(() async => await server.stop()); + + // expired token should fail to log in + var client = createAuthenticatedClient( + env.store, [SyncCredentials.jwtIdToken(testExpiredJwtToken)]); + + final events = []; + client.loginEvents.listen(events.add); + client.start(); + addTearDown(() => client.close()); + + expect( + await client.loginEvents.first.timeout(defaultTimeout, + onTimeout: () => throw TimeoutException( + "Did not receive login event within $defaultTimeout")), + equals(SyncLoginEvent.credentialsRejected)); + + // valid token should succeed to log in + client.setCredentials(SyncCredentials.jwtIdToken(testJwtToken)); + + waitUntilLoggedIn(client); + await yieldExecution(); + + expect( + events, + equals([ + SyncLoginEvent.credentialsRejected, + SyncLoginEvent.loggedIn + ])); + }); }); }, skip: SyncServer.isAvailable() @@ -483,19 +602,27 @@ class SyncServer { } } - Future start({bool keepDb = false}) async { + Future start( + {bool keepDb = false, List authArguments = const []}) async { _port ??= await _getUnusedPort(); _dir ??= Directory('testdata-sync-server-$_port'); if (!keepDb) _deleteDb(); - _process = Process.start('sync-server', [ - '--unsecured-no-authentication', + final arguments = [ '--db-directory=${_dir!.path}', '--model=${Directory.current.path}/test/objectbox-model.json', '--bind=ws://127.0.0.1:$_port', '--admin-bind=http://127.0.0.1:${await _getUnusedPort()}' - ]); + ]; + if (authArguments.isNotEmpty) { + arguments.addAll(authArguments); + } else { + arguments.add('--unsecured-no-authentication'); + } + + print("Starting Sync server with arguments: $arguments"); + _process = Process.start('sync-server', arguments); return _port!; } From 0eaccf883fc9fb5fe497fc280fd792d299f69e83 Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 4 Feb 2025 09:01:28 +0100 Subject: [PATCH 09/16] Sync server tests: always print server output --- objectbox_test/test/sync_test.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/objectbox_test/test/sync_test.dart b/objectbox_test/test/sync_test.dart index ae61f447..5ab68c00 100644 --- a/objectbox_test/test/sync_test.dart +++ b/objectbox_test/test/sync_test.dart @@ -584,7 +584,7 @@ void main() { class SyncServer { Directory? _dir; int? _port; - Future? _process; + Process? _process; static bool isAvailable() { // Note: this causes an additional valgrind summary output with a leak. @@ -622,7 +622,12 @@ class SyncServer { } print("Starting Sync server with arguments: $arguments"); - _process = Process.start('sync-server', arguments); + final process = await Process.start('sync-server', arguments); + _process = process; + + // Make log output visible when running tests + stdout.addStream(process.stdout); + stderr.addStream(process.stderr); return _port!; } @@ -650,14 +655,12 @@ class SyncServer { }).timeout(defaultTimeout); Future stop({bool keepDb = false}) async { - if (_process == null) return; - final proc = await _process!; + final proc = _process; + if (proc == null) return; _process = null; proc.kill(ProcessSignal.sigint); final exitCode = await proc.exitCode; if (exitCode != 0) { - await stdout.addStream(proc.stdout); - await stderr.addStream(proc.stderr); expect(await proc.exitCode, isZero); } if (!keepDb) _deleteDb(); From 3b39cbcbc2707274801da6985001765c1afa4d98 Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 4 Feb 2025 09:02:08 +0100 Subject: [PATCH 10/16] Sync server tests: more obvious message when online check times out --- objectbox_test/test/sync_test.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/objectbox_test/test/sync_test.dart b/objectbox_test/test/sync_test.dart index 5ab68c00..45041976 100644 --- a/objectbox_test/test/sync_test.dart +++ b/objectbox_test/test/sync_test.dart @@ -652,7 +652,9 @@ class SyncServer { } } httpClient.close(force: true); - }).timeout(defaultTimeout); + }).timeout(defaultTimeout, + onTimeout: () => throw TimeoutException( + "Server did not come online within $defaultTimeout ms")); Future stop({bool keepDb = false}) async { final proc = _process; From 4e1b1a26a3aec42c289a67f4490a6fa50a45dd97 Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 4 Feb 2025 10:00:31 +0100 Subject: [PATCH 11/16] JWT auth: supply auth via conf file #134 --- objectbox_test/test/sync_test.dart | 80 ++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/objectbox_test/test/sync_test.dart b/objectbox_test/test/sync_test.dart index 45041976..7df9680e 100644 --- a/objectbox_test/test/sync_test.dart +++ b/objectbox_test/test/sync_test.dart @@ -510,28 +510,33 @@ void main() { "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJzeW5jLXNlcnZlciIsImlzcyI6Im9iamVjdGJveC1hdXRoIn0.YZSt5XIp7KLSIEtYegEGInea2IvyZajEOWEXcH8p0kYTvhU07LFcxbPWxnNeBtQSjkGp0U0XQUQkCaRjRbNDiHKHCtQHOsUtLefAfQc-WENzSSrGqbb7YKw7FHgsGCQX7FRblcdw3ExU9w8NBgt0xQaDqnwBYfltfu6bmJG5QabGnljcmLGB3Q5EcppxBgWZdLzhmVRiqkiIsCp8kBtELz3Lk8a2LIJP80khJWdls1zIK_NR0XtV6Dbbac1fFN0v5F2VN61VjL9HXZWm68zf2ueW_jobN8IBcJkOAfefgsQu_1e5B0iVAxyRki6F99V1B8Ci_5wbTXRs4bob1Nsl2Q"; final String testExpiredJwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJzeW5jLXNlcnZlciIsImlzcyI6Im9iamVjdGJveC1hdXRoIiwiZXhwIjoxNzM4MjE1NjAwLCJpYXQiOjE3MzgyMTc0MDN9.3auqtgaSEqpFqXhuCyoDM-LbfTOIEGGF6X0AjCcykJ2Nv1WN6LaVbuMDjMf-tKSLyeqFkzQbIckP4FvLHh7wQJ6rafDiT4H2pb6xhouU1QH3szK2S_7VDl_4BhxRbW5pEUt9086HXaVFHEZVS0417pxomlPHxrc1n4Z_A4QxZM5_xh5xcHV8PiGgXWb6_2basjBj5z6POTrazRs67IOQ-ob6ROIsOUGu3om6b8i0h_QSMmeJbujfr2EZqhYWTKijeyidbjRWZ97NFxtGRYN_jPOvy-T3gANXs2a32Er8XvgZTjr_-O8tl_1fHPo2kDE-UCNdwUfBQFhTokDUdJ81bg"; - final String testJwtPublicKey = ''' - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6S7asUuzq5Q/3U9rbs+P - kDVIdjgmtgWreG5qWPsC9xXZKiMV1AiV9LXyqQsAYpCqEDM3XbfmZqGb48yLhb/X - qZaKgSYaC/h2DjM7lgrIQAp9902Rr8fUmLN2ivr5tnLxUUOnMOc2SQtr9dgzTONY - W5Zu3PwyvAWk5D6ueIUhLtYzpcB+etoNdL3Ir2746KIy/VUsDwAM7dhrqSK8U2xF - CGlau4ikOTtvzDownAMHMrfE7q1B6WZQDAQlBmxRQsyKln5DIsKv6xauNsHRgBAK - ctUxZG8M4QJIx3S6Aughd3RZC4Ca5Ae9fd8L8mlNYBCrQhOZ7dS0f4at4arlLcaj - twIDAQAB - -----END PUBLIC KEY----- - '''; + final String testJwtPublicKey = '''-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6S7asUuzq5Q/3U9rbs+P +kDVIdjgmtgWreG5qWPsC9xXZKiMV1AiV9LXyqQsAYpCqEDM3XbfmZqGb48yLhb/X +qZaKgSYaC/h2DjM7lgrIQAp9902Rr8fUmLN2ivr5tnLxUUOnMOc2SQtr9dgzTONY +W5Zu3PwyvAWk5D6ueIUhLtYzpcB+etoNdL3Ir2746KIy/VUsDwAM7dhrqSK8U2xF +CGlau4ikOTtvzDownAMHMrfE7q1B6WZQDAQlBmxRQsyKln5DIsKv6xauNsHRgBAK +ctUxZG8M4QJIx3S6Aughd3RZC4Ca5Ae9fd8L8mlNYBCrQhOZ7dS0f4at4arlLcaj +twIDAQAB +-----END PUBLIC KEY-----'''; test('Auth with JSON Web Token (JWT)', () async { // Note: the objectbox project covers all cases, this test just // ensures the Dart parts work as expected. final server = SyncServer(); - final publicKeyString = testJwtPublicKey.replaceAll(" ", ""); - serverPort = await server.start(authArguments: [ - "--jwt-public-key $publicKeyString", - "--jwt-claim-aud sync-server", - "--jwt-claim-iss objectbox-auth" - ]); + // JSON does not allow line breaks in values so escape \n + final publicKeyString = testJwtPublicKey.replaceAll("\n", "\\n"); + serverPort = await server.start(configContents: """ +{ + "auth": { + "jwt": { + "publicKey": "$publicKeyString", + "claimAud": "sync-server", + "claimIss": "objectbox-auth" + } + } +} + """); await server.online(); addTearDown(() async => await server.stop()); @@ -602,21 +607,44 @@ class SyncServer { } } - Future start( - {bool keepDb = false, List authArguments = const []}) async { - _port ??= await _getUnusedPort(); + static Future _writeConfFile( + Directory directory, String contents) async { + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + final configFile = File('${directory.path}/sync-server-conf-test.json'); + await configFile.writeAsString(contents); // overwrites by default + + return configFile; + } - _dir ??= Directory('testdata-sync-server-$_port'); + /// By default uses `--unsecured-no-authentication`. To use a custom + /// authentication configuration, pass [configContents]. + /// It will be used to create a config file that is passed using `--conf`. + /// + /// Set [keepDb] to not delete an existing database before starting the + /// server. + Future start({bool keepDb = false, String? configContents}) async { + final port = _port ??= await _getUnusedPort(); + _port = port; + + final dir = _dir ??= Directory('testdata-sync-server-$port'); + _dir = dir; if (!keepDb) _deleteDb(); + // Note: add arguments using the '--arg=value' syntax (unlike at the + // command line, which uses '--arg value')! final arguments = [ - '--db-directory=${_dir!.path}', + '--db-directory=${dir.path}', '--model=${Directory.current.path}/test/objectbox-model.json', - '--bind=ws://127.0.0.1:$_port', + '--bind=ws://127.0.0.1:$port', '--admin-bind=http://127.0.0.1:${await _getUnusedPort()}' ]; - if (authArguments.isNotEmpty) { - arguments.addAll(authArguments); + if (configContents != null && configContents.isNotEmpty) { + // Note: command line arguments overwrite values in a conf file + final configFile = await _writeConfFile(dir, configContents); + arguments.add("--conf=${configFile.absolute.path}"); } else { arguments.add('--unsecured-no-authentication'); } @@ -629,7 +657,7 @@ class SyncServer { stdout.addStream(process.stdout); stderr.addStream(process.stderr); - return _port!; + return port; } /// Wait for the server to respond to a simple http request. From 8cc0c4d7ef0b7753c12b698e8dc7bd6317887397 Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 4 Feb 2025 14:17:41 +0100 Subject: [PATCH 12/16] JWT auth: tests require an already running server #134 --- objectbox_test/test/sync_test.dart | 530 +++++++++++++---------------- 1 file changed, 237 insertions(+), 293 deletions(-) diff --git a/objectbox_test/test/sync_test.dart b/objectbox_test/test/sync_test.dart index 7df9680e..3e0138e0 100644 --- a/objectbox_test/test/sync_test.dart +++ b/objectbox_test/test/sync_test.dart @@ -248,331 +248,270 @@ void main() { ]); }); - group('Sync tests with server', () { - group('Sync tests with server (no auth)', () { - late SyncServer server; + group('Server tests using sync-server in PATH', () { + late SyncServer server; - setUp(() async { - server = SyncServer(); - serverPort = await server.start(); - }); - - tearDown(() async => await server.stop()); - - test('SyncClient data sync', () async { - await server.online(); - final client1 = loggedInClient(env.store); - final client2 = loggedInClient(env2.store); - - final box = env.store.box(); - final box2 = env2.store.box(); - int id = box.put(TestEntitySynced(value: Random().nextInt(1 << 32))); - expect(waitUntil(() => box2.get(id) != null), isTrue); - - TestEntitySynced? read1 = box.get(id); - TestEntitySynced? read2 = box2.get(id); - expect(read1, isNotNull); - expect(read2, isNotNull); - expect(read1!.id, equals(read2!.id)); - expect(read1.value, equals(read2.value)); - client1.close(); - client2.close(); - }); + setUp(() async { + server = SyncServer(); + serverPort = await server.start(); + }); - test('SyncClient listeners: connection', () async { - final client = createClient(env.store); + tearDown(() async => await server.stop()); + + test('SyncClient data sync', () async { + await server.online(); + final client1 = loggedInClient(env.store); + final client2 = loggedInClient(env2.store); + + final box = env.store.box(); + final box2 = env2.store.box(); + int id = box.put(TestEntitySynced(value: Random().nextInt(1 << 32))); + expect(waitUntil(() => box2.get(id) != null), isTrue); + + TestEntitySynced? read1 = box.get(id); + TestEntitySynced? read2 = box2.get(id); + expect(read1, isNotNull); + expect(read2, isNotNull); + expect(read1!.id, equals(read2!.id)); + expect(read1.value, equals(read2.value)); + client1.close(); + client2.close(); + }); - // collect connection events - final events = []; - final streamSub = client.connectionEvents.listen(events.add); + test('SyncClient listeners: connection', () async { + final client = createClient(env.store); - // multiple subscriptions work as well - final events2 = []; - final streamSub2 = client.connectionEvents.listen(events2.add); + // collect connection events + final events = []; + final streamSub = client.connectionEvents.listen(events.add); - await server.online(); - client.start(); + // multiple subscriptions work as well + final events2 = []; + final streamSub2 = client.connectionEvents.listen(events2.add); - waitUntilLoggedIn(client); - await yieldExecution(); - expect(events, equals([SyncConnectionEvent.connected])); - expect(events2, equals([SyncConnectionEvent.connected])); + await server.online(); + client.start(); - await streamSub2.cancel(); + waitUntilLoggedIn(client); + await yieldExecution(); + expect(events, equals([SyncConnectionEvent.connected])); + expect(events2, equals([SyncConnectionEvent.connected])); - await server.stop(keepDb: true); + await streamSub2.cancel(); - expect(waitUntil(() => client.state() == SyncState.disconnected), - isTrue); - await yieldExecution(); - expect( - events, - equals([ - SyncConnectionEvent.connected, - SyncConnectionEvent.disconnected - ])); + await server.stop(keepDb: true); - await server.start(keepDb: true); - await server.online(); + expect( + waitUntil(() => client.state() == SyncState.disconnected), isTrue); + await yieldExecution(); + expect( + events, + equals([ + SyncConnectionEvent.connected, + SyncConnectionEvent.disconnected + ])); - waitUntilLoggedIn(client); - await yieldExecution(); + await server.start(keepDb: true); + await server.online(); - expect( - events, - equals([ - SyncConnectionEvent.connected, - SyncConnectionEvent.disconnected, - SyncConnectionEvent.connected - ])); - expect(events2, equals([SyncConnectionEvent.connected])); + waitUntilLoggedIn(client); + await yieldExecution(); - await streamSub.cancel(); - client.close(); - }); + expect( + events, + equals([ + SyncConnectionEvent.connected, + SyncConnectionEvent.disconnected, + SyncConnectionEvent.connected + ])); + expect(events2, equals([SyncConnectionEvent.connected])); - test('SyncClient listeners: login', () async { - final client = createClient(env.store); - - client.setCredentials(SyncCredentials.sharedSecretString('foo')); + await streamSub.cancel(); + client.close(); + }); - // collect login events - final events = []; - client.loginEvents.listen(events.add); + test('SyncClient listeners: login', () async { + final client = createClient(env.store); - await server.online(); - client.start(); + client.setCredentials(SyncCredentials.sharedSecretString('foo')); - expect(await client.loginEvents.first.timeout(defaultTimeout), - equals(SyncLoginEvent.credentialsRejected)); + // collect login events + final events = []; + client.loginEvents.listen(events.add); - client.setCredentials(SyncCredentials.none()); + await server.online(); + client.start(); - waitUntilLoggedIn(client); - await yieldExecution(); - expect( - events, - equals([ - SyncLoginEvent.credentialsRejected, - SyncLoginEvent.loggedIn - ])); + expect(await client.loginEvents.first.timeout(defaultTimeout), + equals(SyncLoginEvent.credentialsRejected)); - client.close(); - }); + client.setCredentials(SyncCredentials.none()); - test('SyncClient listeners: completion', () async { - await server.online(); - final client = loggedInClient(store); - addTearDown(() { - client.close(); - }); - final box = env.store.box(); - final box2 = env2.store.box(); - expect(box.isEmpty(), isTrue); - // Do multiple changes to verify only a single completion event is sent - // after all changes are received. - box.put(TestEntitySynced(value: 1)); - box.put(TestEntitySynced(value: 100)); - - // Note: wait for the client to finish sending to the server. - // There's currently no other way to recognize this. - sleep(const Duration(milliseconds: 100)); - - final client2 = createClient(env2.store); - addTearDown(() { - client2.close(); - }); - final Completer firstEvent = Completer(); - var receivedEvents = 0; - final subscription = client2.completionEvents.listen((event) { - if (!firstEvent.isCompleted) { - firstEvent.complete(); - } - receivedEvents++; - }); - - client2.start(); - waitUntilLoggedIn(client2); - - // Yield and wait for the first event... - await firstEvent.future.timeout(defaultTimeout); - // ...and some more on any additional events (should be none) - await Future.delayed(Duration(milliseconds: 200)); - expect(receivedEvents, 1); - // Note: the ID just happens to be the same as the box was unused - expect(box2.get(2)!.value, 100); - - // Do another change - box.put(TestEntitySynced(value: 200)); - // Yield and wait for event(s) to come in - await Future.delayed(Duration(milliseconds: 200)); - await subscription.cancel(); - expect(receivedEvents, 2); - }); + waitUntilLoggedIn(client); + await yieldExecution(); + expect( + events, + equals( + [SyncLoginEvent.credentialsRejected, SyncLoginEvent.loggedIn])); - test('SyncClient listeners: changes', () async { - await server.online(); - final client = loggedInClient(store); - final client2 = loggedInClient(env2.store); - - final events = >[]; - client2.changeEvents.listen(events.add); - - expect(env2.store.box().get(1), isNull); - final box = env.store.box(); - final box2 = env2.store.box(); - box.put(TestEntitySynced(value: 10)); - env.store.runInTransaction(TxMode.write, () { - Box(env.store).put(TestEntity()); // not synced - box.put(TestEntitySynced(value: 20)); - box.put(TestEntitySynced(value: 1)); - expect(box.remove(1), isTrue); - }); - - // wait for the data to be transferred - expect(waitUntil(() => box2.count() == 2), isTrue); - - // check the events - await yieldExecution(); - expect(events.length, 2); - - // box.put(TestEntitySynced(value: 10)); - expect(events[0].length, 1); - expect(events[0][0].entity, TestEntitySynced); - expect( - events[0][0].entityId, - InternalStoreAccess.entityDef(store) - .model - .id - .id); - expect(events[0][0].puts, [1]); - expect(events[0][0].removals, isEmpty); - - // env.store.runInTransaction(TxMode.Write, () { - // Box(env.store).put(TestEntity()); // not synced - // box.put(TestEntitySynced(value: 20)); - // box.put(TestEntitySynced(value: 1)); - // expect(box.remove(1), isTrue); - // }); - expect(events[1].length, 1); - expect(events[1][0].entity, TestEntitySynced); - expect( - events[1][0].entityId, - InternalStoreAccess.entityDef(store) - .model - .id - .id); - expect(events[1][0].puts, [2, 3]); - expect(events[1][0].removals, [1]); + client.close(); + }); + test('SyncClient listeners: completion', () async { + await server.online(); + final client = loggedInClient(store); + addTearDown(() { client.close(); + }); + final box = env.store.box(); + final box2 = env2.store.box(); + expect(box.isEmpty(), isTrue); + // Do multiple changes to verify only a single completion event is sent + // after all changes are received. + box.put(TestEntitySynced(value: 1)); + box.put(TestEntitySynced(value: 100)); + + // Note: wait for the client to finish sending to the server. + // There's currently no other way to recognize this. + sleep(const Duration(milliseconds: 100)); + + final client2 = createClient(env2.store); + addTearDown(() { client2.close(); }); + final Completer firstEvent = Completer(); + var receivedEvents = 0; + final subscription = client2.completionEvents.listen((event) { + if (!firstEvent.isCompleted) { + firstEvent.complete(); + } + receivedEvents++; + }); + + client2.start(); + waitUntilLoggedIn(client2); + + // Yield and wait for the first event... + await firstEvent.future.timeout(defaultTimeout); + // ...and some more on any additional events (should be none) + await Future.delayed(Duration(milliseconds: 200)); + expect(receivedEvents, 1); + // Note: the ID just happens to be the same as the box was unused + expect(box2.get(2)!.value, 100); + + // Do another change + box.put(TestEntitySynced(value: 200)); + // Yield and wait for event(s) to come in + await Future.delayed(Duration(milliseconds: 200)); + await subscription.cancel(); + expect(receivedEvents, 2); }); - group('Sync tests with auth', () { - /// The following JWT tokens are generated with https://token.dev. - /// - /// Use the following RSA256 private key to sign the JWTs: - /// - /// -----BEGIN PRIVATE KEY----- - /// MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDpLtqxS7OrlD/d - /// T2tuz4+QNUh2OCa2Bat4bmpY+wL3FdkqIxXUCJX0tfKpCwBikKoQMzddt+ZmoZvj - /// zIuFv9eploqBJhoL+HYOMzuWCshACn33TZGvx9SYs3aK+vm2cvFRQ6cw5zZJC2v1 - /// 2DNM41hblm7c/DK8BaTkPq54hSEu1jOlwH562g10vcivbvjoojL9VSwPAAzt2Gup - /// IrxTbEUIaVq7iKQ5O2/MOjCcAwcyt8TurUHpZlAMBCUGbFFCzIqWfkMiwq/rFq42 - /// wdGAEApy1TFkbwzhAkjHdLoC6CF3dFkLgJrkB7193wvyaU1gEKtCE5nt1LR/hq3h - /// quUtxqO3AgMBAAECggEBANX6C+7EA/TADrbcCT7fMuNnMb5iGovPuiDCWc6bUIZC - /// Q0yac45l7o1nZWzfzpOkIprJFNZoSgIF7NJmQeYTPCjAHwsSVraDYnn3Y4d1D3tM - /// 5XjJcpX2bs1NactxMTLOWUl0JnkGwtbWp1Qq+DBnMw6ghc09lKTbHQvhxSKNL/0U - /// C+YmCYT5ODmxzLBwkzN5RhxQZNqol/4LYVdji9bS7N/UITw5E6LGDOo/hZHWqJsE - /// fgrJTPsuCyrYlwrNkgmV2KpRrGz5MpcRM7XHgnqVym+HyD/r9E7MEFdTLEaiiHcm - /// Ish1usJDEJMFIWkF+rnEoJkQHbqiKlQBcoqSbCmoMWECgYEA/4379mMPF0JJ/EER - /// 4VH7/ZYxjdyphenx2VYCWY/uzT0KbCWQF8KXckuoFrHAIP3EuFn6JNoIbja0NbhI - /// HGrU29BZkATG8h/xjFy/zPBauxTQmM+yS2T37XtMoXNZNS/ubz2lJXMOapQQiXVR - /// l/tzzpyWaCe9j0NT7DAU0ZFmDbECgYEA6ZbjkcOs2jwHsOwwfamFm4VpUFxYtED7 - /// 9vKzq5d7+Ii1kPKHj5fDnYkZd+mNwNZ02O6OGxh40EDML+i6nOABPg/FmXeVCya9 - /// Vump2Yqr2fAK3xm6QY5KxAjWWq2kVqmdRmICSL2Z9rBzpXmD5o06y9viOwd2bhBo - /// 0wB02416GecCgYEA+S/ZoEa3UFazDeXlKXBn5r2tVEb2hj24NdRINkzC7h23K/z0 - /// pDZ6tlhPbtGkJodMavZRk92GmvF8h2VJ62vAYxamPmhqFW5Qei12WL+FuSZywI7F - /// q/6oQkkYT9XKBrLWLGJPxlSKmiIGfgKHrUrjgXPutWEK1ccw7f10T2UXvgECgYEA - /// nXqLa58G7o4gBUgGnQFnwOSdjn7jkoppFCClvp4/BtxrxA+uEsGXMKLYV75OQd6T - /// IhkaFuxVrtiwj/APt2lRjRym9ALpqX3xkiGvz6ismR46xhQbPM0IXMc0dCeyrnZl - /// QKkcrxucK/Lj1IBqy0kVhZB1IaSzVBqeAPrCza3AzqsCgYEAvSiEjDvGLIlqoSvK - /// MHEVe8PBGOZYLcAdq4YiOIBgddoYyRsq5bzHtTQFgYQVK99Cnxo+PQAvzGb+dpjN - /// /LIEAS2LuuWHGtOrZlwef8ZpCQgrtmp/phXfVi6llcZx4mMm7zYmGhh2AsA9yEQc - /// acgc4kgDThAjD7VlXad9UHpNMO8= - /// -----END PRIVATE KEY----- - /// - /// The expired JWT token is obtained by setting `iat` and `exp` to the - /// same value, which is a time since Unix Epoch. - final String testJwtToken = - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJzeW5jLXNlcnZlciIsImlzcyI6Im9iamVjdGJveC1hdXRoIn0.YZSt5XIp7KLSIEtYegEGInea2IvyZajEOWEXcH8p0kYTvhU07LFcxbPWxnNeBtQSjkGp0U0XQUQkCaRjRbNDiHKHCtQHOsUtLefAfQc-WENzSSrGqbb7YKw7FHgsGCQX7FRblcdw3ExU9w8NBgt0xQaDqnwBYfltfu6bmJG5QabGnljcmLGB3Q5EcppxBgWZdLzhmVRiqkiIsCp8kBtELz3Lk8a2LIJP80khJWdls1zIK_NR0XtV6Dbbac1fFN0v5F2VN61VjL9HXZWm68zf2ueW_jobN8IBcJkOAfefgsQu_1e5B0iVAxyRki6F99V1B8Ci_5wbTXRs4bob1Nsl2Q"; - final String testExpiredJwtToken = - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJzeW5jLXNlcnZlciIsImlzcyI6Im9iamVjdGJveC1hdXRoIiwiZXhwIjoxNzM4MjE1NjAwLCJpYXQiOjE3MzgyMTc0MDN9.3auqtgaSEqpFqXhuCyoDM-LbfTOIEGGF6X0AjCcykJ2Nv1WN6LaVbuMDjMf-tKSLyeqFkzQbIckP4FvLHh7wQJ6rafDiT4H2pb6xhouU1QH3szK2S_7VDl_4BhxRbW5pEUt9086HXaVFHEZVS0417pxomlPHxrc1n4Z_A4QxZM5_xh5xcHV8PiGgXWb6_2basjBj5z6POTrazRs67IOQ-ob6ROIsOUGu3om6b8i0h_QSMmeJbujfr2EZqhYWTKijeyidbjRWZ97NFxtGRYN_jPOvy-T3gANXs2a32Er8XvgZTjr_-O8tl_1fHPo2kDE-UCNdwUfBQFhTokDUdJ81bg"; - final String testJwtPublicKey = '''-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6S7asUuzq5Q/3U9rbs+P -kDVIdjgmtgWreG5qWPsC9xXZKiMV1AiV9LXyqQsAYpCqEDM3XbfmZqGb48yLhb/X -qZaKgSYaC/h2DjM7lgrIQAp9902Rr8fUmLN2ivr5tnLxUUOnMOc2SQtr9dgzTONY -W5Zu3PwyvAWk5D6ueIUhLtYzpcB+etoNdL3Ir2746KIy/VUsDwAM7dhrqSK8U2xF -CGlau4ikOTtvzDownAMHMrfE7q1B6WZQDAQlBmxRQsyKln5DIsKv6xauNsHRgBAK -ctUxZG8M4QJIx3S6Aughd3RZC4Ca5Ae9fd8L8mlNYBCrQhOZ7dS0f4at4arlLcaj -twIDAQAB ------END PUBLIC KEY-----'''; - - test('Auth with JSON Web Token (JWT)', () async { - // Note: the objectbox project covers all cases, this test just - // ensures the Dart parts work as expected. - final server = SyncServer(); - // JSON does not allow line breaks in values so escape \n - final publicKeyString = testJwtPublicKey.replaceAll("\n", "\\n"); - serverPort = await server.start(configContents: """ -{ - "auth": { - "jwt": { - "publicKey": "$publicKeyString", - "claimAud": "sync-server", - "claimIss": "objectbox-auth" - } - } -} - """); - await server.online(); - addTearDown(() async => await server.stop()); - - // expired token should fail to log in - var client = createAuthenticatedClient( - env.store, [SyncCredentials.jwtIdToken(testExpiredJwtToken)]); - - final events = []; - client.loginEvents.listen(events.add); - client.start(); - addTearDown(() => client.close()); - - expect( - await client.loginEvents.first.timeout(defaultTimeout, - onTimeout: () => throw TimeoutException( - "Did not receive login event within $defaultTimeout")), - equals(SyncLoginEvent.credentialsRejected)); - - // valid token should succeed to log in - client.setCredentials(SyncCredentials.jwtIdToken(testJwtToken)); - - waitUntilLoggedIn(client); - await yieldExecution(); - - expect( - events, - equals([ - SyncLoginEvent.credentialsRejected, - SyncLoginEvent.loggedIn - ])); + test('SyncClient listeners: changes', () async { + await server.online(); + final client = loggedInClient(store); + final client2 = loggedInClient(env2.store); + + final events = >[]; + client2.changeEvents.listen(events.add); + + expect(env2.store.box().get(1), isNull); + final box = env.store.box(); + final box2 = env2.store.box(); + box.put(TestEntitySynced(value: 10)); + env.store.runInTransaction(TxMode.write, () { + Box(env.store).put(TestEntity()); // not synced + box.put(TestEntitySynced(value: 20)); + box.put(TestEntitySynced(value: 1)); + expect(box.remove(1), isTrue); }); + + // wait for the data to be transferred + expect(waitUntil(() => box2.count() == 2), isTrue); + + // check the events + await yieldExecution(); + expect(events.length, 2); + + // box.put(TestEntitySynced(value: 10)); + expect(events[0].length, 1); + expect(events[0][0].entity, TestEntitySynced); + expect(events[0][0].entityId, + InternalStoreAccess.entityDef(store).model.id.id); + expect(events[0][0].puts, [1]); + expect(events[0][0].removals, isEmpty); + + // env.store.runInTransaction(TxMode.Write, () { + // Box(env.store).put(TestEntity()); // not synced + // box.put(TestEntitySynced(value: 20)); + // box.put(TestEntitySynced(value: 1)); + // expect(box.remove(1), isTrue); + // }); + expect(events[1].length, 1); + expect(events[1][0].entity, TestEntitySynced); + expect(events[1][0].entityId, + InternalStoreAccess.entityDef(store).model.id.id); + expect(events[1][0].puts, [2, 3]); + expect(events[1][0].removals, [1]); + + client.close(); + client2.close(); }); }, skip: SyncServer.isAvailable() ? null : 'sync-server executable is not available in PATH - tests requiring it are skipped'); + + group('Server tests expecting running Sync server', () { + final String testJwtToken = "INSERT_VALID_JWT"; + final String testInvalidJwtToken = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJzeW5jLXNlcnZlciIsImlzcyI6Im9iamVjdGJveC1hdXRoIiwiZXhwIjoxNzM4MjE1NjAwLCJpYXQiOjE3MzgyMTc0MDN9.3auqtgaSEqpFqXhuCyoDM-LbfTOIEGGF6X0AjCcykJ2Nv1WN6LaVbuMDjMf-tKSLyeqFkzQbIckP4FvLHh7wQJ6rafDiT4H2pb6xhouU1QH3szK2S_7VDl_4BhxRbW5pEUt9086HXaVFHEZVS0417pxomlPHxrc1n4Z_A4QxZM5_xh5xcHV8PiGgXWb6_2basjBj5z6POTrazRs67IOQ-ob6ROIsOUGu3om6b8i0h_QSMmeJbujfr2EZqhYWTKijeyidbjRWZ97NFxtGRYN_jPOvy-T3gANXs2a32Er8XvgZTjr_-O8tl_1fHPo2kDE-UCNdwUfBQFhTokDUdJ81bg"; + + /// NOTE Unlike the other tests, this test assumes a Sync server is + /// already running on [serverPort] and has JWT auth configured. + /// Then obtain a JWT and insert it in [testJwtToken] above. + /// + /// Background: Sync server needs to run in a supported environment to + /// enable JWT authentication. So this test can not interact with a + /// sync-server binary like the other tests. + test('Auth with JSON Web Token (JWT)', () async { + // Note: the objectbox project covers all cases, this test just + // ensures the Dart parts work as expected. + + expect(testJwtToken, isNot("INSERT_VALID_JWT"), + reason: + "Paste a valid JWT into testJwtToken before running this test"); + + // Using an already running server, at least check it's available + await SyncServer.onlineAt(serverPort); + + // invalid token should fail to log in + var client = createAuthenticatedClient( + env.store, [SyncCredentials.jwtIdToken(testInvalidJwtToken)]); + + final events = []; + client.loginEvents.listen(events.add); + client.start(); + addTearDown(() => client.close()); + + expect( + await client.loginEvents.first.timeout(defaultTimeout, + onTimeout: () => throw TimeoutException( + "Did not receive login event within $defaultTimeout")), + equals(SyncLoginEvent.credentialsRejected)); + + // valid token should succeed to log in + client.setCredentials(SyncCredentials.jwtIdToken(testJwtToken)); + + waitUntilLoggedIn(client); + await yieldExecution(); + + expect( + events, + equals( + [SyncLoginEvent.credentialsRejected, SyncLoginEvent.loggedIn])); + }); + }, skip: "Test requires to manually run Sync server"); } else { // TESTS to run when SYNC is NOT available @@ -660,14 +599,19 @@ class SyncServer { return port; } + /// Like [onlineAt], but assuming the [_port] of this server. + Future online() async { + return onlineAt(_port!); + } + /// Wait for the server to respond to a simple http request. /// This simple check speeds up test by only trying to log in after the server /// has started, avoiding the reconnect backoff intervals altogether. - Future online() async => Future(() async { + static Future onlineAt(int port) async => Future(() async { final httpClient = HttpClient(); while (true) { try { - await httpClient.get('127.0.0.1', _port!, ''); + await httpClient.get('127.0.0.1', port, ''); break; } on SocketException catch (e) { // Only retry if "Connection refused" (not using error codes as they From 8b6df598d6fd17beb5c7bf7007fd8c8f4ac460a5 Mon Sep 17 00:00:00 2001 From: Shubham Date: Wed, 18 Dec 2024 19:47:35 +0530 Subject: [PATCH 13/16] VectorDistance: add new distance-type 'Geo', add vectorSearchCitiesGeo test #129 --- objectbox/CHANGELOG.md | 2 ++ .../vectorsearch_cities/lib/model.dart | 2 +- objectbox/lib/src/annotations.dart | 13 ++++++- .../lib/src/modelinfo/model_hnsw_params.dart | 2 ++ objectbox_test/test/annotations_test.dart | 1 + objectbox_test/test/entity.dart | 4 +++ objectbox_test/test/hnsw_test.dart | 35 +++++++++++++++++++ objectbox_test/test/objectbox-model.json | 11 ++++-- 8 files changed, 66 insertions(+), 4 deletions(-) diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index c7af74b1..23f4e9f3 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,5 +1,7 @@ ## latest +* Vector Search: You can now use the new `VectorDistanceType.GEO` distance-type to perform vector searches on + geographical coordinates. This is particularly useful for location-based applications. * Flutter for Linux/Windows, Dart Native: update to [objectbox-c 4.1.0](https://github.com/objectbox/objectbox-c/releases/tag/v4.1.0). * Flutter for Android: update to [objectbox-android 4.1.0](https://github.com/objectbox/objectbox-java/releases/tag/V4.1.0). If your project is [using Admin](https://docs.objectbox.io/data-browser#admin-for-android), make sure to diff --git a/objectbox/example/dart-native/vectorsearch_cities/lib/model.dart b/objectbox/example/dart-native/vectorsearch_cities/lib/model.dart index 0910694e..9e65611a 100644 --- a/objectbox/example/dart-native/vectorsearch_cities/lib/model.dart +++ b/objectbox/example/dart-native/vectorsearch_cities/lib/model.dart @@ -7,7 +7,7 @@ class City { String? name; - @HnswIndex(dimensions: 2) + @HnswIndex(dimensions: 2, distanceType: VectorDistanceType.geo) @Property(type: PropertyType.floatVector) List? location; diff --git a/objectbox/lib/src/annotations.dart b/objectbox/lib/src/annotations.dart index b924960d..562f567f 100644 --- a/objectbox/lib/src/annotations.dart +++ b/objectbox/lib/src/annotations.dart @@ -341,7 +341,18 @@ enum VectorDistanceType { /// The more negative the dot product, the higher the distance is (the farther the vectors are). /// /// Value range: 0.0 - 2.0 (nonlinear; 0.0: nearest, 1.0: orthogonal, 2.0: farthest) - dotProductNonNormalized + dotProductNonNormalized, + + /// For geospatial coordinates aka latitude/longitude pairs. + /// + /// Note, that the vector dimension must be 2, with the latitude being the first element and longitude the second. + /// If the vector has more than 2 dimensions, the first 2 dimensions are used. + /// If the vector has fewer than 2 dimensions, the distance is zero. + /// + /// Internally, this uses haversine distance. + /// + /// Value range: 0 km - 6371 * π km (approx. 20015.09 km; half the Earth's circumference) + geo } /// Flags as a part of the [HnswIndex] configuration. diff --git a/objectbox/lib/src/modelinfo/model_hnsw_params.dart b/objectbox/lib/src/modelinfo/model_hnsw_params.dart index f621e97d..dee5c1e6 100644 --- a/objectbox/lib/src/modelinfo/model_hnsw_params.dart +++ b/objectbox/lib/src/modelinfo/model_hnsw_params.dart @@ -189,6 +189,8 @@ extension ModelVectorDistanceType on VectorDistanceType { return OBXVectorDistanceType.DotProduct; } else if (this == VectorDistanceType.dotProductNonNormalized) { return OBXVectorDistanceType.DotProductNonNormalized; + } else if (this == VectorDistanceType.geo) { + return OBXVectorDistanceType.Geo; } else { throw ArgumentError.value(this, "distanceType"); } diff --git a/objectbox_test/test/annotations_test.dart b/objectbox_test/test/annotations_test.dart index 9fdf2bf1..fc1c7241 100644 --- a/objectbox_test/test/annotations_test.dart +++ b/objectbox_test/test/annotations_test.dart @@ -22,6 +22,7 @@ void main() { OBXVectorDistanceType.DotProduct); expect(VectorDistanceType.dotProductNonNormalized.toConstant(), OBXVectorDistanceType.DotProductNonNormalized); + expect(VectorDistanceType.geo.toConstant(), OBXVectorDistanceType.Geo); }); test("ModelHnswParams maps values", () { diff --git a/objectbox_test/test/entity.dart b/objectbox_test/test/entity.dart index 9ffbff5d..5712476d 100644 --- a/objectbox_test/test/entity.dart +++ b/objectbox_test/test/entity.dart @@ -465,6 +465,10 @@ class HnswObject { @HnswIndex(dimensions: 2) List? floatVector; + @Property(type: PropertyType.floatVector) + @HnswIndex(dimensions: 2, distanceType: VectorDistanceType.geo) + List? floatVectorGeoCoordinates; + final rel = ToOne(); } diff --git a/objectbox_test/test/hnsw_test.dart b/objectbox_test/test/hnsw_test.dart index 0d1447a7..8291a858 100644 --- a/objectbox_test/test/hnsw_test.dart +++ b/objectbox_test/test/hnsw_test.dart @@ -72,6 +72,41 @@ void main() { expect(closest2.name, "node8"); }); + test('vectorSearchCitiesGeo', () { + // capital cities across Europe + List cities = ["Berlin", "Paris", "Rome", "Madrid", "London"]; + List> coordinates = [ + [52.5200, 13.4050], + [48.8566, 2.3522], + [41.9028, 12.4964], + [40.4168, -3.7038], + [51.5074, -0.1278] + ]; + + box.putMany(List.generate(cities.length, (i) { + return HnswObject() + ..name = cities[i] + ..floatVectorGeoCoordinates = coordinates[i]; + })); + + // lat/lng for Munich + final List searchVector = [48.1371, 11.5754]; + + final query = box + .query(HnswObject_.floatVectorGeoCoordinates + .nearestNeighborsF32(searchVector, 5)) + .build(); + addTearDown(() => query.close()); + + final nearestCities = query.find(); + expect(nearestCities.length, 5); + expect(nearestCities[0].name, "Berlin"); + expect(nearestCities[1].name, "Paris"); + expect(nearestCities[2].name, "Rome"); + expect(nearestCities[3].name, "Madrid"); + expect(nearestCities[4].name, "London"); + }); + test('find offset limit', () { box.putMany(List.generate(15, (index) { final i = index + 1; // start at 1 diff --git a/objectbox_test/test/objectbox-model.json b/objectbox_test/test/objectbox-model.json index 23803d53..99635f56 100644 --- a/objectbox_test/test/objectbox-model.json +++ b/objectbox_test/test/objectbox-model.json @@ -667,7 +667,7 @@ }, { "id": "14:880388751413233760", - "lastPropertyId": "4:2308158275756661586", + "lastPropertyId": "5:1476994841170736323", "name": "HnswObject", "properties": [ { @@ -695,6 +695,13 @@ "flags": 520, "indexId": "22:6527315700526716999", "relationTarget": "RelatedNamedEntity" + }, + { + "id": "5:1476994841170736323", + "name": "floatVectorGeoCoordinates", + "type": 28, + "flags": 8, + "indexId": "23:6649884639373473085" } ], "relations": [] @@ -720,7 +727,7 @@ } ], "lastEntityId": "15:4803284427984871569", - "lastIndexId": "22:6527315700526716999", + "lastIndexId": "23:6649884639373473085", "lastRelationId": "1:2155747579134420981", "lastSequenceId": "0:0", "modelVersion": 5, From e22bc416cd73cdb40f07375f6fa1a173bdd43682 Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 4 Feb 2025 15:06:06 +0100 Subject: [PATCH 14/16] Follow-up: objectbox-android 4.1.0 requires Android 5.0 (API level 21) --- flutter_libs/android/build.gradle | 2 +- objectbox/CHANGELOG.md | 1 + .../example/flutter/objectbox_demo/android/app/build.gradle | 2 +- .../flutter/objectbox_demo_relations/android/app/build.gradle | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/flutter_libs/android/build.gradle b/flutter_libs/android/build.gradle index 233ae152..79bf4f15 100644 --- a/flutter_libs/android/build.gradle +++ b/flutter_libs/android/build.gradle @@ -45,7 +45,7 @@ android { } defaultConfig { - minSdkVersion 19 // ObjectBox Android requires Android 4.4 (API level 19) + minSdkVersion 21 // ObjectBox Android requires Android 5.0 (API level 21) } dependencies { diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index 23f4e9f3..39e22de4 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,5 +1,6 @@ ## latest +* Flutter for Android: requires Android 5.0 (API level 21). * Vector Search: You can now use the new `VectorDistanceType.GEO` distance-type to perform vector searches on geographical coordinates. This is particularly useful for location-based applications. * Flutter for Linux/Windows, Dart Native: update to [objectbox-c 4.1.0](https://github.com/objectbox/objectbox-c/releases/tag/v4.1.0). diff --git a/objectbox/example/flutter/objectbox_demo/android/app/build.gradle b/objectbox/example/flutter/objectbox_demo/android/app/build.gradle index 136b9f09..cb51c6ca 100644 --- a/objectbox/example/flutter/objectbox_demo/android/app/build.gradle +++ b/objectbox/example/flutter/objectbox_demo/android/app/build.gradle @@ -47,7 +47,7 @@ android { // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. // minSdkVersion flutter.minSdkVersion - minSdkVersion 19 // ObjectBox requires at least SDK 19 (Android 4.4) + minSdkVersion 21 // ObjectBox Android requires Android 5.0 (API level 21) targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/objectbox/example/flutter/objectbox_demo_relations/android/app/build.gradle b/objectbox/example/flutter/objectbox_demo_relations/android/app/build.gradle index f4fc2951..2f620725 100644 --- a/objectbox/example/flutter/objectbox_demo_relations/android/app/build.gradle +++ b/objectbox/example/flutter/objectbox_demo_relations/android/app/build.gradle @@ -47,7 +47,7 @@ android { // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. // minSdkVersion flutter.minSdkVersion - minSdkVersion 19 // ObjectBox requires at least SDK 19 (Android 4.4) + minSdkVersion 21 // ObjectBox Android requires Android 5.0 (API level 21) targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName From 3522b2fdb30fe0a297bfc498e032dcca1ec990ce Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 4 Feb 2025 15:26:17 +0100 Subject: [PATCH 15/16] Prepare release 4.1.0 --- flutter_libs/pubspec.yaml | 4 ++-- generator/lib/src/version.dart | 2 +- generator/pubspec.yaml | 4 ++-- objectbox/CHANGELOG.md | 2 +- .../example/dart-native/vectorsearch_cities/pubspec.yaml | 2 +- .../event_management_tutorial/event_manager/pubspec.yaml | 2 +- .../event_management_tutorial/many_to_many/pubspec.yaml | 2 +- objectbox/example/flutter/objectbox_demo/pubspec.yaml | 2 +- .../example/flutter/objectbox_demo_relations/pubspec.yaml | 2 +- objectbox/example/flutter/objectbox_demo_sync/pubspec.yaml | 2 +- objectbox/pubspec.yaml | 2 +- sync_flutter_libs/pubspec.yaml | 4 ++-- 12 files changed, 15 insertions(+), 15 deletions(-) diff --git a/flutter_libs/pubspec.yaml b/flutter_libs/pubspec.yaml index cd23368d..57a25122 100644 --- a/flutter_libs/pubspec.yaml +++ b/flutter_libs/pubspec.yaml @@ -3,7 +3,7 @@ description: Superfast NoSQL Flutter / Dart database. This package contains Flut # Link to actual directory in repository so file links on pub.dev work. repository: https://github.com/objectbox/objectbox-dart/tree/main/flutter_libs homepage: https://objectbox.io -version: 4.0.3 +version: 4.1.0 environment: sdk: '>=2.18.0 <4.0.0' @@ -14,7 +14,7 @@ dependencies: sdk: flutter # This is here just to ensure compatibility between objectbox-dart code and the libraries used # You should still depend on objectbox directly in your Flutter application. - objectbox: 4.0.3 + objectbox: 4.1.0 path_provider: ^2.0.0 dev_dependencies: diff --git a/generator/lib/src/version.dart b/generator/lib/src/version.dart index deb429e6..6c73c95d 100644 --- a/generator/lib/src/version.dart +++ b/generator/lib/src/version.dart @@ -4,5 +4,5 @@ class Version { /// /// This string is updated by the /tool/set-version.sh script /// as part of the release process. - static const String current = "4.0.3"; + static const String current = "4.1.0"; } diff --git a/generator/pubspec.yaml b/generator/pubspec.yaml index d755efd7..3ea269b7 100644 --- a/generator/pubspec.yaml +++ b/generator/pubspec.yaml @@ -3,13 +3,13 @@ description: ObjectBox Flutter / Dart database binding code generator - finds an # Link to actual directory in repository so file links on pub.dev work. repository: https://github.com/objectbox/objectbox-dart/tree/main/generator homepage: https://objectbox.io -version: 4.0.3 +version: 4.1.0 environment: sdk: '>=2.18.0 <4.0.0' dependencies: - objectbox: 4.0.3 + objectbox: 4.1.0 analyzer: '>=5.2.0 <7.0.0' # 5.1.0 has a bug where DartType.element has been removed. build: ^2.0.0 collection: ^1.15.0 diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index 39e22de4..0d38bcb0 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,4 +1,4 @@ -## latest +## 4.1.0 (2025-02-04) * Flutter for Android: requires Android 5.0 (API level 21). * Vector Search: You can now use the new `VectorDistanceType.GEO` distance-type to perform vector searches on diff --git a/objectbox/example/dart-native/vectorsearch_cities/pubspec.yaml b/objectbox/example/dart-native/vectorsearch_cities/pubspec.yaml index 87394443..f1ec8fe3 100644 --- a/objectbox/example/dart-native/vectorsearch_cities/pubspec.yaml +++ b/objectbox/example/dart-native/vectorsearch_cities/pubspec.yaml @@ -6,7 +6,7 @@ environment: sdk: ^2.18.6 dependencies: - objectbox: ^4.0.3 + objectbox: ^4.1.0 dev_dependencies: build_runner: ^2.4.9 diff --git a/objectbox/example/flutter/event_management_tutorial/event_manager/pubspec.yaml b/objectbox/example/flutter/event_management_tutorial/event_manager/pubspec.yaml index 42d128e6..88470a1f 100644 --- a/objectbox/example/flutter/event_management_tutorial/event_manager/pubspec.yaml +++ b/objectbox/example/flutter/event_management_tutorial/event_manager/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: flutter: sdk: flutter - objectbox: ^4.0.3 + objectbox: ^4.1.0 objectbox_flutter_libs: any intl: any diff --git a/objectbox/example/flutter/event_management_tutorial/many_to_many/pubspec.yaml b/objectbox/example/flutter/event_management_tutorial/many_to_many/pubspec.yaml index 42d128e6..88470a1f 100644 --- a/objectbox/example/flutter/event_management_tutorial/many_to_many/pubspec.yaml +++ b/objectbox/example/flutter/event_management_tutorial/many_to_many/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: flutter: sdk: flutter - objectbox: ^4.0.3 + objectbox: ^4.1.0 objectbox_flutter_libs: any intl: any diff --git a/objectbox/example/flutter/objectbox_demo/pubspec.yaml b/objectbox/example/flutter/objectbox_demo/pubspec.yaml index 276eba60..418146e2 100644 --- a/objectbox/example/flutter/objectbox_demo/pubspec.yaml +++ b/objectbox/example/flutter/objectbox_demo/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: flutter: sdk: flutter - objectbox: ^4.0.3 + objectbox: ^4.1.0 objectbox_flutter_libs: any intl: any path_provider: ^2.0.10 diff --git a/objectbox/example/flutter/objectbox_demo_relations/pubspec.yaml b/objectbox/example/flutter/objectbox_demo_relations/pubspec.yaml index bacccdad..f464a0e5 100644 --- a/objectbox/example/flutter/objectbox_demo_relations/pubspec.yaml +++ b/objectbox/example/flutter/objectbox_demo_relations/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: flutter: sdk: flutter - objectbox: ^4.0.3 + objectbox: ^4.1.0 objectbox_flutter_libs: any intl: any path_provider: ^2.0.10 # 2.0.11+ requires Flutter 2.8.0 diff --git a/objectbox/example/flutter/objectbox_demo_sync/pubspec.yaml b/objectbox/example/flutter/objectbox_demo_sync/pubspec.yaml index 3b631515..1d20088d 100644 --- a/objectbox/example/flutter/objectbox_demo_sync/pubspec.yaml +++ b/objectbox/example/flutter/objectbox_demo_sync/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: flutter: sdk: flutter - objectbox: ^4.0.3 + objectbox: ^4.1.0 objectbox_sync_flutter_libs: any # For Sync support use this instead of objectbox_flutter_libs. intl: any path_provider: ^2.0.10 diff --git a/objectbox/pubspec.yaml b/objectbox/pubspec.yaml index bfea4c36..aec52b70 100644 --- a/objectbox/pubspec.yaml +++ b/objectbox/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://objectbox.io # Link to actual directory in repository so file links on pub.dev work. repository: https://github.com/objectbox/objectbox-dart/tree/main/objectbox documentation: https://docs.objectbox.io -version: 4.0.3 +version: 4.1.0 environment: # minimum Dart SDK (also see generator and flutter_libs) diff --git a/sync_flutter_libs/pubspec.yaml b/sync_flutter_libs/pubspec.yaml index 21e15d07..46544410 100644 --- a/sync_flutter_libs/pubspec.yaml +++ b/sync_flutter_libs/pubspec.yaml @@ -3,7 +3,7 @@ description: Fast Flutter database for persisting Dart objects. This package con # Link to actual directory in repository so file links on pub.dev work. repository: https://github.com/objectbox/objectbox-dart/tree/main/sync_flutter_libs homepage: https://objectbox.io -version: 4.0.3 +version: 4.1.0 environment: sdk: '>=2.18.0 <4.0.0' @@ -14,7 +14,7 @@ dependencies: sdk: flutter # This is here just to ensure compatibility between objectbox-dart code and the libraries used # You should still depend on objectbox directly in your Flutter application. - objectbox: 4.0.3 + objectbox: 4.1.0 path_provider: ^2.0.0 dev_dependencies: From 48ca32917b7ebccf710b3af8358b8e949d349770 Mon Sep 17 00:00:00 2001 From: Uwe Date: Tue, 4 Feb 2025 15:27:06 +0100 Subject: [PATCH 16/16] Prepare for next release --- objectbox/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index 0d38bcb0..910addf0 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,3 +1,7 @@ +## latest + + + ## 4.1.0 (2025-02-04) * Flutter for Android: requires Android 5.0 (API level 21).