Skip to content

Consolidate move_node API - Remove move_node_atomic pattern#545

Merged
malibio merged 8 commits into
mainfrom
bug/misc-issues
Nov 17, 2025
Merged

Consolidate move_node API - Remove move_node_atomic pattern#545
malibio merged 8 commits into
mainfrom
bug/misc-issues

Conversation

@malibio

@malibio malibio commented Nov 17, 2025

Copy link
Copy Markdown
Collaborator

Summary

This PR consolidates the node movement API by eliminating the architectural redundancy of having both move_node() and move_node_atomic() methods. As you correctly pointed out, we should have just modified the move_node() method to use atomic logic.

Changes

Rust Backend

  • Deleted old non-atomic move_node method from SurrealStore
  • Kept only the atomic move_node method (takes 3 parameters: node_id, new_parent_id, new_before_sibling_id)
  • Updated all test calls to use the consolidated method

Tauri IPC Bridge

  • Deleted old move_node command that used NodeOperations
  • Kept single move_node command that calls atomic SurrealStore method
  • Updated documentation and examples from move_node_atomic to move_node

TypeScript Frontend

  • Renamed moveNodeAtomicmoveNode across all layers:
    • backend-adapter.ts (interface + TauriAdapter + HttpAdapter)
    • mock-backend-adapter.ts
    • reactive-node-service.svelte.ts

Call Sites

  • Updated node_service.rs call sites to use 3-parameter signature

Quality Assurance

  • ✅ All linting and formatting passes
  • ✅ All quality checks pass
  • ✅ No compilation errors
  • ✅ All test updates completed

Architecture Benefit

This change creates a single, clear API where moveNode/move_node always performs atomic transactions. No more confusion about which version to use—atomicity is guaranteed by design.

Generated with Claude Code

malibio and others added 8 commits November 17, 2025 20:30
## Problem
Issue #447/#536 changed schema loading from hardcoded to database-driven,
but the GET /api/schemas endpoint was never implemented in dev-proxy.

Frontend SchemaPluginLoader was failing with:
- GET /api/schemas → 404 Not Found
- [SchemaPluginLoader] Failed to initialize

## Solution
Added GET /api/schemas endpoint to return all schema definitions:
- Route: .route("/api/schemas", get(get_all_schemas))
- Handler: async fn get_all_schemas() -> ApiResult<Vec<SchemaDefinition>>
- Converts SchemaService::get_all_schemas() tuple format to JSON array

## Testing
- Endpoint returns empty array when no schemas exist
- After schema initialization, returns all 7 core schemas
- SchemaPluginLoader now initializes successfully
- No more console errors on page load

## Files Changed
- packages/desktop-app/src-tauri/src/bin/dev-proxy.rs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
## Problem
Issues #447/#536 changed schema loading from hardcoded to database-driven,
but the dev-proxy was missing the GET /api/schemas endpoint. Frontend
SchemaPluginLoader initialization was failing with 404 errors.

## Solution
- Added route: .route("/api/schemas", get(get_all_schemas))
- Implemented handler that calls SchemaService::get_all_schemas()
- Converts Vec<(String, SchemaDefinition)> to Vec<SchemaDefinition>
- Returns JSON array of all schemas

## Testing
- Endpoint now returns 200 OK with array of 7 core schemas
- SchemaPluginLoader initializes successfully
- All console and network errors resolved

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…reation

## Issue
Schema nodes were duplicating data in both hub (node.properties) and spoke
(schema:id table), violating hub-and-spoke architecture pattern.

## Root Cause
The create_node() function was:
1. Storing properties in hub node table
2. Creating spoke record with same properties
3. Setting data link to spoke
But NOT clearing the hub properties after spoke creation.

## Fix
Update create_node() in surreal_store.rs line 916 to clear hub properties:
- Changed: `SET data = type::thing($type_table, $id);`
- To: `SET data = type::thing($type_table, $id), properties = {};`

## Architecture
Hub-and-spoke pattern for types with dedicated tables (task, schema):
- Hub (node table): Common metadata + data link to spoke
- Spoke (type table): Type-specific properties
- Properties stored ONLY in spoke, not duplicated in hub

## Testing
Requires database reset to apply:
1. `rm -rf ~/.nodespace/database/nodespace-dev`
2. `bun run dev:browser:db &`
3. `bun run dev:browser:proxy &`
4. `bun run dev:browser:init`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
## Backend Fixes
- Fix virtual date node validation in operations/mod.rs
  - Call store.get_node() directly to check database state
  - Previously used node_service.get_node() which returns virtual nodes
