This document provides a comprehensive analysis of the current authentication system's usage of the Implexus database and maps out the migration path to InvercargillSql. The authentication system uses Implexus as a document store with catalogue-based indexing for user and session management.
| File | Implexus Usage | Complexity |
|---|---|---|
UserService.vala |
Heavy - all CRUD operations | High |
SessionService.vala |
Heavy - all CRUD operations | High |
AuthenticationMigration.vala |
Medium - schema setup | Medium |
PermissionService.vala |
None - delegates to UserService | N/A |
User.vala |
None - data model only | N/A |
Session.vala |
None - data model only | N/A |
UserIdentityProvider.vala |
None - adapter only | N/A |
Components/*.vala |
None - use inject<> for services | N/A |
// Standard imports in files using Implexus
using Implexus.Core; // Engine, EntityPath, Container, Document
using Invercargill; // Element, Properties
using Invercargill.DataStructures; // Vector, Dictionary, Series
graph TB
subgraph Implexus Storage Structure
Root["/spry"]
Auth["/spry/authentication"]
Users["/spry/authentication/users"]
Sessions["/spry/authentication/sessions"]
SessionsByUser["/spry/authentication/sessions_by_user"]
end
Root --> Auth
Auth --> Users
Auth --> Sessions
Auth --> SessionsByUser
subgraph User Document
UserDoc["Document: {user_id}"]
UserProps["Properties: username, email, password_hash, etc."]
end
subgraph Session Document
SessionDoc["Document: {session_id}"]
SessionProps["Properties: user_id, created_at, expires_at, etc."]
end
Users --> UserDoc
UserDoc --> UserProps
Sessions --> SessionDoc
SessionDoc --> SessionProps
| Catalogue | Path | Indexed Property | Purpose |
|---|---|---|---|
by_username |
/spry/authentication/users |
username |
Unique username lookup |
by_email |
/spry/authentication/users |
email |
Unique email lookup |
File: src/Authentication/User.vala
public class Spry.Authentication.User : Object, Authorisation.Identity {
public string id { get; set; }
public string username { get; set; }
public string email { get; set; }
public string password_hash { get; set; }
public Vector<string> permissions { get; set; }
public Dictionary<string, string> app_data { get; set; }
public bool is_active { get; set; }
public DateTime created_at { get; set; }
public DateTime? updated_at { get; set; }
public DateTime? last_login_at { get; set; }
}
Implexus Property Mapping:
| Property | Implexus Storage | Type |
|---|---|---|
id |
Document ID | string |
username |
Property | string |
email |
Property | string |
password_hash |
Property | string |
permissions |
Property (JSON array) | Vector<string> |
app_data |
Property (JSON object) | Dictionary<string, string> |
is_active |
Property | bool |
created_at |
Property (ISO 8601) | DateTime |
updated_at |
Property (ISO 8601) | DateTime? |
last_login_at |
Property (ISO 8601) | DateTime? |
File: src/Authentication/Session.vala
public class Spry.Authentication.Session : Object {
public string id { get; set; }
public string user_id { get; set; }
public DateTime created_at { get; set; }
public DateTime expires_at { get; set; }
public string? ip_address { get; set; }
public string? user_agent { get; set; }
}
Implexus Property Mapping:
| Property | Implexus Storage | Type |
|---|---|---|
id |
Document ID | string |
user_id |
Property | string |
created_at |
Property (ISO 8601) | DateTime |
expires_at |
Property (ISO 8601) | DateTime |
ip_address |
Property | string? |
user_agent |
Property | string? |
File: src/Authentication/UserService.vala
// Current Implexus pattern
var user_path = new EntityPath("/spry/authentication/users");
var user_doc = yield engine.create_document_async(user_path);
// Set properties
yield engine.set_entity_property_async(user_doc.path, "username", new NativeElement<string>(username));
yield engine.set_entity_property_async(user_doc.path, "email", new NativeElement<string>(email));
yield engine.set_entity_property_async(user_doc.path, "password_hash", new NativeElement<string>(hash));
// ... more properties
// Current Implexus pattern
var user_path = new EntityPath("/spry/authentication/users/%s".printf(id));
var user_doc = yield engine.get_entity_or_null_async(user_path);
if (user_doc == null) return null;
var props = yield engine.get_properties_async(user_doc.path);
var user = new User();
user.id = user_doc.id;
user.username = props.get("username")?.as_string_or_null();
// ... more properties
// Current Implexus pattern - property by property updates
yield engine.set_entity_property_async(user_path, "email", new NativeElement<string>(email));
yield engine.set_entity_property_async(user_path, "updated_at", new NativeElement<string>(now));
// Current Implexus pattern
var user_path = new EntityPath("/spry/authentication/users/%s".printf(id));
yield engine.delete_entity_async(user_path);
// Current Implexus pattern
var catalogue = yield engine.get_catalogue_async("/spry/authentication/users/by_username");
var entry = catalogue.get(username);
if (entry != null) {
var user_id = entry.value.as_string_or_null();
// Then fetch user by ID
}
File: src/Authentication/SessionService.vala
// Current Implexus pattern
var session_path = new EntityPath("/spry/authentication/sessions");
var session_doc = yield engine.create_document_async(session_path);
// Set properties
yield engine.set_entity_property_async(session_doc.path, "user_id", new NativeElement<string>(user_id));
yield engine.set_entity_property_async(session_doc.path, "created_at", new NativeElement<string>(created_at));
// ... more properties
// Also update sessions_by_user index
var index_path = new EntityPath("/spry/authentication/sessions_by_user/%s".printf(user_id));
// ... index management
// Current Implexus pattern - uses secondary index
var index_path = new EntityPath("/spry/authentication/sessions_by_user/%s".printf(user_id));
var index_doc = yield engine.get_entity_or_null_async(index_path);
// Parse session IDs from index
File: src/Authentication/AuthenticationMigration.vala
public class Spry.Authentication.AuthenticationMigration : Implexus.Migrations.Migration {
public override async void up_async(Engine engine) throws Error {
// Create containers
var users_path = new EntityPath("/spry/authentication/users");
yield engine.create_container_async(users_path);
var sessions_path = new EntityPath("/spry/authentication/sessions");
yield engine.create_container_async(sessions_path);
// Create catalogues for unique constraints
yield engine.create_catalogue_async(users_path, "by_username", "username");
yield engine.create_catalogue_async(users_path, "by_email", "email");
}
}
These interfaces must be preserved during migration:
public async User? get_user_by_id_async(string id)
public async User? get_user_by_username_async(string username)
public async User? get_user_by_email_async(string email)
public async User create_user_async(string username, string email, string password)
public async User update_user_async(User user)
public async void delete_user_async(string id)
public async bool validate_credentials_async(string username, string password)
public async bool username_exists_async(string username)
public async bool email_exists_async(string email)
public async Session create_session_async(string user_id, string? ip_address, string? user_agent)
public async Session? get_session_async(string session_id)
public async void delete_session_async(string session_id)
public async void delete_user_sessions_async(string user_id)
public async Vector<Session> get_user_sessions_async(string user_id)
public async bool validate_session_async(string session_id)
public async Session? refresh_session_async(string session_id)
public async void grant_permission_async(string user_id, string permission)
public async void revoke_permission_async(string user_id, string permission)
public async bool has_permission_async(string user_id, string permission)
public async Vector<string> get_user_permissions_async(string user_id)
// Inversion IoC registration pattern
container.register<UserService>().as_singleton();
container.register<SessionService>().as_singleton();
container.register<PermissionService>().as_singleton();
container.register<UserIdentityProvider>().as_singleton();
graph LR
subgraph Implexus - Document Store
I1[Engine]
I2[Container]
I3[Document]
I4[Properties]
I5[Catalogue]
end
subgraph InvercargillSql - SQL Database
S1[Connection]
S2[Command]
S3[Table/Row]
S4[Properties]
S5[Index]
end
I1 --> I2
I2 --> I3
I3 --> I4
I2 --> I5
S1 --> S2
S2 --> S3
S3 --> S4
S1 --> S5
| Feature | Implexus | InvercargillSql |
|---|---|---|
| Data Model | Hierarchical document store | Relational tables |
| Schema | Schemaless properties | Fixed schema with migrations |
| Indexing | Catalogues (auto-maintained) | SQL indexes (manual) |
| Querying | Path-based + catalogues | SQL queries |
| Transactions | Not explicit | Full transaction support |
| Async | Native async API | Thread-based async |
| Result Type | Properties |
Enumerable<Properties> |
| Parameter Binding | N/A | Fluent with_parameter<T>() |
| Relationships | Manual (via paths) | Foreign keys |
| Operation | Implexus | InvercargillSql |
|---|---|---|
| Connect | Engine construction |
ConnectionFactory.create() + open() |
| Create | create_document_async() |
INSERT via execute_non_query() |
| Read | get_entity_or_null_async() + get_properties_async() |
SELECT via execute_query() |
| Update | set_entity_property_async() |
UPDATE via execute_non_query() |
| Delete | delete_entity_async() |
DELETE via execute_non_query() |
| Query | Catalogue lookup | SELECT WHERE via execute_query() |
| Transaction | N/A | begin_transaction() + commit()/rollback() |
Implexus (Current):
var path = new EntityPath("/spry/authentication/users");
var doc = yield engine.create_document_async(path);
yield engine.set_entity_property_async(doc.path, "username", new NativeElement<string>(username));
yield engine.set_entity_property_async(doc.path, "email", new NativeElement<string>(email));
// ... more properties
return doc.id;
InvercargillSql (Target):
var sql = """
INSERT INTO users (id, username, email, password_hash, is_active, created_at)
VALUES (:id, :username, :email, :password_hash, :is_active, :created_at)
""";
yield conn.create_command(sql)
.with_parameter("id", generate_uuid())
.with_parameter("username", username)
.with_parameter("email", email)
.with_parameter("password_hash", hash)
.with_parameter("is_active", true)
.with_parameter("created_at", new DateTime.now_utc().format_iso8601())
.execute_non_query_async();
return conn.last_insert_rowid.to_string();
Implexus (Current):
var path = new EntityPath("/spry/authentication/users/%s".printf(id));
var doc = yield engine.get_entity_or_null_async(path);
if (doc == null) return null;
var props = yield engine.get_properties_async(doc.path);
return user_from_properties(props);
InvercargillSql (Target):
var sql = "SELECT * FROM users WHERE id = :id";
var results = yield conn.create_command(sql)
.with_parameter("id", id)
.execute_query_async();
var row = results.first_or_default();
if (row == null) return null;
return user_from_row(row);
Implexus (Current):
var catalogue = yield engine.get_catalogue_async("/spry/authentication/users/by_username");
var entry = catalogue.get(username);
if (entry == null) return null;
return yield get_user_by_id_async(entry.value.as_string_or_null());
InvercargillSql (Target):
var sql = "SELECT * FROM users WHERE username = :username";
var results = yield conn.create_command(sql)
.with_parameter("username", username)
.execute_query_async();
var row = results.first_or_default();
if (row == null) return null;
return user_from_row(row);
-- Users table
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT,
last_login_at TEXT
);
-- User permissions (normalized)
CREATE TABLE user_permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
permission TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, permission)
);
-- User app data (key-value store)
CREATE TABLE user_app_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, key)
);
-- Sessions table
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
ip_address TEXT,
user_agent TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Indexes for common queries
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
flowchart TB
subgraph Phase 1 - Infrastructure
A1[Create SQL schema migration]
A2[Create ConnectionProvider]
A3[Update meson.build dependencies]
end
subgraph Phase 2 - Data Access Layer
B1[Create UserRepository interface]
B2[Implement SqlUserRepository]
B3[Create SessionRepository interface]
B4[Implement SqlSessionRepository]
end
subgraph Phase 3 - Service Migration
C1[Refactor UserService to use repository]
C2[Refactor SessionService to use repository]
C3[Update PermissionService]
end
subgraph Phase 4 - Cleanup
D1[Remove Implexus dependency]
D2[Delete AuthenticationMigration]
D3[Update documentation]
end
A1 --> A2 --> A3 --> B1
B1 --> B2 --> B3 --> B4
B4 --> C1 --> C2 --> C3
C3 --> D1 --> D2 --> D3
To minimize impact on existing code and allow for future database changes, implement a repository pattern:
// Abstract repository interfaces
public interface UserRepository : Object {
public abstract async User? get_by_id_async(string id);
public abstract async User? get_by_username_async(string username);
public abstract async User? get_by_email_async(string email);
public abstract async User create_async(User user);
public abstract async User update_async(User user);
public abstract async void delete_async(string id);
public abstract async bool username_exists_async(string username);
public abstract async bool email_exists_async(string email);
}
public interface SessionRepository : Object {
public abstract async Session create_async(Session session);
public abstract async Session? get_by_id_async(string id);
public abstract async void delete_async(string id);
public abstract async void delete_by_user_async(string user_id);
public abstract async Vector<Session> get_by_user_async(string user_id);
}
Data Type Mapping:
DateTime → ISO 8601 string storageVector<string> (permissions) → Separate table with foreign keyDictionary<string, string> (app_data) → Separate key-value tableCatalogue Replacement:
by_username catalogue → UNIQUE constraint + indexby_email catalogue → UNIQUE constraint + indexsessions_by_user index → Foreign key + index on user_idTransaction Support:
Async Patterns:
Connection Management:
| File | Changes Required |
|---|---|
src/Authentication/UserService.vala |
Replace Implexus with repository |
src/Authentication/SessionService.vala |
Replace Implexus with repository |
src/Authentication/PermissionService.vala |
May need updates if directly accessing user permissions |
src/Authentication/meson.build |
Replace implexus_dep with invercargill_sql_dep |
meson.build (root) |
Update dependency declaration |
| File | Purpose |
|---|---|
src/Authentication/Repositories/UserRepository.vala |
Abstract user repository interface |
src/Authentication/Repositories/SqlUserRepository.vala |
SQLite implementation |
src/Authentication/Repositories/SessionRepository.vala |
Abstract session repository interface |
src/Authentication/Repositories/SqlSessionRepository.vala |
SQLite implementation |
src/Authentication/Migrations/CreateAuthTables.vala |
SQL schema migration |
| File | Reason |
|---|---|
src/Authentication/AuthenticationMigration.vala |
Replaced by SQL migration |
# src/Authentication/meson.build
dependencies: [spry_dep, spry_authorisation_dep, implexus_dep, sodium_deps, invercargill_dep, astralis_dep]
# src/Authentication/meson.build
dependencies: [spry_dep, spry_authorisation_dep, invercargill_sql_dep, sodium_deps, invercargill_dep, astralis_dep]
# Current
implexus_dep = dependency('implexus-0.1')
# Proposed
invercargill_sql_dep = dependency('invercargill-sql-1')
| Risk | Impact | Mitigation |
|---|---|---|
| Data loss during migration | High | Implement data migration script, backup strategy |
| Breaking existing API | High | Maintain service interfaces, use repository pattern |
| Performance regression | Medium | Benchmark critical paths, optimize queries |
| Async behavior differences | Low | Both use async/await patterns consistently |
| Transaction semantics | Low | InvercargillSql provides more robust transactions |
The authentication system's Implexus usage is concentrated in two main service classes: UserService and SessionService. The migration to InvercargillSql is straightforward from an API perspective since both libraries:
Properties interface for data representationThe main differences are:
Implementing a repository pattern will isolate the database implementation details and allow the services to remain largely unchanged in their public API.