BrandApp Playground

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;   // string

Using 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"]>;