a121d00dddcc3a60ce1eadbe5a365ee6972f60ff
345 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a121d00ddd |
feat: add color property to ObjectMetadata for object icon customization (#18672)
## Summary - Adds a `color` column to `ObjectMetadataEntity` with full GraphQL support so object icon colors are persisted at the metadata level - Adds a `type` column to `NavigationMenuItemEntity` (enum: `OBJECT`, `VIEW`, `FOLDER`, `LINK`, `RECORD`) replacing field-based type inference - Updates frontend to read object colors from `objectMetadata.color` (falling back to standard defaults) in the sidebar nav, record index header, and record show breadcrumb - Simplifies `NavigationMenuItemIcon` color resolution via `getEffectiveNavigationMenuItemColor` util ## Color rules | Item type | Color source | Editable in sidebar? | |-----------|-------------|---------------------| | **Object** | `objectMetadata.color` | Yes — persisted to `objectMetadata.color` on Save | | **Folder** | `navigationMenuItem.color` | Yes | | **Link** | Fixed default (`DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK`) | No | | **View** | `objectMetadata.color` (from the parent object) | No | | **Record** | None | No | - **Object** items represent the whole object (e.g. "Companies") and point to the INDEX view. Changing their color updates `objectMetadata.color` via `useSaveObjectMetadataColorsFromDraft`. - **View** items represent specific non-INDEX views. Their color comes from the parent object's metadata (read-only). - Only **folders** store their color on `navigationMenuItem.color` — enforced by `hasNavigationMenuItemOwnColor` util. - `getEffectiveNavigationMenuItemColor` returns `objectColor` for both OBJECT and VIEW items, folder's own color for folders, and the fixed default for links. ## NavigationMenuItemType enum - Shared enum created in `twenty-shared` with values: `OBJECT`, `VIEW`, `FOLDER`, `LINK`, `RECORD` - Registered as a GraphQL enum on the backend - Replaces string literals across entity, DTOs, input, converters, and frontend hooks - Migration backfills existing rows: INDEX views → `OBJECT`, non-INDEX views → `VIEW`, based on join with the view table ## Design decisions - **OBJECT vs VIEW distinction**: Items pointing to INDEX views are typed as `OBJECT` (represent the whole object, color editable). Items pointing to non-INDEX views are typed as `VIEW` (specific view, color read-only from parent object). - **Dual color storage**: `navigationMenuItem.color` is preserved for folders only. Objects use `objectMetadata.color` as their source of truth. - **Type discriminator**: The `type` column replaces field-based inference (checking `viewId`, `link`, `targetRecordId` presence) with an explicit enum, simplifying `isNavigationMenuItemLink` / `isNavigationMenuItemFolder` to simple `item.type ===` checks. - **No settings page color picker**: Object color editing is done from the sidebar edit panel, not the data model settings page. ## Test plan - [ ] Verify objects display their default standard colors in the sidebar - [ ] Verify object color editing works in the sidebar edit panel (persists to objectMetadata.color) - [ ] Verify folder color editing works in the sidebar edit panel - [ ] Verify views, links, and records do NOT show a color picker in the sidebar edit panel - [ ] Run `npx nx typecheck twenty-front` and `npx nx typecheck twenty-server` - [ ] Verify the database migrations add `color` to `objectMetadata` and `type` to `navigationMenuItem` Made with [Cursor](https://cursor.com) |
||
|
|
d9eb317bb5 |
feat: rename RICH_TEXT_V2 → RICH_TEXT in codebase (keep DB value) (#18628)
## Summary - Renames the `FieldMetadataType` enum key from `RICH_TEXT_V2` to `RICH_TEXT` across the entire codebase, while keeping the underlying string value as `'RICH_TEXT_V2'` to maintain PostgreSQL database compatibility - Renames all related types, guards, hooks, components, and files from `*RichTextV2*` / `*rich-text-v2*` to `*RichText*` / `*rich-text*` (e.g. `FormRichTextV2FieldInput` → `FormRichTextFieldInput`, `isFieldRichTextV2` → `isFieldRichText`) - Updates generated files (GraphQL schema, SDK types) to use the new key while preserving the `RICH_TEXT_V2` string value for DB/API layer - Updates i18n locale files, test snapshots, and integration tests to reflect the rename ## Context The legacy `RICH_TEXT` (V1) field type was deprecated and migrated to `TEXT` in a previous PR (#18623). With V1 gone, the `RICH_TEXT_V2` naming is no longer necessary — `RICH_TEXT` is now the canonical name. The DB enum value stays `'RICH_TEXT_V2'` to avoid confusion with the just-deprecated V1 type and to prevent a database migration. ## Test plan - [x] `twenty-server` typecheck passes - [x] `twenty-front` typecheck passes (only pre-existing Apollo client errors remain) - [x] `twenty-server` lint passes - [x] `twenty-front` lint passes - [x] `twenty-shared` build passes - [ ] CI passes Made with [Cursor](https://cursor.com) |
||
|
|
46e515436e |
Deprecate legacy RICH_TEXT field metadata type (#18623)
## Summary - Removes the deprecated `RICH_TEXT` (V1) field metadata type from the codebase entirely - Adds a 1.20 upgrade command that migrates existing `RICH_TEXT` fields to `TEXT` in `core.fieldMetadata` - Cleans up ~70 files across `twenty-shared`, `twenty-server`, `twenty-front`, `twenty-sdk`, and `twenty-zapier` ## Context `RICH_TEXT` was a legacy field type that stored rich text as a single `text` column. It was already **read-only** — writes threw errors directing users to `RICH_TEXT_V2` instead. `RICH_TEXT_V2` is the current approach: a composite type with `blocknote` (editor JSON) and `markdown` subfields. Keeping the deprecated type added maintenance burden without any value. Since the underlying database column type for `RICH_TEXT` was already `text` (same as `TEXT`), the migration only needs to update the metadata — no data migration or column changes required. ## Changes ### Upgrade command (new) - `1-20-migrate-rich-text-to-text.command.ts` — runs `UPDATE core."fieldMetadata" SET "type" = 'TEXT' WHERE "type" = 'RICH_TEXT'` per workspace, with cache invalidation ### Enum & shared types - Removed `RICH_TEXT` from `FieldMetadataType` enum - Removed from `FieldMetadataDefaultValueMapping`, `isFieldMetadataTextKind` ### Server (~30 files) - Removed from type mapper (scalar, filter, order-by), data processors, input transformer, filter operators, zod schemas, column type mapping, searchable fields, RLS matching, OpenAPI schema, fake value generators - Removed from field creation flow and field metadata type validator - Updated dev seeder Pet `bio` field to `TEXT` - Cleaned up mocks, snapshots, integration tests ### Frontend (~25 files) - Deleted: `RichTextFieldDisplay`, `isFieldRichText`, `isFieldRichTextValue`, `useRichTextFieldDisplay` - Removed from `FieldDisplay`, `usePersistField`, `isFieldValueEmpty`, `isRecordMatchingFilter`, `generateEmptyFieldValue`, `isFieldCellSupported`, spreadsheet import, workflow fake values - Removed from settings types, field type configs, and field creation exclusion list - Updated tests, mocks, and stories ### SDK & Zapier - Removed from generated GraphQL schema and TypeScript types - Removed from Zapier `computeInputFields` |
||
|
|
ab13020e2b |
Fix wrong uuid error on field metadata (#18598)
## Summary - Same fix as #18590 but applied to `FieldMetadataDTO` - Changed `universalIdentifier` from `UUID` to `String` type since field metadata universal identifiers are not necessarily valid UUIDs - Removed `universalIdentifier` from `FieldFilter` (was using `UUIDFilterComparison`) - Updated generated SDK and frontend types accordingly |
||
|
|
cb3e32df86 |
Fix AI demo workspace skill (#18575)
This PR fixes what allows to have a working demo workspace skill. - Skill updated many times into something that works - Fixed infinite loop in AI chat by memoizing ai-sdk output - Finished navigateToView implementation - Increased MAX_STEPS to 300 so the chat don't quit in the middle of a long running skill - Added CreateManyRelationFields |
||
|
|
9d57bc39e5 |
Migrate from ESLint to OxLint (#18443)
## Summary Fully replaces ESLint with OxLint across the entire monorepo: - **Replaced all ESLint configs** (`eslint.config.mjs`) with OxLint configs (`.oxlintrc.json`) for every package: `twenty-front`, `twenty-server`, `twenty-emails`, `twenty-ui`, `twenty-shared`, `twenty-sdk`, `twenty-zapier`, `twenty-docs`, `twenty-website`, `twenty-apps/*`, `create-twenty-app` - **Migrated custom lint rules** from ESLint plugin format to OxLint JS plugin system (`@oxlint/plugins`), including `styled-components-prefixed-with-styled`, `no-hardcoded-colors`, `sort-css-properties-alphabetically`, `graphql-resolvers-should-be-guarded`, `rest-api-methods-should-be-guarded`, `max-consts-per-file`, and Jotai-related rules - **Migrated custom rule tests** from ESLint `RuleTester` + Jest to `oxlint/plugins-dev` `RuleTester` + Vitest - **Removed all ESLint dependencies** from `package.json` files and regenerated lockfiles - **Updated Nx targets** (`lint`, `lint:diff-with-main`, `fmt`) in `nx.json` and per-project `project.json` to use `oxlint` commands with proper `dependsOn` for plugin builds - **Updated CI workflows** (`.github/workflows/ci-*.yaml`) — no more ESLint executor - **Updated IDE setup**: replaced `dbaeumer.vscode-eslint` with `oxc.oxc-vscode` extension, configured `source.fixAll.oxc` and format-on-save with Prettier - **Replaced all `eslint-disable` comments** with `oxlint-disable` equivalents across the codebase - **Updated docs** (`twenty-docs`) to reference OxLint instead of ESLint - **Renamed** `twenty-eslint-rules` package to `twenty-oxlint-rules` ### Temporarily disabled rules (tracked in `OXLINT_MIGRATION_TODO.md`) | Rule | Package | Violations | Auto-fixable | |------|---------|-----------|-------------| | `twenty/sort-css-properties-alphabetically` | twenty-front | 578 | Yes | | `typescript/consistent-type-imports` | twenty-server | 3814 | Yes | | `twenty/max-consts-per-file` | twenty-server | 94 | No | ### Dropped plugins (no OxLint equivalent) `eslint-plugin-project-structure`, `lingui/*`, `@stylistic/*`, `import/order`, `prefer-arrow/prefer-arrow-functions`, `eslint-plugin-mdx`, `@next/eslint-plugin-next`, `eslint-plugin-storybook`, `eslint-plugin-react-refresh`. Partial coverage for `jsx-a11y` and `unused-imports`. ### Additional fixes (pre-existing issues exposed by merge) - Fixed `EmailThreadPreview.tsx` broken import from main rename (`useOpenEmailThreadInSidePanel`) - Restored truthiness guard in `getActivityTargetObjectRecords.ts` - Fixed `AgentTurnResolver` return types to match entity (virtual `fileMediaType`/`fileUrl` are resolved via `@ResolveField()`) ## Test plan - [x] `npx nx lint twenty-front` passes - [x] `npx nx lint twenty-server` passes - [x] `npx nx lint twenty-docs` passes - [x] Custom oxlint rules validated with Vitest: `npx nx test twenty-oxlint-rules` - [x] `npx nx typecheck twenty-front` passes - [x] `npx nx typecheck twenty-server` passes - [x] CI workflows trigger correctly with `dependsOn: ["twenty-oxlint-rules:build"]` - [x] IDE linting works with `oxc.oxc-vscode` extension |
||
|
|
2a82df7073 |
AI tools to create a demo workspace (#18236)
This PR adds the necessary tool to create a demo workspace with : relevant custom objects and fields, mock data and a real dashboard with graph widgets. It is still a bit under-optimized and slow but it works. This PR also adds an AI tool that allows to see what happens in real time, it navigates the app and waits when necessary. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
0e89c96170 |
feat: add npm and tarball app distribution with upgrade mechanism (#18358)
## Summary - **npm + tarball app distribution**: Apps can be installed from the npm registry (public or private) or uploaded as `.tar.gz` tarballs, with `AppRegistrationSourceType` tracking the origin - **Upgrade mechanism**: `AppUpgradeService` checks for newer versions, supports rollback for npm-sourced apps, and a cron job runs every 6 hours to update `latestAvailableVersion` on registrations - **Security hardening**: Tarball extraction uses path traversal protection, and `enableScripts: false` in `.yarnrc.yml` disables all lifecycle scripts during `yarn install` to prevent RCE - **Frontend**: "Install from npm" and "Upload tarball" modals, upgrade button on app detail page, blue "Update" badge on installed apps table when a newer version is available - **Marketplace catalog sync**: Hourly cron job syncs a hardcoded catalog index into `ApplicationRegistration` entities - **Integration tests**: Coverage for install, upgrade, tarball upload, and catalog sync flows ## Backend changes | Area | Files | |------|-------| | Entity & migration | `ApplicationRegistrationEntity` (sourceType, sourcePackage, latestAvailableVersion), `ApplicationEntity` (applicationRegistrationId), migration | | Services | `AppPackageResolverService`, `ApplicationInstallService`, `AppUpgradeService`, `MarketplaceCatalogSyncService` | | Cron jobs | `MarketplaceCatalogSyncCronJob` (hourly), `AppVersionCheckCronJob` (every 6h) | | REST endpoint | `AppRegistrationUploadController` — tarball upload with secure extraction | | Resolver | `MarketplaceResolver` — simplified `installMarketplaceApp` (removed redundant `sourcePackage` arg) | | Security | `.yarnrc.yml` — `enableScripts: false` to block postinstall RCE | ## Frontend changes | Area | Files | |------|-------| | Modals | `SettingsInstallNpmAppModal`, `SettingsUploadTarballModal`, `SettingsAppModalLayout` | | Hooks | `useUploadAppTarball`, `useInstallMarketplaceApp` (cleaned up) | | Upgrade UI | `SettingsApplicationVersionContainer`, `SettingsApplicationDetailAboutTab` | | Badge | `SettingsApplicationTableRow` — blue "Update" tag, `SettingsApplicationsInstalledTab` — fetches registrations for version comparison | | Styling | Migrated to Linaria (matching main) | ## Test plan - [ ] Install an app from npm via the "Install from npm" modal - [ ] Upload a `.tar.gz` tarball via the "Upload tarball" modal - [ ] Verify upgrade badge appears when `latestAvailableVersion > version` - [ ] Verify upgrade flow from app detail page - [ ] Run integration tests: `app-distribution.integration-spec.ts`, `marketplace-catalog-sync.integration-spec.ts` - [ ] Verify `enableScripts: false` blocks postinstall scripts during yarn install Made with [Cursor](https://cursor.com) |
||
|
|
b11f77df2a |
[FRONT COMPONENTS] Introduce conditionalAvailabilityExpression to command menu items (#18319)
## PR Description - Uses `expr-eval` to enable front components (SDK plugins) to define conditional availability as declarative expressions. - Moves shared types and constants to `twenty-shared` - Introduces a `conditionalAvailabilityExpression` field on `CommandMenuItemEntity`, allowing command menu items to store an `expr-eval` compatible expression string that is evaluated against a CommandMenuContext to determine if the item should be shown. - Creates an esbuild transform plugin `conditional-availability-transform-plugin` in `twenty-sdk` that converts TypeScript conditional availability expressions into `expr-eval` compatible syntax at build time, so SDK developers can write natural TS expressions that get transformed to evaluable strings. - Removes deprecated `forceRegisteredActionsByKey` state and its usage. - Creates `useCommandMenuContext` hook that builds the full `CommandMenuContext` object from React state, which is then passed to `useCommandMenuItemFrontComponentActions` for evaluating conditional availability expressions. |
||
|
|
c97d872b9f |
[BREAKING_CHANGE_VIEW_SORT] Refactor view sort to v2 (#17609)
Fixes https://github.com/twentyhq/core-team-issues/issues/2187 --------- Co-authored-by: prastoin <paul@twenty.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> |
||
|
|
f080b952eb |
Add manifest integration tests (#18203)
- add integration test on sync manifest endpoint - fix invalid standard uuid + migration command |
||
|
|
f3faa11dd2 |
New field creates fields widget field (#18022)
## Context
Introducing "NewFieldDefaultConfiguration" to FIELDS widget
configurations
```typescript
{
isVisible: boolean;
viewFieldGroupId: string | null;
}
```
This configuration will define where a new field should be added (which
section) and its default visibility inside FIELDS widget views.
The new field position should always be at the end (meaning the last
position for the view fields OR the last position of a viewFieldGroup)
See "New fields" on this screenshot
<img width="401" height="724" alt="Layout V1"
src="https://github.com/user-attachments/assets/4969bcaa-f244-4504-8947-778a02c24c47"
/>
|
||
|
|
163c1175cb |
File - Migrate core pictures (workspace and member logo) + workflow attachments (#17924)
- Create a common file-by-id download controller - Create core picture module with resolver and logic to handle workspaceLogo and workspaceMemberProfilePicture update - Create workflow file module (same) - Data migration |
||
|
|
5544b5dcfe |
Fix and refactor all metadata relation (#17978)
# Introduction The initial motivation was that in the workspace migration create action some universal foreign key aggregators weren't correctly deleted before returned due to constant missconfiguration <img width="2300" height="972" alt="image" src="https://github.com/user-attachments/assets/9401eb02-2bb2-4e69-9c5f-9a354ff61079" /> It also meant that under the hood some optimistic behavior wasn't correctly rendered for some aggregators ## Solution Refactored the `ALL_METADATA_RELATIONS` as follows: This way we can infer the FK and transpile it to a universalFK, also the aggregators are one to one instead of one versus all available Making the only manual configuration to be defined the `foreignKey` and `inverseOneToManyProperty` ``` ┌──────────────────────────────────────┐ ┌─────────────────────────────────────────────┐ │ ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY│ │ ALL_ONE_TO_MANY_METADATA_RELATIONS │ │──────────────────────────────────────│ │─────────────────────────────────────────────│ │ Derived from: Entity types │ │ Derived from: Entity types │ │ │ │ │ │ Provides: │ │ Provides: │ │ • foreignKey │ │ • metadataName │ │ │ │ • flatEntityForeignKeyAggregator │ │ Standalone low-level primitive │ │ • universalFlatEntityForeignKeyAggregator │ └──────────────┬───────────────────────┘ └──────────────┬──────────────────────────────┘ │ │ │ foreignKey type + │ inverseOneToManyProperty │ universalForeignKey derivation │ keys (type constraint) │ │ ▼ ▼ ┌───────────────────────────────────────────────────────────────┐ │ ALL_MANY_TO_ONE_METADATA_RELATIONS │ │───────────────────────────────────────────────────────────────│ │ Derived from: │ │ • Entity types (metadataName, isNullable) │ │ • ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY (FK → universalFK) │ │ • ALL_ONE_TO_MANY_METADATA_RELATIONS (inverse keys) │ │ │ │ Provides: │ │ • metadataName │ │ • foreignKey (replicated from FK constant) │ │ • inverseOneToManyProperty │ │ • isNullable │ │ • universalForeignKey │ └──────────────────────────┬────────────────────────────────────┘ │ ┌──────────────────┼──────────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────────────┐ ┌────────────────┐ ┌──────────────────────┐ │ Type consumers │ │ Atomic utils │ │ Optimistic utils │ │───────────────────│ │────────────────│ │──────────────────────│ │ • JoinColumn │ │ • resolve-* │ │ • add/delete flat │ │ • RelatedNames │ │ • get-* │ │ entity maps │ │ • UniversalFlat │ │ │ │ • add/delete │ │ EntityFrom │ │ │ │ universal flat │ │ │ │ │ │ entity maps │ └───────────────────┘ └────────────────┘ │ │ │ (bridge via │ │ inverseOneToMany │ │ Property → │ │ ONE_TO_MANY for │ │ aggregator lookup) │ └──────────────────────┘ ``` ### Previously ``` ┌─────────────────────────────────────────────────────────────────────┐ │ ALL_METADATA_RELATIONS │ │─────────────────────────────────────────────────────────────────────│ │ Derived from: Entity types │ │ │ │ Structure: { [metadataName]: { manyToOne: {...}, oneToMany: {...},│ │ serializedRelations?: {...} } } │ │ │ │ manyToOne provides: │ │ • metadataName │ │ • foreignKey │ │ • flatEntityForeignKeyAggregator (nullable, often wrong/null) │ │ • isNullable │ │ │ │ oneToMany provides: │ │ • metadataName │ │ │ │ Monolithic single source of truth │ └──────────────────────────┬──────────────────────────────────────────┘ │ │ manyToOne entries transformed via │ ToUniversalMetadataManyToOneRelationConfiguration │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ ALL_UNIVERSAL_METADATA_RELATIONS │ │─────────────────────────────────────────────────────────────────────│ │ Derived from: ALL_METADATA_RELATIONS (type-level transform) │ │ │ │ Structure: { [metadataName]: { manyToOne: {...}, oneToMany: {...} │ │ } } │ │ │ │ manyToOne provides: │ │ • metadataName │ │ • foreignKey │ │ • universalForeignKey (derived: FK → replace Id → UniversalId) │ │ • universalFlatEntityForeignKeyAggregator (derived from │ │ flatEntityForeignKeyAggregator → replace Ids → UniversalIds) │ │ • isNullable │ │ │ │ oneToMany: passthrough from ALL_METADATA_RELATIONS │ │ │ │ Duplicated monolith with universal key transforms │ └──────────────────────────┬──────────────────────────────────────────┘ │ ┌──────────────────┼──────────────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────────┐ ┌────────────────────┐ ┌──────────────────────┐ │ Type consumers│ │ Atomic utils │ │ Optimistic utils │ │───────────────│ │────────────────────│ │──────────────────────│ │ • JoinColumn │ │ • resolve-entity- │ │ • add/delete flat │ │ • RelatedNames│ │ relation-univ-id │ │ entity maps │ │ • Universal │ │ (ALL_METADATA_ │ │ (ALL_METADATA_ │ │ FlatEntity │ │ RELATIONS │ │ RELATIONS │ │ From │ │ .manyToOne) │ │ .manyToOne) │ │ │ │ │ │ │ │ Mixed usage │ │ • resolve-univ- │ │ • add/delete univ │ │ of both │ │ relation-ids │ │ flat entity maps │ │ constants │ │ (ALL_UNIVERSAL_ │ │ (ALL_UNIVERSAL_ │ │ │ │ METADATA_REL │ │ METADATA_REL │ │ │ │ .manyToOne) │ │ .manyToOne) │ │ │ │ │ │ │ │ │ │ • resolve-univ- │ │ universalFlatEntity │ │ │ │ update-rel-ids │ │ ForeignKeyAggregator │ │ │ │ (ALL_UNIVERSAL_ │ │ read directly from │ │ │ │ METADATA_REL │ │ the constant │ │ │ │ .manyToOne) │ │ │ │ │ │ │ │ │ │ │ │ • regex hack: │ │ │ │ │ │ foreignKey │ │ │ │ │ │ .replace(/Id$/, │ │ │ │ │ │ 'UniversalId') │ │ │ └───────────────┘ └────────────────────┘ └──────────────────────┘ ``` |
||
|
|
2b7b05de2e |
[OBJECT_MANIFEST_BREAKING_CHANGE] Sync returns workspace migration (#17918)
# Introduction In this PR we start returning a workspace migration post sync so it can committed and provided within the tarball ## Universal aggregators utils Created two utils ### deleteUniversalFlatEntityForeignKeyAggregators Used when building a universal create action, a newly created actions should not contain any aggregated foreign key so they won't be codegen in the workspace migration but also they are overriden at uninversal to flat transpilation anw ### resetUniversalFlatEntityForeignKeyAggregators Used before validating a new flat entity creation, some validator will consume the fk aggregator in order to validate integrity, but of optimstically provided it can result to errors. To avoid caller responsability we override them here ## create-field-action refactor Refactored the universal and flat field create action to be following the base actions in order to ease typing Also it was tailored to handle unlimited amount of flat field metadata in the same actions whereas in the reality we were always only sending at max 2 ( for relation fields ) Note: relation field has to be provided at the same as if not optimistic would fail to retrieve circular universal identifiers ## ObjectManifest Now always expect a `labelIdentifierFieldMetadataUniversalIdentifier` ## Integration test Created an integration test that creates an app, sync a first manifest and a second implying update workspace migration action generation |
||
|
|
13c2234856 |
feat: emit metadata events for schema changes with actor context for webhooks (#17622)
## Summary
This PR adds **metadata eventing**: when schema metadata
(objectMetadata, fieldMetadata, view, viewField, etc.) is created,
updated, or deleted, we now emit events that can trigger webhooks and
future audit logs. It also adds **actor context** (`userId`,
`workspaceMemberId`) to those events so subscribers can attribute
changes to a user or API key.
## What changed
### 1. Metadata eventing (first commit)
- **MetadataEventEmitter**
New service that emits batch events after successful workspace
migrations. Event names follow `metadata.{entity}.{action}` (e.g.
`metadata.objectMetadata.created`, `metadata.fieldMetadata.updated`).
- **MetadataEventsToDbListener**
Listens for metadata events and enqueues webhook delivery via
`CallWebhookJobsForMetadataJob`.
- **Event types** (twenty-shared)
`MetadataEventAction`, `MetadataEventBatch`, and record event types for
create/update/delete.
- **WorkspaceMigrationValidateBuildAndRunService**
Calls the metadata event emitter after running migrations so all
metadata changes (from any module) emit events from a single place.
- **Create events**
Sourced from the create action payload (`flatEntity` /
`flatFieldMetadatas`) because `fromToAllFlatEntityMaps` does not provide
a before/after diff for creates. Update/delete events still use the
fromToAllFlatEntityMaps comparison.
### 2. Actor context (second commit)
- **MetadataEventEmitter**
Accepts optional `actorContext` (`userId`, `workspaceMemberId`) and
includes it on emitted batch events.
- **WorkspaceMigrationValidateBuildAndRunService**
Passes `actorContext` from the request into the metadata event emitter.
- **Metadata resolvers & services**
All metadata modules resolve `@AuthUser({ allowUndefined: true })` and
`@AuthUserWorkspaceId()` and pass `userId` and `workspaceMemberId`
through to the migration/event pipeline. Both are optional so
API-key–authenticated requests (no user) still emit events without a
user identity.
Shared some questions on Discord about the PR.
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: prastoin <paul@twenty.com>
|
||
|
|
9e21e55db4 |
Prevent leak between /metadata and /graphql GQL schemas (#17845)
## Fix resolver schema leaking between `/metadata` and `/graphql` endpoints ### Summary - Patch `@nestjs/graphql` to support a `resolverSchemaScope` option that filters resolvers at both schema generation and runtime, preventing cross-endpoint leaking - Introduce `@CoreResolver()` and `@MetadataResolver()` decorators to explicitly scope each resolver to its endpoint - Move most resolvers (auth, billing, workspace, user, etc.) to the metadata schema where the frontend expects them; only workflow and timeline calendar/messaging resolvers remain on `/graphql` - Fix frontend `SSEQuerySubscribeEffect` to use the default (metadata) Apollo client instead of the core client ### Problem NestJS GraphQL's module-based resolver discovery traverses transitive imports, causing resolvers from `/metadata` modules to leak into the `/graphql` schema and vice versa. This made the schemas unpredictable and tightly coupled to module import order. ### Approach - Added `resolverSchemaScope` to `GqlModuleOptions` via a patch on `@nestjs/graphql`, filtering in both `filterResolvers()` (runtime binding) and `getAllCtors()` (schema generation) - Each resolver is explicitly decorated with `@CoreResolver()` or `@MetadataResolver()` - Organized decorator, constant, and type files under `graphql-config/` following project conventions Core GQL Schema: (see: no more fields!) <img width="827" height="894" alt="image" src="https://github.com/user-attachments/assets/668f3f0f-485e-43f0-92be-4345aeccacb6" /> Metadata GQL Schema (see no more getTimelineCalendarEventsFromCompany) <img width="827" height="894" alt="image" src="https://github.com/user-attachments/assets/443913db-e5fe-4161-b0e7-4a971cc80a71" /> |
||
|
|
8c951d3623 |
Migrate Views-xxx Index Field Object Skill to be fully universal ( all actions and metadata runner and builder ) + all metadata update actions runner (#17687)
# What this PR does Overall naming `universal` versus `flat` is not always the most updated and so on Will make a big cleaning tour after I've finished the whole migration Migrating all `view` and ( filter fields etc ) `field` `object` `index` to the universal pattern on all `services`, `builder` and `runner` levels ## Universal and flat optimistic tooling `addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow` and its delete counterpart maintain the consistency of `UniversalFlatEntityMaps` when an entity is created or removed. Beyond inserting/removing the entity from its own maps, they walk through `ALL_UNIVERSAL_METADATA_RELATIONS` to update the **aggregator arrays** on related parent entities — the add appends the new entity's `universalIdentifier` to the parent's aggregator (e.g. a new viewField's identifier gets appended to its parent view's `viewFieldUniversalIdentifiers`), and the delete filters it out. This keeps the maps in sync so that diff computations and relation lookups remain accurate throughout the migration building process. ## ALL_UNIVERSAL_METADATA_RELATIONS `ALL_UNIVERSAL_METADATA_RELATIONS` is the universal counterpart of `ALL_METADATA_RELATIONS`. It maps each metadata entity to its many-to-one and one-to-many relations using universal foreign keys (`*UniversalIdentifier`) instead of database IDs (`*Id`). This allows migration actions to reference related entities in a workspace-agnostic way. Relations that are workspace-specific (e.g. `workspace`, `dataSource`, `userWorkspace`) are set to `null` and skipped during resolution. ## `workspaceMigrationCreateIdEnrichment` Reserved to API metadata ( will be able to validate at app installation lvl ) - Workspace migration `create` actions now carry an optional `id` (and `fieldIdByUniversalIdentifier` for object/field actions) so that caller-provided IDs flow through the entire build-validate-run pipeline. - New `enrichCreateWorkspaceMigrationActionsWithIds` utility resolves `universalIdentifier → id` mappings after the builder runs and injects them into the migration actions before the runner persists entities. - Runner action handlers use the provided IDs instead of generating new UUIDs, enabling deterministic entity creation for synchronization workflows. ## `resolveUniversalUpdateRelationIdentifiersToIds` `resolveUniversalUpdateRelationIdentifiersToIds` converts universal identifiers (workspace-agnostic, stable keys) in a migration update payload into concrete database UUIDs, so the update can be applied to a specific workspace. It iterates over the many-to-one relations defined in `ALL_UNIVERSAL_METADATA_RELATIONS` for the given entity type, replaces each `*UniversalIdentifier` property with its corresponding `*Id` by looking up the target entity in `allFlatEntityMaps`, and throws if a non-null identifier can't be resolved. Used by all `update` action handlers in `transpileUniversalActionToFlatAction`, avoiding duplicated resolution logic across handlers. ## What this PR does not - Migrating twenty-standard declaration to universal - Migrating all the inputs transpilers to universal - Migrating all metadata to be fully universal ( we still need to de-scope the type of all of them and refactor their validator very close ) --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
d35d5c0463 |
[BREAKING_CHANGE] Deprecate remaining entities standardId (#17639)
# Introduction Following https://github.com/twentyhq/twenty/pull/17632 and https://github.com/twentyhq/twenty/pull/17572 This PR deprecates the agent, skill, field metadata and role `standardId` in favor of the `universalIdentifier` usage ## Note - Removed previous standard ids declaration modules - Twenty-sdk now re-exports the `STANDARD_OBJECTS` universalIdentifier hashmap constant - deleted some sync-metadata deadcode too ( mainly types ) |
||
|
|
bd9688421f |
ObjectMetadata and FieldMetadata agnostic workspace migration runner (#17572)
# Introduction
Important note: This PR officially deprecates the `standardId`, about to
drop col and entity property after this has been merged
Important note2: Haven't updated the optimistic tool to also update the
universal identifier aggregators only the ids one, they should not be
consumed in the runner context -> need to improve typing or either the
optimistic tooling
In this PR we're introducing all the devxp allowing future metadata
incremental universal migration -> this has an impact on all existing
metadata actions handler ( explaining its size )
This PR also introduce workspace agnostic create update actions runner
for both field and object metadata in order to battle test the described
above devxp
Noting that these two metadata are the most complex to handle
Notes:
- A workspace migration is now highly bind to a
`applicationUniversalIdentifier`. Though we don't strictly validate
application scope for the moment
## Next
Migrate both object and field builder to universal comparison
## Universal Actions vs Flat Actions Architecture
### Concept
The migration system uses a two-phase action model:
1. **Universal Actions** - Actions defined using `universalIdentifier`
(stable, portable identifiers like `standardId` + `applicationId`)
2. **Flat Actions** - Actions defined using database `entityId` (UUIDs
specific to a workspace)
### Why This Separation?
- **Universal actions are portable**: They can be serialized, stored,
and replayed across different workspaces
- **Flat actions are executable**: They contain the actual database IDs
needed to perform operations
- **Decoupling**: The builder produces universal actions; the runner
transpiles them to flat actions at execution time
### Transpiler Pattern
Each action handler must implement
`transpileUniversalActionToFlatAction()`:
```typescript
@Injectable()
export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'fieldMetadata',
) {
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalCreateFieldAction>,
): Promise<FlatCreateFieldAction> {
// Resolve universal identifiers to database IDs
const flatObjectMetadata = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: allFlatEntityMaps.flatObjectMetadataMaps,
universalIdentifier: action.objectMetadataUniversalIdentifier,
});
return {
type: action.type,
metadataName: action.metadataName,
objectMetadataId: flatObjectMetadata.id, // Resolved ID
flatFieldMetadatas: /* ... transpiled entities ... */,
};
}
}
```
### Action Handler Base Class
`BaseWorkspaceMigrationRunnerActionHandlerService<TActionType,
TMetadataName>` provides:
- **`transpileUniversalActionToFlatAction()`** - Abstract method each
handler must implement
- **`transpileUniversalDeleteActionToFlatDeleteAction()`** - Shared
helper for delete actions
## FlatEntityMaps custom properties
Introduced a `TWithCustomMapsProperties` generic parameter to control
whether custom indexing structures are included:
- **`false` (default)**: Returns `FlatEntityMaps<MetadataFlatEntity<T>>`
- used in builder/runner contexts
- **`true`**: Returns the full maps type with custom properties (e.g.,
`byUserWorkspaceIdAndFolderId`) - used in cache contexts
## Create Field Actions Refactor
Refactored create-field actions to support relation field pairs
bundling.
**Problem:** Relation fields (e.g., `Attachment.targetTask` ↔
`Task.attachments`) couldn't resolve each other's IDs during
transpilation because they were in separate actions with independent
`fieldIdByUniversalIdentifier` maps.
**Solution:**
- Removed `objectMetadataUniversalIdentifier` from
`UniversalCreateFieldAction` and `objectMetadataId` from
`FlatCreateFieldAction` - each field now carries its own
- Runner groups fields by object internally and processes each table
separately
- Split aggregator into two focused utilities:
- `aggregateNonRelationFieldsIntoObjectActions` - merges non-relation
fields into object actions
- `aggregateRelationFieldPairs` - bundles relation pairs with shared
`fieldIdByUniversalIdentifier`
|
||
|
|
0001c2e7d0 |
[Apps] Apps marketplace (first draft) (#17562)
https://github.com/user-attachments/assets/c4e63edb-6e98-44bc-841f-ee110ae712d4 How it works - `manifest.json` files are now committed when apps are published to our repo - to display available apps, from the server, we read into our github repo, using a pod-scoped cache - feature flagged - app installation will be behind permission gate MARKETPLACE_APPS Limitations and what is yet to develop - content and settings tabs - installed apps tab - app installation - test and potentially fix reading from .manifest.json once [Reshape manifest structure](https://github.com/twentyhq/core-team-issues/issues/2183) is done. additional work is expected on assets notably. (couldnt properly do it here as manifest.json will only be committed after this pr) - we only read in community/ folder for now - we may want tochange that - the cache is rather artisanal for now and scoped by pod - we may want to change that |
||
|
|
fbbd8fe967 |
[REQUIRES_CACHE_FLUSH_FOR_FIELD_AND_OBJECT]FlatFieldMetadata and FlatObjectMetadata required universal (#17557)
# Introduction
In this PR we're migrating both the `field` and `object` metadata to be
using the new `FlatEntityFromV2` that requires all the
`UniversalFlatEntityExtraProperties` to be spread at the flat entity
root.
This means that we have to update all of their flat declaration
This type swap allows to isole a specific entity migration into his own
type scope and avoid to have everything handled at once
```ts
/**
* Currently under migration but aims to replace FlatEntity afterwards
*/
export type FlatEntityFromV2<
TEntity,
TMetadataName extends AllMetadataName | undefined = undefined,
TInnerFlatEntity extends { __universal?: unknown } = FlatEntityFrom<
TEntity,
TMetadataName
>,
> = Omit<TInnerFlatEntity, '__universal'> & TInnerFlatEntity['__universal'];
```
## Impact
Both object and field:
- Create input transpilation utils
- from entity to flat tools
- mocks
## Note
Removed from the universal extra properties the jsonb properties that do
not contain a serialized
## Next
Next step is to incrementally make the builder and runner expect
`UniversalFlatEntity` for both of these metadata
This way we will be able to fully migrate an entity e2e typesafely
|
||
|
|
fe9d6f34ff |
[REQUIRES_FULL_CACHE_FLUSH_WHEN_RELEASED] Refactor FlatEntity to be UniversalFlatEntity superset (#17452)
# Introduction
In this PR we're refactoring the `FlatEntity` type to become a superset
of the `UniversalFlatEntity`.
Right now we're storing all the extra properties in `__universal`
property, at some point it might just be sibling to other entity and we
might rely on the `propertiesToCompare` constants and TypeScript
allowing passing a superset type into a smaller subset type
## FromTo utils
The entity to flat entity method now computes the universal information,
standardized a typing and pattern to do
## Example
Also strictly type
```ts
"bbb019ea-6205-498c-aea5-67bc53bce8a9": {
"workspaceId": "20202020-1c25-4d02-bf25-6aeccf7ea419",
"universalIdentifier": "20202020-d111-4d11-8d11-da5ab0a11002",
"applicationId": "d01b010d-b984-465b-b40b-370e954e5188",
"id": "bbb019ea-6205-498c-aea5-67bc53bce8a9",
"pageLayoutTabId": "791a512f-169f-4209-b731-aa86716668c6",
"title": "Deals by Company",
"type": "GRAPH",
"objectMetadataId": "9e14efea-df5b-4c0e-aba9-cfe455f32397",
"gridPosition": { "row": 0, "column": 6, "rowSpan": 6, "columnSpan": 6 },
"configuration": {
"color": "orange",
"orderBy": "FIELD_ASC",
"timezone": "UTC",
"displayLegend": true,
"displayDataLabel": false,
"showCenterMetric": true,
"configurationType": "PIE_CHART",
"firstDayOfTheWeek": 0,
"aggregateOperation": "COUNT",
"groupBySubFieldName": "name",
"groupByFieldMetadataId": "6673ff18-63d2-47a1-8f85-2b9b09ca27a5",
"aggregateFieldMetadataId": "8d64ee41-5dd4-4de6-945a-7c0c18399715"
},
"createdAt": "2026-01-28T14:08:52.140Z",
"updatedAt": "2026-01-28T14:08:52.140Z",
"deletedAt": null,
"__universal": {
"universalIdentifier": "20202020-d111-4d11-8d11-da5ab0a11002",
"applicationUniversalIdentifier": "20202020-64aa-4b6f-b003-9c74b97cee20",
"pageLayoutTabUniversalIdentifier": "20202020-d011-4d11-8d11-da5ab0a01001",
"objectMetadataUniversalIdentifier": "20202020-9549-49dd-b2b2-883999db8938",
"gridPosition": {
"row": 0,
"column": 6,
"rowSpan": 6,
"columnSpan": 6
},
"configuration": {
"color": "orange",
"orderBy": "FIELD_ASC",
"timezone": "UTC",
"displayLegend": true,
"displayDataLabel": false,
"showCenterMetric": true,
"configurationType": "PIE_CHART",
"firstDayOfTheWeek": 0,
"aggregateOperation": "COUNT",
"groupBySubFieldName": "name",
"aggregateFieldMetadataUniversalIdentifier": "20202020-d01a-4131-8a31-f123456789ab",
"groupByFieldMetadataUniversalIdentifier": "20202020-cbac-457e-b565-adece5fc815f"
}
}
},
```
|
||
|
|
44202668fd |
[TYPES] UniversalEntity JsonbProperty and SerializedRelation (#17396)
# Introduction
In this PR we're introducing mainly two branded type signatures for both
`JsonbProperty` entities properties and `SerializedRelation` (jsonb
serialized property storing another entity id).
Allowing to dynamically map over them later in order to build universal
`jsonb` `serialized` relations.
## `JsonbProperty`
A branded wrapper type that marks entity properties stored as PostgreSQL
JSONB columns. It adds a phantom brand `__JsonbPropertyBrand__` to
object types while leaving primitives unchanged. The branded key is
optional and typed as never, also omitted when transpiled to
`UniversalFlat`
**Should be used at entities lvl only:**
```typescript
@Column({ type: 'jsonb', nullable: false })
gridPosition: JsonbProperty<GridPosition>;
@Column({ nullable: false, type: 'jsonb', default: [] })
publishedVersions: JsonbProperty<string[]>;
```
## `SerializedRelation`
A branded string type that marks foreign key IDs stored inside JSONB
objects. These are entity references serialized within a JSONB column
rather than being a regular database foreign key.
**Usage in jsonb property generic***
```ts
type FieldMetadataRelationSettings = {
relationType: RelationType;
onDelete?: RelationOnDeleteAction;
joinColumnName?: string | null;
junctionTargetFieldId?: SerializedRelation;
};
```
## `FormatJsonbSerializedRelation<T>`
A transformation type that processes JSONB properties for universal
entity mapping. It:
1. Detects properties with the `JsonbProperty` brand
2. Finds `SerializedRelation` properties
3. Renames them from `*Id` to `*UniversalIdentifier`
4. Removes the brand from the output type ( optional though )
```typescript
// Input: JsonbProperty<{ targetFieldMetadataId: SerializedRelation }>
// Output: { targetFieldMetadataUniversalIdentifier: SerializedRelation }
```
## Result
An example of the dynamic type mapping, through a type-test example
```ts
type SettingsTestCase = UniversalFlatFieldMetadata<
| FieldMetadataType.RELATION
| FieldMetadataType.NUMBER
| FieldMetadataType.TEXT
>['settings']
type SettingsExpectedResult =
| {
relationType: RelationType;
onDelete?: RelationOnDeleteAction | undefined;
joinColumnName?: string | null | undefined;
junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined;
}
| {
dataType?: NumberDataType | undefined;
decimals?: number | undefined;
type?: FieldNumberVariant | undefined;
}
| {
displayedMaxRows?: number | undefined;
}
| null;
type Assertions = [
Expect<Equal<SettingsTestCase, SettingsExpectedResult>>,
]
```
## Remarks
- Removed duplicated twenty-server and twenty-shared typed
- Removed class validator instances for default value that were not used
at runtime, we will refactor that to add validation across all entities
following a same pattern
|
||
|
|
0091ef5f6c |
Sync built files (#17379)
as title, upload built files to local storage --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
4c93ab5259 |
Introduce UniversalFlatEntityFrom (#17367)
# Introduction
Creating a `UniversalFlatEntityFrom` that strips out all the relation
and foreignKey properties in order to replace them with
`UniversalIdentifier` suffix
This data type will be major for the workspace migration workspace
agnostic refactor
## Chore
- renamed `flat-entity.type` to `flat-entity-from.type.ts` ( more
accurate to exported module )
- create static test type over the field metadata entity on quite
complex utils as both coverage and documentation
## Example
Here's an example of a `UniversalFlatEntityFrom<FieldMetadataEntity>`
```ts
const universalFlatFieldMetadata: UniversalFlatFieldMetadata<FieldMetadataType.RELATION> = {
// Base properties (from FieldMetadataEntity, excluding relations and applicationId)
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
applicationUniversalIdentifier: '5800681c-088e-4e2b-9fc3-bcf6e8ec2051',
type: FieldMetadataType.RELATION,
name: 'firstName',
label: 'First Name',
defaultValue: null,
description: 'The first name of the person',
icon: 'IconUser',
standardOverrides: null,
options: null,
settings: {
relationType: RelationType.ONE_TO_MANY,
},
isCustom: false,
isActive: true,
isSystem: false,
isUIReadOnly: false,
isNullable: true,
isUnique: false,
isLabelSyncedWithName: true,
morphId: null,
// Date properties cast to string
createdAt: '2024-01-15T10:30:00.000Z',
updatedAt: '2024-01-15T10:30:00.000Z',
// ManyToOne relation universal identifiers (from FieldMetadataEntity relations)
relationTargetFieldMetadataUniversalIdentifier:
'550e8400-e29b-41d4-a716-446655440012',
relationTargetObjectMetadataUniversalIdentifier:
'550e8400-e29b-41d4-a716-446655440013',
// Join column universal identifiers (foreignKey -> universalIdentifier)
objectMetadataUniversalIdentifier: '550e8400-e29b-41d4-a716-446655440010',
// OneToMany relation universal identifiers (array of related entity identifiers)
viewFieldUniversalIdentifiers: [
'550e8400-e29b-41d4-a716-446655440020',
'550e8400-e29b-41d4-a716-446655440021',
],
viewFilterUniversalIdentifiers: ['550e8400-e29b-41d4-a716-446655440030'],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
};
```
## Settings
Will hop on the settings typing next. Might not be dynamic but
declarative though
|
||
|
|
6aa43b68f7 |
Identification cleanup (#17301)
# Introduction following https://github.com/twentyhq/twenty/pull/17279 As we've finally identified all the syncable metadata entities, which means they're expected to have non nullable applicationId and universalIdentifier at pg_level we can remove previous retro comp universalIdentifier fallbacking and update the dto too ~~This needs IdentifyRemainingEntitiesMetadataCommand and MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand to be run~~ ```ts [Nest] 197 - 01/21/2026, 3:08:35 PM LOG [MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand] Successfully run MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand ``` |
||
|
|
596b7cc62d |
Deprecate nullable syncableEntity (#17279)
# Introduction As we've been identifying both standard and custom entities for all the metadata that had standard We now still need to identify all custom entities enforcing them to have an `applicationId` and `universalIdentifier` In this PR we've removed the `SyncableEntityRequired` in favor requiring props directly in the `SyncableEntity` Which means that all metadata in db will now expect non nullable applicationId and universalIdentifier across the whole application Will add some type cleanup later in https://github.com/twentyhq/twenty/pull/17277 |
||
|
|
3ed67b825e |
feat: implement generic many-to-many junction relation support (#16820)
## Overview
This PR implements **generic many-to-many relation support** through
junction tables (also known as associative entities or join tables).
This replaces the need for hardcoded taskTarget/noteTarget logic and
provides a flexible foundation for modeling complex entity
relationships.
## Architecture
### Data Model
Many-to-many relationships are implemented using a **junction object
pattern**:
```
┌─────────┐ ┌──────────────────┐ ┌─────────┐
│ Pet │──────>│ PetRocket │<──────│ Rocket │
│ │ 1:N │ (junction) │ N:1 │ │
│ rockets ├───────┤ pet : Pet ├───────┤ │
└─────────┘ │ rocket : Rocket │ └─────────┘
└──────────────────┘
```
The junction object (PetRocket) has:
- A `MANY_TO_ONE` relation to **Pet** (the source)
- A `MANY_TO_ONE` relation to **Rocket** (the target)
The source object (Pet) has a `ONE_TO_MANY` relation pointing to the
junction, with **field settings** that specify which target field to
follow.
### Field Settings Schema
Junction configuration is stored in `FieldMetadataRelationSettings`:
```typescript
{
relationType: "ONE_TO_MANY",
// Points to the target field on the junction object
junctionTargetFieldId?: string; // For regular relations
junctionTargetMorphId?: string; // For polymorphic relations
}
```
**Two configuration modes:**
1. **`junctionTargetFieldId`** - References a specific `RELATION` field
on the junction
2. **`junctionTargetMorphId`** - References a `morphId` group for
polymorphic targets (e.g., link to Person OR Company)
### GraphQL Query Generation
When a junction relation is detected, the GraphQL fields are generated
to fetch the nested target:
```graphql
query GetPetWithRockets {
pet(id: "...") {
rockets { # ONE_TO_MANY to junction
id
rocket { # Target field on junction
id
name
__typename
}
}
}
}
```
For polymorphic junction targets:
```graphql
caretakerPerson { id, name }
caretakerCompany { id, name }
```
## Frontend Architecture
### Display Flow
1. **Detection**: `hasJunctionConfig()` checks if field has junction
settings
2. **Config Resolution**: `getJunctionConfig()` resolves junction object
metadata and target fields
3. **Record Extraction**: `extractTargetRecordsFromJunction()` extracts
target records from junction records
4. **Rendering**: Target records displayed as chips (not junction
records)
### Edit Flow
1. **Picker Opening**: Initializes the multi-record picker with:
- Searchable object types (derived from junction target fields)
- Pre-selected items (extracted from existing junction records)
2. **Selection Handling**: Manages create/delete of junction records:
- **Select**: Creates new junction record with source + target IDs
- **Deselect**: Finds and deletes the junction record
- **Optimistic Updates**: Manually updates Recoil store before API call
### Key Trade-offs
| Decision | Trade-off |
|----------|-----------|
| Junction records managed manually | More control over optimistic
updates, but requires manual cache management |
| Settings stored per-field | Flexible (same junction can power
different views), but requires UI to configure |
| Polymorphic via morphId groups | Supports N target types, but adds
query complexity |
| Feature flag gated | Safe rollout, but requires flag management |
## Backend Changes
- **Validation**: Junction target field must exist and be a valid
`MANY_TO_ONE` relation
- **Settings**: Extended `FieldMetadataRelationSettings` type with
junction fields
- **Dev Seeder**: Added sample junction objects (PetRocket,
EmploymentHistory, PetCareAgreement) for testing
## How to Test
1. Enable the `IS_JUNCTION_RELATIONS_ENABLED` feature flag
2. Create objects with junction pattern (Pet → PetRocket → Rocket)
3. Configure the junction target in field settings (advanced mode)
4. Verify:
- Display shows target objects (Rockets), not junction records
(PetRockets)
- Picker allows selecting/deselecting targets
- Changes persist correctly
https://github.com/user-attachments/assets/d04f057a-228c-4de8-af48-76bb2d72cac1
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
a6f371a42a |
Identify standard field do deploy until IS_WORKSPACE_CREATION_V2_ENABLED is enabled in prod (#16981)
# Introduction fixes https://github.com/twentyhq/twenty/issues/16905 Do not merge until `IS_WORKSPACE_CREATION_V2_ENABLED` has been activated by default, and so sync metadata has been deprecated by doing so. As the sync metadata will attempt to insert `null` `applicationId` and `universalIdentifier` values while creating a workspace In this PR we're introducing a new `SyncableEntityRequired` which enforces the non nullable `applicationId` and `universalIdentifier` on extending entity In this PR we also migrate the field metadata entity to extend the required ## Identification upgrade command This command will search for workspace field metadata entities that aren't associated to an applicationId, dispatch them to either the workspace-custom `applicationId` or the twenty-standard `applicationId`. For the standard entities it will also set their universal identifier based on the `STANDARD_OBJECTS` const hashmap ## Typeorm migration As the non nullable `applicationId` and `universalIdentifier`migration won't pass in the first we've been using the save point and upgrade command migration fallback pattern ## Tests Tested the command on a prod extract locally Both `twenty-eng` and `twenty-for-twenty` have unexpected standard objects Please note that we will deprecate the `isCustom` and `standardId` col later in the future ### Twenty-eng ```ts [Nest] 98971 - 01/01/2026, 3:18:00 PM LOG [IdentifyStandardEntitiesCommand] Successfully validated 600/600 field metadata update(s) for workspace 9870323e-22c3-4d14-9b7f-5bdc84f7d6ee (309 custom, 291 standard) [Nest] 98971 - 01/01/2026, 3:18:00 PM WARN [IdentifyStandardEntitiesCommand] Found 35 warning(s) while processing field metadata for workspace 9870323e-22c3-4d14-9b7f-5bdc84f7d6ee. These fields will become custom. ``` ### Twenty for twenty ### Just created workspace |
||
|
|
942d2fef83 |
Remove sync-metadata and IS_WORKSPACE_CREATION_V2_ENABLED feature flag (#16997)
# Introduction Followup of https://github.com/twentyhq/twenty/pull/17001#pullrequestreview-3638508738 close https://github.com/twentyhq/core-team-issues/issues/1910 We've completely decom the `sync-metadata` in production. We're now then removing its implementation in favor of the v2. ## TODO: - [x] Remove sync-metadata implem and commands - [x] Remove workspace decorators - [x] Type each deprecated field to deprecated on their workspaceEntity - [x] Remove the `workspace-sync-metadata` folder entirely - [x] remove workspace migration - [x] workspace migration removal migration - [x] remove the `v2` references from workspace manager file names - [x] remove the `v2` references from workspace manager modules - [ ] Double check impact on translation file path updates ## Note - Removed the gate logic - Remains some service v2 naming, serverless needs to be migrated on v2 fully - Removed workspaceMigration service app health consumption, making it always returning up ( no more down ) cc @FelixMalfait ( quite obsolete health check now, will require complete refactor once we introduce inter app dependency etc ) |
||
|
|
0173e40a20 |
feat: Serverless Functions as AI Tools (#16919)
## Summary This PR enables serverless functions to be exposed as AI tools, allowing them to be used by AI agents. ### Changes - Added new `SERVERLESS_FUNCTION` tool category - Added `toolDescription`, `toolInputSchema`, and `toolOutputSchema` fields to serverless functions - Created database migration for the new schema columns - Added tool index query and resolver for fetching available tools - Added Settings AI page tabs (Skills, Tools, Settings) with new tools table - Added utility to convert tool schema to JSON schema format - Updated frontend to display tools in the settings page ### Implementation Details - Serverless functions can now define tool metadata (description, input/output schemas) - These functions are automatically registered in the tool registry - The tool index endpoint allows querying available tools with their schemas - Settings page now has a dedicated Tools tab showing all available tools |
||
|
|
a6415db775 |
Refactor workspace migration and validation error types and centralize runner optimistic rendering (#16920)
# Introduction In this PR we're: - Refactoring the workspace migration action type introducing grain over metadata and operation type ( for example operation `create` and metadata `field` ) - Thanks to above point we can now factorize the runner optimistic rendering out of each runner actions-handler file using the existing into the generic one ( -3200 lines of code here ) - Still thanks to action type refactor we're able to dynamically compose the response error type only send data when there's here. No more static counter and static summary error message. This way we won't have to re run snapshot every time we add a new entity to the engine ( huge snapshot diff here ) ## Noticeable points: - We introduce an index update action to avoid any complex typing for not having one or a tuple of actions instead. Now the drop and insert logic is directly inferred from the update action handler instead of being two action ( delete index and create index ) ## TODO - [x] Define base actions types - [x] Migrate all actions to action type and metadata name pattern ( base actions ) - [x] Refactor flat entity validation type to embed metadata name - [x] Refactor optimistic rendering within runner - [x] Refactor legacy cache invalidation switch - [x] Refactor response error format ( dynamic counter again + no empty entries ) - [x] Try factorizing and removing redundant nor unused type declaration in metadata actions type intermediary files - [x] Adapt front to new response error format ## Remarks - ~~Should create an issue for generic replace flat entity in related flat entity maps~~ overkill - Should create an issue for oneToMany foreignKey being nullable not always cascade delete optimistic rendering edge case to either docs or fix it in delete flat entity and related entity ( re-code the pg cascading behavior ) - We could also factorize the builder to only implement validators and not the intermediary file |
||
|
|
42c9ae1ebc |
Centralize metadata relations constant + simplification (#16901)
# Introduction As we introduced a new grain on relation extraction thanks to low level `SyncableEntity` and `WorkspaceRelatedEntity` we're able to strictly typesafe extract metadata entity The new constant centralizes both many to one and one to many constants metadata entity constants in a more strictly typesafe way. Remains only the flatEntityForeignKey aggregator which has to be chosen manually across all available targeted flat entity ids properties |
||
|
|
f8fa709abf |
refactor: Migrate CRUD services to use Common API (#16869)
This PR migrates the workflow CRUD services to use the Common API (CommonQueryRunners) instead of directly accessing TwentyORM. ## Changes - Created CommonApiContextBuilderService to build context for Common API - Migrated CreateRecordService to use CommonCreateOneQueryRunnerService - Migrated UpdateRecordService to use CommonUpdateOneQueryRunnerService - Migrated DeleteRecordService to use CommonDeleteOneQueryRunnerService - Migrated FindRecordsService to use CommonFindManyQueryRunnerService - Migrated UpsertRecordService to use Common API with upsert flag - Removed unused get-selected-columns-from-restricted-fields.util.ts - Updated module dependencies ## Benefits - Consistent permission checking via Common API - Query hooks (before/after execution) - Automatic input transformation - Same behavior as REST/GraphQL APIs - Reduced code duplication |
||
|
|
1128331cc1 |
Fix field item type tag (#16860)
# Introduction This has been fixed on main already 1 hour ago, this PR now only passes the field application id instead of the object applicationId that could be different for example when creating a custom field on a standard object For the moment not introducing any logic around application integrity directly and still relying on the isCustom and standardId definition This will have to be refactored once we deprecate the standardId |
||
|
|
e3ffdb0c2b |
[BREAKING_CHANGE_NESTED_WORKSPACE]Refactor FlatEntity typing in aim of introducing UniversalFlatEntity (#16701)
# Introduction
Added a `WorkspaceRelated` and `AllNonWorkspaceRelatedEntity` to
simplify the `FlatEntityFrom` that now do not expect a string literal to
omit and itself builds the related many to one entities foreign key
aggregators
We now have the type grain over relation to syncable or just workspace
related entities
Added a migrations that sets the fk on missing entities
## Next
In upcoming PR we will be able to introduce such below type
```ts
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
import { type ExtractEntityManyToOneEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type';
import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type';
import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type';
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/remove-suffix.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface';
export type UniversalFlatEntityFrom<TEntity extends SyncableEntity> = Omit<
TEntity,
| `${ExtractEntityManyToOneEntityRelationProperties<TEntity> & string}Id`
| ExtractEntityRelatedEntityProperties<TEntity>
| 'application'
| 'workspaceId'
| 'applicationId'
| keyof CastRecordTypeOrmDatePropertiesToString<TEntity>
> &
CastRecordTypeOrmDatePropertiesToString<TEntity> & {
[P in ExtractEntityManyToOneEntityRelationProperties<TEntity> &
string as `${RemoveSuffix<P, 's'>}UniversalIdentifier`]: string;
} & {
[P in ExtractEntityOneToManyEntityRelationProperties<
TEntity,
SyncableEntity
> &
string as `${RemoveSuffix<P, 's'>}UniversalIdentifiers`]: string[];
};
```
|
||
|
|
720348c583 |
Add upgrade commands to backfill updatedBy field and view fields (#16845)
--- prastoin edit ## Introduction As we're deprecating the sync metadata we cannot rely on it anymore in order to create the new standard updatedBy field In this PR we introduce a command that will backfill the dynamic field on both workspace standard and custom objects, for standard object it will create a standard fields ( twenty-standard app scoped ) and for custom one it will create a custom field ( worksapce-custom-app scoped ) ## Testing method Checkout `1.14.0` `yarn` and `npx nx database:reset twenty-server` checkout PR and `yarn` `npx nx build twenty-server` and `yarn database:migrate:prod` finally running the command #### Before <img width="2244" height="1464" alt="image" src="https://github.com/user-attachments/assets/2fb866cd-13b8-4152-99b8-1fcc813b1d46" /> #### After <img width="2244" height="1464" alt="image" src="https://github.com/user-attachments/assets/b836ac06-c7b8-4add-bee1-bc1963418f29" /> #### Logs ```ts [Nest] 20678 - 12/30/2025, 1:47:35 PM LOG [BackfillUpdatedByFieldCommand] Found 12 objects that need updatedBy field [EntityBuilder fieldMetadata] matrix computation: 3.994ms [EntityBuilder fieldMetadata] creation validation: 1.822ms [EntityBuilder fieldMetadata] deletion validation: 0.016ms [EntityBuilder fieldMetadata] update validation: 0.009ms [EntityBuilder fieldMetadata] entity processing: 6.098ms [EntityBuilder fieldMetadata] validateAndBuild: 10.217ms [EntityBuilder index] matrix computation: 0.857ms [EntityBuilder index] creation validation: 0.003ms [EntityBuilder index] deletion validation: 0.017ms [EntityBuilder index] update validation: 0.008ms [EntityBuilder index] entity processing: 4.721ms [EntityBuilder index] validateAndBuild: 5.632ms [Runner] Initial cache retrieval: 0.07ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 7.69ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 10.531ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 5.354ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 6.134ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.507ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 3.745ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.113ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.585ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.223ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.221ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 5.375ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 5.943ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.363ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.997ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.03ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.982ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 0.88ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.977ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 9.638ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 11.297ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.581ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.248ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 0.847ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.732ms [Runner] Transaction execution: 82.555ms [Runner] Cache invalidation flatFieldMetadataMaps,flatIndexMaps,flatObjectMetadataMaps: 89.247ms [Runner] Total execution: 171.963ms [Nest] 20678 - 12/30/2025, 1:47:35 PM LOG [BackfillUpdatedByFieldCommand] Successfully backfilled updatedBy field for 12 objects in workspace 20202020-1c25-4d02-bf25-6aeccf7ea419 [Nest] 20678 - 12/30/2025, 1:47:35 PM LOG [BackfillUpdatedByFieldCommand] Running command on workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db 2/2 [Nest] 20678 - 12/30/2025, 1:47:35 PM LOG [BackfillUpdatedByFieldCommand] Starting backfill of updatedBy field for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db [Nest] 20678 - 12/30/2025, 1:47:35 PM LOG [BackfillUpdatedByFieldCommand] Found 10 objects that need updatedBy field [EntityBuilder fieldMetadata] matrix computation: 3.224ms [EntityBuilder fieldMetadata] creation validation: 0.969ms [EntityBuilder fieldMetadata] deletion validation: 0.014ms [EntityBuilder fieldMetadata] update validation: 0.002ms [EntityBuilder fieldMetadata] entity processing: 5.149ms [EntityBuilder fieldMetadata] validateAndBuild: 8.472ms [EntityBuilder index] matrix computation: 0.785ms [EntityBuilder index] creation validation: 0.002ms [EntityBuilder index] deletion validation: 0.014ms [EntityBuilder index] update validation: 0.002ms [EntityBuilder index] entity processing: 3.818ms [EntityBuilder index] validateAndBuild: 4.637ms [Runner] Initial cache retrieval: 0.05ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.964ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 3.902ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.502ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.039ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.673ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 3.403ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.143ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.123ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.033ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.954ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.826ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.903ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.495ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.106ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 14.014ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 14.373ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.669ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.48ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 0.62ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.048ms [Runner] Transaction execution: 73.222ms [Runner] Cache invalidation flatFieldMetadataMaps,flatIndexMaps,flatObjectMetadataMaps: 37.703ms [Runner] Total execution: 111.071ms ``` --- --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
19c9f957b1 |
Improve userFriendlyMessage devX (#16815)
Two challenges with error messages - always provide a useful/meaningful error message for the end user instead of the generic one. eg: show "Wrong password" and not "An error occured" - avoid technical details unless error regards a technical feature. eg: show "An error occured" and not "Invalid post-hook payload."; but do show "Invalid issuer URL." as it occurs while configuring SSO What this PR does - Make userFriendlyMessage mandatory for widely used GraphqlQueryRunnerException and CommonQueryRunnerException, so that developers are forced to ask themselves what the error message should be, and as it contains very wide error codes (eg: "Bad request") which should not be mapped to just one default message - Keep userFriendlyMessage optional for service-specific exceptions (eg: workflowStepExecutorException), but convert the error code to userFriendlyMessage mapper to a switch case function with a typecheck ensuring that all codes are mapped to a message. These default messages are still overridable where they are thrown. |
||
|
|
04c596817a |
feat(server): enforce userFriendlyMessage on all exceptions (#16589)
## Summary
This PR enforces that all custom exceptions must provide a
`userFriendlyMessage`, ensuring end users always see readable error
messages.
## Changes
### Core Changes
- **`CustomException` simplified**: Removed the `ForceFriendlyMessage`
generic parameter - `userFriendlyMessage` is now always required
- **Type safety**: The constructor now requires `{ userFriendlyMessage:
MessageDescriptor }` (no longer optional)
### Updated Files
- **74+ exception classes** updated to provide default user-friendly
messages using Lingui `msg` macro
- Each exception class has a sensible fallback message (e.g., `msg\`An
authentication error occurred.\``)
- Exception classes that had code-specific message maps retain their
behavior
## Benefits
- **Compile-time enforcement**: Forgetting to add a user-friendly
message now causes a TypeScript error
- **Better UX**: End users always see a localized, human-readable error
message
- **Simpler API**: No more boolean generic parameter to think about
## Testing
- `npx nx run twenty-server:typecheck` passes
- `npx nx run twenty-server:lint` passes
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Enforces `userFriendlyMessage` on `CustomException` and updates all
exception classes to supply localized default messages, with
filters/tests adjusted accordingly.
>
> - **Core**:
> - Enforce required `userFriendlyMessage` in `CustomException` (remove
optional generic; constructor now requires `{ userFriendlyMessage:
MessageDescriptor }`).
> - **Exceptions**:
> - Update ~70+ exception classes to set default localized messages via
Lingui `msg` maps and pass them in constructors (e.g., `AuthException`,
`ObjectMetadataException`, `FieldMetadataException`, etc.).
> - Add fallback messages where needed (e.g., `INTERNAL_SERVER_ERROR` or
domain-specific defaults).
> - **HTTP/GraphQL Filters**:
> - Ensure fallbacks create `UnknownException` with `msg` for
user-friendly text in REST/GraphQL exception filters.
> - **Tests**:
> - Adjust unit tests to pass `userFriendlyMessage` to exceptions.
> - Update Jest snapshots to include `extensions.userFriendlyMessage` or
message objects where applicable.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
221004fdfc0d97b7d152a258b347bf571e70f10e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
|
||
|
|
a0b963ef86 |
Remove viewGroup.fieldMetadataId (#16571)
Final step of https://github.com/orgs/twentyhq/projects/1/views/8?pane=issue&itemId=142348748&issue=twentyhq%7Ccore-team-issues%7C1965 Removing viewGroup.fieldMetadataId. It's already not used in FE anymore |
||
|
|
70a78aafe9 |
feat(ai): replace agent search with skills system (#16513)
## Summary - Replace the agent search mechanism with a new skills-based system - Add a `skills` module with predefined skill definitions that the AI can load on demand - Remove specialized agents (workflow-builder, data-manipulator, dashboard-builder, metadata-builder, researcher), keeping only the helper agent - Add `recordReferences` to workflow creation tool for chip linking in the UI ## Changes ### New Skills Module - `skill-definitions.ts` - Contains 5 skill definitions with detailed instructions - `skills.service.ts` - Service to get skills by name - `load-skill.tool.ts` - Tool for AI to load skills explicitly ### Removed - `agent-search.tool.ts` - Replaced by skill loading - Specialized agent definitions (converted to skills) ### Updated - Chat execution now shows skill catalog in system prompt - Workflow creation returns `recordReferences` for UI linking ## Test plan - [ ] Verify AI can load skills using `load_skill` tool - [ ] Verify skill content is returned correctly - [ ] Verify workflow creation shows clickable chip in chat - [ ] Verify helper agent still works |
||
|
|
3cea19baf4 |
feat(ai): add view management tools for AI chat (#16495)
## Summary Adds a new **VIEW** tool category for the AI chat, enabling it to work with views: - **get-views**: List views in the workspace, optionally filtered by object metadata ID - **get-view-query-parameters**: Convert a view's filters and sorts into GraphQL query parameters that can be passed to existing `find_*` data tools - **create-view**, **update-view**, **delete-view**: CRUD operations for view management ### Key design decisions 1. **No pagination duplication**: Instead of creating a `find-records-from-view` tool that would duplicate pagination logic, `get-view-query-parameters` returns filter/sort parameters that the AI can pass to existing record-fetching tools. 2. **Permission model**: - Read tools (get-views, get-view-query-parameters) are available to all users - Write tools require the `VIEW` permission - UNLISTED views can only be modified by their creator 3. **Leverages existing utilities**: Uses `computeRecordGqlOperationFilter` from `twenty-shared` for filter conversion. ### Files changed - Added `ViewToolProvider`, `ViewToolsFactory`, and `ViewQueryParamsService` - Added `VIEW` to `ToolCategory` enum and tool registry - Updated `chat-execution.service.ts` to include view tools in the catalog and pass viewId in browsing context - Extracted shared `formatValidationErrors` utility to reduce duplication ## Test plan - [x] Unit tests for `ViewToolsFactory` - [x] Unit tests for `ViewQueryParamsService` - [x] Lint and typecheck pass |
||
|
|
4996f3dd28 |
Finalize twenty standard app as workspace migration object and fields (#16353)
# Introduction Related to https://github.com/twentyhq/core-team-issues/issues/1995 In this PR we're fixing the remaining object/fields validation errors resulting from standard objects and fields now passing a validation that wasn't when using the sync metadata ## Key Changes - **Field naming**: Renamed `iCalUID` to `iCalUid` for consistent camelCase convention across calendar events - **Enum standardization**: Uppercased enum values for message channels (email→EMAIL), message participants (from→FROM, to→TO, cc→CC, bcc→BCC), and message direction (incoming→INCOMING, outgoing→OUTGOING) - **Label simplification**: Removed example values from workspace member number format labels for cleaner UI - **Migration infrastructure**: Added `isSystemBuild` flag throughout field metadata service pipeline to allow system-level updates of standard fields that bypass normal restrictions ## Migrating the existing data We've created an upgrade command that will identify using the existing object and field standard id field that needs to be updated, even though the sync metadata still in usage could have fix them ( and the goal is to deprecate it by the end of the sprint ) We will call the updateOneField for each of them, we're passing by the field service in order to battle test what are going to be the temporary way to handle standard migrations when we will start deprecating the sync metadata but haven't still refactored the v2 workspace migration to be workspace agnostic ## Twenty eng migration Tested the whole migration + upgrade on twenty eng Here are generated workspace migration Records are handled natively gracefully too ### ICalUid ```json { "status": "success", "workspaceMigration": { "relatedFlatEntityMapsKeys": [ "flatFieldMetadataMaps", "flatIndexMaps", "flatViewFilterMaps", "flatViewGroupMaps", "flatViewMaps", "flatViewFieldMaps", "flatObjectMetadataMaps" ], "actions": [ { "type": "update_field", "fieldMetadataId": "", "objectMetadataId": "", "updates": [ { "from": "iCalUID", "to": "iCalUid", "property": "name" } ] } ], "workspaceId": "" } } ``` ### Incoming Outgoing None as already caps in database somehow ```json { "status": "success", "workspaceMigration": { "relatedFlatEntityMapsKeys": [ "flatFieldMetadataMaps", "flatIndexMaps", "flatViewFilterMaps", "flatViewGroupMaps", "flatViewMaps", "flatViewFieldMaps", "flatObjectMetadataMaps" ], "actions": [], "workspaceId": "" } } ``` ### EMAIL ```json { "status": "success", "workspaceMigration": { "relatedFlatEntityMapsKeys": [ "flatFieldMetadataMaps", "flatIndexMaps", "flatViewFilterMaps", "flatViewGroupMaps", "flatViewMaps", "flatViewFieldMaps", "flatObjectMetadataMaps" ], "actions": [ { "type": "update_field", "fieldMetadataId": "", "objectMetadataId": "", "updates": [ { "from": "'email'", "to": "'EMAIL'", "property": "defaultValue" }, { "from": [ { "color": "green", "id": "", "label": "Email", "position": 0, "value": "email" }, { "color": "blue", "id": "", "label": "SMS", "position": 1, "value": "sms" } ], "to": [ { "color": "green", "id": "", "label": "Email", "position": 0, "value": "EMAIL" }, { "color": "blue", "id": "", "label": "SMS", "position": 1, "value": "SMS" } ], "property": "options" } ] } ], "workspaceId": "e" } } ``` ### MessageParticipantRole ```json { "status": "success", "workspaceMigration": { "relatedFlatEntityMapsKeys": [ "flatFieldMetadataMaps", "flatIndexMaps", "flatViewFilterMaps", "flatViewGroupMaps", "flatViewMaps", "flatViewFieldMaps", "flatObjectMetadataMaps" ], "actions": [ { "type": "update_field", "fieldMetadataId": "", "objectMetadataId": "", "updates": [ { "from": "'from'", "to": "'FROM'", "property": "defaultValue" }, { "from": [ { "color": "green", "id": "", "label": "From", "position": 0, "value": "from" }, { "color": "blue", "id": "", "label": "To", "position": 1, "value": "to" }, { "color": "orange", "id": "", "label": "Cc", "position": 2, "value": "cc" }, { "color": "red", "id": "", "label": "Bcc", "position": 3, "value": "bcc" } ], "to": [ { "color": "green", "id": "", "label": "From", "position": 0, "value": "FROM" }, { "color": "blue", "id": "", "label": "To", "position": 1, "value": "TO" }, { "color": "orange", "id": "", "label": "Cc", "position": 2, "value": "CC" }, { "color": "red", "id": "", "label": "Bcc", "position": 3, "value": "BCC" } ], "property": "options" } ] } ], "workspaceId": "" } } ``` ### Workspace member number format labels ```json { "status": "success", "workspaceMigration": { "relatedFlatEntityMapsKeys": [ "flatFieldMetadataMaps", "flatIndexMaps", "flatViewFilterMaps", "flatViewGroupMaps", "flatViewMaps", "flatViewFieldMaps", "flatObjectMetadataMaps" ], "actions": [ { "type": "update_field", "fieldMetadataId": "", "objectMetadataId": "", "updates": [ { "from": [ { "color": "turquoise", "id": "", "label": "System", "position": 0, "value": "SYSTEM" }, { "color": "blue", "id": "", "label": "Commas and dot (1,234.56)", "position": 1, "value": "COMMAS_AND_DOT" }, { "color": "green", "id": "", "label": "Spaces and comma (1 234,56)", "position": 2, "value": "SPACES_AND_COMMA" }, { "color": "orange", "id": "", "label": "Dots and comma (1.234,56)", "position": 3, "value": "DOTS_AND_COMMA" }, { "color": "purple", "id": "", "label": "Apostrophe and dot (1'234.56)", "position": 4, "value": "APOSTROPHE_AND_DOT" } ], "to": [ { "color": "turquoise", "id": "", "label": "System", "position": 0, "value": "SYSTEM" }, { "color": "blue", "id": "", "label": "Commas and dot", "position": 1, "value": "COMMAS_AND_DOT" }, { "color": "green", "id": "", "label": "Spaces and comma", "position": 2, "value": "SPACES_AND_COMMA" }, { "color": "orange", "id": "", "label": "Dots and comma", "position": 3, "value": "DOTS_AND_COMMA" }, { "color": "purple", "id": "", "label": "Apostrophe and dot", "position": 4, "value": "APOSTROPHE_AND_DOT" } ], "property": "options" } ] } ], "workspaceId": "" } } ``` |
||
|
|
07cfaa78ef |
Fix unique standard field (#16371)
Fixes https://github.com/twentyhq/twenty/issues/15925 - update field metadata update logic - uniformize the way index are named - command to migrate v1-named unique index - add integration testing --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
7f1e69740a |
1895 extensibility v1 application tokens (#16365)
First PR to implement application tokens - add new application role in twenty-server - move duplicated constants and types to twenty-shared - will add role configuration utils into twenty-sdk in another PR |
||
|
|
c8f541618d |
Refactor validate build and run for configuration to be less verbose and more reliable (#16343)
# Introduction Refactored the api `validateBuildAndRunWorkspaceMigration` to be require less configuration but to infer required args dynamically depending on provided metadata maps to compare ## `inferDeletionFromMissingEntities` Is not dynamically computed avoiding any miss configuration issue and any missleading devxp ## Maps computation Making only one call to redis to build both dependency and to be compared entity maps. It does not matter to avoid passing a about to compared flat entity maps to could also be a depedency, it's handled directly in the builder setup optimistic cache logic Please note that the flat maps used for the service input transpilation might differ from the one that we will dynamically compute and inject in the builder. Leading to do 2 redis calls but also race condition prone validation error We prefer that this occurs at the builder rather than at the runner level as the pg instance is not cache and reflect the real state of a given workspace In a nutshell, there's a possible race condition between cache invalidation and computation in both service input transpilers and builder but we're totally ok with that |
||
|
|
2803292521 |
feat: add Metadata Builder agent for data model management (#16350)
## Summary This PR adds a new **Metadata Builder** AI agent that specializes in managing the workspace data model (creating objects, adding fields, etc.). ## Changes ### New Files - `data-model-manager-role.ts` - New standard role with `DATA_MODEL` permission flag - `metadata-builder-agent.ts` - New standard agent for data model management ### Modified Files - **ChatToolsProviderService**: Refactored to consolidate all permission-based tools into a single `getChatTools()` method. Now injects both workflow tools and metadata tools based on permissions. - **AgentChatRoutingService**: Updated to use the new consolidated `getChatTools()` method - **AiChatModule**: Added imports for `ObjectMetadataModule` and `FieldMetadataModule` - **Router system prompt**: Added metadata-builder agent selection rules with clear distinction between schema operations vs data operations - **Metadata tools factories**: Improved error messages to show detailed validation errors instead of generic messages ### Refactoring - Renamed `index.ts` files to `standard-agent-definitions.ts` and `standard-role-definitions.ts` to follow naming conventions - Renamed exports from `standardAgentDefinitions` to `STANDARD_AGENT_DEFINITIONS` (SCREAMING_SNAKE_CASE) ## Key Features 1. **Metadata Builder Agent** can: - Create new custom objects - Add fields to existing objects - Update object and field properties - Create relations between objects 2. **Permission-based tool injection**: Tools are automatically injected based on the `DATA_MODEL` permission flag 3. **Improved routing**: The router now correctly distinguishes between: - "Create an object called Project" → metadata-builder (schema) - "Create a company called Acme" → data-manipulator (data) 4. **Better error messages**: Validation errors now show detailed messages like: ``` Validation errors: [objectMetadata] Name must be in camelCase format [objectMetadata] Label is required ``` |
||
|
|
34d7d82099 |
refactor(mcp): call metadata services directly instead of REST layer (#16349)
## Summary Refactors MCP metadata tools to call underlying services directly instead of going through the REST layer. This makes MCP a pure presentation layer. ### Changes **Created:** - `packages/twenty-server/src/engine/metadata-modules/metadata-tools/metadata-tools.module.ts` - Module that exports MetadataToolsFactory - `packages/twenty-server/src/engine/metadata-modules/metadata-tools/services/metadata-tools.factory.ts` - Factory that generates 8 metadata tools using Zod schemas: - `get-object-metadata`, `create-object-metadata`, `update-object-metadata`, `delete-object-metadata` - `get-field-metadata`, `create-field-metadata`, `update-field-metadata`, `delete-field-metadata` **Modified:** - `packages/twenty-server/src/engine/api/mcp/services/mcp-metadata.service.ts` - Uses new factory instead of REST-based services - `packages/twenty-server/src/engine/api/mcp/mcp.module.ts` - Imports MetadataToolsModule, removes old service imports **Deleted:** - `packages/twenty-server/src/engine/api/mcp/services/tools/create.tools.service.ts` - `packages/twenty-server/src/engine/api/mcp/services/tools/update.tools.service.ts` - `packages/twenty-server/src/engine/api/mcp/services/tools/delete.tools.service.ts` - `packages/twenty-server/src/engine/api/mcp/services/tools/get.tools.service.ts` - `packages/twenty-server/src/engine/api/mcp/services/tools/mcp-metadata-tools.service.ts` ### Architecture Improvement **Before:** ``` MCP Tool → MetadataQueryBuilderFactory → RestApiService → GraphQL API → Service ``` **After:** ``` MCP Tool → Service (ObjectMetadataService / FieldMetadataService) ``` This follows the pattern established by `direct-record-tools.factory.ts` and workflow tools. |
||
|
|
77409b6eb2 |
[Requires "warm" cache flush (no immediate downtime before flush)] Migrate viewGroup.fieldMetadataId -> view.mainGroupByFieldMetadataId (1/3) (#16206)
In this PR (1/3) - introduce view.mainGroupByFieldMetadataId as the new reference determining which fieldMetadataId is used in a grouped view, in order to deprecate viewGroup.fieldMetadataId which creates inconsistencies. view.mainGroupByFieldMetadataId is now filled at every view creation, though not in use yet. - Introduce a command to backfill view.mainGroupByFieldMetadataId for existing views + delete all viewGroup.fieldMetadataId with a fieldMetadataId that is not view.mainGroupByFieldMetadataId. (It should concern 37 active workspaces) - Temporarily disable the option to change a grouped view's fieldMetadataId as for now it creates inconsistencies. This feature can be reintroduced when we have done the full migration. In a next PR - (2/3) use view.mainGroupByFieldMetadataId instead of viewGroup.fieldMetadataId. In FE we may keep viewGroup.fieldMetadataId as a state (TBD). View groups will now be created / deleted as a side effect of view's mainGroupByFieldMetadataId update. - (3/3) remove viewGroup.fieldMetadataId --------- Co-authored-by: Charles Bochet <charles@twenty.com> |