mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-12 09:57:03 -04:00
## Summary
Brings indexes management into the per-object Settings tab as a section
under Search (no feature flag, advanced mode only). Admins can create /
delete non-unique indexes with the UI; apps can declare indexes in code
with `defineIndex`. Composite-typed fields are now indexable by picking
a specific sub-column (e.g. `Address > City`).
A few related polish items also land here (invite-user dropdown lands on
the Invite tab; standard warning callout above the new-index form).
## What ships
### UI — custom indexes on per-object Settings
- New section directly under Search, wrapped in
`AdvancedSettingsWrapper`.
- Filter dropdown on the search bar toggles system-index visibility
(shown by default since advanced mode).
- **+ Add Index** button (disabled with tooltip once the per-object cap
is reached) navigates to a dedicated `SettingsObjectNewIndex` page
(matches the field-creation pattern, not a modal):
- Field picker mirrors the webhook event-form layout (rows of dropdowns,
implicit trailing empty row).
- Composite fields surface their sub-properties (`Address > City`,
`Currency > Amount`, …).
- BTREE / GIN type selector.
- Standard warning Callout: "Use indexes sparingly — each one speeds
reads but slows writes."
- Trash icon on `isCustom: true` rows → confirmation modal →
`deleteOneIndex`.
### Server — `createOneIndex` / `deleteOneIndex` mutations
- Gated by `SettingsPermissionGuard(DATA_MODEL)`.
- `IndexMetadataService` wraps the existing migration runner via
`WorkspaceMigrationValidateBuildAndRunService` so the metadata row and
the SQL index land atomically.
- Validation: rejects empty fields, duplicate `(fieldMetadataId,
subFieldName)` pairs, fields not on the object, requires `subFieldName`
for composite parents, forbids `subFieldName` on scalar/relation,
enforces `MAX_CUSTOM_INDEXES_PER_OBJECT = 10`.
- Delete refuses on `isCustom: false` rows so system indexes can't be
removed via this API.
- Dedicated GraphQL exception handler maps each typed error to the right
transport error class.
### Composite sub-field indexing
- Adds `subFieldName: string | null` column to
`IndexFieldMetadataEntity` (fast instance command).
- The flat-entity flow (`UniversalFlatIndexFieldMetadata`,
`FlatIndexFieldMetadata`, `from-universal-flat-index-to-flat-index`,
runner column resolution) all carry `subFieldName` through.
- For composite parents, the runner uses
`computeCompositeColumnName({...}, property)` for the picked sub-column;
for non-composite parents, behavior is unchanged.
- The `'::'` separator encodes `(fieldMetadataId, subFieldName)` for
dedup on the wire; the frontend uses the same separator inside the
Select component's string value.
### Apps can declare indexes in code (`defineIndex`)
- New `IndexManifest` + `IndexFieldManifest` types in
`twenty-shared/application` wired into the `Manifest` type.
- `defineIndex` SDK helper + `IndexConfig`. CLI manifest builder +
extractor recognize `defineIndex` / `ManifestEntityKey.Indexes`.
- Server: `from-index-manifest-to-universal-flat-index` converter
resolves field IDs, validates composite/scalar `subFieldName` rules, and
delegates to `generateFlatIndexMetadataWithNameOrThrow` for the
deterministic name.
- Orchestrator wires the loop after the field-resolution pass;
per-object cap enforced inline against the manifest.
- Cascade on uninstall is automatic — when an app disappears its indexes
drop with it (universal-flat-entity diff handles it).
- Rich-app fixture ships a real `defineIndex` on `PostCard.status`,
exercising the full manifest → install path in CI.
### Closed for now (open later if needed)
- Apps cannot declare `isUnique` indexes — unique constraints stay with
the field-creation flow.
- Apps cannot use a partial-`indexWhereClause` — the UI surface keeps
the framework's hardcoded allowlist.
- UI cannot create unique or partial indexes either; same reasons.
### Cleanups along the way
- Reused the existing `getCompositeSubFieldLabel` +
`COMPOSITE_FIELD_SUB_FIELD_LABELS` (deleted the duplicates I'd created
early in the PR).
- Moved `MAX_CUSTOM_INDEXES_PER_OBJECT` to `twenty-shared/constants`
(single source for FE + BE).
- Replaced inline `isDefined(x) && x !== ''` with `isNonEmptyString`
(from `@sniptt/guards`).
- Hoisted the per-object fields Map + inlined the cap counter into the
indexes orchestrator loop (drops the install scan from O(indexes ×
totalFields) to O(totalFields + indexes)).
- Per design-feedback: page-based create flow (not a modal), filter
dropdown on the SearchInput (not a separate toggle), webhook-style
picker, field icons.
### Unrelated polish that lands here
- "Invite user" link in the multi-workspace dropdown now lands on the
Invite tab directly (`#invite`) instead of the first tab of the members
page.
## Test plan
- [ ] `npx nx typecheck twenty-server / twenty-front / twenty-sdk /
twenty-shared` — passes
- [ ] `npx nx lint:diff-with-main twenty-server / twenty-front` — clean
- [ ] `npx jest index-metadata.service.spec` — green
- [ ] `npx jest from-index-manifest-to-universal-flat-index` — green
(new converter spec, 8 cases)
- [ ] `npx vitest run
src/sdk/define/indexes/__tests__/define-index.spec.ts` (twenty-sdk) —
green (6 cases)
- [ ] `npx vitest run --config vitest.integration.config.ts -t
"rich-app"` — green (rich-app app-dev integration exercises the new
manifest path with the PostCard.status index)
- [ ] Advanced mode → Settings → any object → Settings tab → Indexes
section is visible under Search
- [ ] Create a single-field BTREE index, confirm SQL index exists
(verify via `pg_indexes`)
- [ ] Create a composite-field index (`Address > City`) and confirm the
column is `addressAddressCity`
- [ ] Create an index spanning two columns; column order matches the
picker order
- [ ] Attempt to create an 11th custom index → button is disabled with
tooltip
- [ ] Delete a custom index → confirmation modal → row disappears, PG
index dropped
- [ ] System indexes have no trash icon and are hidden by default
123 lines
5.4 KiB
Plaintext
123 lines
5.4 KiB
Plaintext
---
|
|
title: Fields
|
|
description: Understand the role of fields and how to manage them.
|
|
---
|
|
|
|
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
|
|
|
|
|
## About Fields
|
|
|
|
Fields are like columns in a spreadsheet. They store different types of data like text, numbers, or dates. Fields can be standard (built-in) or custom (the ones you create).
|
|
|
|
### Standard Fields
|
|
|
|
Standard fields come built-in with Twenty to handle common business needs.
|
|
|
|
For example, `First Name` and `Last Name` are standard fields in the `People` object. They store text data for individual names.
|
|
|
|
You cannot delete standard fields, but you can deactivate them if you don't need them.
|
|
|
|
You can also customize the options of the standard `SELECT` type fields, for example the options for the `Stage` on Opportunities.
|
|
|
|
<img src="/images/user-guide/fields/standard-fields.png" style={{width:'100%'}}/>
|
|
|
|
### Custom Fields
|
|
|
|
Custom fields can be added to any object. You can store text, numbers, dates, dropdown selections, and more. Use custom fields to track information that's specific to your business.
|
|
|
|
For instance, a custom field for SpaceX could be `Rocket Active Status`, indicating if a rocket is operational.
|
|
|
|
<img src="/images/user-guide/fields/custom-fields.png" style={{width:'100%'}}/>
|
|
|
|
## Field Types
|
|
|
|
Twenty supports various field types:
|
|
|
|
| Type | Description | Example |
|
|
|------|-------------|---------|
|
|
| Address | Structured address with street, city, state, country, postal code | Office Address |
|
|
| Array | List of text values | Tags |
|
|
| Boolean | True/false checkbox | Is Active |
|
|
| Currency | Monetary value with currency code | Deal Amount (USD) |
|
|
| Date | Date values | Close Date |
|
|
| Date & Time | Date with time | Meeting Time |
|
|
| Domain | Website domain (used for Companies) | acme.com |
|
|
| Email | Email addresses (with primary + additional) | Contact Email |
|
|
| JSON | Structured JSON data | Custom metadata |
|
|
| Links | URLs with labels (primary + secondary) | Website, LinkedIn |
|
|
| Long Text | Multi-line text | Description, Notes |
|
|
| Multi-Select | Multiple choices from a predefined list | Tags, Categories |
|
|
| Number | Numeric values (integers or decimals) | Quantity, Score |
|
|
| Phone | Phone numbers with country code | Work Phone |
|
|
| Rating | Star rating (1-5) | Priority, Score |
|
|
| Relation | Links to records in other objects | Company → People |
|
|
| Select | Single choice from a predefined list | Stage, Status |
|
|
| Text | Single line of text | Name, Title |
|
|
|
|
## Create a Custom Field
|
|
|
|
To add a custom field to any object, follow these steps:
|
|
1. Go to `Settings` in the left sidebar.
|
|
2. Go to `Data Model`, then select the object you wish to customize.
|
|
3. Proceed by clicking on `Add Field`.
|
|
4. Choose a field name and type that suits your requirements. Consider adding a field description for better understanding.
|
|
|
|
Your newly created field is now available within the application's fields. To display it on a specific view, click on the options menu, then select `Fields`.
|
|
|
|
<VimeoEmbed videoId="927628219" title="Video demonstration" />
|
|
|
|
**Quick way:** Click the **+** button at the top right of any object table, then select `Customize fields`. This takes you directly to the Data Model settings.
|
|
|
|
<img src="/images/user-guide/fields/quick-new-field.png" style={{width:'100%'}}/>
|
|
|
|
## Deactivate a Field
|
|
|
|
You can deactivate a field to hide it from the app without losing your data. Think of it as hiding the field rather than deleting it.
|
|
|
|
Here's how you can do it:
|
|
|
|
1. Find the field you want to deactivate in your object settings.
|
|
|
|
2. Click the three dots `⋮` next to the field to open the menu.
|
|
|
|
3. Select `Deactivate` from the dropdown.
|
|
|
|
<img src="/images/user-guide/fields/deactivate-field.png" style={{width:'100%'}}/>
|
|
|
|
What happens when you deactivate a field?
|
|
|
|
1. **In the app:** The field disappears and you can't add new values to it.
|
|
|
|
2. **Existing relationships:** If it's a relation field, existing connections stay but you can't create new ones.
|
|
|
|
3. **API access:** You can still access the field and its data through the API.
|
|
|
|
You can reactivate Standard and Custom Fields or have the option to permanently delete them.
|
|
|
|
## Make Fields Unique
|
|
|
|
Make a field unique to ensure distinct records cannot have the same value. For example, email addresses are unique for each person.
|
|
|
|
If you get an error when setting uniqueness, check for duplicate values in your data (including deleted records).
|
|
|
|
## Indexes (Advanced)
|
|
|
|
Database indexes are managed automatically — adding your own is rarely necessary and easy to get wrong. With Advanced mode on, each object has an **Indexes** section under `Settings → Data Model → <object>` for the cases where you know you need one.
|
|
|
|
## Field Configuration Best Practices
|
|
|
|
### Naming Conventions and Limitations
|
|
- **Singular and plural named must be distinct**: Our GraphQL API needs distinct names for mutations
|
|
- **Protected field names**: some names are reserved for system usage (e.g., `Type`, `Application`)
|
|
|
|
### Currency and Phone Fields
|
|
- **Default currency**: can be configured via the data model
|
|
- **Default country codes**: can be configured for phone fields via the data model
|
|
|
|
### Select Fields
|
|
- **A default option can be selected** for each Select field
|
|
|
|
### Record Text Fields
|
|
- **Each object has one main display field**: This field appears in the leftmost column and represents the record when linked to other objects. It must be a text field. For example, People uses `Name` as the main field, so when you link a person to a company, you'll see their name in the company's view.
|