/** * User-defined fields. * * Every CRM grows fields its designers did not anticipate, and a schema that * refuses them is a schema that gets worked around in a spreadsheet. The * variant here carries one idea worth borrowing from Comp AI CRM (MIT, see * NOTICE): `agentBrief` — a prose instruction telling the agent *how* to fill * this particular field. A custom field is otherwise opaque to an agent, which * knows the column exists but nothing about what would constitute a good value. */ import { boolean, index, integer, numeric, pgTable, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core'; import { accounts, contacts } from './crm'; import { demandDeals } from './demand'; import { supplyDeals } from './supply'; import { users } from './identity'; export const fieldDefinitions = pgTable( 'field_definitions', { id: uuid('id').primaryKey().defaultRandom(), /** 'account' | 'contact' | 'demand_deal' | 'supply_deal' */ entity: text('entity').notNull(), key: text('key').notNull(), label: text('label').notNull(), /** * 'text' | 'long_text' | 'number' | 'date' | 'checkbox' | 'select' | * 'url' | 'email' | 'user' */ type: text('type').notNull().default('text'), /** Whether the agent may populate this field at all. */ agentFilled: boolean('agent_filled').notNull().default(true), /** * How to fill it, in prose. e.g. "The provider's InfiniBand * oversubscription ratio, as 1:1, 2:1 or 4:1. Only record it if stated * explicitly — never infer it from marketing copy." */ agentBrief: text('agent_brief'), isRequired: boolean('is_required').notNull().default(false), showOnTable: boolean('show_on_table').notNull().default(false), position: integer('position').notNull().default(0), archivedAt: timestamp('archived_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }, (t) => [uniqueIndex('field_definitions_entity_key').on(t.entity, t.key)], ); export const fieldOptions = pgTable( 'field_options', { id: uuid('id').primaryKey().defaultRandom(), fieldId: uuid('field_id') .notNull() .references(() => fieldDefinitions.id, { onDelete: 'cascade' }), label: text('label').notNull(), position: integer('position').notNull().default(0), archivedAt: timestamp('archived_at', { withTimezone: true }), }, (t) => [index('field_options_field_idx').on(t.fieldId)], ); /** * Values, typed by column rather than serialised into JSON, so that Postgres * can sort and filter them properly. The uniqueness constraints keep one value * per field per record. */ export const fieldValues = pgTable( 'field_values', { id: uuid('id').primaryKey().defaultRandom(), fieldId: uuid('field_id') .notNull() .references(() => fieldDefinitions.id, { onDelete: 'cascade' }), accountId: uuid('account_id').references(() => accounts.id, { onDelete: 'cascade' }), contactId: uuid('contact_id').references(() => contacts.id, { onDelete: 'cascade' }), demandDealId: uuid('demand_deal_id').references(() => demandDeals.id, { onDelete: 'cascade', }), supplyDealId: uuid('supply_deal_id').references(() => supplyDeals.id, { onDelete: 'cascade', }), textValue: text('text_value'), numberValue: numeric('number_value', { precision: 24, scale: 4 }), dateValue: timestamp('date_value', { withTimezone: true }), boolValue: boolean('bool_value'), optionId: uuid('option_id').references(() => fieldOptions.id, { onDelete: 'set null' }), userValue: uuid('user_value').references(() => users.id, { onDelete: 'set null' }), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }, (t) => [ uniqueIndex('field_values_account_key').on(t.fieldId, t.accountId), uniqueIndex('field_values_contact_key').on(t.fieldId, t.contactId), uniqueIndex('field_values_demand_deal_key').on(t.fieldId, t.demandDealId), uniqueIndex('field_values_supply_deal_key').on(t.fieldId, t.supplyDealId), ], ); export type FieldDefinition = typeof fieldDefinitions.$inferSelect; export type FieldValue = typeof fieldValues.$inferSelect;