- Remove obsolete container validation preventing root node siblings
- Remove unused can_be_container_type() function and tests (dead code cleanup)

## Frontend Fixes
- Rename containerNodeId → parentId in Node interface (node.ts)
- Update base-node-viewer.svelte to pass explicitParentId
- Fix backend-adapter.ts to send parentId in HTTP requests
- Update reactive-node-service.svelte.ts createNode() calls
- Update all test files to use parentId instead of containerNodeId

## Test Fixes
- test-helpers.ts: Updated createTestNode() signature and implementation
- node-ordering.test.ts: Updated parent filtering logic
- hierarchy-service.test.ts: Updated node creation

All TypeScript errors resolved. All Rust tests passing. Quality checks passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Fixed line 90 in test-helpers.ts: Changed 'containerNodeId' to 'parentId' in legacy positional parameter path
  This was causing all tests using positional parameters to create nodes with missing parent relationships
- Updated JSDoc comment on line 37 to reference 'parentId' instead of 'containerNodeId'

Both changes ensure the migration from containerNodeId → parentId is complete across all code paths.

Severity: 🔴 Critical - This bug broke parent-child relationships in tests

Addresses reviewer recommendations from PR #544 code review.
…parent-child changes

## Problem
The indent operation was performing sequential non-atomic database operations:
1. updateNode() updated beforeSiblingId synchronously in frontend cache
2. setParent() called backend via HTTP/IPC asynchronously
3. During the gap between these operations, graph queries returned inconsistent state
4. Frontend cache refresh saw node as child of both old and new parent simultaneously
5. Caused Svelte "each_key_duplicate" errors with duplicate node IDs in render cache

## Solution
Implemented atomic database transaction by:
- Added Tauri command `move_node_atomic` exposing existing backend implementation
- Added `moveNodeAtomic()` to BackendAdapter interface for both TauriAdapter and HttpAdapter
- Updated `indentNode()` function to use single atomic operation instead of sequential calls
- Removed deduplication band-aid (line 285-288) from getVisibleNodesRecursive() - no longer needed
- Fixed unused variable in updated indentNode implementation
- Added moveNodeAtomic() to MockBackendAdapter for tests

## Impact
- Eliminates race condition window where system can be in inconsistent state
- Removes defensive deduplication layer - fixes root cause rather than symptom
- Tests: All 63 test files passing, no new failures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit consolidates the node movement API across all layers (database,
Tauri IPC, and TypeScript frontend) by eliminating the move_node_atomic/
moveNodeAtomic pattern and keeping only a single move_node/moveNode method
that always performs atomic transactions.

## Changes Made

### Rust Backend (packages/core/)
- **surreal_store.rs**: Deleted old non-atomic move_node method (took 2 params)
  - Kept only the atomic move_node method (takes 3 params: node_id, new_parent_id, new_before_sibling_id)
  - Updated 3 test calls to use the consolidated move_node method
  - All node movements now guaranteed to be atomic

### Tauri IPC Bridge (packages/desktop-app/src-tauri/)
- **commands/nodes.rs**:
  - Deleted old move_node command that called NodeOperations
  - Kept single move_node command that calls SurrealStore atomic method
  - Updated documentation to reflect atomic-only behavior
  - Updated example from move_node_atomic to move_node

### TypeScript Frontend (packages/desktop-app/src/)
- **backend-adapter.ts**: Renamed moveNodeAtomic → moveNode in interface and both implementations
- **mock-backend-adapter.ts**: Renamed moveNodeAtomic → moveNode
- **reactive-node-service.svelte.ts**: Updated call from moveNodeAtomic to moveNode

## Architecture Impact

