Objects
Objects are the core data entities in OpenRegister that store and manage structured information. This document explains everything you need to know about working with objects.
Overview
An object in OpenRegister represents a single record of data that:
- Conforms to a defined schema
- Belongs to a specific register
- Has a unique UUID identifier
- Can contain nested objects and file attachments
- Maintains version history through audit logs
- Can be linked to other objects via relations
Object Structure
Each object contains:
id: Unique UUID identifieruri: Absolute URL to access the objectversion: Semantic version number (e.g. 1.0.0)register: The register this object belongs toschema: The schema this object must conform toobject: The actual data payload as JSONfiles: Array of related file IDsrelations: Array of related object IDstextRepresentation: Text representation of the objectlocked: Lock status and detailsowner: Nextcloud user that owns the objectauthorization: JSON object describing access permissionsupdated: Last modification timestampcreated: Creation timestamp
Key Features
Schema Validation
- Objects are validated against their schema definition
- Supports both soft and hard validation modes
- Ensures data integrity and consistency
Relations & Nesting
- Objects can reference other objects via UUIDs or URLs
- Supports nested object structures up to configured depth
- Maintains bidirectional relationship tracking
Version Control
- Automatic version incrementing
- Full audit trail of changes
- Historical version access
- Ability to revert to any previous version
- Detailed change tracking between versions
Access Control
- Object-level ownership
- Granular authorization rules
- Lock mechanism for concurrent access control
File Attachments
- Support for file attachments
- Secure file storage integration
- File metadata tracking
API Operations
Objects support standard CRUD operations:
- Create new objects
- Read existing objects
- Update object data
- Delete objects
- Search and filter objects
- Export object data
- Revert to previous versions
Object Locking and Versioning
Version Control and Revert Functionality
OpenRegister provides comprehensive version control capabilities:
- Every change creates a new version with full audit trail
- Changes are tracked at the field level
- Previous versions can be viewed and compared
- Objects can be reverted to any historical version
- Revert operation creates new version rather than overwriting
- Audit logs maintain full history of all changes including reverts
- Revert includes all object properties including relations and files
- Batch revert operations supported for related objects
Locking Objects
Objects can be locked to prevent concurrent modifications. This is useful when:
- Long-running processes need exclusive access
- Multiple users/systems are working on the same object
- Ensuring data consistency during complex updates
Object Locking Sequence
The following diagram illustrates the object locking process:
Object Deletion States
Objects in OpenRegister follow a lifecycle with soft deletion and purging capabilities:
Object Relations
Objects in OpenRegister support sophisticated relationships through schema definitions. The way objects relate to each other depends on how you configure properties in your schema.
Schema Property Configuration Options
When defining a property in a schema that references other objects, you have several configuration options:
For Single Objects (type: 'object')
{
"type": "object",
"objectConfiguration": {
"handling": "nested-object" // or "nested-schema", "related-schema", "uri"
},
"$ref": "schema-id-or-slug",
"inversedBy": "property-name",
"register": "register-id-or-slug",
"cascadeDelete": true
}
For Arrays of Objects (type: 'array')
{
"type": "array",
"items": {
"type": "object",
"$ref": "schema-id-or-slug",
"inversedBy": "property-name",
"register": "register-id-or-slug",
"cascadeDelete": true
},
"objectConfiguration": {
"handling": "nested-object" // or "nested-schema", "related-schema", "uri"
}
}
Schema Reference Configuration ($ref and register)
When defining object relationships in your schema properties, OpenRegister uses two key configuration options to determine where and how to store or reference related objects:
$ref Property
The $ref property specifies which schema the related object should conform to. The backend supports two formats:
-
Simple schema reference: Just the schema ID or name
"$ref": "organisation-schema" -
Path-based reference: A path where the schema name is extracted from the last segment
"$ref": "path/to/organisation-schema"
Backend Resolution Logic:
- If
$refcontains a/, the backend extracts everything after the last slash - Otherwise, it uses the value directly as the schema identifier
- This value is passed as the
schemaparameter when creating or updating related objects
register Property
The register property specifies which register the related object belongs to:
"register": "my-register-id"
Backend Behavior:
- If
registeris specified, related objects are created/updated in that register - If
registeris omitted, the current object's register is used as the default - This ensures related objects can be stored in different registers if needed
Complete Configuration Example
Here's how these properties work together in a schema configuration:
{
"properties": {
"deelnemers": {
"type": "array",
"items": {
"$ref": "organisation-schema",
"register": "organisations-register",
"inversedBy": "deelnames",
"writeBack": true,
"removeAfterWriteBack": true
}
}
}
}
What happens when saving:
- Backend identifies that
deelnemerscontains organization objects - Uses
organisation-schemato validate and structure the objects - Stores/updates objects in the
organisations-register - Sets up inverse relations via the
deelnamesproperty - Handles write-back operations as configured
Use Cases
Same Register Relations:
{
"community": {
"type": "object",
"$ref": "community-schema"
// register omitted - uses current object's register
}
}
Cross-Register Relations:
{
"members": {
"type": "array",
"items": {
"$ref": "person-schema",
"register": "people-register",
"inversedBy": "communities"
}
}
}
This configuration system allows for flexible object relationships while maintaining proper data organization across different registers and schemas.
Object Handling Types
When defining object relationships in your schema properties, you can specify how related objects should be handled using the handling configuration option within objectConfiguration. OpenRegister supports four different handling types, each with specific behaviors for storage and retrieval:
1. Nested Object (nested-object)
Description: Stores object data directly within the parent object as embedded JSON data.
Behavior:
- Object data is stored inline within the parent object's JSON structure
- No separate database entity is created for the nested object
- Changes to the nested object are saved as part of the parent object
- Best for simple, tightly coupled data that doesn't need independent lifecycle management
Example Schema Configuration:
{
"address": {
"type": "object",
"objectConfiguration": {
"handling": "nested-object"
},
"properties": {
"street": {"type": "string"},
"city": {"type": "string"},
"zipCode": {"type": "string"}
}
}
}
Resulting Data Structure:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "John Doe",
"address": {
"street": "Main Street 1",
"city": "Amsterdam",
"zipCode": "1000 AA"
}
}
2. Nested Schema (nested-schema)
Description: Stores object as a separate entity in the database but embeds the full object data in API responses.
Behavior:
- Creates a separate
ObjectEntityin the database with its own UUID - Object follows the schema specified in
$ref - During rendering, the full nested object data is included in the response
- Allows independent lifecycle management while providing convenient access to nested data
- Supports cascade operations when configured with
inversedBy
Example Schema Configuration:
{
"profile": {
"type": "object",
"$ref": "profile-schema",
"objectConfiguration": {
"handling": "nested-schema"
},
"inversedBy": "owner",
"register": "users"
}
}
Resulting Data Structure:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "John Doe",
"profile": {
"id": "987fcdeb-51a2-43d7-8f9e-123456789abc",
"bio": "Software developer",
"skills": ["PHP", "JavaScript", "Vue.js"],
"owner": "123e4567-e89b-12d3-a456-426614174000"
}
}
3. Related Schema (related-schema)
Description: Stores object as a separate entity and references it by UUID/ID only.
Behavior:
- Creates a separate
ObjectEntityin the database with its own UUID - Only the UUID reference is stored in the parent object
- Client must make additional API calls to retrieve the full nested object data
- Provides the most separation and independence between objects
- Optimal for loosely coupled relationships and large datasets
- Supports cascade operations when configured with
inversedBy
Example Schema Configuration:
{
"department": {
"type": "object",
"$ref": "department-schema",
"objectConfiguration": {
"handling": "related-schema"
},
"inversedBy": "employees",
"register": "organization"
}
}
Resulting Data Structure:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "John Doe",
"department": "456e7890-e12b-34c5-d678-901234567890"
}
4. URI Reference (uri)
Description: References external objects by URI/URL without local storage.
Behavior:
- No local
ObjectEntityis created - Stores only the URI reference as a string
- External object is not managed by OpenRegister
- Useful for referencing objects in external systems or APIs
- No cascade operations or local validation
- Client is responsible for resolving the URI to retrieve object data
Example Schema Configuration:
{
"externalService": {
"type": "object",
"objectConfiguration": {
"handling": "uri"
},
"format": "uri"
}
}
Resulting Data Structure:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "John Doe",
"externalService": "https://api.external.com/services/auth-service-v2"
}
Handling Type Comparison
| Feature | Nested Object | Nested Schema | Related Schema | URI Reference |
|---|---|---|---|---|
| Separate Entity | ❌ | ✅ | ✅ | ❌ |
| Independent Lifecycle | ❌ | ✅ | ✅ | ✅ |
| Embedded in Response | ✅ | ✅ | ❌ | ❌ |
| Cascade Operations | ❌ | ✅ | ✅ | ❌ |
| Schema Validation | ✅ | ✅ | ✅ | ❌ |
| Performance (Read) | ⭐⭐⭐ | ⭐⭐ | ⭐ | ⭐ |
| Performance (Write) | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| Storage Efficiency | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| Relationship Complexity | ⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐ |
When to Use Each Type
Use Nested Object when:
- Data is simple and tightly coupled to the parent
- You don't need independent lifecycle management
- The nested data is relatively small and stable
- Performance is critical and you want to minimize database queries
Use Nested Schema when:
- You need schema validation for nested objects
- You want independent lifecycle management but convenient access
- The nested object might be referenced by multiple parents
- You need cascade operations but want embedded responses
Use Related Schema when:
- Objects have independent lifecycles and complex relationships
- You're working with large datasets where embedding would be inefficient
- You need maximum flexibility in how relationships are managed
- Performance optimization through selective loading is important
Use URI Reference when:
- Referencing objects in external systems
- You don't want to store or manage the referenced object locally
- Integration with third-party APIs or services
- The referenced resource is managed outside OpenRegister
Inverse Relations
Inverse relations allow objects to automatically show related objects that reference them, creating bidirectional relationships while storing data only once.
How Inverse Relations Work
- Schema Definition: Define
inversedByon the schema that should show the inverse relation - Automatic Detection: Backend automatically finds objects that reference this object
- Dynamic Population: Inverse relations are populated when rendering objects
- Single Storage: Relationship data is stored only once, preventing duplicate references
Basic Example: Person and Addresses
Person Schema:
{
"name": "Person",
"properties": {
"name": {"type": "string"},
"addresses": {
"type": "array",
"items": {"$ref": "address-schema"},
"inversedBy": "person"
}
}
}
Address Schema:
{
"name": "Address",
"properties": {
"street": {"type": "string"},
"person": {"type": "string"}
}
}
Result: When fetching a person, their addresses are automatically included even though they're stored separately.
Advanced Example: Organisation Communities
A more complex example showing parent-child relationships within the same schema:
Organisation Schema:
{
"name": "Organisation",
"properties": {
"name": {"type": "string"},
"type": {"type": "string"}, // "community" or "member"
"deelnames": {
"type": "array",
"items": {"type": "string"},
"description": "UUIDs of communities this organisation participates in"
},
"deelnemers": {
"type": "array",
"items": {
"$ref": "organisation-schema",
"inversedBy": "deelnames",
"writeBack": true,
"removeAfterWriteBack": true
},
"description": "UUIDs of organisations that participate in this community (inverse relation with write-back)"
}
}
}
Inverse Relations with Write-Back
OpenRegister's inverse relations system supports both read and write operations:
- Standard Inverse Relations (
inversedBy): Automatically show related objects during rendering (read-only) - Inverse Relations with Write-Back (
inversedBy+writeBack: true): Additionally update target objects during save operations
How Inverse Relations Write-Back Works
- Schema Definition: Define
inversedBywithwriteBack: truein the schema property configuration - Automatic Updates: When saving an object, the system automatically updates referenced objects
- Bidirectional Maintenance: Target objects get updated to include references back to the current object
- Import-Friendly: Perfect for import processes where you have parent data with child references
Community/Deelnemers Example
Schema Configuration:
{
"name": "Organisation",
"properties": {
"name": {"type": "string"},
"type": {"type": "string"},
"deelnames": {
"type": "array",
"items": {"type": "string"},
"description": "UUIDs of communities this organisation participates in"
},
"deelnemers": {
"type": "array",
"items": {
"$ref": "organisation-schema",
"inversedBy": "deelnames",
"writeBack": true,
"removeAfterWriteBack": true
},
"description": "UUIDs of participants - will update their deelnames arrays"
}
}
}
Usage Example:
Step 1: Create member organisations
// POST /organisations
{
"name": "Organisation A",
"type": "member",
"deelnames": []
}
// Returns: {"id": "org-a-uuid", ...}
// POST /organisations
{
"name": "Organisation B",
"type": "member",
"deelnames": []
}
// Returns: {"id": "org-b-uuid", ...}
Step 2: Create community with deelnemers
// POST /organisations
{
"name": "Tech Community",
"type": "community",
"deelnames": [],
"deelnemers": ["org-a-uuid", "org-b-uuid"]
}
// Returns: {"id": "community-uuid", ...}
What happens automatically:
- Community object is created with
deelnemersproperty removed (since it's a reverse relation) - Organisation A's
deelnamesarray is updated to include"community-uuid" - Organisation B's
deelnamesarray is updated to include"community-uuid"
Final state:
// Organisation A
{
"id": "org-a-uuid",
"name": "Organisation A",
"type": "member",
"deelnames": ["community-uuid"] // Automatically updated
}
// Organisation B
{
"id": "org-b-uuid",
"name": "Organisation B",
"type": "member",
"deelnames": ["community-uuid"] // Automatically updated
}
// Tech Community
{
"id": "community-uuid",
"name": "Tech Community",
"type": "community",
"deelnames": [] // deelnemers property was removed - not stored here
}
Import Process Benefits
This reverse relations pattern is particularly useful during import processes:
CSV Import Example:
name,type,deelnemers
"Tech Community","community","org-a-uuid,org-b-uuid"
"Business Community","community","org-a-uuid,org-c-uuid"
What happens during import:
- ImportService creates community objects with
deelnemersarrays - SaveObject.handleInverseRelationsWriteBack() processes each UUID in
deelnemers - Target organisations'
deelnamesarrays are automatically updated - Community objects are saved without the
deelnemersproperty (ifremoveAfterWriteBack: true)
Backend Implementation Details
File: lib/Service/ObjectHandlers/SaveObject.php
The inverse relations write-back functionality is implemented in:
handleInverseRelationsWriteBack(): Main method that processes inverse relations with write-back- Called during both object creation and updates
- Integrates with cascading and default value setting
Key features:
- Extends existing inverse relations system (
inversedBy) - Supports both single objects and arrays of objects
- Handles missing target objects gracefully (logs errors, continues processing)
- Optionally removes properties from source object after processing
- Prevents duplicate entries in target arrays
Schema Configuration Options:
{
"deelnemers": {
"type": "array",
"items": {
"$ref": "target-schema-id", // Required: target schema
"inversedBy": "property-name", // Required: property to update (same as read-only inverse relations)
"writeBack": true, // Required: enables write-back functionality
"removeAfterWriteBack": true, // Optional: remove property from source after write-back
"register": "register-id" // Optional: defaults to current register
}
}
}
Relation Types Summary
OpenRegister supports three types of object relationships, each serving different use cases:
1. Inverse Relations (inversedBy)
- Purpose: Show related objects during rendering (read-only)
- Direction: Target → Source (automatic lookup)
- Storage: Relationship data stored on source objects
- Use case: Display all addresses for a person, show all posts for a blog
Example:
{
"addresses": {
"type": "array",
"items": {"$ref": "address-schema"},
"inversedBy": "person"
}
}
2. Inverse Relations with Write-Back (inversedBy + writeBack: true)
- Purpose: Show related objects during rendering AND update target objects during save operations
- Direction: Bidirectional (Target → Source for reading, Source → Target for writing)
- Storage: Relationship data stored on target objects
- Use case: Community membership, parent-child relationships, import processes
Example:
{
"deelnemers": {
"type": "array",
"items": {
"$ref": "organisation-schema",
"inversedBy": "deelnames",
"writeBack": true,
"removeAfterWriteBack": true
}
}
}
3. Cascade Relations (inversedBy with objectConfiguration)
- Purpose: Create/update related objects during save operations
- Direction: Source → Target (create new objects)
- Storage: Separate objects created and linked
- Use case: Creating addresses when creating a person, creating posts when creating a blog
Example:
{
"addresses": {
"type": "array",
"items": {
"$ref": "address-schema",
"inversedBy": "person"
},
"objectConfiguration": {"handling": "related-schema"}
}
}
When to Use Each Type
| Use Case | Relation Type | Reason |
|---|---|---|
| Display related objects | Inverse Relations | Read-only, automatic lookup |
| Import with existing children | Inverse Relations with Write-Back | Update existing objects |
| Create objects with new children | Cascade Relations | Create new related objects |
| Parent-child where child owns relationship | Inverse Relations with Write-Back | Maintain single source of truth |
| Parent-child where parent owns relationship | Cascade Relations | Natural parent-driven creation |
| Bidirectional relationships | Inverse Relations with Write-Back | Both read and write capabilities |
How this works:
-
Data Storage (One-Way):
// Organisation A (Member)
{
"id": "org-a-uuid",
"name": "Organisation A",
"type": "member",
"deelnames": ["community-uuid-1", "community-uuid-2"]
}
// Organisation B (Member)
{
"id": "org-b-uuid",
"name": "Organisation B",
"type": "member",
"deelnames": ["community-uuid-1"]
}
// Community Organisation
{
"id": "community-uuid-1",
"name": "Tech Community",
"type": "community",
"deelnames": [] // Empty - this is the parent
} -
Automatic Inverse Population: When fetching the community organisation, the
deelnemersproperty is automatically populated:// GET /organisations/community-uuid-1?extend=deelnemers
{
"id": "community-uuid-1",
"name": "Tech Community",
"type": "community",
"deelnames": [],
"deelnemers": ["org-a-uuid", "org-b-uuid"] // Automatically populated
} -
With Extension:
// GET /organisations/community-uuid-1?extend=deelnemers
{
"id": "community-uuid-1",
"name": "Tech Community",
"type": "community",
"deelnames": [],
"deelnemers": [
{
"id": "org-a-uuid",
"name": "Organisation A",
"type": "member",
"deelnames": ["community-uuid-1", "community-uuid-2"]
},
{
"id": "org-b-uuid",
"name": "Organisation B",
"type": "member",
"deelnames": ["community-uuid-1"]
}
]
}
Benefits of Inverse Relations
- Single Source of Truth: Relationship data stored only once
- Automatic Synchronization: No risk of inconsistent bidirectional references
- Performance: No need to update multiple objects when relationships change
- Flexibility: Can view relationships from either direction
Backend Implementation Details
The inverse relations functionality is handled by several PHP files in the OpenRegister codebase:
Core Files and Their Responsibilities
1. lib/Service/ObjectHandlers/RenderObject.php
- Primary responsibility: Handles rendering objects and populating inverse relations
- Key methods:
getInversedProperties(Schema $schema): Extracts properties withinversedByconfigurations from schemahandleInversedProperties(): Finds and populates inverse relations during object renderingrenderEntity(): Main rendering method that callshandleInversedProperties()when depth < 10
2. lib/Service/ObjectHandlers/SaveObject.php
- Primary responsibility: Handles saving objects and maintaining relation tracking
- Key methods:
scanForRelations(array $data): Scans object data for UUIDs and URLs, stores them in dot notationupdateObjectRelations(ObjectEntity $objectEntity, array $data): Updates the relations property on objectssaveObject(): Main save method that callsupdateObjectRelations()to track relations
3. lib/Service/ObjectService.php
- Primary responsibility: Main service facade that coordinates object operations
- Key methods:
findByRelations(string $search): Finds objects that reference a specific UUID/URLrenderHandler->renderEntity(): Delegates to RenderObject for rendering with inverse relations
4. lib/Db/ObjectEntityMapper.php
- Primary responsibility: Database operations for finding related objects
- Key methods:
findByRelation(string $search): Database query to find objects containing specific UUIDs in their relationsfindByRelationUri(): Alternative method for finding objects by URI references
How Inverse Relations Are Processed
Development Notes for Future Work
When working with inverse relations:
-
Schema Changes: Modify
RenderObject::getInversedProperties()if you need to support new inverse relation configurations -
Performance Optimization: The
ObjectEntityMapper::findByRelation()method can be optimized for large datasets by adding database indexes on therelationsJSON column -
Circular Reference Prevention: The rendering system includes circular reference detection in
RenderObject::renderEntity()using the$visitedIdsparameter -
Caching: Consider implementing caching in
RenderObjectfor frequently accessed inverse relations -
Database Schema: Relations are stored as JSON in the
relationscolumn of theoc_openregister_objectstable in dot notation format
Example of relations storage in database:
{
"deelnames.0": "community-uuid-1",
"deelnames.1": "community-uuid-2",
"contact.email": "user@example.com"
}
This architecture ensures that inverse relations are dynamically calculated and always up-to-date, while maintaining performance through caching and efficient database queries.
UUID Relations and Automatic Detection
The backend automatically detects and tracks relations:
Automatic Relation Detection
// These patterns are automatically detected as relations:
{
"relatedObject": "550e8400-e29b-41d4-a716-446655440000", // UUID
"externalRef": "https://api.example.com/objects/123", // URL
"nestedRefs": [
"550e8400-e29b-41d4-a716-446655440001",
"550e8400-e29b-41d4-a716-446655440002"
]
}
Relations Storage
Relations are stored in dot notation:
{
"relations": {
"relatedObject": "550e8400-e29b-41d4-a716-446655440000",
"externalRef": "https://api.example.com/objects/123",
"nestedRefs.0": "550e8400-e29b-41d4-a716-446655440001",
"nestedRefs.1": "550e8400-e29b-41d4-a716-446655440002"
}
}
Extending Objects with Related Data
You can extend objects to include related data using the extend parameter:
Basic Extension
// Extend a single property
GET /objects/123?extend=addresses
// Extend multiple properties
GET /objects/123?extend=addresses,employer
// Extend all relations
GET /objects/123?extend=all
Nested Extension
// Extend nested properties
GET /objects/123?extend=addresses.person,addresses.contacts
// Wildcard extension for arrays
GET /objects/123?extend=addresses.$.person
Cascade Delete
When cascadeDelete is set to true, deleting a parent object will also delete related objects:
{
"addresses": {
"type": "array",
"items": {
"type": "object",
"$ref": "address-schema",
"cascadeDelete": true
}
}
}
Behavior: Deleting a person will also delete all their addresses.
Complete Example: Blog System
Here's a complete example showing different relation types:
Blog Schema
{
"name": "Blog",
"properties": {
"title": {"type": "string"},
"author": {
"type": "object",
"$ref": "person-schema",
"objectConfiguration": {"handling": "related-schema"}
},
"posts": {
"type": "array",
"items": {
"type": "object",
"$ref": "post-schema",
"inversedBy": "blog",
"cascadeDelete": true
},
"objectConfiguration": {"handling": "related-schema"}
},
"settings": {
"type": "object",
"objectConfiguration": {"handling": "nested-object"},
"properties": {
"theme": {"type": "string"},
"private": {"type": "boolean"}
}
}
}
}
Post Schema
{
"name": "Post",
"properties": {
"title": {"type": "string"},
"content": {"type": "string"},
"blog": {"type": "string"},
"tags": {
"type": "array",
"items": {"type": "string"}
}
}
}
Creating a Blog
Input:
{
"title": "My Tech Blog",
"author": "550e8400-e29b-41d4-a716-446655440000",
"posts": [
{
"title": "First Post",
"content": "Welcome to my blog!",
"tags": ["welcome", "intro"]
},
{
"title": "Second Post",
"content": "More content here",
"tags": ["tech", "programming"]
}
],
"settings": {
"theme": "dark",
"private": false
}
}
What happens:
- Blog object created with UUID
blog-uuid-123 - Two post objects created separately:
- Each gets
"blog": "blog-uuid-123"automatically - Each gets its own UUID
- Each gets
- Blog object stores:
"posts": ["post-uuid-1", "post-uuid-2"] - Settings nested directly in blog object
- Author reference stored as UUID
- Relations automatically tracked
Retrieved with extension:
GET /blogs/blog-uuid-123?extend=posts,author
Response:
{
"id": "blog-uuid-123",
"title": "My Tech Blog",
"author": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"email": "john@example.com"
},
"posts": [
{
"id": "post-uuid-1",
"title": "First Post",
"content": "Welcome to my blog!",
"blog": "blog-uuid-123",
"tags": ["welcome", "intro"]
},
{
"id": "post-uuid-2",
"title": "Second Post",
"content": "More content here",
"blog": "blog-uuid-123",
"tags": ["tech", "programming"]
}
],
"settings": {
"theme": "dark",
"private": false
}
}
Summary
The OpenRegister object relation system provides flexible ways to model data relationships:
- Nested Objects: Simple embedding without validation
- Nested Schema: Embedding with schema validation
- Related Schema: Separate objects with automatic relation management
- URI References: Loose coupling with external resources
- Inverse Relations: Automatic population of referring objects
- Cascade Operations: Automatic creation and deletion of related objects
- Automatic Detection: UUIDs and URLs are automatically tracked as relations
Choose the appropriate handling type based on your data modeling needs and whether you want separate storage, automatic relation management, and cascade operations.
Technical Implementation
This section provides detailed technical information about how objects are implemented in OpenRegister.
Architecture Overview
OpenRegister uses a layered architecture for object management:
Key Components:
- ObjectService: High-level orchestration service that coordinates all object operations
- Handlers: Specialized classes for specific operations (Save, Get, Delete, Render, Validate)
- ObjectEntityMapper: Database access layer for CRUD operations
- Event System: Nextcloud event dispatcher for lifecycle events
- Solr Integration: Automatic indexing for search functionality
Object Lifecycle
Object Creation Flow
Creation Steps:
- Schema Resolution: Convert schema reference (ID/slug/path) to schema entity
- Cascading Pre-Validation: Create nested objects defined in
inversedByproperties before main validation - Default Values: Apply schema-defined default values and generate slugs
- Metadata Hydration: Extract name, description, summary, image from object data based on schema configuration
- Relation Tracking: Scan object data for UUIDs and URLs, store in dot notation
- Database Insert: Persist object entity to MySQL database
- Write-Back Operations: Update related objects for bidirectional relations
- Event Dispatch: Fire
ObjectCreatedEventfor listeners (e.g., Solr indexing)
Object Update Flow
Object Retrieval Flow
Database Schema
Objects are stored in the oc_openregister_objects table:
CREATE TABLE oc_openregister_objects (
id INT AUTO_INCREMENT PRIMARY KEY,
uuid VARCHAR(36) UNIQUE NOT NULL,
slug VARCHAR(255),
uri TEXT,
version VARCHAR(50),
register VARCHAR(36),
schema VARCHAR(36),
object JSON,
files JSON,
relations JSON,
locked JSON,
owner VARCHAR(64),
authorization JSON,
folder VARCHAR(255),
application VARCHAR(255),
organisation VARCHAR(255),
validation JSON,
deleted JSON,
geo JSON,
retention JSON,
size VARCHAR(50),
name VARCHAR(255),
description TEXT,
summary TEXT,
image TEXT,
labels JSON,
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
published DATETIME,
depublished DATETIME,
INDEX idx_uuid (uuid),
INDEX idx_register (register),
INDEX idx_schema (schema),
INDEX idx_owner (owner),
INDEX idx_organisation (organisation),
INDEX idx_published (published),
INDEX idx_slug (slug, register, schema)
);
Key Fields:
object: JSON field containing the actual object data (business logic fields)relations: JSON field with dot-notation storage of all UUIDs/URLs found in object datafiles: JSON array of file IDs attached to this objectpublished: Objects must have a published date to be indexed in Solrslug: URL-friendly identifier, unique within register+schema combination
Relations Storage
Relations are automatically detected and stored in dot notation:
Example Object Data:
{
"name": "John Doe",
"address": "123e4567-e89b-41d4-a716-446655440000",
"contacts": [
"456e7890-e29b-41d4-a716-446655440001",
"789e0123-e29b-41d4-a716-446655440002"
],
"website": "https://example.com"
}
Stored Relations:
{
"address": "123e4567-e89b-41d4-a716-446655440000",
"contacts.0": "456e7890-e29b-41d4-a716-446655440001",
"contacts.1": "789e0123-e29b-41d4-a716-446655440002",
"website": "https://example.com"
}
This enables efficient querying for inverse relations using:
SELECT * FROM oc_openregister_objects
WHERE JSON_SEARCH(relations, 'one', '123e4567-e89b-41d4-a716-446655440000') IS NOT NULL
Event System
OpenRegister dispatches events at key points in the object lifecycle:
Available Events:
| Event | When Fired | Use Cases |
|---|---|---|
ObjectCreatingEvent | Before object creation | Validation, pre-processing |
ObjectCreatedEvent | After object creation | Solr indexing, webhooks, audit |
ObjectUpdatingEvent | Before object update | Change detection, validation |
ObjectUpdatedEvent | After object update | Solr re-indexing, notifications |
ObjectDeletingEvent | Before object deletion | Cascade checks, backups |
ObjectDeletedEvent | After object deletion | Solr cleanup, cleanup tasks |
ObjectLockedEvent | When object is locked | Concurrent access control |
ObjectUnlockedEvent | When object is unlocked | Resume operations |
ObjectRevertedEvent | After version revert | Audit trail, re-indexing |
Event Listener Example:
namespace OCA\OpenRegister\EventListener;
use OCA\OpenRegister\Event\ObjectCreatedEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
class SolrEventListener implements IEventListener
{
public function handle(Event $event): void
{
if ($event instanceof ObjectCreatedEvent) {
$object = $event->getObject();
$this->solrService->indexObject($object);
}
}
}
Solr Integration
Objects are automatically indexed in Solr when:
- Created (if published)
- Updated (if published)
- Published (status change)
Solr Document Structure:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"self_uuid": "123e4567-e89b-12d3-a456-426614174000",
"self_name": "John Doe",
"self_name_s": "John Doe",
"self_description": "Software developer",
"self_schema": "person",
"self_register": "employees",
"self_created": "2024-01-15T10:30:00Z",
"self_updated": "2024-01-20T14:45:00Z",
"self_published": "2024-01-15T10:30:00Z",
"self_relations": ["456e7890-...", "789e0123-..."],
"self_object": "{...complete JSON...}",
"age_i": 30,
"email_s": "john@example.com",
"skills": ["PHP", "JavaScript", "Vue.js"],
"department_s": "Engineering"
}
Field Naming Conventions:
self_*: System metadata fields*_i: Integer fields*_s: String fields (sortable, not tokenized)*_t: Text fields (tokenized for full-text search)- Arrays without suffix: Multi-valued fields
Performance Optimizations
Bulk Operations
For importing large datasets, use bulk operations:
$results = $objectService->saveObjects(
objects: $arrayOfObjects,
register: $register,
schema: $schema,
validation: false, // Skip validation for performance
events: false // Skip event firing for performance
);
Bulk Operation Optimizations:
- Single-pass schema analysis
- Batch database inserts with
INSERT ... ON DUPLICATE KEY UPDATE - Memory-optimized processing with pass-by-reference
- Deferred event firing
- Bulk Solr indexing
Smart Deduplication System
OpenRegister includes an intelligent 3-stage deduplication system that automatically detects and handles duplicate objects during bulk imports:
Stage 1: Multi-ID Extraction The system extracts identifiers from multiple sources:
- UUID: Primary unique identifier
- Slug: URL-friendly identifiers
- URI: External system references
- Custom IDs: Legacy system identifiers (id, identifier, sourceId)
// System automatically extracts all identifier types
$identifiers = [
'uuids' => ['uuid1', 'uuid2', ...],
'slugs' => ['user-profile', 'company-x'],
'uris' => ['https://api.../123', ...],
'custom_ids' => [
'id' => [101, 102, 103],
'identifier' => ['EXT_001', 'EXT_002'],
'sourceId' => ['src_123', 'src_456']
]
];
Stage 2: Intelligent Bulk Lookup
- Single database query retrieves all existing objects
- Multi-index mapping for O(1) lookup time
- Memory-efficient indexing for fast comparisons
Stage 3: Hash-Based Decision Making For each incoming object, the system:
- Finds existing object by any identifier (UUID → Slug → URI → Custom IDs)
- Compares content hashes (excluding metadata and timestamps)
- Makes intelligent decision:
- CREATE: New object (no match found)
- SKIP: Content identical (no database operation needed)
- UPDATE: Content changed (merge and update)
Performance Impact:
- Fresh Import: 100% CREATE operations (same as before)
- Incremental Update: Typically 80% SKIP operations (5x faster)
- Re-import Same Data: 100% SKIP operations (50x faster)
- Database Load Reduction: 80-95% fewer operations
Hash Calculation: The system uses SHA-256 hashing of cleaned object data:
- Excludes
@selfmetadata fields - Excludes timestamps (
created,updated) - Excludes system fields (
_etag, etc.) - Recursively sorted for consistent hashing
Bulk Import Performance
OpenRegister's bulk import system uses optimized database operations for high-performance data processing:
Single-Call Architecture:
- Uses
INSERT...ON DUPLICATE KEY UPDATEwith database-computed classification - Eliminates database lookup overhead (3-5x faster)
- Automatic deduplication via
UNIQUE (uuid)constraint
Database-Managed Timestamps:
createdtimestamp is immutable (preserved on updates)updatedtimestamp automatically managed by database- Smart change detection compares all data fields including JSON
Database-Computed Classification: The system classifies objects as:
- created: Object created during this operation
- updated: Object modified during this operation
- unchanged: Object exists but content unchanged
Performance Results:
- Small Batches (< 1,000 objects): 800-1,200 objects/second
- Medium Batches (1,000-5,000 objects): 1,500-2,000 objects/second
- Large Batches (5,000+ objects): 2,000-2,500 objects/second
Memory Optimization:
- Automatic optimization decision based on available memory
- Uses ultra-fast operations when memory allows (500MB+)
- Falls back to standard operations when memory constrained
- 2x memory safety margin prevents OOM errors
Clean Business Data Storage:
- Metadata fields (id, uuid, register, schema) stored separately
- Business data stored cleanly in
objectcolumn - No metadata pollution in business data
Caching
OpenRegister implements multiple caching layers:
- Object Cache: Recently accessed objects (PSR-6 compliant)
- Schema Cache: Schema definitions
- Facet Cache: Facetable fields per schema
- Query Cache: Search result caching
Validation System
Objects are validated against their schema using JSON Schema validation:
Validation Modes:
- Hard Validation (default): Rejects invalid objects
- Soft Validation: Saves object but stores validation errors in
validationfield
Enhanced Error Messages:
{
"valid": false,
"errors": [
{
"property": "email",
"message": "The property email is required",
"constraint": "required"
},
{
"property": "age",
"message": "Integer value found, but a string is required",
"constraint": "type"
}
]
}
Metadata Extraction
Objects automatically extract metadata from their data based on schema configuration:
Schema Configuration:
{
"configuration": {
"objectNameField": "firstName",
"objectDescriptionField": "bio",
"objectSummaryField": "headline",
"objectImageField": "avatar",
"objectSlugField": "username"
}
}
Extraction Process:
Benefits:
- Consistent object display across UI
- SEO-friendly URLs via slugs
- Rich search results with descriptions
- Thumbnail support via image extraction
File Attachments
Objects can have file attachments stored in Nextcloud Files:
File Storage:
- Files uploaded via API or UI
- Stored in Nextcloud filesystem under
openregister/{register}/{schema}/ - File IDs tracked in
filesJSON array field - Text extraction for searchable content (optional)
Text Extraction:
- Background job processes uploaded files
- Supports PDFs, Word docs, text files, images (OCR)
- Extracted text stored separately for Solr indexing
- Configurable extraction scope and mode
Access Control
Objects support granular access control:
RBAC (Role-Based Access Control):
$objects = $objectService->findObjects(
filters: ['status' => 'active'],
rbac: true // Apply user permissions
);
Authorization Field:
{
"authorization": {
"read": ["user1", "group:admins"],
"write": ["user1"],
"delete": ["group:admins"]
}
}
Multi-Tenancy:
// Filter by organization
$objects = $objectService->findObjects(
filters: [],
multi: true // Apply organization filtering
);
Code Examples
Creating an Object
use OCA\OpenRegister\Service\ObjectService;
// Via service
$object = $objectService->saveObject(
object: [
'name' => 'John Doe',
'email' => 'john@example.com',
'age' => 30
],
register: 'employees',
schema: 'person',
rbac: true,
multi: true
);
// Access result
echo $object->getUuid(); // "123e4567-e89b-12d3-a456-426614174000"
echo $object->getName(); // "John Doe"
Querying Objects
// Find all objects in a register
$objects = $objectService->findObjects(
limit: 50,
offset: 0,
filters: ['status' => 'active'],
register: 'employees',
schema: 'person'
);
// Find by UUID
$object = $objectService->findObject(
uuid: '123e4567-e89b-12d3-a456-426614174000'
);
// Search with extension
$object = $objectService->findObject(
uuid: '123e4567-e89b-12d3-a456-426614174000',
extend: ['addresses', 'employer']
);
Updating an Object
$updated = $objectService->saveObject(
object: [
'name' => 'John Doe',
'email' => 'john.doe@newcompany.com', // Updated
'age' => 31 // Updated
],
uuid: '123e4567-e89b-12d3-a456-426614174000',
schema: 'person',
register: 'employees'
);
Deleting an Object
$result = $objectService->deleteObject(
id: '123e4567-e89b-12d3-a456-426614174000'
);
Testing
OpenRegister includes comprehensive tests for object operations:
# Run all object tests
vendor/bin/phpunit tests/Service/ObjectServiceTest.php
# Run specific test
vendor/bin/phpunit --filter testCreateObject
# Integration tests
vendor/bin/phpunit tests/Integration/ObjectIntegrationTest.php
Test Coverage:
- Object CRUD operations
- Relation handling
- Cascade operations
- Inverse relations
- Write-back operations
- Validation
- Event firing
- Solr indexing