32d7fa09a33b048d1663aec5a16503ed28b96b60
25 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6995420b71 |
Remove IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED feature flag (#18520)
## Summary - Removes the `IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED` feature flag, consolidating tarball-based app installation under the existing `IS_APPLICATION_ENABLED` flag - Removes the runtime feature flag check in `runWorkspaceMigration` resolver (the `@RequireFeatureFlag(IS_APPLICATION_ENABLED)` decorator already gates this endpoint) - Cleans up related integration test setup/teardown and mock feature flag maps ## Test plan - [ ] Verify tarball-based app installation still works when `IS_APPLICATION_ENABLED` is true - [ ] Verify app installation is blocked when `IS_APPLICATION_ENABLED` is false - [ ] Run `failing-install-application.integration-spec.ts` to confirm it passes without the removed flag Made with [Cursor](https://cursor.com) |
||
|
|
06bdb5ad6a |
[SDK] Agent in manifest (#18431)
# Introduction Adding agent in the manifest, required for twenty standard app extraction out of twenty-server |
||
|
|
66d93c4d28 |
Fix app:dev CLI by removing deleted createOneApplication mutation (#18460)
## Summary - The `createOneApplication` GraphQL mutation was removed from the server during the application architecture refactor (#18432), but the SDK CLI (`app:dev`, `app:build --sync`) still called it, causing failures. - Simplified the SDK to use `syncApplication` (which now internally creates the `ApplicationEntity` via `ensureApplicationExists`) instead of a separate create step. - On first run (clean install), the orchestrator now runs an initial sync before initializing the file uploader, so file uploads can proceed (they require the `ApplicationEntity` to exist). ## Test plan - [x] Typecheck passes for both `twenty-sdk` and `twenty-server` - [x] `app:dev` tested locally with existing app (finds app, uploads, syncs) - [x] `app:dev` tested locally after `app:uninstall` (creates app via sync, uploads, syncs) - [x] SDK unit tests pass (23/26 files, 3 pre-existing failures unrelated) Made with [Cursor](https://cursor.com) |
||
|
|
514d0017ea |
Refactor application module architecture for clarity and explicitness (#18432)
## Summary - **Module reorganization**: Moved `ApplicationUpgradeService` and cron jobs to `application-upgrade/`, `ApplicationSyncService` to `application-manifest/`, and `runWorkspaceMigration`/`uninstallApplication` mutations to the manifest resolver — each module now has a single clear responsibility. - **Explicit install flow**: Removed implicit `ApplicationEntity` creation from `ApplicationSyncService`. The install service and dev resolver now explicitly create the `ApplicationEntity` before syncing. npm packages are resolved at registration time to extract manifest metadata (universalIdentifier, name, description, etc.), eliminating the `reconcileUniversalIdentifier` hack. - **Better error handling**: Frontend hooks now surface actual server error messages in snackbars instead of swallowing them. Replaced the ugly `ConfirmationModal` for transfer ownership with a proper form modal. Fixed `SettingsAdminTableCard` row height overflow and corrected the `yarn-engine` asset path. ## Test plan - [ ] Register an npm package — verify manifest metadata (name, description, universalIdentifier) is extracted correctly - [ ] Install a registered npm app on a workspace — verify ApplicationEntity is created and sync succeeds - [ ] Test `app:dev` CLI flow — verify local app registration and sync work - [ ] Upload a tarball — verify registration and install flow - [ ] Transfer ownership — verify the new modal UX works - [ ] Verify error messages appear correctly in snackbars when operations fail Made with [Cursor](https://cursor.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. |
||
|
|
2e9624858c |
Fix name singular updates in dev mode (#18339)
as title |
||
|
|
58e37a118c |
Builder runs delete update and then create (#18272)
# Introduction We need to build and validate the flat entity operation in the following order delete update and create For example if not, if a created field has the same name than a deleted one than it will fail whereas it should not |
||
|
|
5e19361494 |
Allow uuidv5 for universal identifier (#18265)
Twenty apps are using v5 |
||
|
|
fde8168a85 | Centralized universal identifier validation on create (#18258) | ||
|
|
f080b952eb |
Add manifest integration tests (#18203)
- add integration test on sync manifest endpoint - fix invalid standard uuid + migration command |
||
|
|
af0af0a237 |
Fix cross app installation (#18151)
# Introduction - Fixing app dependencyFlatEntityMaps load ( to load current app + dependent app ) - Fix empty flat entity maps and undefined optimistic override - Refactor the optimistic rendering to be mutation oriented at orchestrator level |
||
|
|
175df59c21 |
Support Skill in manifest (#18092)
# Introduction Support skill in manifest, pre-requisite for the twenty standard app migration |
||
|
|
88424611ec |
Refactor and standardize isSystem field and object (#17992)
# Introduction ## Centralize system field definitions - Extract a single `PARTIAL_SYSTEM_FLAT_FIELD_METADATAS` constant as the source of truth for all 8 system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`), eliminating duplication across custom object and standard app field builders - Refactor `buildDefaultFlatFieldMetadatasForCustomObject` to use the shared constant via a new `buildObjectSystemFlatFieldMetadatas` helper ## Mark system fields as `isSystem: true` - Fields `id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector` are now properly flagged as system fields across all standard objects and custom object creation - Standard app field builders for all ~30 standard objects updated to set `isSystem: true` on `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy` - System-only standard objects (blocklist, calendar channels, message threads, etc.) now also include `createdBy`, `updatedBy`, `position`, `searchVector` field definitions that were previously missing ## Validate system fields on object creation - New transversal validation (`crossEntityTransversalValidation`) runs after all atomic entity validations in the build orchestrator, ensuring all 8 system fields are present with correct `type` and `isSystem: true` when an object is created - New `buildUniversalFlatObjectFieldByNameAndJoinColumnMaps` utility to resolve field names to universal identifiers for a given object - New exception codes: `MISSING_SYSTEM_FIELD` and `INVALID_SYSTEM_FIELD` on `ObjectMetadataExceptionCode` ## Protect system fields and objects from mutation - Field validators now block update/delete of `isSystem` fields by non-system callers (`FIELD_MUTATION_NOT_ALLOWED`) - Object validators now block update/delete of `isSystem` objects by non-system callers - `POSITION` and `TS_VECTOR` field type validators replaced: instead of rejecting creation outright, they now validate that the field is named correctly (`position` / `searchVector`) and has `isSystem: true` ## Distinguish `isSystemBuild` from `isCallerTwentyStandardApp` - New `isCallerTwentyStandardApp` utility checks whether the caller's `applicationUniversalIdentifier` matches the twenty standard app - Name-sync logic (`isFlatFieldMetadataNameSyncedWithLabel`, `areFlatObjectMetadataNamesSyncedWithLabels`) refactored to use `isCallerTwentyStandardApp` for custom suffix decisions, keeping `isSystemBuild` for mutation permission checks - `WorkspaceMigrationBuilderOptions` type updated to include `applicationUniversalIdentifier` ## Adapt frontend filtering - New `HIDDEN_SYSTEM_FIELD_NAMES` constant (`id`, `position`, `searchVector`) and `isHiddenSystemField` utility to only hide truly internal fields while keeping user-facing system fields (`createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`) visible in the UI - ~20 frontend files updated to replace `!field.isSystem` checks with `!isHiddenSystemField(field)` across record index, settings, data model, charts, workflows, spreadsheet import, aggregations, and role permissions ## Add 1.19 upgrade commands - **`backfill-system-fields-is-system`**: Raw SQL command to set `isSystem = true` on existing workspace fields matching system field names, and fix `position` field type from `NUMBER` to `POSITION` for `favorite`/`favoriteFolder` objects. Includes proper cache invalidation. - **`add-missing-system-fields-to-standard-objects`**: Codegen'd workspace migration to create missing `position`, `searchVector`, `createdBy`, `updatedBy` fields on standard objects that didn't previously have them. Runs via `WorkspaceMigrationRunnerService` in a single transaction with idempotency check. **Known limitation**: assumes all standard objects exist and are valid in the target workspace. ## Add `universalIdentifier` for system fields in standard object constants - `standard-object.constant.ts` updated to include `universalIdentifier` for `createdBy`, `updatedBy`, `position`, and `searchVector` across all standard objects - `fieldManifestType.ts` updated to support the new field manifest shape ## System relation Completely removed and backfilled all `isSystem` relation to be false false As we won't require an object to have any relation system fields ## Add integration tests - New test suite `failing-sync-application-object-system-fields` covering: missing system fields, wrong field types (`id` as TEXT, `createdAt` as TEXT, `position` as TEXT), system field deletion attempts, and system field update attempts - New test utilities: `buildDefaultObjectManifest` (builds an object manifest with all 8 system fields) and `setupApplicationForSync` (centralizes application setup) - Existing successful sync test updated to verify system fields are created with correct properties ## Next step Make the builder scope the compared entity to be the currently built app + nor twenty standard app |
||
|
|
c3781e87cc |
Sync page Layout (#18034)
## Sync page layouts, tabs, and widgets Adds the ability for SDK applications to synchronize `pageLayout`, `pageLayoutTab`, and `pageLayoutWidget` entities, following the same pattern established in #18003 for views and navigation menu items. ### Changes **`twenty-shared`** - New `PageLayoutManifest`, `PageLayoutTabManifest`, and `PageLayoutWidgetManifest` types with a hierarchical structure (page layout → tabs → widgets) - Added `pageLayouts: PageLayoutManifest[]` to the `Manifest` type **`twenty-sdk`** - New `definePageLayout()` SDK function with validation for universalIdentifier, name, and nested tabs/widgets - Wired into the manifest extraction and build pipeline (`DefinePageLayout` target function, `PageLayouts` entity key) - Exported from the SDK entry point **`twenty-server`** - Added `pageLayout`, `pageLayoutTab`, `pageLayoutWidget` to `APPLICATION_MANIFEST_METADATA_NAMES` - New conversion utilities: manifest → universal flat entity for all three entity types - Updated `computeApplicationManifestAllUniversalFlatEntity |
||
|
|
7332379d26 |
Improve API Client usage and add Typescript check (#18023)
## Summary https://github.com/user-attachments/assets/1e75cc9d-d9d2-4ef2-99f9-34450f5d8de7 Add background incremental type checking (`tsc --watch`) to the SDK dev mode, so type regressions are caught when the generated API client changes — without requiring a full rebuild of source files. Previously, removing a field from the data model would regenerate the API client, but existing front components/logic functions referencing the removed field wouldn't surface type errors (since their source didn't change, esbuild wouldn't rebuild them). ## What changed - **Background `tsc --watch`**: a long-lived TypeScript watcher runs alongside esbuild watchers, incrementally re-checking all files when the generated client changes. Only logs on state transitions (errors appear / errors clear) to stay quiet. - **Atomic client generation**: API client is now generated into a temp directory and swapped in atomically, avoiding a race condition where `tsc --watch` could see an empty `generated/` directory mid-regeneration. - **Step decoupling**: orchestrator steps no longer receive `uploadFilesStep` directly. Instead, they use callbacks (`onFileBuilt`, `onApiClientGenerated`), and each step manages its own `builtFileInfos` state. - **`apiClientChecksum` omitted from `ApplicationConfig`**: it's a build-time computed value, same as `packageJsonChecksum`. <img width="327" height="177" alt="image" src="https://github.com/user-attachments/assets/02bd25bb-fa41-42b0-8d96-01c51bd4580c" /> <img width="529" height="452" alt="image" src="https://github.com/user-attachments/assets/61f6e968-365b-4a5b-8f2b-a8419d6b1bd3" /> |
||
|
|
c0cc0689d6 |
Add Client Api generation (#17961)
## Add API client generation to SDK dev mode and refactor orchestrator into step-based pipeline ### Why The SDK dev mode lacked typed API client generation, forcing developers to work without auto-generated GraphQL types when building applications. Additionally, the orchestrator was a monolithic class that mixed watcher management, token handling, and sync logic — making it difficult to extend with new steps like client generation. ### How - **Refactored the orchestrator** into a step-based pipeline with dedicated classes: `CheckServer`, `EnsureValidTokens`, `ResolveApplication`, `BuildManifest`, `UploadFiles`, `GenerateApiClient`, `SyncApplication`, and `StartWatchers`. Each step has typed input/output/status, managed by a new `OrchestratorState` class. - **Added `GenerateApiClientOrchestratorStep`** that detects object/field schema changes and regenerates a typed GraphQL client (via `@genql/cli`) into `node_modules/twenty-sdk/generated` for seamless imports. - **Replaced `checkApplicationExist`** with `findOneApplication` on both server resolver and SDK API service, returning the entity data instead of a boolean. - **Added application token pair mutations** (`generateApplicationToken`, `renewApplicationToken`) to the API service, with the server now returning `ApplicationTokenPairDTO` containing both access and refresh tokens. - **Restructured the dev UI** into `dev/ui/components/` with dedicated panel, section, and event log components. - **Simplified `AppDevCommand`** from ~180 lines of watcher management down to ~40 lines that delegate entirely to the orchestrator. |
||
|
|
b4e924b671 |
Sync views and navigation items (#18003)
Both objects are necessary to fully enjoy objects within applications <img width="770" height="311" alt="Capture d’écran 2026-02-17 à 15 19 43" src="https://github.com/user-attachments/assets/48c51fa4-63f4-45b2-a40a-df73f3aa79be" /> |
||
|
|
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 |
||
|
|
9bc63a01c9 |
Generate GQL schema based on applicationId (#17860)
## Add application-scoped GraphQL schema generation When an application token is used to authenticate, the `/graphql` schema is now dynamically filtered to only include entities belonging to that application (plus the Twenty Standard Application). This enables third-party applications and the SDK to introspect a schema that is relevant to their scope, rather than seeing the full workspace schema with all custom objects. ### Changes - **New `generateApplicationToken` mutation** on the `/metadata` endpoint, allowing callers to exchange an API key for an application-scoped JWT token - **Schema filtering by application** in `WorkspaceSchemaFactory` — when `request.application` is present (from an application token), flat entity maps are filtered by `[appId, standardAppId]` before schema generation - **Per-app caching** — both the Yoga in-memory cache and Redis cache now include the `appId` in their keys to avoid serving wrong schemas - **Consolidated `getSubFlatEntityMapsByApplicationIdsOrThrow`** — unified the single-ID and multi-ID filtering utilities into one - **Integration tests** covering token generation (admin + API key auth) and schema introspection filtering (standard app token excludes custom objects) Schema generated on seeds with applicationToken (see that pets is missing) <img width="782" height="994" alt="image" src="https://github.com/user-attachments/assets/82510031-0965-435d-bc26-77c9f5d74e1f" /> |
||
|
|
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" /> |
||
|
|
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`
|
||
|
|
9b7bd1a6aa |
Refactor delete objectMetadata action type and handler to be workspace agnostic (#17530)
# Introduction Refactoring the delete and update field to use cached flatEntitty.__universal.objectMetadataUniversalIdentifier to infer related object |
||
|
|
2a28c34a37 |
Refactor workspace migration runner exception handling (#17310)
# Introduction
In this PR we catch all the runner errors coming from a single workspace
migration action execution.
**1. `WorkspaceMigrationActionExecutionException`** (low-level,
action-specific)
- Thrown from action handlers, utils, and helper functions
- Contains specific error codes: `FIELD_METADATA_NOT_FOUND`,
`OBJECT_METADATA_NOT_FOUND`, `ENUM_OPERATION_FAILED`, `NOT_SUPPORTED`,
etc.
- Simple structure: `message`, `code`, `userFriendlyMessage`
- No action context - just describes what went wrong
**2. `WorkspaceMigrationRunnerException`** (high-level, runner-scoped)
- Only two codes: `INTERNAL_SERVER_ERROR` and `EXECUTION_FAILED`
- `EXECUTION_FAILED` **requires** `action` + `errors` (contains the
action context)
- `INTERNAL_SERVER_ERROR` **requires** `message` (no action context)
## Refactor
- Removed the `relatedFlatEntityMapsKeys` from the `WorkspaceMigration`
type as they're directly inferred from passed actions
- Swallowing actions rollbacks errors in order to iterate over all of
them
## Testing
Created a very straigthforward install application from workspace
migration endpoint in order to start testing the introduced
`WorkspaceMigrationActionExecutionException`
Introduced a feature flag that stop the access to the endpoint if not
enabled
Whole taken direction are totally subjective and highly prone to
mutations ( endpoint location, naming and input schema see
`ts-expect-error` comment )
cc @martmull
## Response error
```ts
{
"eventId": "evt_a1b2c3d4-5678-90ab-cdef-1234567890ab",
"extensions": {
"action": {
"metadataName": "fieldMetadata",
"type": "delete",
"universalIdentifier": "20202020-6110-4547-9fd0-2525257a2c3f"
},
"code": "APPLICATION_INSTALLATION_FAILED",
"errors": {
"metadata": {
"code": "ENTITY_NOT_FOUND",
"message": "Could not find flat entity with universal identifier 20202020-6110-4547-9fd0-2525257a2c3f"
},
"workspaceSchema": {
"code": "ENTITY_NOT_FOUND",
"message": "Could not find flat entity in maps"
}
},
"exceptionEventId": "exc_f9e8d7c6-5432-10ba-fedc-ba0987654321",
"userFriendlyMessage": "Migration execution failed."
},
"message": "Migration action 'delete' for 'fieldMetadata' failed",
"name": "GraphQLError"
}
```
|