**Before**: Confusing pattern with both move_node() and move_node_atomic()
- Implied that regular move_node was non-atomic (it wasn't)
- Duplicated logic across layers
- Unclear which version to use

**After**: Single, clear API
- moveNode/move_node ALWAYS performs atomic transactions
- No ambiguity about atomicity guarantees
- Simplified integration across all layers
- All node movements are transactionally safe

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@malibio
malibio merged commit abb2666 into main Nov 17, 2025
@malibio
malibio deleted the bug/misc-issues branch November 17, 2025 23:26
malibio added a commit that referenced this pull request Feb 4, 2026
* Fix: Add missing GET /api/schemas endpoint to dev-proxy

## Problem
Issue #447/#536 changed schema loading from hardcoded to database-driven,
but the GET /api/schemas endpoint was never implemented in dev-proxy.

Frontend SchemaPluginLoader was failing with:
- GET /api/schemas → 404 Not Found
- [SchemaPluginLoader] Failed to initialize

## Solution
Added GET /api/schemas endpoint to return all schema definitions:
- Route: .route("/api/schemas", get(get_all_schemas))
- Handler: async fn get_all_schemas() -> ApiResult<Vec<SchemaDefinition>>
- Converts SchemaService::get_all_schemas() tuple format to JSON array

## Testing
- Endpoint returns empty array when no schemas exist
- After schema initialization, returns all 7 core schemas
- SchemaPluginLoader now initializes successfully
- No more console errors on page load

## Files Changed
- packages/desktop-app/src-tauri/src/bin/dev-proxy.rs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix missing /api/schemas endpoint and add get_all_schemas handler

## Problem
Issues #447/#536 changed schema loading from hardcoded to database-driven,
but the dev-proxy was missing the GET /api/schemas endpoint. Frontend
SchemaPluginLoader initialization was failing with 404 errors.

## Solution
- Added route: .route("/api/schemas", get(get_all_schemas))
- Implemented handler that calls SchemaService::get_all_schemas()
- Converts Vec<(String, SchemaDefinition)> to Vec<SchemaDefinition>
- Returns JSON array of all schemas

## Testing
- Endpoint now returns 200 OK with array of 7 core schemas
- SchemaPluginLoader initializes successfully
- All console and network errors resolved

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix hub-and-spoke schema storage - Clear hub properties after spoke creation

## Issue
Schema nodes were duplicating data in both hub (node.properties) and spoke
(schema:id table), violating hub-and-spoke architecture pattern.

## Root Cause
The create_node() function was:
1. Storing properties in hub node table
2. Creating spoke record with same properties
3. Setting data link to spoke
But NOT clearing the hub properties after spoke creation.

## Fix
Update create_node() in surreal_store.rs line 916 to clear hub properties:
- Changed: `SET data = type::thing($type_table, $id);`
- To: `SET data = type::thing($type_table, $id), properties = {};`

## Architecture
Hub-and-spoke pattern for types with dedicated tables (task, schema):
- Hub (node table): Common metadata + data link to spoke
- Spoke (type table): Type-specific properties
- Properties stored ONLY in spoke, not duplicated in hub

## Testing
Requires database reset to apply:
1. `rm -rf ~/.nodespace/database/nodespace-dev`
2. `bun run dev:browser:db &`
3. `bun run dev:browser:proxy &`
4. `bun run dev:browser:init`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Revert "Fix hub-and-spoke schema storage - Clear hub properties after spoke creation"

This reverts commit 51f8c5e.

* Complete containerNodeId → parentId migration (Issue #535)

## Backend Fixes
- Fix virtual date node validation in operations/mod.rs
  - Call store.get_node() directly to check database state
  - Previously used node_service.get_node() which returns virtual nodes
- Remove obsolete container validation preventing root node siblings
- Remove unused can_be_container_type() function and tests (dead code cleanup)

## Frontend Fixes
- Rename containerNodeId → parentId in Node interface (node.ts)
- Update base-node-viewer.svelte to pass explicitParentId
- Fix backend-adapter.ts to send parentId in HTTP requests
- Update reactive-node-service.svelte.ts createNode() calls
- Update all test files to use parentId instead of containerNodeId

## Test Fixes
- test-helpers.ts: Updated createTestNode() signature and implementation
- node-ordering.test.ts: Updated parent filtering logic
- hierarchy-service.test.ts: Updated node creation

All TypeScript errors resolved. All Rust tests passing. Quality checks passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Address review: Fix critical bug in test helper legacy path (Issue #535)

- Fixed line 90 in test-helpers.ts: Changed 'containerNodeId' to 'parentId' in legacy positional parameter path
  This was causing all tests using positional parameters to create nodes with missing parent relationships
- Updated JSDoc comment on line 37 to reference 'parentId' instead of 'containerNodeId'

Both changes ensure the migration from containerNodeId → parentId is complete across all code paths.

Severity: 🔴 Critical - This bug broke parent-child relationships in tests

Addresses reviewer recommendations from PR #544 code review.

* Fix indent operation atomicity - use atomic database transaction for parent-child changes

## Problem
The indent operation was performing sequential non-atomic database operations:
1. updateNode() updated beforeSiblingId synchronously in frontend cache
2. setParent() called backend via HTTP/IPC asynchronously
3. During the gap between these operations, graph queries returned inconsistent state
4. Frontend cache refresh saw node as child of both old and new parent simultaneously
5. Caused Svelte "each_key_duplicate" errors with duplicate node IDs in render cache

## Solution
Implemented atomic database transaction by:
- Added Tauri command `move_node_atomic` exposing existing backend implementation
- Added `moveNodeAtomic()` to BackendAdapter interface for both TauriAdapter and HttpAdapter
- Updated `indentNode()` function to use single atomic operation instead of sequential calls
- Removed deduplication band-aid (line 285-288) from getVisibleNodesRecursive() - no longer needed
- Fixed unused variable in updated indentNode implementation
- Added moveNodeAtomic() to MockBackendAdapter for tests

## Impact
- Eliminates race condition window where system can be in inconsistent state
- Removes defensive deduplication layer - fixes root cause rather than symptom
- Tests: All 63 test files passing, no new failures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Consolidate move_node API - Remove move_node_atomic pattern

This commit consolidates the node movement API across all layers (database,
Tauri IPC, and TypeScript frontend) by eliminating the move_node_atomic/
moveNodeAtomic pattern and keeping only a single move_node/moveNode method
that always performs atomic transactions.

## Changes Made

### Rust Backend (packages/core/)
- **surreal_store.rs**: Deleted old non-atomic move_node method (took 2 params)
  - Kept only the atomic move_node method (takes 3 params: node_id, new_parent_id, new_before_sibling_id)
  - Updated 3 test calls to use the consolidated move_node method
  - All node movements now guaranteed to be atomic

### Tauri IPC Bridge (packages/desktop-app/src-tauri/)
- **commands/nodes.rs**:
  - Deleted old move_node command that called NodeOperations
  - Kept single move_node command that calls SurrealStore atomic method
  - Updated documentation to reflect atomic-only behavior
  - Updated example from move_node_atomic to move_node

### TypeScript Frontend (packages/desktop-app/src/)
- **backend-adapter.ts**: Renamed moveNodeAtomic → moveNode in interface and both implementations
- **mock-backend-adapter.ts**: Renamed moveNodeAtomic → moveNode
- **reactive-node-service.svelte.ts**: Updated call from moveNodeAtomic to moveNode

## Architecture Impact

**Before**: Confusing pattern with both move_node() and move_node_atomic()
- Implied that regular move_node was non-atomic (it wasn't)
- Duplicated logic across layers
- Unclear which version to use

**After**: Single, clear API
- moveNode/move_node ALWAYS performs atomic transactions
- No ambiguity about atomicity guarantees
- Simplified integration across all layers
- All node movements are transactionally safe

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
malibio added a commit that referenced this pull request Feb 5, 2026
* Fix: Add missing GET /api/schemas endpoint to dev-proxy

## Problem
Issue #447/#536 changed schema loading from hardcoded to database-driven,
but the GET /api/schemas endpoint was never implemented in dev-proxy.

Frontend SchemaPluginLoader was failing with:
- GET /api/schemas → 404 Not Found
- [SchemaPluginLoader] Failed to initialize

## Solution
Added GET /api/schemas endpoint to return all schema definitions:
- Route: .route("/api/schemas", get(get_all_schemas))
- Handler: async fn get_all_schemas() -> ApiResult<Vec<SchemaDefinition>>
- Converts SchemaService::get_all_schemas() tuple format to JSON array

## Testing
- Endpoint returns empty array when no schemas exist
- After schema initialization, returns all 7 core schemas
- SchemaPluginLoader now initializes successfully
- No more console errors on page load

## Files Changed
- packages/desktop-app/src-tauri/src/bin/dev-proxy.rs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix missing /api/schemas endpoint and add get_all_schemas handler

## Problem
Issues #447/#536 changed schema loading from hardcoded to database-driven,
but the dev-proxy was missing the GET /api/schemas endpoint. Frontend
SchemaPluginLoader initialization was failing with 404 errors.

## Solution
- Added route: .route("/api/schemas", get(get_all_schemas))
- Implemented handler that calls SchemaService::get_all_schemas()
- Converts Vec<(String, SchemaDefinition)> to Vec<SchemaDefinition>
- Returns JSON array of all schemas

## Testing
- Endpoint now returns 200 OK with array of 7 core schemas
- SchemaPluginLoader initializes successfully
- All console and network errors resolved

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix hub-and-spoke schema storage - Clear hub properties after spoke creation

## Issue
Schema nodes were duplicating data in both hub (node.properties) and spoke
(schema:id table), violating hub-and-spoke architecture pattern.

## Root Cause
The create_node() function was:
1. Storing properties in hub node table
2. Creating spoke record with same properties
3. Setting data link to spoke
But NOT clearing the hub properties after spoke creation.

## Fix
Update create_node() in surreal_store.rs line 916 to clear hub properties:
- Changed: `SET data = type::thing($type_table, $id);`
- To: `SET data = type::thing($type_table, $id), properties = {};`

## Architecture
Hub-and-spoke pattern for types with dedicated tables (task, schema):
- Hub (node table): Common metadata + data link to spoke
- Spoke (type table): Type-specific properties
- Properties stored ONLY in spoke, not duplicated in hub

## Testing
Requires database reset to apply:
1. `rm -rf ~/.nodespace/database/nodespace-dev`
2. `bun run dev:browser:db &`
3. `bun run dev:browser:proxy &`
4. `bun run dev:browser:init`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Revert "Fix hub-and-spoke schema storage - Clear hub properties after spoke creation"

This reverts commit 51f8c5e.

* Complete containerNodeId → parentId migration (Issue #535)

## Backend Fixes
- Fix virtual date node validation in operations/mod.rs
  - Call store.get_node() directly to check database state
  - Previously used node_service.get_node() which returns virtual nodes
- Remove obsolete container validation preventing root node siblings
- Remove unused can_be_container_type() function and tests (dead code cleanup)

## Frontend Fixes
- Rename containerNodeId → parentId in Node interface (node.ts)
- Update base-node-viewer.svelte to pass explicitParentId
- Fix backend-adapter.ts to send parentId in HTTP requests
- Update reactive-node-service.svelte.ts createNode() calls
- Update all test files to use parentId instead of containerNodeId

## Test Fixes
- test-helpers.ts: Updated createTestNode() signature and implementation
- node-ordering.test.ts: Updated parent filtering logic
- hierarchy-service.test.ts: Updated node creation

All TypeScript errors resolved. All Rust tests passing. Quality checks passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Address review: Fix critical bug in test helper legacy path (Issue #535)

- Fixed line 90 in test-helpers.ts: Changed 'containerNodeId' to 'parentId' in legacy positional parameter path
  This was causing all tests using positional parameters to create nodes with missing parent relationships
- Updated JSDoc comment on line 37 to reference 'parentId' instead of 'containerNodeId'

Both changes ensure the migration from containerNodeId → parentId is complete across all code paths.

Severity: 🔴 Critical - This bug broke parent-child relationships in tests

Addresses reviewer recommendations from PR #544 code review.

* Fix indent operation atomicity - use atomic database transaction for parent-child changes

## Problem
The indent operation was performing sequential non-atomic database operations:
1. updateNode() updated beforeSiblingId synchronously in frontend cache
2. setParent() called backend via HTTP/IPC asynchronously
3. During the gap between these operations, graph queries returned inconsistent state
4. Frontend cache refresh saw node as child of both old and new parent simultaneously
5. Caused Svelte "each_key_duplicate" errors with duplicate node IDs in render cache

## Solution
Implemented atomic database transaction by:
- Added Tauri command `move_node_atomic` exposing existing backend implementation
- Added `moveNodeAtomic()` to BackendAdapter interface for both TauriAdapter and HttpAdapter
- Updated `indentNode()` function to use single atomic operation instead of sequential calls
- Removed deduplication band-aid (line 285-288) from getVisibleNodesRecursive() - no longer needed
- Fixed unused variable in updated indentNode implementation
- Added moveNodeAtomic() to MockBackendAdapter for tests

## Impact
- Eliminates race condition window where system can be in inconsistent state
- Removes defensive deduplication layer - fixes root cause rather than symptom
- Tests: All 63 test files passing, no new failures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Consolidate move_node API - Remove move_node_atomic pattern

This commit consolidates the node movement API across all layers (database,
Tauri IPC, and TypeScript frontend) by eliminating the move_node_atomic/
moveNodeAtomic pattern and keeping only a single move_node/moveNode method
that always performs atomic transactions.

## Changes Made

### Rust Backend (packages/core/)
- **surreal_store.rs**: Deleted old non-atomic move_node method (took 2 params)
  - Kept only the atomic move_node method (takes 3 params: node_id, new_parent_id, new_before_sibling_id)
  - Updated 3 test calls to use the consolidated move_node method
  - All node movements now guaranteed to be atomic

### Tauri IPC Bridge (packages/desktop-app/src-tauri/)
- **commands/nodes.rs**:
  - Deleted old move_node command that called NodeOperations
  - Kept single move_node command that calls SurrealStore atomic method
  - Updated documentation to reflect atomic-only behavior
  - Updated example from move_node_atomic to move_node

### TypeScript Frontend (packages/desktop-app/src/)
- **backend-adapter.ts**: Renamed moveNodeAtomic → moveNode in interface and both implementations
- **mock-backend-adapter.ts**: Renamed moveNodeAtomic → moveNode
- **reactive-node-service.svelte.ts**: Updated call from moveNodeAtomic to moveNode

## Architecture Impact

**Before**: Confusing pattern with both move_node() and move_node_atomic()
- Implied that regular move_node was non-atomic (it wasn't)
- Duplicated logic across layers
- Unclear which version to use

**After**: Single, clear API
- moveNode/move_node ALWAYS performs atomic transactions
- No ambiguity about atomicity guarantees
- Simplified integration across all layers
- All node movements are transactionally safe

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
malibio added a commit that referenced this pull request Feb 26, 2026
* Fix: Add missing GET /api/schemas endpoint to dev-proxy

## Problem
Issue #447/#536 changed schema loading from hardcoded to database-driven,
but the GET /api/schemas endpoint was never implemented in dev-proxy.

Frontend SchemaPluginLoader was failing with:
- GET /api/schemas → 404 Not Found
- [SchemaPluginLoader] Failed to initialize

## Solution
Added GET /api/schemas endpoint to return all schema definitions:
- Route: .route("/api/schemas", get(get_all_schemas))
- Handler: async fn get_all_schemas() -> ApiResult<Vec<SchemaDefinition>>
- Converts SchemaService::get_all_schemas() tuple format to JSON array

## Testing
- Endpoint returns empty array when no schemas exist
- After schema initialization, returns all 7 core schemas
- SchemaPluginLoader now initializes successfully
- No more console errors on page load

## Files Changed
- packages/desktop-app/src-tauri/src/bin/dev-proxy.rs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix missing /api/schemas endpoint and add get_all_schemas handler

## Problem
Issues #447/#536 changed schema loading from hardcoded to database-driven,
but the dev-proxy was missing the GET /api/schemas endpoint. Frontend
SchemaPluginLoader initialization was failing with 404 errors.

## Solution
- Added route: .route("/api/schemas", get(get_all_schemas))
- Implemented handler that calls SchemaService::get_all_schemas()
- Converts Vec<(String, SchemaDefinition)> to Vec<SchemaDefinition>
- Returns JSON array of all schemas

## Testing
- Endpoint now returns 200 OK with array of 7 core schemas
- SchemaPluginLoader initializes successfully
- All console and network errors resolved

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix hub-and-spoke schema storage - Clear hub properties after spoke creation

## Issue
Schema nodes were duplicating data in both hub (node.properties) and spoke
(schema:id table), violating hub-and-spoke architecture pattern.

## Root Cause
The create_node() function was:
1. Storing properties in hub node table
2. Creating spoke record with same properties
3. Setting data link to spoke
But NOT clearing the hub properties after spoke creation.

## Fix
Update create_node() in surreal_store.rs line 916 to clear hub properties:
- Changed: `SET data = type::thing($type_table, $id);`
- To: `SET data = type::thing($type_table, $id), properties = {};`

## Architecture
Hub-and-spoke pattern for types with dedicated tables (task, schema):
- Hub (node table): Common metadata + data link to spoke
- Spoke (type table): Type-specific properties
- Properties stored ONLY in spoke, not duplicated in hub

## Testing
Requires database reset to apply:
1. `rm -rf ~/.nodespace/database/nodespace-dev`
2. `bun run dev:browser:db &`
3. `bun run dev:browser:proxy &`
4. `bun run dev:browser:init`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Revert "Fix hub-and-spoke schema storage - Clear hub properties after spoke creation"

This reverts commit 5c492eab7c24289e52101d1bd94e53dd0e06441c.

* Complete containerNodeId → parentId migration (Issue #535)

## Backend Fixes
- Fix virtual date node validation in operations/mod.rs
  - Call store.get_node() directly to check database state
  - Previously used node_service.get_node() which returns virtual nodes
- Remove obsolete container validation preventing root node siblings
- Remove unused can_be_container_type() function and tests (dead code cleanup)

## Frontend Fixes
- Rename containerNodeId → parentId in Node interface (node.ts)
- Update base-node-viewer.svelte to pass explicitParentId
- Fix backend-adapter.ts to send parentId in HTTP requests
- Update reactive-node-service.svelte.ts createNode() calls
- Update all test files to use parentId instead of containerNodeId

## Test Fixes
- test-helpers.ts: Updated createTestNode() signature and implementation
- node-ordering.test.ts: Updated parent filtering logic
- hierarchy-service.test.ts: Updated node creation

All TypeScript errors resolved. All Rust tests passing. Quality checks passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Address review: Fix critical bug in test helper legacy path (Issue #535)

- Fixed line 90 in test-helpers.ts: Changed 'containerNodeId' to 'parentId' in legacy positional parameter path
  This was causing all tests using positional parameters to create nodes with missing parent relationships
- Updated JSDoc comment on line 37 to reference 'parentId' instead of 'containerNodeId'

Both changes ensure the migration from containerNodeId → parentId is complete across all code paths.

Severity: 🔴 Critical - This bug broke parent-child relationships in tests

Addresses reviewer recommendations from PR #544 code review.

* Fix indent operation atomicity - use atomic database transaction for parent-child changes

## Problem
The indent operation was performing sequential non-atomic database operations:
1. updateNode() updated beforeSiblingId synchronously in frontend cache
2. setParent() called backend via HTTP/IPC asynchronously
3. During the gap between these operations, graph queries returned inconsistent state
4. Frontend cache refresh saw node as child of both old and new parent simultaneously
5. Caused Svelte "each_key_duplicate" errors with duplicate node IDs in render cache

## Solution
Implemented atomic database transaction by:
- Added Tauri command `move_node_atomic` exposing existing backend implementation
- Added `moveNodeAtomic()` to BackendAdapter interface for both TauriAdapter and HttpAdapter
- Updated `indentNode()` function to use single atomic operation instead of sequential calls
- Removed deduplication band-aid (line 285-288) from getVisibleNodesRecursive() - no longer needed
- Fixed unused variable in updated indentNode implementation
- Added moveNodeAtomic() to MockBackendAdapter for tests

## Impact
- Eliminates race condition window where system can be in inconsistent state
- Removes defensive deduplication layer - fixes root cause rather than symptom
- Tests: All 63 test files passing, no new failures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Consolidate move_node API - Remove move_node_atomic pattern

This commit consolidates the node movement API across all layers (database,
Tauri IPC, and TypeScript frontend) by eliminating the move_node_atomic/
moveNodeAtomic pattern and keeping only a single move_node/moveNode method
that always performs atomic transactions.

## Changes Made

### Rust Backend (packages/core/)
- **surreal_store.rs**: Deleted old non-atomic move_node method (took 2 params)
  - Kept only the atomic move_node method (takes 3 params: node_id, new_parent_id, new_before_sibling_id)
  - Updated 3 test calls to use the consolidated move_node method
  - All node movements now guaranteed to be atomic

### Tauri IPC Bridge (packages/desktop-app/src-tauri/)
- **commands/nodes.rs**:
  - Deleted old move_node command that called NodeOperations
  - Kept single move_node command that calls SurrealStore atomic method
  - Updated documentation to reflect atomic-only behavior
  - Updated example from move_node_atomic to move_node

### TypeScript Frontend (packages/desktop-app/src/)
- **backend-adapter.ts**: Renamed moveNodeAtomic → moveNode in interface and both implementations
- **mock-backend-adapter.ts**: Renamed moveNodeAtomic → moveNode
- **reactive-node-service.svelte.ts**: Updated call from moveNodeAtomic to moveNode

## Architecture Impact

**Before**: Confusing pattern with both move_node() and move_node_atomic()
- Implied that regular move_node was non-atomic (it wasn't)
- Duplicated logic across layers
- Unclear which version to use

**After**: Single, clear API
- moveNode/move_node ALWAYS performs atomic transactions
- No ambiguity about atomicity guarantees
- Simplified integration across all layers
- All node movements are transactionally safe

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant