Typed Client
Use TypedEavRecord and generated types for type-safe EAV record access.
Setup Flow
Define a schema, run sync to generate types, then use them in your code.
lib/eav.schema.ts
// 1. Define your schema
// lib/eav.schema.ts
import { defineSchema, defineEntity } 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,
},
},
});
export default defineSchema({ entities: { contacts } });
// 2. Run sync to generate types
// npx reopt brandapp eav sync -s ./lib/eav.schema.ts -o ./lib/generated/eav.ts
// 3. Use generated types in your code
import type { ContactsValues } from "@/lib/generated/eav";TypedEavRecord
A generic type that maps your schema values to a record shape.
example.ts
import { TypedEavRecord } from "@reopt-ai/brandapp-sdk/eav";
import type { ContactsValues } from "@/lib/generated/eav";
// Create a typed record from raw EAV data
type ContactRecord = TypedEavRecord<ContactsValues>;
const record: ContactRecord = {
id: "rec_123",
values: {
name: "Jane Doe", // string (required)
email: "jane@example.com", // string (required)
phone: "+1-555-0123", // string | undefined
status: "active", // "active" | "inactive" | undefined
},
};
// TypeScript knows the shape of values
record.values.name; // string
record.values.status; // "active" | "inactive" | undefined
record.values.email; // stringUsing with the Client (generated types)
Combine CLI-generated types with the EAV client for end-to-end type safety.
app/contacts/page.tsx
import { getEav } from "@/lib/eav";
import type { ContactsValues } from "@/lib/generated/eav";
import { ENTITY_IDS } from "@/lib/generated/eav";
const eav = await getEav();
// Create with typed values
const created = await eav.records.create(ENTITY_IDS.contacts, {
values: {
name: "Jane Doe",
email: "jane@example.com",
status: "active",
// phone is optional, can be omitted
} satisfies ContactsValues,
});
// List with type assertion
const { records } = await eav.records.list(ENTITY_IDS.contacts);
const typedRecords = records as Array<{
id: string;
values: ContactsValues;
}>;
// Now access is type-safe
for (const r of typedRecords) {
console.log(r.values.name); // string
console.log(r.values.status); // "active" | "inactive" | undefined
}Typed entity() client (no codegen)
Pass your schema into the SDK config and sdk.eav.entity("contacts") returns a fully typed client — values inferred via InferValues, no casts and no generated file required.
app/contacts/typed-entity.ts
import { createReoptEavClient } from "@reopt-ai/brandapp-sdk/eav";
import type { InferValues } from "@reopt-ai/brandapp-sdk/eav/schema";
import schema from "@/lib/eav.schema";
// schema MUST be in the config for inference — entity() resolves the name → ID.
const eav = createReoptEavClient({
clientId: process.env.BRANDAPP_CLIENT_ID!,
clientSecret: process.env.BRANDAPP_CLIENT_SECRET!,
brandappId: process.env.BRANDAPP_ID!,
schema,
});
const contacts = eav.entity("contacts");
// create() — values are typed, no `satisfies` / cast needed
const created = await contacts.records.create({
values: { name: "Jane Doe", email: "jane@example.com", status: "active" },
});
created.values.name; // string
created.values.status; // "active" | "inactive" | undefined
// list() returns TypedEavRecord<...> — inferred, no cast
const { records } = await contacts.records.list();
for (const r of records) {
console.log(r.values.name); // string
}
// findOne() narrows the same way
const one = await contacts.records.findOne({
filters: [{ attributeId: "status", operator: "eq", value: "active" }],
});
// Reuse the inferred value type elsewhere
type ContactsValues = InferValues<(typeof schema.entities)["contacts"]>;