@objectql/protocol-odata-v4
v4.2.2
Published
OData V4 Protocol Plugin for ObjectStack
Downloads
27
Maintainers
Readme
@objectql/protocol-odata-v4
OData V4 Protocol Plugin for ObjectStack
Overview
This plugin provides full OData V4 protocol support for ObjectStack applications. It automatically generates OData metadata (EDMX) from ObjectStack metadata and provides a RESTful API for querying and manipulating data.
Features
- ✅ Service Document (
/) - Discover available entity sets - ✅ Metadata Document (
/$metadata) - EDMX schema generation - ✅ Entity Queries - Query entity sets with OData query options
- ✅ Query Options - Support for
$filter,$select,$orderby,$top,$skip,$count,$expand - ✅ CRUD Operations - Full Create, Read, Update, Delete support
- ✅ CORS Support - Configurable Cross-Origin Resource Sharing
- ✅ Type Mapping - Automatic ObjectQL to EDM type conversion
- ✅ No Direct DB Access - All operations through ObjectStackRuntimeProtocol
Installation
pnpm add @objectql/protocol-odata-v4Usage
Basic Setup
import { ObjectKernel } from '@objectstack/runtime';
import { ODataV4Plugin } from '@objectql/protocol-odata-v4';
const kernel = new ObjectKernel([
new ODataV4Plugin({
port: 8080,
basePath: '/odata',
namespace: 'MyApp',
enableCORS: true
})
]);
await kernel.start();Configuration Options
interface ODataV4PluginConfig {
/** Port to listen on (default: 8080) */
port?: number;
/** Base path for OData endpoints (default: '/odata') */
basePath?: string;
/** Enable CORS (default: true) */
enableCORS?: boolean;
/** Service namespace (default: 'ObjectStack') */
namespace?: string;
}Endpoints
Service Document
GET {basePath}/
Returns a list of available entity sets.
curl http://localhost:8080/odata/Response:
{
"@odata.context": "http://localhost:8080/odata/$metadata",
"value": [
{ "name": "users", "kind": "EntitySet", "url": "users" },
{ "name": "projects", "kind": "EntitySet", "url": "projects" }
]
}Metadata Document
GET {basePath}/$metadata
Returns the EDMX metadata document describing the data model.
curl http://localhost:8080/odata/\$metadataResponse: XML/EDMX schema
Query Entity Set
GET {basePath}/{EntitySet}
Query all entities in a set.
curl http://localhost:8080/odata/usersResponse:
{
"@odata.context": "http://localhost:8080/odata/$metadata#users",
"value": [
{ "id": "1", "name": "Alice", "email": "[email protected]" },
{ "id": "2", "name": "Bob", "email": "[email protected]" }
]
}Query Options
$filter
Filter results based on criteria.
curl "http://localhost:8080/odata/users?\$filter=name eq 'Alice'"$orderby
Sort results.
curl "http://localhost:8080/odata/users?\$orderby=name desc"$top and $skip
Pagination.
curl "http://localhost:8080/odata/users?\$top=10&\$skip=20"$count
Include count in response.
curl "http://localhost:8080/odata/users?\$count=true"Response includes @odata.count field:
{
"@odata.context": "http://localhost:8080/odata/$metadata#users",
"@odata.count": 42,
"value": [...]
}You can also get just the count:
curl "http://localhost:8080/odata/users/\$count"Response: 42 (plain text)
$expand
Expand related entities (navigation properties).
Single property:
curl "http://localhost:8080/odata/orders?\$expand=customer"Multiple properties:
curl "http://localhost:8080/odata/orders?\$expand=customer,shipper"With filter:
curl "http://localhost:8080/odata/orders?\$expand=items(\$filter=status eq 'active')"With select:
curl "http://localhost:8080/odata/orders?\$expand=customer(\$select=name,email)"Supported expand options:
$filter- Filter expanded entities$select- Select specific fields from expanded entities$orderby- Sort expanded entities$top- Limit number of expanded entities
Current Limitations:
- ⚠️ Nested expand (e.g.,
$expand=owner($expand=department)) is not yet supported - Only single-level relationship expansion is available
- See Phase 2 Roadmap for planned nested expand support
Response includes expanded entities:
{
"@odata.context": "http://localhost:8080/odata/$metadata#orders",
"value": [
{
"_id": "order1",
"total": 100,
"customer": "user123",
"customer@expanded": {
"_id": "user123",
"name": "Alice",
"email": "[email protected]"
}
}
]
}Get Single Entity
GET {basePath}/{EntitySet}('{id}')
Retrieve a single entity by ID.
curl http://localhost:8080/odata/users\('123'\)Create Entity
POST {basePath}/{EntitySet}
Create a new entity.
curl -X POST http://localhost:8080/odata/users \
-H "Content-Type: application/json" \
-d '{"name":"Charlie","email":"[email protected]"}'Update Entity
PUT/PATCH {basePath}/{EntitySet}('{id}')
Update an existing entity.
curl -X PUT http://localhost:8080/odata/users\('123'\) \
-H "Content-Type: application/json" \
-d '{"name":"Charlie Updated"}'Delete Entity
DELETE {basePath}/{EntitySet}('{id}')
Delete an entity.
curl -X DELETE http://localhost:8080/odata/users\('123'\)Batch Operations ($batch)
The OData V4 plugin supports batch requests for executing multiple operations in a single HTTP request. Batch operations can include both read operations and changesets (for transactional writes).
Batch Request Format
POST {basePath}/$batch
Batch requests use multipart MIME format with a boundary delimiter.
Example: Mixed Read and Write Operations
POST /odata/$batch HTTP/1.1
Content-Type: multipart/mixed; boundary=batch_123
--batch_123
Content-Type: application/http
GET /odata/users HTTP/1.1
--batch_123
Content-Type: application/http
GET /odata/users('user1') HTTP/1.1
--batch_123
Content-Type: multipart/mixed; boundary=changeset_456
--changeset_456
Content-Type: application/http
POST /odata/users HTTP/1.1
Content-Type: application/json
{"name":"Alice","email":"[email protected]"}
--changeset_456
Content-Type: application/http
POST /odata/projects HTTP/1.1
Content-Type: application/json
{"name":"Project Alpha","owner":"user1"}
--changeset_456--
--batch_123--Changesets (Atomic Operations)
Changesets are groups of write operations (POST, PATCH, PUT, DELETE) that are executed atomically. Either all operations in a changeset succeed, or all fail and are rolled back.
Example: Atomic Multi-Entity Creation
POST /odata/$batch HTTP/1.1
Content-Type: multipart/mixed; boundary=batch_123
--batch_123
Content-Type: multipart/mixed; boundary=changeset_456
--changeset_456
Content-Type: application/http
POST /odata/users HTTP/1.1
Content-Type: application/json
{"name":"Alice","email":"[email protected]"}
--changeset_456
Content-Type: application/http
POST /odata/users HTTP/1.1
Content-Type: application/json
{"name":"Bob","email":"[email protected]"}
--changeset_456
Content-Type: application/http
POST /odata/users HTTP/1.1
Content-Type: application/json
{"name":"Charlie","email":"[email protected]"}
--changeset_456--
--batch_123--Atomic Behavior:
- If any operation in the changeset fails, all operations are rolled back
- No partial commits - it's all or nothing
- Ideal for maintaining data consistency
Enhanced Error Handling
The plugin provides detailed error information when batch operations fail:
Error Response Format:
{
"error": {
"code": "CHANGESET_FAILED",
"message": "Changeset operation 2/5 failed: Validation failed for field 'email'",
"details": {
"completedOperations": 1,
"totalOperations": 5,
"rollbackAttempted": true
}
}
}Error Details:
code- Error code identifying the failure typemessage- Detailed error message including which operation faileddetails.completedOperations- Number of operations completed before failuredetails.totalOperations- Total number of operations in the changesetdetails.rollbackAttempted- Whether rollback was attempted
Batch Operation Best Practices
Use Changesets for Related Writes
- Group related POST/PATCH/DELETE operations in a changeset
- Ensures data consistency across multiple entities
Separate Read and Write Operations
- Put read operations (GET) outside changesets
- Only write operations (POST, PATCH, PUT, DELETE) should be in changesets
Handle Errors Gracefully
- Check for
CHANGESET_FAILEDerror code - Use the
detailsobject to understand what failed - Implement retry logic if appropriate
- Check for
Limit Batch Size
- Keep batch requests reasonable in size
- Consider breaking very large batches into multiple requests
Example: JavaScript Client
async function createMultipleUsers(users) {
const boundary = `batch_${Date.now()}`;
const changesetBoundary = `changeset_${Date.now()}`;
let body = '';
// Start changeset
body += `--${boundary}\r\n`;
body += `Content-Type: multipart/mixed; boundary=${changesetBoundary}\r\n\r\n`;
// Add each user creation
users.forEach(user => {
body += `--${changesetBoundary}\r\n`;
body += `Content-Type: application/http\r\n\r\n`;
body += `POST /odata/users HTTP/1.1\r\n`;
body += `Content-Type: application/json\r\n\r\n`;
body += JSON.stringify(user) + '\r\n';
});
// Close changeset and batch
body += `--${changesetBoundary}--\r\n`;
body += `--${boundary}--\r\n`;
const response = await fetch('http://localhost:8080/odata/$batch', {
method: 'POST',
headers: {
'Content-Type': `multipart/mixed; boundary=${boundary}`
},
body
});
if (!response.ok) {
const error = await response.json();
console.error('Batch operation failed:', error);
if (error.error?.code === 'CHANGESET_FAILED') {
console.error(`Failed at operation ${error.error.details.completedOperations + 1}/${error.error.details.totalOperations}`);
console.error(`Rollback attempted: ${error.error.details.rollbackAttempted}`);
}
throw new Error(error.error?.message || 'Batch operation failed');
}
return response;
}
// Usage
const users = [
{ name: 'Alice', email: '[email protected]' },
{ name: 'Bob', email: '[email protected]' },
{ name: 'Charlie', email: '[email protected]' }
];
try {
await createMultipleUsers(users);
console.log('All users created successfully');
} catch (error) {
console.error('Failed to create users:', error);
}Type Mapping
ObjectQL field types are automatically mapped to OData EDM types:
| ObjectQL Type | EDM Type | |---------------|----------| | text, textarea, email, url | Edm.String | | number, currency, percent | Edm.Double | | autonumber | Edm.Int32 | | boolean | Edm.Boolean | | date | Edm.Date | | datetime | Edm.DateTimeOffset | | select | Edm.String | | lookup, master_detail | Edm.String (ID reference) |
Architecture
This plugin follows the ObjectStack protocol plugin pattern:
- RuntimePlugin Interface - Implements the standard plugin lifecycle
- ObjectStackRuntimeProtocol Bridge - Uses the protocol bridge for all kernel interactions
- No Direct DB Access - All data operations through the bridge layer
- Lifecycle Management - Proper initialization and cleanup
export class ODataV4Plugin implements RuntimePlugin {
async install(ctx: RuntimeContext) {
// Initialize protocol bridge
this.protocol = new ObjectStackRuntimeProtocol(ctx.engine);
}
async onStart(ctx: RuntimeContext) {
// Start HTTP server
// Use this.protocol.getMetaTypes(), this.protocol.findData(), etc.
}
async onStop(ctx: RuntimeContext) {
// Stop HTTP server
}
}Examples
See the multi-protocol-server example for a complete working example.
License
MIT
