Eloquent

Documentation

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

MethodPathDescription
GET/api/v1/entities/{name}/recordsList records
POST/api/v1/entities/{name}/recordsCreate 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/queryQuery 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

OperatorDescriptionExample
eqEqual{ status: { eq: "active" } }
neqNot equal{ status: { neq: "deleted" } }
gtGreater than{ count: { gt: 10 } }
gteGreater or equal{ created_at: { gte: "2024-01-01" } }
ltLess than{ price: { lt: 100 } }
lteLess or equal{ price: { lte: 100 } }
containsString contains{ name: { contains: "john" } }
inIn array{ status: { in: ["active", "pending"] } }

Best Practices

  1. Use useEntities hook - Don't create custom API wrappers
  2. Transform to app models - Convert EntityRecord to typed interfaces
  3. Validate on backend - Always validate against JSON Schema
  4. Use SSR for initial load - Faster page load with server actions
  5. Index frequently queried fields - Configure in entity definition

Next Steps