Entities Management
The Entities service provides dynamic CRUD operations with JSON Schema validation. Use this when creating entity definitions, managing records, querying with filters, or building entity-driven features.
Overview
Entities are dynamically-typed data structures defined by JSON Schema. Each organization can create custom entity types (customers, tickets, products) without code changes.
Core Concepts
Entity Definition
A schema that defines the structure of records:
{
"id": "cust-def-123",
"name": "customer",
"displayName": "Customer",
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"status": { "type": "string", "enum": ["active", "inactive"] }
},
"required": ["name", "email"]
}
}
Entity Record
An instance of an entity definition:
{
"id": "cust-rec-456",
"definitionId": "cust-def-123",
"definitionName": "customer",
"fields": {
"name": "John Doe",
"email": "john@example.com",
"status": "active"
},
"createdAt": "2024-03-01T10:00:00Z",
"updatedAt": "2024-03-01T10:00:00Z"
}
API Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/entities/{name}/records | List records |
| POST | /api/v1/entities/{name}/records | Create record |
| GET | /api/v1/entities/{name}/records/{id} | Get record |
| PUT | /api/v1/entities/{name}/records/{id} | Update record |
| DELETE | /api/v1/entities/{name}/records/{id} | Delete record |
| POST | /api/v1/entities/{name}/records/query | Query with filters |
Frontend Usage
Using useEntities Hook
import { useEntities } from "@elqnt/entity/hooks";
import type { EntityRecord } from "@elqnt/entity/models";
import { useAppConfig } from "@/app/providers";
interface Customer {
id: string;
name: string;
email: string;
status: string;
}
function toCustomer(record: EntityRecord): Customer {
return {
id: record.id,
name: record.fields.name,
email: record.fields.email,
status: record.fields.status,
};
}
export function useCustomers() {
const config = useAppConfig();
const {
queryRecords,
createRecord,
updateRecord,
deleteRecord,
loading,
error
} = useEntities({
baseUrl: config.apiGatewayUrl,
orgId: config.orgId,
});
const fetchCustomers = async (filters?: Record<string, unknown>) => {
const result = await queryRecords("customer", { filters });
return result?.records?.map(toCustomer) ?? [];
};
const createCustomer = async (data: Omit<Customer, "id">) => {
const record = await createRecord("customer", { fields: data });
return record ? toCustomer(record) : null;
};
const updateCustomer = async (id: string, data: Partial<Customer>) => {
const record = await updateRecord("customer", id, { fields: data });
return record ? toCustomer(record) : null;
};
const deleteCustomer = async (id: string) => {
return deleteRecord("customer", id);
};
return {
fetchCustomers,
createCustomer,
updateCustomer,
deleteCustomer,
loading,
error
};
}
Query with Filters
// Filter by status
const activeCustomers = await fetchCustomers({
status: { eq: "active" }
});
// Filter with multiple conditions
const results = await queryRecords("customer", {
filters: {
status: { eq: "active" },
created_at: { gte: "2024-01-01" }
},
sort: { field: "name", order: "asc" },
limit: 50,
offset: 0
});
Server-Side (SSR)
// actions/customer-actions.ts
"use server";
import { createServerClient } from "@elqnt/api-client/server";
const client = createServerClient({
gatewayUrl: process.env.API_GATEWAY_URL!,
jwtSecret: process.env.JWT_SECRET!,
});
export async function getCustomersSSR(orgId: string) {
const result = await client.get<{ records: Customer[] }>(
"/api/v1/entities/customer/records",
{ orgId }
);
return { customers: result.data?.records || [] };
}
Backend Implementation
Creating Records
func (h *Handler) CreateRecord(w http.ResponseWriter, r *http.Request) {
orgID := r.Header.Get("X-Org-ID")
entityName := r.PathValue("entityName")
var req CreateRecordRequest
json.NewDecoder(r.Body).Decode(&req)
// Validate against JSON Schema
if err := h.validator.ValidateFields(entityName, req.Fields); err != nil {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", err.Error())
return
}
record, err := h.service.CreateRecord(r.Context(), orgID, entityName, req.Fields)
if err != nil {
writeError(w, http.StatusInternalServerError, "CREATE_FAILED", err.Error())
return
}
writeJSON(w, http.StatusCreated, record)
}
Query Records
func (h *Handler) QueryRecords(w http.ResponseWriter, r *http.Request) {
orgID := r.Header.Get("X-Org-ID")
entityName := r.PathValue("entityName")
var req QueryRequest
json.NewDecoder(r.Body).Decode(&req)
records, total, err := h.service.QueryRecords(
r.Context(),
orgID,
entityName,
req.Filters,
req.Sort,
req.Limit,
req.Offset,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "QUERY_FAILED", err.Error())
return
}
writeJSON(w, http.StatusOK, QueryResponse{
Records: records,
Total: total,
})
}
Filter Operators
| Operator | Description | Example |
|---|---|---|
eq | Equal | { status: { eq: "active" } } |
neq | Not equal | { status: { neq: "deleted" } } |
gt | Greater than | { count: { gt: 10 } } |
gte | Greater or equal | { created_at: { gte: "2024-01-01" } } |
lt | Less than | { price: { lt: 100 } } |
lte | Less or equal | { price: { lte: 100 } } |
contains | String contains | { name: { contains: "john" } } |
in | In array | { status: { in: ["active", "pending"] } } |
Best Practices
- Use useEntities hook - Don't create custom API wrappers
- Transform to app models - Convert EntityRecord to typed interfaces
- Validate on backend - Always validate against JSON Schema
- Use SSR for initial load - Faster page load with server actions
- Index frequently queried fields - Configure in entity definition
Next Steps
- Frontend Apps - Using entities in React
- Type System - TypeScript generation
- Workflow Engine - Automating entity operations