Skip to content

🧩 Schemas & Records ​

@tellia-solutions/schemas is the single source of truth for types, validation, and UI metadata shared between frontend, backend, and AI agents.

Location: packages/schemas/Build: pnpm --filter @tellia-solutions/schemas build (outputs CJS + ESM + .d.ts)

Structure ​

packages/schemas/src/
  properties/   β€” field type system (value schemas, property schemas, registry)
  records/      β€” parse result schemas + field validation
  common/       β€” cross-cutting infra (MongoId, auth, permissions, language)
  workspace.ts  β€” workspace + tableHeader schemas
  company.ts
  workspace-assistant/

properties/ is the core of the type system. Everything else consumes it.

How property types work ​

Each type is a file in properties/ that exports two things:

  • A value schema β€” what the raw value looks like (z.string(), z.number(), etc.)
  • A property schema β€” the full field definition (type, name, displayName, config options)

Value schemas carry metadata via Zod's .meta():

typescript
export const StringSchema = z.string().meta({
  id: 'string',
  label: 'Text',
  iconName: 'TextFields',
  description: 'Single line or multi-line text',
  category: 'primitive',
  needsConfiguration: false,
  active: true,
  suggestFor: ['name', 'title', 'description'],
} satisfies PropertyTypeMetadata);

active: true means the type appears in the property picker UI. Set it to false for types that exist but shouldn't be user-selectable (e.g. email, address).

All value schemas are collected in ALL_PROPERTY_SCHEMAS in properties/meta.ts. All property schemas are collected in ALL_PROPERTY_TYPE_SCHEMAS in properties/all.ts. These two lists are the only place you register a new type β€” everything else (the UI picker, the discriminated union, the validation map) derives from them automatically.

Adding a new property type ​

  1. Create properties/<type-name>.ts β€” follow any existing type file as the pattern
  2. Add the value schema to ALL_PROPERTY_SCHEMAS in properties/meta.ts
  3. Add the property schema to ALL_PROPERTY_TYPE_SCHEMAS in properties/all.ts
  4. Export from properties/index.ts

That's it. The UI picker, TableHeaderFieldSchema, CanonicalFieldValueSchema, and the field validator all update automatically.

name vs displayName ​

FieldPurposeFormat
nameJSON key, internalcamelCase
displayNameUI label, any formatfree text

createPropertySchema auto-derives displayName from name if not provided. Use normalizePropertyName when you need to convert user input to a valid name.

TableHeaderFieldSchema ​

A workspace's tableHeader is an array of field definitions. TableHeaderFieldSchema is a discriminated union over ALL_PROPERTY_TYPE_SCHEMAS β€” TypeScript narrows to the exact type when you check field.type:

typescript
if (field.type === 'array') {
  field.arrayType; // available, type-safe
}

Adding a new property type extends this union automatically.

Multi-Tenancy Base ​

All company-scoped documents extend CompanyAwareBase (not a Zod schema β€” a Mongoose abstract class):

typescript
abstract class CompanyAwareBase {
  companyId: Types.ObjectId; // Tenant isolation
  visibility: 'private' | 'company' | 'public';
  sharedWithCompanies: Types.ObjectId[];
  sharedWithUsers: Types.ObjectId[];
}

Records & parse results ​

A parse result is a MongoDB document that mixes system fields (workspaceId, userId, …) with dynamic workspace fields β€” whatever the AI extracted. The dynamic fields use .catchall(CanonicalFieldValueSchema), a union of all value schemas plus null and a generic object fallback.

This means any canonical value type is accepted at the DB layer without explicit field declaration.

Validating AI-extracted fields ​

Before storing an AI parse result, validate the dynamic fields against the workspace tableHeader:

typescript
import { validateParseResultFields } from '@tellia-solutions/schemas';

const result = validateParseResultFields(aiFields, workspace.tableHeader);
if (!result.valid) {
  // Feed result.errors back to the agent for self-correction
  // Each error has: field, declaredType, value, message
}

This runs the type-appropriate Zod schema for each declared field. null/undefined values are always valid (cleared field). Unknown fields in the record are ignored β€” they pass through the .catchall() at the DB layer.