Schema Definition
Define your EAV data model with type-safe schema definitions.
defineEntity()
Define an entity with its attributes, types, and validation rules.
import { defineEntity } from "@reopt-ai/brandapp-sdk/eav/schema";
const contacts = defineEntity({
name: "contacts",
description: "Contact management",
icon: "👤",
attributes: {
name: {
dataType: "string",
label: "Name",
isRequired: true,
},
email: {
dataType: "email",
label: "Email",
isRequired: true,
isUnique: true,
},
phone: {
dataType: "string",
label: "Phone",
},
status: {
dataType: "select",
label: "Status",
options: [
{ value: "active", label: "Active" },
{ value: "inactive", label: "Inactive" },
] as const,
},
},
});defineSchema()
Combine multiple entities into a schema that can be synced with the server.
import { defineSchema, defineEntity } from "@reopt-ai/brandapp-sdk/eav/schema";
const contacts = defineEntity({ /* ... */ });
const products = defineEntity({ /* ... */ });
export default defineSchema({
entities: { contacts, products },
});Generate types (CLI)
Run reopt brandapp eav sync to push the schema and generate TypeScript types + ID mappings. The same flow is available programmatically via syncSchema(eav, schema, options) — see the Schema Sync page for the live walkthrough.
# Generate TypeScript types from your schema
npx reopt brandapp eav sync \
-s ./lib/eav.schema.ts \
-o ./lib/generated/eav.ts
# This generates:
# - Type definitions (e.g., ContactsValues)
# - Entity IDs map (ENTITY_IDS)
# - Attribute IDs map (ATTRIBUTE_IDS)Generated Output
The sync command generates type definitions and ID constants.
// Auto-generated by @reopt-ai/cli
export type ContactsValues = {
name: string;
email: string;
phone?: string;
status?: "active" | "inactive";
};
export const ENTITY_IDS = {
contacts: "clxyz...",
} as const;
export const ATTRIBUTE_IDS = {
contacts: {
name: "clxyz...",
email: "clxyz...",
phone: "clxyz...",
status: "clxyz...",
},
} as const;InferValues (zero codegen)
Derive the record value type directly from a defineEntity() call — the runtime-inference alternative to the CLI-generated ContactsValues type.
import {
defineEntity,
type InferValues,
} from "@reopt-ai/brandapp-sdk/eav/schema";
const contacts = defineEntity({
name: "contacts",
attributes: {
name: { dataType: "string", label: "Name", isRequired: true },
email: { dataType: "email", label: "Email", isRequired: true },
status: {
dataType: "select",
label: "Status",
options: [
{ value: "active", label: "Active" },
{ value: "inactive", label: "Inactive" },
] as const,
},
},
});
// No `reopt brandapp eav sync` required — the type is inferred in place.
type ContactsValues = InferValues<typeof contacts>;
// { name: string; email: string; status?: "active" | "inactive" }JSON Schema
Convert an entity or the whole schema to draft-07 JSON Schema — handy as an AI tool/function parameter schema, a form-builder spec, or a validator input.
import {
entityToJsonSchema,
schemaToJsonSchema,
} from "@reopt-ai/brandapp-sdk/eav/schema";
import schema from "@/lib/eav.schema";
// A single entity → object schema
const contactsJson = entityToJsonSchema(schema.entities.contacts);
// {
// type: "object",
// title: "contacts",
// properties: {
// name: { type: "string", title: "Name" },
// email: { type: "string", format: "email", title: "Email" },
// status: { type: "string", enum: ["active", "inactive"], title: "Status" },
// },
// required: ["name", "email"],
// }
// The whole schema → one object per entity (great for AI structured output)
const fullJson = schemaToJsonSchema(schema);
// { $schema: "...draft-07...", type: "object", properties: { contacts: {...} } }Drift detection
Compare a build-time schema hash against the live server with verifyEavSchema() so a forgotten migration surfaces as a 503 in your /api/health route.
import {
createReoptEavClient,
verifyEavSchema,
} from "@reopt-ai/brandapp-sdk/eav";
const eav = createReoptEavClient({
clientId: process.env.BRANDAPP_CLIENT_ID!,
clientSecret: process.env.BRANDAPP_CLIENT_SECRET!,
brandappId: process.env.BRANDAPP_ID!,
});
export async function GET() {
// localHash is embedded at build time (e.g. NEXT_PUBLIC_BRANDAPP_EAV_HASH).
const localHash = process.env.NEXT_PUBLIC_BRANDAPP_EAV_HASH ?? "";
const result = await verifyEavSchema({ client: eav, localHash });
// result: { ok, localHash, remoteHash, drift?, schemaCount, fieldCount, ... }
if (!result.ok) console.warn(result.drift);
return Response.json({ eav: result }, { status: result.ok ? 200 : 503 });
}
// The build step computes localHash from the canonical schema shape:
// import { computeEavSchemaHash } from "@reopt-ai/brandapp-sdk/eav";
// const hash = await computeEavSchemaHash(canonicalSchemas);computeDiff()
Preview exactly what a sync would change — create / update / delete / unchanged — by diffing the local schema against the live entities.
import { computeDiff } from "@reopt-ai/brandapp-sdk/eav/schema";
import { getEav } from "@/lib/eav";
import schema from "@/lib/eav.schema";
const eav = await getEav();
// Server entities WITH attributes (list() is metadata-only, so hydrate via get())
const summaries = await eav.entities.list();
const serverEntities = await Promise.all(
summaries.map((e) => eav.entities.get(e.id)),
);
const diff = computeDiff(schema, serverEntities, { deleteOrphans: false });
if (diff.hasChanges) {
for (const entity of diff.entities) {
console.log(entity.type, entity.key); // "create" | "update" | "delete" | "unchanged"
}
}