Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## [Unreleased]
- Add `Surreal.info()` / `Surreal.info(Class<T>)` returning the record of the currently-authenticated record user (the `$auth` record) as an `Optional`, empty when the session is not authenticated as a record user; matches the `info` RPC method and the JS SDK's `auth()` [#190](https://github.com/surrealdb/surrealdb.java/pull/190).

## [2.1.2] - 2026-06-24
- Add full geometry-type support for reading and writing all seven types — Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, and GeometryCollection — via `Geometry` type-discrimination accessors (`getType()`, `isPolygon()`, …), readers returning `java.awt.geom.Point2D.Double` coordinates (x = longitude, y = latitude), and factory methods that serialize through `create`/`update` content and bound parameters. Also fixes an `UnsatisfiedLinkError` thrown when a `Geometry` was finalized (the native `deleteInstance` had no matching Rust symbol) [#183](https://github.com/surrealdb/surrealdb.java/pull/183).
- Add `Surreal.kill(String)` / `Surreal.kill(java.util.UUID)` to terminate a live query by id, and `LiveStream.getQueryId()` to read the live-query UUID immediately, before the first notification; `selectLive` now starts the subscription through the public `LIVE SELECT` query path so the id is available up front. `kill()` stops notifications but does not close a local `LiveStream` — use `LiveStream.close()` to release a blocked `next()` [#184](https://github.com/surrealdb/surrealdb.java/pull/184).
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/com/surrealdb/Surreal.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ private static native Token signinRecord(long ptr, String namespace, String data

private static native boolean invalidate(long ptr);

private static native long info(long ptr);

private static native NsDb useNs(long ptr, String ns);

private static native NsDb useDb(long ptr, String db);
Expand Down Expand Up @@ -420,6 +422,51 @@ public Surreal invalidate() {
return this;
}

/**
* Returns the record of the currently-authenticated record user, i.e. the
* record pointed to by the {@code $auth} parameter (implemented as
* {@code SELECT * FROM $auth}). This matches the {@code info} RPC method and
* the JavaScript SDK's {@code auth()} (called {@code info()} in the v1 JS SDK).
* <p>
* A record user can only read its own record when the table permissions allow
* it (e.g.
* {@code DEFINE TABLE user PERMISSIONS FOR select WHERE id = $auth.id}).
* <p>
* For more details, check the <a href=
* "https://surrealdb.com/docs/surrealdb/security/authentication#record-users">authentication
* documentation</a>.
*
* @return an Optional containing the authenticated record user's Value, or an
* empty Optional when the session is not authenticated as a record user
* (e.g. unauthenticated, invalidated, or a system user) or the record

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Javadoc says empty is also returned when "the record is not visible to it" (permissions). That's plausible but untested and server-version-dependent — the only permissions case covered is the permissive WHERE id = $auth.id. Consider softening the wording or adding a test.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test rather than softening the wording: info_recordNotVisibleToUser_returnsEmpty (8e375e8) signs up a record user against a table defined with PERMISSIONS FOR select NONE, asserts the session is still authenticated (RETURN $auth yields a user record id), and then asserts both info() and info(Class) return an empty Optional. So the behavior matches the Javadoc on the embedded engine (SurrealDB 3.1.5): SELECT * FROM $auth returns an empty result set when permissions hide the record, which info() maps to Optional.empty(). Kept the Javadoc as-is now that the claim is pinned by a test.

* is not visible to it
* @throws SurrealException
* if the request fails
*/
public Optional<Value> info() {
final long valuePtr = info(getPtr());
if (valuePtr == 0) {
return Optional.empty();
}
return Optional.of(new Value(valuePtr));
}

/**
* Returns the record of the currently-authenticated record user, converted to
* an instance of the specified type.
*
* @param <T>
* the type of the instance to be returned
* @param type
* the class type to convert the authenticated record to
* @return an Optional containing the authenticated record user converted to the
* specified type, otherwise an empty Optional
* @see #info()
*/
public <T> Optional<T> info(Class<T> type) {
return info().map(v -> v.get(type));
}

/**
* Creates a new session that shares the same connection but has its own
* namespace, database, and authentication state. Use this for multi-session
Expand Down
36 changes: 36 additions & 0 deletions src/main/rust/surreal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,42 @@ pub extern "system" fn Java_com_surrealdb_Surreal_invalidate<'local>(
})
}

/// JNI implementation of `Surreal.info(long ptr)`.
///
/// Fetches the record of the currently-authenticated record user by running
/// `SELECT * FROM $auth`. The non-`ONLY` form is used deliberately: when the
/// session is not authenticated as a record user, `$auth` is NONE and the
/// query yields an empty array (mapped to 0, i.e. `Optional.empty()`),
/// whereas `SELECT * FROM ONLY $auth` would raise "Expected a single result
/// output when using the ONLY keyword".
#[no_mangle]
pub extern "system" fn Java_com_surrealdb_Surreal_info<'local>(
mut env: EnvUnowned<'local>,
_class: JClass<'local>,
ptr: jlong,
) -> jlong {
with_env_body!(env, env, {
// Retrieve the Surreal instance
let surreal = get_surreal_ref!(env, ptr, || 0);
// Execute the query
let res = surrealdb_query::<()>(surreal, "SELECT * FROM $auth", None);
// Check the result
let mut res = check_query_result!(env, res, || 0);
// There is only one statement
let mut res = take_one_result!(env, res, || 0);
// There should be at most one result
return_value_array_first!(res);
// If the array is empty, return null (0) for Optional.empty()
if let Value::Array(ref a) = res {
if a.is_empty() {
return 0;
}
}
// Otherwise throw an error
return_unexpected_result!(env, res.to_sql(), || 0)
})
}

#[no_mangle]
pub extern "system" fn Java_com_surrealdb_Surreal_useNs<'local>(
mut env: EnvUnowned<'local>,
Expand Down
118 changes: 118 additions & 0 deletions src/test/java/com/surrealdb/InfoTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.surrealdb;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;

import com.surrealdb.pojos.User;
import com.surrealdb.signin.RecordCredential;
import com.surrealdb.signin.Token;

/**
* Tests for {@link Surreal#info()} and {@link Surreal#info(Class)}: fetching
* the currently-authenticated record user ({@code SELECT * FROM $auth}).
*/
public class InfoTests {

/** Java 8 compatible: Map.of("email", "a@b.com", "pass", "p") */
private static Map<String, String> params(String k1, String v1, String k2, String v2) {
Map<String, String> m = new HashMap<>();
m.put(k1, v1);
m.put(k2, v2);
return m;
}

/**
* Defines a `user` table (readable by its own record user) with a record
* access, then signs up a record user with the given email.
*/
private static Token signupUser(Surreal surreal, String email) {
return signupUser(surreal, email, "WHERE id = $auth.id");
}

/**
* Defines a `user` table with the given select permission clause and a record
* access, then signs up a record user with the given email.
*/
private static Token signupUser(Surreal surreal, String email, String selectPermission) {
surreal.query("DEFINE TABLE user SCHEMALESS PERMISSIONS FOR select " + selectPermission);
surreal.query("DEFINE ACCESS user ON DATABASE TYPE RECORD "
+ "SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) "
+ "SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) "
+ "DURATION FOR TOKEN 15m, FOR SESSION 12h");
return surreal.signup(new RecordCredential("user", params("email", email, "pass", "p")));
}

@Test
void info_afterSignup_returnsAuthenticatedRecord() {
try (Surreal surreal = new Surreal()) {
surreal.connect("memory").useNs("test_ns").useDb("test_db");
Token token = signupUser(surreal, "tobie@example.com");
assertNotNull(token.getAccess());
// info() returns the record user pointed to by $auth
Optional<Value> info = surreal.info();
assertTrue(info.isPresent());
Value value = info.get();
assertTrue(value.isObject());
assertEquals("tobie@example.com", value.getObject().get("email").getString());
// The returned record is the $auth record
RecordId auth = surreal.query("RETURN $auth").take(0).getRecordId();
assertEquals(auth, value.getObject().get("id").getRecordId());
}
}

@Test
void info_typed_hydratesPojo() {
try (Surreal surreal = new Surreal()) {
surreal.connect("memory").useNs("test_ns").useDb("test_db");
signupUser(surreal, "jaime@example.com");
Optional<User> user = surreal.info(User.class);
assertTrue(user.isPresent());
assertEquals("jaime@example.com", user.get().email);
assertNotNull(user.get().id);
assertEquals("user", user.get().id.getTable());
}
}

@Test
void info_unauthenticated_returnsEmpty() {
try (Surreal surreal = new Surreal()) {
surreal.connect("memory").useNs("test_ns").useDb("test_db");
assertFalse(surreal.info().isPresent());
assertFalse(surreal.info(User.class).isPresent());
}
}

@Test
void info_recordNotVisibleToUser_returnsEmpty() {
try (Surreal surreal = new Surreal()) {
surreal.connect("memory").useNs("test_ns").useDb("test_db");
// The table's select permission excludes the record user's own record
Token token = signupUser(surreal, "denied@example.com", "NONE");
assertNotNull(token.getAccess());
// The session is authenticated: $auth points to the record user...
RecordId auth = surreal.query("RETURN $auth").take(0).getRecordId();
assertEquals("user", auth.getTable());
// ...but the record is not visible to it, so info() is empty
assertFalse(surreal.info().isPresent());
assertFalse(surreal.info(User.class).isPresent());
}
}

@Test
void info_afterInvalidate_returnsEmpty() {
try (Surreal surreal = new Surreal()) {
surreal.connect("memory").useNs("test_ns").useDb("test_db");
signupUser(surreal, "emmanuel@example.com");
assertTrue(surreal.info().isPresent());
surreal.invalidate();
assertFalse(surreal.info().isPresent());
}
}
}
39 changes: 39 additions & 0 deletions src/test/java/com/surrealdb/pojos/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.surrealdb.pojos;

import java.util.Objects;

import com.surrealdb.RecordId;

public class User {

public RecordId id;
public String email;

public User() {
}

public User(RecordId id, String email) {
this.id = id;
this.email = email;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
final User user = (User) o;
return Objects.equals(id, user.id) && Objects.equals(email, user.email);
}

@Override
public String toString() {
return "id: " + id + ", email: " + email;
}

@Override
public int hashCode() {
return Objects.hash(id, email);
}
}
Loading