Merge remote-tracking branch 'origin/main' into claude/add-primary-email-utility-PSSBG

This commit is contained in:
Claude
2026-04-09 20:39:59 +00:00
107 changed files with 1540 additions and 270 deletions
+22
View File
@@ -0,0 +1,22 @@
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "bash",
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
"env": {}
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["@playwright/mcp@latest", "--no-sandbox", "--headless"],
"env": {}
},
"context7": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"],
"env": {}
}
}
}
+220
View File
@@ -0,0 +1,220 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Twenty is an open-source CRM built with modern technologies in a monorepo structure. The codebase is organized as an Nx workspace with multiple packages.
## Key Commands
### Development
```bash
# Start development environment (frontend + backend + worker)
yarn start
# Individual package development
npx nx start twenty-front # Start frontend dev server
npx nx start twenty-server # Start backend server
npx nx run twenty-server:worker # Start background worker
```
### Testing
```bash
# Preferred: run a single test file (fast)
npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
# Run all tests for a package
npx nx test twenty-front # Frontend unit tests
npx nx test twenty-server # Backend unit tests
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
# To run an indivual test or a pattern of tests, use the following command:
cd packages/{workspace} && npx jest "pattern or filename"
# Storybook
npx nx storybook:build twenty-front
npx nx storybook:test twenty-front
# When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
```
### Code Quality
```bash
# Linting (diff with main - fastest, always prefer this)
npx nx lint:diff-with-main twenty-front
npx nx lint:diff-with-main twenty-server
npx nx lint:diff-with-main twenty-front --configuration=fix # Auto-fix
# Linting (full project - slower, use only when needed)
npx nx lint twenty-front
npx nx lint twenty-server
# Type checking
npx nx typecheck twenty-front
npx nx typecheck twenty-server
# Format code
npx nx fmt twenty-front
npx nx fmt twenty-server
```
### Build
```bash
# Build packages (twenty-shared must be built first)
npx nx build twenty-shared
npx nx build twenty-front
npx nx build twenty-server
```
### Database Operations
```bash
# Database management
npx nx database:reset twenty-server # Reset database
npx nx run twenty-server:database:init:prod # Initialize database
npx nx run twenty-server:database:migrate:prod # Run migrations
# Generate migration
npx nx run twenty-server:database:migrate:generate
```
### Database Inspection (Postgres MCP)
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
- Inspect workspace data, metadata, and object definitions while developing
- Verify migration results (columns, types, constraints) after running migrations
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
- Inspect metadata tables to debug GraphQL schema generation issues
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
### GraphQL
```bash
# Generate GraphQL types (run after schema changes)
npx nx run twenty-front:graphql:generate
npx nx run twenty-front:graphql:generate --configuration=metadata
```
## Architecture Overview
### Tech Stack
- **Frontend**: React 18, TypeScript, Jotai (state management), Linaria (styling), Vite
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
- **Monorepo**: Nx workspace managed with Yarn 4
### Package Structure
```
packages/
├── twenty-front/ # React frontend application
├── twenty-server/ # NestJS backend API
├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email
├── twenty-website/ # Next.js documentation website
├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests
```
### Key Development Principles
- **Functional components only** (no class components)
- **Named exports only** (no default exports)
- **Types over interfaces** (except when extending third-party interfaces)
- **String literals over enums** (except for GraphQL enums)
- **No 'any' type allowed** — strict TypeScript enforced
- **Event handlers preferred over useEffect** for state updates
- **Props down, events up** — unidirectional data flow
- **Composition over inheritance**
- **No abbreviations** in variable names (`user` not `u`, `fieldMetadata` not `fm`)
### Naming Conventions
- **Variables/functions**: camelCase
- **Constants**: SCREAMING_SNAKE_CASE
- **Types/Classes**: PascalCase (suffix component props with `Props`, e.g. `ButtonProps`)
- **Files/directories**: kebab-case with descriptive suffixes (`.component.tsx`, `.service.ts`, `.entity.ts`, `.dto.ts`, `.module.ts`)
- **TypeScript generics**: descriptive names (`TData` not `T`)
### File Structure
- Components under 300 lines, services under 500 lines
- Components in their own directories with tests and stories
- Use `index.ts` barrel exports for clean imports
- Import order: external libraries first, then internal (`@/`), then relative
### Comments
- Use short-form comments (`//`), not JSDoc blocks
- Explain WHY (business logic), not WHAT
- Do not comment obvious code
- Multi-line comments use multiple `//` lines, not `/** */`
### State Management
- **Jotai** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
- Component-specific state with React hooks (`useState`, `useReducer` for complex logic)
- GraphQL cache managed by Apollo Client
- Use functional state updates: `setState(prev => prev + 1)`
### Backend Architecture
- **NestJS modules** for feature organization
- **TypeORM** for database ORM with PostgreSQL
- **GraphQL** API with code-first approach
- **Redis** for caching and session management
- **BullMQ** for background job processing
### Database & Migrations
- **PostgreSQL** as primary database
- **Redis** for caching and sessions
- **ClickHouse** for analytics (when enabled)
- Always generate migrations when changing entity files
- Migration names must be kebab-case (e.g. `add-agent-turn-evaluation`)
- Include both `up` and `down` logic in migrations
- Never delete or rewrite committed migrations
### Utility Helpers
Use existing helpers from `twenty-shared` instead of manual type guards:
- `isDefined()`, `isNonEmptyString()`, `isNonEmptyArray()`
## Development Workflow
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
### Before Making Changes
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
2. Test changes with relevant test suites (prefer single-file test runs)
3. Ensure database migrations are generated for entity changes
4. Check that GraphQL schema changes are backward compatible
5. Run `graphql:generate` after any GraphQL schema changes
### Code Style Notes
- Use **Linaria** for styling with zero-runtime CSS-in-JS (styled-components pattern)
- Follow **Nx** workspace conventions for imports
- Use **Lingui** for internationalization
- Apply security first, then formatting (sanitize before format)
### Testing Strategy
- **Test behavior, not implementation** — focus on user perspective
- **Test pyramid**: 70% unit, 20% integration, 10% E2E
- Query by user-visible elements (text, roles, labels) over test IDs
- Use `@testing-library/user-event` for realistic interactions
- Descriptive test names: "should [behavior] when [condition]"
- Clear mocks between tests with `jest.clearAllMocks()`
## Dev Environment Setup
All dev environments (Claude Code web, Cursor, local) use one script:
```bash
bash packages/twenty-utils/setup-dev-env.sh
```
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times.
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
- `--down` — stop services
- `--reset` — wipe data and restart fresh
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script.
## Important Files
- `nx.json` - Nx workspace configuration with task definitions
- `tsconfig.base.json` - Base TypeScript configuration
- `package.json` - Root package with workspace definitions
- `.cursor/rules/` - Detailed development guidelines and best practices
@@ -1,13 +1,19 @@
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import {
definePostInstallLogicFunction,
type InstallLogicFunctionPayload,
} from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
console.log(
'Post install logic function executed successfully!',
payload.previousVersion,
);
};
export default definePostInstallLogicFunction({
universalIdentifier: '8c726dcc-1709-4eac-aa8b-f99960a9ec1b',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
timeoutSeconds: 30,
handler,
});
@@ -44,7 +44,9 @@ describe('App installation', () => {
if (!uninstallResult.success) {
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
`App uninstall failed: ${
uninstallResult.error?.message ?? 'Unknown error'
}`,
);
}
});
@@ -0,0 +1,35 @@
import { CoreApiClient } from 'twenty-client-sdk/core';
import { definePostInstallLogicFunction } from 'twenty-sdk';
const SEED_POST_CARDS = [
{
name: 'Greetings from Paris',
content: 'Wish you were here! The Eiffel Tower is breathtaking.',
},
{
name: 'Hello from Tokyo',
content: 'Cherry blossoms are in full bloom. Sending love!',
},
];
const handler = async () => {
const client = new CoreApiClient();
await client.mutation({
createPostCards: {
__args: { data: SEED_POST_CARDS as any },
id: true,
},
} as any);
console.log(`Seeded ${SEED_POST_CARDS.length} post cards on install.`);
return {};
};
export default definePostInstallLogicFunction({
universalIdentifier: '852c6321-1563-4396-b7c5-9d370f3d30a9',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 30,
handler,
});
@@ -0,0 +1,17 @@
import { definePreInstallLogicFunction } from 'twenty-sdk';
const handler = async (params: any) => {
console.log(
`Pre-install logic function executed successfully with params`,
params,
);
return {};
};
export default definePreInstallLogicFunction({
universalIdentifier: 'bf27f558-4ec6-481f-b76e-1dbcd05aef1f',
name: 'pre-install',
description: 'Runs before migrations to set up the application.',
timeoutSeconds: 10,
handler,
});
@@ -1,59 +0,0 @@
import { CoreApiClient } from 'twenty-client-sdk/core';
import { definePostInstallLogicFunction } from 'twenty-sdk';
const POST_CARDS_TO_SEED = [
{
name: 'Greetings from Paris',
content:
'Wish you were here! The Eiffel Tower looks even better in person. - Alex',
},
{
name: 'Hello from Tokyo',
content:
'The cherry blossoms are amazing this time of year. See you soon! - Sam',
},
];
const handler = async (): Promise<{
message: string;
createdIds: string[];
}> => {
console.log('Seeding 2 post cards...');
const client = new CoreApiClient();
const createdIds: string[] = [];
for (const postCard of POST_CARDS_TO_SEED) {
const { createPostCard } = await client.mutation({
createPostCard: {
__args: {
data: {
name: postCard.name,
content: postCard.content,
},
},
id: true,
},
});
if (!createPostCard?.id) {
throw new Error(`Failed to create post card "${postCard.name}"`);
}
createdIds.push(createPostCard.id);
}
console.log('Seeding complete!');
return {
message: `Seeded ${createdIds.length} post cards`,
createdIds,
};
};
export default definePostInstallLogicFunction({
universalIdentifier: '9f3d8c21-b471-4a82-8e5c-6f3a7b8c9d01',
name: 'seed-post-cards',
description: 'Seeds the workspace with 2 sample post card records.',
timeoutSeconds: 10,
handler,
});
@@ -3554,7 +3554,7 @@ type Mutation {
updateWebhook(input: UpdateWebhookInput!): Webhook!
deleteWebhook(id: UUID!): Webhook!
createChatThread: AgentChatThread!
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String): SendChatMessageResult!
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileIds: [UUID!]): SendChatMessageResult!
stopAgentChatStream(threadId: UUID!): Boolean!
deleteQueuedChatMessage(messageId: UUID!): Boolean!
createSkill(input: CreateSkillInput!): Skill!
@@ -6321,7 +6321,7 @@ export interface MutationGenqlSelection{
updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} })
deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
createChatThread?: AgentChatThreadGenqlSelection
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null)} })
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileIds?: (Scalars['UUID'][] | null)} })
stopAgentChatStream?: { __args: {threadId: Scalars['UUID']} }
deleteQueuedChatMessage?: { __args: {messageId: Scalars['UUID']} }
createSkill?: (SkillGenqlSelection & { __args: {input: CreateSkillInput} })
@@ -8363,6 +8363,10 @@ export default {
],
"modelId": [
1
],
"fileIds": [
3,
"[UUID!]"
]
}
],
@@ -644,49 +644,15 @@ export default defineLogicFunction({
**Write a good `description`.** AI agents rely on the function's `description` field to decide when to use the tool. Be specific about what the tool does and when it should be called.
</Note>
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Define a pre-install logic function (one per app)">
A pre-install function is a logic function that runs automatically before your app is installed on a workspace. This is useful for validation tasks, prerequisite checks, or preparing workspace state before the main installation proceeds.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
You can also manually execute the pre-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Key points:
- Pre-install functions use `definePreInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
- The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
- Only one pre-install function is allowed per application. The manifest build will error if more than one is detected.
- The function's `universalIdentifier` is automatically set as `preInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer preparation tasks.
</Accordion>
<Accordion title="definePostInstallLogicFunction" description="Define a post-install logic function (one per app)">
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
A post-install function is a logic function that runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services.
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
@@ -695,6 +661,8 @@ export default definePostInstallLogicFunction({
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
@@ -707,10 +675,168 @@ yarn twenty exec --postInstall
Key points:
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
- The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
- The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic.
- **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook.
- **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed.
- `shouldRunSynchronously: false` *(default)* — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window.
- `shouldRunSynchronously: true` — the hook is executed **inline during the install flow** (same executor as pre-install). The install request blocks until the handler finishes, and if it throws, the install caller receives a `POST_INSTALL_ERROR`. No automatic retries. **Use this for fast, must-complete-before-response work** — for example, emitting a validation error to the user, or quick setup that the client will rely on immediately after the install call returns. Keep in mind the metadata migration has already been applied by the time post-install runs, so a sync-mode failure does **not** roll back the schema changes — it only surfaces the error.
- Make sure your handler is idempotent. In async mode the queue may retry up to three times; in either mode the hook may run again on upgrades when `shouldRunOnVersionUpgrade: true`.
- The environment variables `APPLICATION_ID`, `APP_ACCESS_TOKEN`, and `API_URL` are available inside the handler (same as any other logic function), so you can call the Twenty API with an application access token scoped to your app.
- Only one post-install function is allowed per application. The manifest build will error if more than one is detected.
- The function's `universalIdentifier` is automatically set as `postInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
- The function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in `defineApplication()`.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
- **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` to trigger it manually against a running workspace.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="Define a pre-install logic function (one per app)">
A pre-install function is a logic function that runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped.
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
You can also manually execute the pre-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
Key points:
- Pre-install functions use `definePreInstallLogicFunction()` — same specialized config as post-install, just attached to a different lifecycle slot.
- Both pre- and post-install handlers receive the same `InstallPayload` type: `{ previousVersion?: string; newVersion: string }`. Import it once and reuse it for both hooks.
- **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state.
- **Execution model**: pre-install is executed **synchronously** and **blocks the install**. If the handler throws, the install is aborted before any schema changes are applied — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade.
- As with post-install, only one pre-install function is allowed per application. It is attached to the application manifest under `preInstallLogicFunction` automatically during the build.
- **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty exec --preInstall` to trigger it manually.
</Accordion>
<Accordion title="Pre-install vs post-install: when to use which" description="Choosing the right install hook">
Both hooks are part of the same install flow and receive the same `InstallPayload`. The difference is **when** they run relative to the workspace metadata migration, and that changes what data they can safely touch.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
Pre-install is always **synchronous** (it blocks the install and can abort it). Post-install is **asynchronous by default** — enqueued on a worker with automatic retries — but can opt into synchronous execution with `shouldRunSynchronously: true`. See the `definePostInstallLogicFunction` accordion above for when to use each mode.
**Use `post-install` for anything that needs the new schema to exist.** This is the common case:
- Seeding default data (creating initial records, default views, demo content) against newly-added objects and fields.
- Registering webhooks with third-party services now that the app has its credentials.
- Calling your own API to finish setup that depends on the synchronized metadata.
- Idempotent "ensure this exists" logic that should reconcile state on every upgrade — combine with `shouldRunOnVersionUpgrade: true`.
Example — seed a default `PostCard` record after install:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = createClient();
await client.postCard.create({
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
handler,
});
```
**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the *previous* schema and its failure rolls back the upgrade, it is the right place for anything risky:
- **Backing up data that is about to be dropped or restructured** — e.g. you are removing a field in v2 and need to copy its values into another field or export them to storage before the migration runs.
- **Archiving records that a new constraint would invalidate** — e.g. a field is becoming `NOT NULL` and you need to delete or fix rows with null values first.
- **Validating compatibility and refusing the upgrade if the current data cannot be migrated cleanly** — throw from the handler and the install aborts with no changes applied. This is safer than discovering the incompatibility mid-migration.
- **Renaming or rekeying data** ahead of a schema change that would lose the association.
Example — archive records before a destructive migration:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
import { createClient } from './generated/client';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = createClient();
const legacyRecords = await client.postCard.findMany({
where: { notes: { isNotNull: true } },
});
if (legacyRecords.length === 0) return;
// Copy legacy `notes` into the new `description` field before the migration
// drops the `notes` column. If this fails, the upgrade is aborted and the
// workspace stays on v1 with all data intact.
await Promise.all(
legacyRecords.map((record) =>
client.postCard.update({
where: { id: record.id },
data: { description: record.notes },
}),
),
);
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
**Rule of thumb:**
| You want to… | Use |
|---|---|
| Seed default data, configure the workspace, register external resources | `post-install` |
| Run long-running seeding or third-party calls that shouldn't block the install response | `post-install` (default — `shouldRunSynchronously: false`, with worker retries) |
| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` |
| Read or back up data that the upcoming migration would lose | `pre-install` |
| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) |
| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` |
| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) |
<Note>
If in doubt, default to **post-install**. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone.
</Note>
</Accordion>
<Accordion title="defineFrontComponent" description="Define front components for custom UI" >
@@ -1818,8 +1944,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute pre-install or post-install functions
yarn twenty exec --preInstall
# Execute the post-install function
yarn twenty exec --postInstall
```
@@ -3389,6 +3389,7 @@ export type MutationSaveImapSmtpCaldavAccountArgs = {
export type MutationSendChatMessageArgs = {
browsingContext?: InputMaybe<Scalars['JSON']>;
fileIds?: InputMaybe<Array<Scalars['UUID']>>;
messageId: Scalars['UUID'];
modelId?: InputMaybe<Scalars['String']>;
text: Scalars['String'];
@@ -6450,6 +6451,7 @@ export type SendChatMessageMutationVariables = Exact<{
messageId: Scalars['UUID'];
browsingContext?: InputMaybe<Scalars['JSON']>;
modelId?: InputMaybe<Scalars['String']>;
fileIds?: InputMaybe<Array<Scalars['UUID']> | Scalars['UUID']>;
}>;
@@ -8410,7 +8412,7 @@ export const DeleteSkillDocument = {"kind":"Document","definitions":[{"kind":"Op
export const EvaluateAgentTurnDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"EvaluateAgentTurn"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"turnId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"evaluateAgentTurn"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"turnId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"turnId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"turnId"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode<EvaluateAgentTurnMutation, EvaluateAgentTurnMutationVariables>;
export const RemoveRoleFromAgentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveRoleFromAgent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeRoleFromAgent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"agentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}}}]}]}}]} as unknown as DocumentNode<RemoveRoleFromAgentMutation, RemoveRoleFromAgentMutationVariables>;
export const RunEvaluationInputDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RunEvaluationInput"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runEvaluationInput"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"agentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"threadId"}},{"kind":"Field","name":{"kind":"Name","value":"agentId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"evaluations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode<RunEvaluationInputMutation, RunEvaluationInputMutationVariables>;
export const SendChatMessageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendChatMessage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"text"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sendChatMessage"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}},{"kind":"Argument","name":{"kind":"Name","value":"text"},"value":{"kind":"Variable","name":{"kind":"Name","value":"text"}}},{"kind":"Argument","name":{"kind":"Name","value":"messageId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}}},{"kind":"Argument","name":{"kind":"Name","value":"browsingContext"},"value":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}}},{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"messageId"}},{"kind":"Field","name":{"kind":"Name","value":"queued"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}}]}}]}}]} as unknown as DocumentNode<SendChatMessageMutation, SendChatMessageMutationVariables>;
export const SendChatMessageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendChatMessage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"text"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fileIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sendChatMessage"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}},{"kind":"Argument","name":{"kind":"Name","value":"text"},"value":{"kind":"Variable","name":{"kind":"Name","value":"text"}}},{"kind":"Argument","name":{"kind":"Name","value":"messageId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}}},{"kind":"Argument","name":{"kind":"Name","value":"browsingContext"},"value":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}}},{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"fileIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fileIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"messageId"}},{"kind":"Field","name":{"kind":"Name","value":"queued"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}}]}}]}}]} as unknown as DocumentNode<SendChatMessageMutation, SendChatMessageMutationVariables>;
export const StopAgentChatStreamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"StopAgentChatStream"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stopAgentChatStream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}]}]}}]} as unknown as DocumentNode<StopAgentChatStreamMutation, StopAgentChatStreamMutationVariables>;
export const UpdateOneAgentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneAgent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateAgentInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneAgent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AgentFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AgentFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Agent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"prompt"}},{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"responseFormat"}},{"kind":"Field","name":{"kind":"Name","value":"roleId"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"modelConfiguration"}},{"kind":"Field","name":{"kind":"Name","value":"evaluationInputs"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<UpdateOneAgentMutation, UpdateOneAgentMutationVariables>;
export const UpdateSkillDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSkill"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateSkillInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSkill"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SkillFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SkillFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Skill"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<UpdateSkillMutation, UpdateSkillMutationVariables>;
@@ -7,6 +7,7 @@ export const SEND_CHAT_MESSAGE = gql`
$messageId: UUID!
$browsingContext: JSON
$modelId: String
$fileIds: [UUID!]
) {
sendChatMessage(
threadId: $threadId
@@ -14,6 +15,7 @@ export const SEND_CHAT_MESSAGE = gql`
messageId: $messageId
browsingContext: $browsingContext
modelId: $modelId
fileIds: $fileIds
) {
messageId
queued
@@ -114,6 +114,8 @@ export const useAgentChat = (
store.set(messagesAtom, [...currentMessages, optimisticUserMessage]);
const fileIds = agentChatUploadedFiles.map((file) => file.fileId);
setAgentChatUploadedFiles([]);
try {
@@ -131,6 +133,7 @@ export const useAgentChat = (
messageId,
browsingContext: browsingContext ?? null,
modelId: modelIdForRequest ?? undefined,
fileIds: fileIds.length > 0 ? fileIds : undefined,
},
});
@@ -163,8 +163,8 @@ export const registerCommands = (program: Command): void => {
program
.command('exec [appPath]')
.option('--preInstall', 'Execute pre-install logic function if defined')
.option('--postInstall', 'Execute post-install logic function if defined')
.option('--preInstall', 'Execute pre-install logic function if defined')
.option(
'-p, --payload <payload>',
'JSON payload to send to the function',
@@ -183,22 +183,22 @@ export const registerCommands = (program: Command): void => {
async (
appPath?: string,
options?: {
preInstall?: boolean;
postInstall?: boolean;
preInstall?: boolean;
payload?: string;
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
if (
!options?.preInstall &&
!options?.postInstall &&
!options?.preInstall &&
!options?.functionUniversalIdentifier &&
!options?.functionName
) {
console.error(
chalk.red(
'Error: Either --preInstall, --postInstall, --functionName (-n), or --functionUniversalIdentifier (-u) is required.',
'Error: Either --postInstall, --preInstall, --functionName (-n), or --functionUniversalIdentifier (-u) is required.',
),
);
process.exit(1);
+10 -10
View File
@@ -7,15 +7,15 @@ import { isDefined } from 'twenty-shared/utils';
export class LogicFunctionExecuteCommand {
async execute({
appPath = CURRENT_EXECUTION_DIRECTORY,
preInstall = false,
postInstall = false,
preInstall = false,
functionUniversalIdentifier,
functionName,
payload = '{}',
}: {
appPath?: string;
preInstall?: boolean;
postInstall?: boolean;
preInstall?: boolean;
functionUniversalIdentifier?: string;
functionName?: string;
payload?: string;
@@ -30,19 +30,19 @@ export class LogicFunctionExecuteCommand {
process.exit(1);
}
const identifier = preInstall
? 'pre install'
: postInstall
? 'post install'
const identifier = postInstall
? 'post install'
: preInstall
? 'pre install'
: (functionUniversalIdentifier ?? functionName);
console.log(chalk.blue(`🚀 Executing function "${identifier}"...`));
console.log(chalk.gray(` Payload: ${JSON.stringify(parsedPayload)}\n`));
const executeOptions = preInstall
? { appPath, preInstall: true as const, payload: parsedPayload }
: postInstall
? { appPath, postInstall: true as const, payload: parsedPayload }
const executeOptions = postInstall
? { appPath, postInstall: true as const, payload: parsedPayload }
: preInstall
? { appPath, preInstall: true as const, payload: parsedPayload }
: functionUniversalIdentifier
? { appPath, functionUniversalIdentifier, payload: parsedPayload }
: { appPath, functionName: functionName!, payload: parsedPayload };
@@ -15,8 +15,8 @@ export type FunctionExecuteOptions = {
remote?: string;
payload?: Record<string, unknown>;
} & (
| { preInstall: true }
| { postInstall: true }
| { preInstall: true }
| { functionUniversalIdentifier: string }
| { functionName: string }
);
@@ -39,8 +39,8 @@ const belongsToApplication = (
};
const resolveIdentifier = (options: FunctionExecuteOptions): string => {
if ('preInstall' in options) return 'pre install';
if ('postInstall' in options) return 'post install';
if ('preInstall' in options) return 'pre install';
if ('functionUniversalIdentifier' in options)
return options.functionUniversalIdentifier;
if ('functionName' in options) return options.functionName;
@@ -92,16 +92,16 @@ const innerFunctionExecute = async (
);
const targetFunction = appFunctions.find((logicFunction) => {
if ('preInstall' in options && options.preInstall) {
return (
logicFunction.universalIdentifier ===
manifest.application.preInstallLogicFunctionUniversalIdentifier
);
}
if ('postInstall' in options && options.postInstall) {
return (
logicFunction.universalIdentifier ===
manifest.application.postInstallLogicFunctionUniversalIdentifier
manifest.application.postInstallLogicFunction?.universalIdentifier
);
}
if ('preInstall' in options && options.preInstall) {
return (
logicFunction.universalIdentifier ===
manifest.application.preInstallLogicFunction?.universalIdentifier
);
}
if ('functionUniversalIdentifier' in options) {
@@ -34,10 +34,14 @@ import {
type RoleManifest,
type SkillManifest,
type ViewManifest,
type PostInstallLogicFunctionApplicationManifest,
type PreInstallLogicFunctionApplicationManifest,
} from 'twenty-shared/application';
import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function';
import { assertUnreachable } from 'twenty-shared/utils';
import { addMissingFieldOptionIds } from '@/cli/utilities/build/manifest/utils/add-missing-field-option-ids';
import { type PostInstallLogicFunctionConfig } from '@/sdk/logic-functions/post-install-logic-function-config';
import { type PreInstallLogicFunctionConfig } from '@/sdk/logic-functions/pre-install-logic-function-config';
import { fromRoleConfigToRoleManifest } from '@/cli/utilities/build/manifest/utils/from-role-config-to-role-manifest';
import { type RoleConfig } from '@/sdk/roles/role-config';
@@ -80,9 +84,10 @@ export const buildManifest = async (
const views: ViewManifest[] = [];
const navigationMenuItems: NavigationMenuItemManifest[] = [];
const pageLayouts: PageLayoutManifest[] = [];
const preInstallLogicFunctionUniversalIdentifiers: string[] = [];
const postInstallLogicFunctionUniversalIdentifiers: string[] = [];
const postInstallLogicFunctions: PostInstallLogicFunctionApplicationManifest[] =
[];
const preInstallLogicFunctions: PreInstallLogicFunctionApplicationManifest[] =
[];
const applicationFilePaths: string[] = [];
const objectsFilePaths: string[] = [];
const fieldsFilePaths: string[] = [];
@@ -231,19 +236,31 @@ export const buildManifest = async (
logicFunctionsFilePaths.push(relativePath);
if (
targetFunctionName === TargetFunction.DefinePreInstallLogicFunction
targetFunctionName === TargetFunction.DefinePostInstallLogicFunction
) {
preInstallLogicFunctionUniversalIdentifiers.push(
extract.config.universalIdentifier,
);
const postInstallHookConfig =
extract.config as PostInstallLogicFunctionConfig;
postInstallLogicFunctions.push({
universalIdentifier: extract.config.universalIdentifier,
shouldRunOnVersionUpgrade:
postInstallHookConfig.shouldRunOnVersionUpgrade ?? false,
shouldRunSynchronously:
postInstallHookConfig.shouldRunSynchronously ?? false,
});
}
if (
targetFunctionName === TargetFunction.DefinePostInstallLogicFunction
targetFunctionName === TargetFunction.DefinePreInstallLogicFunction
) {
postInstallLogicFunctionUniversalIdentifiers.push(
extract.config.universalIdentifier,
);
const preInstallHookConfig =
extract.config as PreInstallLogicFunctionConfig;
preInstallLogicFunctions.push({
universalIdentifier: extract.config.universalIdentifier,
shouldRunOnVersionUpgrade:
preInstallHookConfig.shouldRunOnVersionUpgrade ?? false,
});
}
break;
@@ -346,31 +363,29 @@ export const buildManifest = async (
);
}
if (preInstallLogicFunctionUniversalIdentifiers.length > 1) {
errors.push(
'Only one pre install logic function is allowed per application',
);
}
if (postInstallLogicFunctionUniversalIdentifiers.length > 1) {
if (postInstallLogicFunctions.length > 1) {
errors.push(
'Only one post install logic function is allowed per application',
);
}
if (application && preInstallLogicFunctionUniversalIdentifiers.length >= 1) {
if (preInstallLogicFunctions.length > 1) {
errors.push(
'Only one pre install logic function is allowed per application',
);
}
if (application && postInstallLogicFunctions.length >= 1) {
application = {
...application,
preInstallLogicFunctionUniversalIdentifier:
preInstallLogicFunctionUniversalIdentifiers[0],
postInstallLogicFunction: postInstallLogicFunctions[0],
};
}
if (application && postInstallLogicFunctionUniversalIdentifiers.length >= 1) {
if (application && preInstallLogicFunctions.length >= 1) {
application = {
...application,
postInstallLogicFunctionUniversalIdentifier:
postInstallLogicFunctionUniversalIdentifiers[0],
preInstallLogicFunction: preInstallLogicFunctions[0],
};
}
@@ -4,8 +4,8 @@ export enum TargetFunction {
DefineApplication = 'defineApplication',
DefineField = 'defineField',
DefineLogicFunction = 'defineLogicFunction',
DefinePreInstallLogicFunction = 'definePreInstallLogicFunction',
DefinePostInstallLogicFunction = 'definePostInstallLogicFunction',
DefinePreInstallLogicFunction = 'definePreInstallLogicFunction',
DefineObject = 'defineObject',
DefineRole = 'defineRole',
DefineSkill = 'defineSkill',
@@ -40,10 +40,10 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
[TargetFunction.DefineApplication]: ManifestEntityKey.Application,
[TargetFunction.DefineField]: ManifestEntityKey.Fields,
[TargetFunction.DefineLogicFunction]: ManifestEntityKey.LogicFunctions,
[TargetFunction.DefinePreInstallLogicFunction]:
ManifestEntityKey.LogicFunctions,
[TargetFunction.DefinePostInstallLogicFunction]:
ManifestEntityKey.LogicFunctions,
[TargetFunction.DefinePreInstallLogicFunction]:
ManifestEntityKey.LogicFunctions,
[TargetFunction.DefineObject]: ManifestEntityKey.Objects,
[TargetFunction.DefineRole]: ManifestEntityKey.Roles,
[TargetFunction.DefineSkill]: ManifestEntityKey.Skills,
@@ -38,6 +38,14 @@ const findUniversalIdentifiers = (obj: object): string[] => {
if (key === 'universalIdentifier' && typeof val === 'string') {
universalIdentifiers.push(val);
}
if (
key === 'postInstallLogicFunction' ||
key === 'preInstallLogicFunction'
) {
continue;
}
if (typeof val === 'object') {
universalIdentifiers.push(...findUniversalIdentifiers(val));
}
@@ -4,6 +4,6 @@ export type ApplicationConfig = Omit<
ApplicationManifest,
| 'packageJsonChecksum'
| 'yarnLockChecksum'
| 'postInstallLogicFunctionUniversalIdentifier'
| 'preInstallLogicFunctionUniversalIdentifier'
| 'postInstallLogicFunction'
| 'preInstallLogicFunction'
>;
@@ -4,8 +4,11 @@ import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-c
import { type ObjectConfig } from '@/sdk/objects/object-config';
import { type PageLayoutConfig } from '@/sdk/page-layouts/page-layout-config';
import { type ViewConfig } from '@/sdk/views/view-config';
import { type PostInstallLogicFunctionConfig } from '@/sdk/logic-functions/post-install-logic-function-config';
import { type PreInstallLogicFunctionConfig } from '@/sdk/logic-functions/pre-install-logic-function-config';
import { type RoleConfig } from '@/sdk/roles/role-config';
import {
type AgentManifest,
type FieldManifest,
type NavigationMenuItemManifest,
type SkillManifest,
@@ -23,6 +26,9 @@ export type DefinableEntity =
| FieldManifest
| FrontComponentConfig
| LogicFunctionConfig
| PostInstallLogicFunctionConfig
| PreInstallLogicFunctionConfig
| AgentManifest
| RoleConfig
| SkillManifest
| ViewConfig
+3 -3
View File
@@ -40,9 +40,9 @@ export { defineLogicFunction } from './logic-functions/define-logic-function';
export { definePostInstallLogicFunction } from './logic-functions/define-post-install-logic-function';
export { definePreInstallLogicFunction } from './logic-functions/define-pre-install-logic-function';
export type {
InstallLogicFunctionHandler,
InstallLogicFunctionPayload,
} from './logic-functions/install-logic-function-payload-type';
InstallHandler,
InstallPayload,
} from '@/sdk/logic-functions/install-payload-type';
export type {
LogicFunctionConfig,
LogicFunctionHandler,
@@ -1,7 +1,7 @@
import { definePostInstallLogicFunction } from '@/sdk/logic-functions/define-post-install-logic-function';
import { type InstallLogicFunctionPayload } from '@/sdk/logic-functions/install-logic-function-payload-type';
import { type InstallPayload } from '@/sdk/logic-functions/install-payload-type';
const mockHandler = async (payload: InstallLogicFunctionPayload) => ({
const mockHandler = async (payload: InstallPayload) => ({
success: true,
previousVersion: payload.previousVersion,
});
@@ -24,12 +24,14 @@ describe('definePostInstallLogicFunction', () => {
...validRouteConfig,
description: 'Send a postcard to a contact',
timeoutSeconds: 30,
shouldRunOnVersionUpgrade: true,
};
const result = definePostInstallLogicFunction(config as any);
expect(result.config.description).toBe('Send a postcard to a contact');
expect(result.config.timeoutSeconds).toBe(30);
expect(result.config.shouldRunOnVersionUpgrade).toBe(true);
});
it('should return error when universalIdentifier is missing', () => {
@@ -1,7 +1,7 @@
import { definePreInstallLogicFunction } from '@/sdk/logic-functions/define-pre-install-logic-function';
import { type InstallLogicFunctionPayload } from '@/sdk/logic-functions/install-logic-function-payload-type';
import { type InstallPayload } from '@/sdk/logic-functions/install-payload-type';
const mockHandler = async (payload: InstallLogicFunctionPayload) => ({
const mockHandler = async (payload: InstallPayload) => ({
success: true,
previousVersion: payload.previousVersion,
});
@@ -9,11 +9,11 @@ const mockHandler = async (payload: InstallLogicFunctionPayload) => ({
describe('definePreInstallLogicFunction', () => {
const validRouteConfig = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'Send Postcard',
name: 'Prepare Install',
handler: mockHandler,
};
it('should return the config when valid with route trigger', () => {
it('should return the config when valid', () => {
const result = definePreInstallLogicFunction(validRouteConfig);
expect(result.config).toEqual(validRouteConfig);
@@ -22,19 +22,21 @@ describe('definePreInstallLogicFunction', () => {
it('should pass through optional fields', () => {
const config = {
...validRouteConfig,
description: 'Send a postcard to a contact',
description: 'Prepare state before install',
timeoutSeconds: 30,
shouldRunOnVersionUpgrade: true,
};
const result = definePreInstallLogicFunction(config as any);
expect(result.config.description).toBe('Send a postcard to a contact');
expect(result.config.description).toBe('Prepare state before install');
expect(result.config.timeoutSeconds).toBe(30);
expect(result.config.shouldRunOnVersionUpgrade).toBe(true);
});
it('should return error when universalIdentifier is missing', () => {
const config = {
name: 'Send Postcard',
name: 'Prepare Install',
handler: mockHandler,
};
const result = definePreInstallLogicFunction(config as any);
@@ -48,7 +50,7 @@ describe('definePreInstallLogicFunction', () => {
it('should return error when handler is missing', () => {
const config = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'Send Postcard',
name: 'Prepare Install',
};
const result = definePreInstallLogicFunction(config as any);
@@ -62,7 +64,7 @@ describe('definePreInstallLogicFunction', () => {
it('should return error when handler is not a function', () => {
const config = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'Send Postcard',
name: 'Prepare Install',
handler: 'not-a-function',
};
@@ -1,19 +1,9 @@
import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config';
import { type InstallLogicFunctionHandler } from '@/sdk/logic-functions/install-logic-function-payload-type';
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
import type { DefineEntity } from '@/sdk/common/types/define-entity.type';
import { type PostInstallLogicFunctionConfig } from '@/sdk/logic-functions/post-install-logic-function-config';
export const definePostInstallLogicFunction: DefineEntity<
Omit<
LogicFunctionConfig,
| 'cronTriggerSettings'
| 'databaseEventTriggerSettings'
| 'httpRouteTriggerSettings'
| 'isTool'
| 'handler'
> & {
handler: InstallLogicFunctionHandler;
}
PostInstallLogicFunctionConfig
> = (config) => {
const errors = [];
@@ -1,19 +1,9 @@
import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config';
import { type InstallLogicFunctionHandler } from '@/sdk/logic-functions/install-logic-function-payload-type';
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
import type { DefineEntity } from '@/sdk/common/types/define-entity.type';
import { type PreInstallLogicFunctionConfig } from '@/sdk/logic-functions/pre-install-logic-function-config';
export const definePreInstallLogicFunction: DefineEntity<
Omit<
LogicFunctionConfig,
| 'cronTriggerSettings'
| 'databaseEventTriggerSettings'
| 'httpRouteTriggerSettings'
| 'isTool'
| 'handler'
> & {
handler: InstallLogicFunctionHandler;
}
PreInstallLogicFunctionConfig
> = (config) => {
const errors = [];
@@ -1,7 +0,0 @@
export type InstallLogicFunctionPayload = {
previousVersion: string;
};
export type InstallLogicFunctionHandler = (
payload: InstallLogicFunctionPayload,
) => any | Promise<any>;
@@ -0,0 +1,6 @@
export type InstallPayload = {
previousVersion?: string;
newVersion: string;
};
export type InstallHandler = (payload: InstallPayload) => any | Promise<any>;
@@ -0,0 +1,5 @@
import { type PreInstallLogicFunctionConfig } from '@/sdk/logic-functions/pre-install-logic-function-config';
export type PostInstallLogicFunctionConfig = PreInstallLogicFunctionConfig & {
shouldRunSynchronously?: boolean;
};
@@ -0,0 +1,13 @@
import type { InstallHandler, LogicFunctionConfig } from '@/sdk';
export type PreInstallLogicFunctionConfig = Omit<
LogicFunctionConfig,
| 'cronTriggerSettings'
| 'databaseEventTriggerSettings'
| 'httpRouteTriggerSettings'
| 'isTool'
| 'handler'
> & {
handler: InstallHandler;
shouldRunOnVersionUpgrade?: boolean;
};
@@ -33,6 +33,8 @@ export class ApplicationExceptionFilter implements ExceptionFilter {
case ApplicationExceptionCode.CANNOT_DOWNGRADE_APPLICATION:
throw new UserInputError(exception);
case ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED:
case ApplicationExceptionCode.POST_INSTALL_ERROR:
case ApplicationExceptionCode.PRE_INSTALL_ERROR:
case ApplicationExceptionCode.TARBALL_EXTRACTION_FAILED:
case ApplicationExceptionCode.UPGRADE_FAILED:
throw new InternalServerError(exception);
@@ -10,8 +10,10 @@ import { ApplicationPackageModule } from 'src/engine/core-modules/application/ap
import { ApplicationInstallResolver } from 'src/engine/core-modules/application/application-install/application-install.resolver';
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
import { LogicFunctionModule } from 'src/engine/core-modules/logic-function/logic-function.module';
import { SdkClientModule } from 'src/engine/core-modules/sdk-client/sdk-client.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Module({
imports: [
@@ -21,9 +23,11 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
ApplicationPackageModule,
CacheLockModule,
FeatureFlagModule,
LogicFunctionModule,
SdkClientModule,
PermissionsModule,
FileStorageModule,
WorkspaceCacheModule,
],
providers: [ApplicationInstallResolver, ApplicationInstallService],
exports: [ApplicationInstallService],
@@ -25,7 +25,17 @@ import {
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import {
LogicFunctionTriggerJob,
type LogicFunctionTriggerJobData,
} from 'src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service';
@Injectable()
export class ApplicationInstallService {
private readonly logger = new Logger(ApplicationInstallService.name);
@@ -37,8 +47,12 @@ export class ApplicationInstallService {
private readonly applicationPackageFetcherService: ApplicationPackageFetcherService,
private readonly applicationSyncService: ApplicationSyncService,
private readonly fileStorageService: FileStorageService,
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
private readonly cacheLockService: CacheLockService,
private readonly sdkClientGenerationService: SdkClientGenerationService,
@InjectMessageQueue(MessageQueue.logicFunctionQueue)
private readonly messageQueueService: MessageQueueService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
async installApplication(params: {
@@ -110,7 +124,27 @@ export class ApplicationInstallService {
const universalIdentifier = appRegistration.universalIdentifier;
const existingApplication =
await this.applicationService.findByUniversalIdentifier({
universalIdentifier,
workspaceId: params.workspaceId,
});
const previousVersion = existingApplication?.version ?? undefined;
const newVersion = resolvedPackage.packageJson.version;
if (!isDefined(newVersion)) {
throw new ApplicationException(
`Package ${universalIdentifier} has no version`,
ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED,
);
}
const isVersionUpgrade = isDefined(existingApplication);
const { application, wasCreated } = await this.ensureApplicationExists({
existingApplication,
universalIdentifier,
name: resolvedPackage.manifest.application.displayName,
workspaceId: params.workspaceId,
@@ -156,6 +190,16 @@ export class ApplicationInstallService {
params.workspaceId,
);
await this.runPreInstallHook({
manifest: resolvedPackage.manifest,
workspaceId: params.workspaceId,
applicationRegistrationId: appRegistration.id,
previousVersion,
newVersion,
isVersionUpgrade,
universalIdentifier,
});
const { hasSchemaMetadataChanged } =
await this.applicationSyncService.synchronizeFromManifest({
workspaceId: params.workspaceId,
@@ -171,6 +215,15 @@ export class ApplicationInstallService {
});
}
await this.runPostInstallHook({
manifest: resolvedPackage.manifest,
workspaceId: params.workspaceId,
previousVersion,
newVersion,
isVersionUpgrade,
universalIdentifier,
});
this.logger.log(
`Successfully installed app ${universalIdentifier} v${resolvedPackage.packageJson.version ?? 'unknown'}`,
);
@@ -191,6 +244,185 @@ export class ApplicationInstallService {
}
}
private async runPreInstallHook(params: {
manifest: Manifest;
workspaceId: string;
applicationRegistrationId?: string;
previousVersion?: string;
newVersion: string;
isVersionUpgrade: boolean;
universalIdentifier: string;
}): Promise<void> {
const {
manifest,
workspaceId,
applicationRegistrationId,
previousVersion,
newVersion,
isVersionUpgrade,
universalIdentifier,
} = params;
if (!isDefined(manifest.application.preInstallLogicFunction)) {
return;
}
await this.applicationSyncService.preInstallSynchronizeFromManifest({
workspaceId: params.workspaceId,
manifest,
applicationRegistrationId,
});
const {
universalIdentifier: preInstallLogicFunctionUniversalIdentifier,
shouldRunOnVersionUpgrade,
} = manifest.application.preInstallLogicFunction;
if (isVersionUpgrade && !shouldRunOnVersionUpgrade) {
this.logger.log(
`Skipping pre-install hook for app ${universalIdentifier}: version upgrade and shouldRunOnVersionUpgrade is false`,
);
return;
}
const { flatLogicFunctionMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatLogicFunctionMaps',
]);
const flatLogicFunction =
flatLogicFunctionMaps.byUniversalIdentifier[
preInstallLogicFunctionUniversalIdentifier
];
// preInstallSynchronizeFromManifest should have registered this function
// moments ago — a miss here means the pared-down sync did not persist the
// entry, which is a real failure and should abort the install.
if (!isDefined(flatLogicFunction)) {
throw new ApplicationException(
`Pre-install logic function "${preInstallLogicFunctionUniversalIdentifier}" not found for application "${universalIdentifier}" after pre-install sync. The pared-down sync did not register the function as expected.`,
ApplicationExceptionCode.ENTITY_NOT_FOUND,
);
}
const payload = { previousVersion, newVersion };
this.logger.log(
`Executing pre-install hook for app ${universalIdentifier} with payload:`,
JSON.stringify(payload),
);
const result = await this.logicFunctionExecutorService.execute({
logicFunctionId: flatLogicFunction.id,
workspaceId,
payload,
});
if (!isDefined(result)) {
this.logger.log('Pre-install hook executed successfully');
}
if (result.error) {
throw new ApplicationException(
result.error.errorMessage,
ApplicationExceptionCode.PRE_INSTALL_ERROR,
);
}
}
private async runPostInstallHook(params: {
manifest: Manifest;
workspaceId: string;
previousVersion?: string;
newVersion: string;
isVersionUpgrade: boolean;
universalIdentifier: string;
}): Promise<void> {
const {
manifest,
workspaceId,
previousVersion,
newVersion,
isVersionUpgrade,
universalIdentifier,
} = params;
if (!isDefined(manifest.application.postInstallLogicFunction)) {
return;
}
const {
universalIdentifier: postInstallLogicFunctionUniversalIdentifier,
shouldRunOnVersionUpgrade,
shouldRunSynchronously,
} = manifest.application.postInstallLogicFunction;
if (isVersionUpgrade && !shouldRunOnVersionUpgrade) {
this.logger.log(
`Skipping post-install hook for app ${universalIdentifier}: version upgrade and shouldRunOnVersionUpgrade is false`,
);
return;
}
const { flatLogicFunctionMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatLogicFunctionMaps',
]);
const flatLogicFunction =
flatLogicFunctionMaps.byUniversalIdentifier[
postInstallLogicFunctionUniversalIdentifier
];
if (!isDefined(flatLogicFunction)) {
throw new ApplicationException(
`Post-install logic function "${postInstallLogicFunctionUniversalIdentifier}" not found for application "${universalIdentifier}" after sync. Manifest may reference a stale identifier.`,
ApplicationExceptionCode.ENTITY_NOT_FOUND,
);
}
const payload = { previousVersion, newVersion };
this.logger.log(
`Enqueuing post-install hook for app ${universalIdentifier} with payload:`,
JSON.stringify(payload),
);
if (!shouldRunSynchronously) {
await this.messageQueueService.add<LogicFunctionTriggerJobData[]>(
LogicFunctionTriggerJob.name,
[
{
logicFunctionId: flatLogicFunction.id,
workspaceId,
payload,
},
],
{ retryLimit: 3 },
);
return;
}
const result = await this.logicFunctionExecutorService.execute({
logicFunctionId: flatLogicFunction.id,
workspaceId,
payload,
});
if (!isDefined(result)) {
this.logger.log('Post-install hook executed successfully');
}
if (result.error) {
throw new ApplicationException(
result.error.errorMessage,
ApplicationExceptionCode.POST_INSTALL_ERROR,
);
}
}
private async writeFilesToStorage(
extractedDir: string,
manifest: Manifest,
@@ -269,19 +501,15 @@ export class ApplicationInstallService {
}
private async ensureApplicationExists(params: {
existingApplication: ApplicationEntity | null;
universalIdentifier: string;
name: string;
workspaceId: string;
applicationRegistrationId: string;
sourceType: ApplicationRegistrationSourceType;
}): Promise<{ application: ApplicationEntity; wasCreated: boolean }> {
const existing = await this.applicationService.findByUniversalIdentifier({
universalIdentifier: params.universalIdentifier,
workspaceId: params.workspaceId,
});
if (isDefined(existing)) {
return { application: existing, wasCreated: false };
if (isDefined(params.existingApplication)) {
return { application: params.existingApplication, wasCreated: false };
}
const application = await this.applicationService.create({
@@ -4,15 +4,15 @@ import { type Manifest } from 'twenty-shared/application';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util';
import { computeApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util';
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util';
import { computeApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util';
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@@ -33,6 +33,149 @@ export class ApplicationManifestMigrationService {
private readonly applicationService: ApplicationService,
) {}
async syncPreInstallLogicFunctionFromManifest({
manifest,
workspaceId,
ownerFlatApplication,
}: {
manifest: Manifest;
workspaceId: string;
ownerFlatApplication: FlatApplication;
}): Promise<void> {
const preInstallLogicFunction =
manifest.application.preInstallLogicFunction;
if (!isDefined(preInstallLogicFunction)) {
return;
}
const preInstallLogicFunctionManifest = manifest.logicFunctions.find(
(logicFunction) =>
logicFunction.universalIdentifier ===
preInstallLogicFunction.universalIdentifier,
);
if (!isDefined(preInstallLogicFunctionManifest)) {
throw new ApplicationException(
`Pre-install logic function "${preInstallLogicFunction.universalIdentifier}" is declared on the application manifest but not present in manifest.logicFunctions`,
ApplicationExceptionCode.ENTITY_NOT_FOUND,
);
}
const defaultRoleManifest = manifest.roles.find(
(role) =>
role.universalIdentifier ===
manifest.application.defaultRoleUniversalIdentifier,
);
// Pared-down manifest: only the pre-install logic function, every other
// entity array intentionally empty. Combined with
// inferDeletionFromMissingEntities: false below, this produces a purely
// additive migration that registers the pre-install logic function without
// touching any previously-synced metadata (important on upgrades).
const strippedDefaultRoleManifest = isDefined(defaultRoleManifest)
? {
...defaultRoleManifest,
objectPermissions: [],
fieldPermissions: [],
}
: undefined;
const preInstallOnlyManifest: Manifest = {
application: manifest.application,
objects: [],
fields: [],
logicFunctions: [preInstallLogicFunctionManifest],
frontComponents: [],
roles: isDefined(strippedDefaultRoleManifest)
? [strippedDefaultRoleManifest]
: [],
skills: [],
agents: [],
publicAssets: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
};
const now = new Date().toISOString();
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const cacheResult = await this.workspaceCacheService.getOrRecompute(
workspaceId,
[
...Object.values(ALL_METADATA_NAME).map(getMetadataFlatEntityMapsKey),
'featureFlagsMap',
],
);
const { featureFlagsMap, ...existingAllFlatEntityMaps } = cacheResult;
const fromAllFlatEntityMaps = getApplicationSubAllFlatEntityMaps({
applicationIds: [ownerFlatApplication.id],
fromAllFlatEntityMaps: existingAllFlatEntityMaps,
});
const toAllUniversalFlatEntityMaps =
computeApplicationManifestAllUniversalFlatEntityMaps({
manifest: preInstallOnlyManifest,
ownerFlatApplication,
now,
});
const dependencyAllFlatEntityMaps = getApplicationSubAllFlatEntityMaps({
applicationIds:
ownerFlatApplication.universalIdentifier ===
TWENTY_STANDARD_APPLICATION.universalIdentifier
? [twentyStandardFlatApplication.id]
: [ownerFlatApplication.id, twentyStandardFlatApplication.id],
fromAllFlatEntityMaps: existingAllFlatEntityMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromTo(
{
// inferDeletionFromMissingEntities is intentionally omitted (undefined)
// so this pared-down sync is purely additive — existing metadata for
// objects/fields/other logic functions that are absent from
// preInstallOnlyManifest are left untouched on upgrades.
buildOptions: {
isSystemBuild: false,
applicationUniversalIdentifier:
ownerFlatApplication.universalIdentifier,
},
fromToAllFlatEntityMaps: buildFromToAllUniversalFlatEntityMaps({
fromAllFlatEntityMaps,
toAllUniversalFlatEntityMaps,
}),
workspaceId,
dependencyAllFlatEntityMaps,
additionalCacheDataMaps: { featureFlagsMap },
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while syncing pre-install logic function',
);
}
await this.syncDefaultRoleAndSettingsCustomTab({
manifest,
workspaceId,
ownerFlatApplication,
});
this.logger.log(
`Pre-install logic function synced for application ${ownerFlatApplication.universalIdentifier}`,
);
}
async syncMetadataFromManifest({
manifest,
workspaceId,
@@ -71,6 +71,42 @@ export class ApplicationSyncService {
return syncResult;
}
// Registers the application + only the pre-install logic function in
// workspace metadata so the pre-install hook can resolve and execute it
// before the main synchronizeFromManifest runs the full migrations.
// No-op when the manifest does not declare a pre-install logic function.
public async preInstallSynchronizeFromManifest({
workspaceId,
manifest,
applicationRegistrationId,
}: {
workspaceId: string;
manifest: Manifest;
applicationRegistrationId?: string;
}): Promise<void> {
if (!isDefined(manifest.application.preInstallLogicFunction)) {
return;
}
const application = await this.syncApplication({
workspaceId,
manifest,
applicationRegistrationId,
});
const ownerFlatApplication: FlatApplication = application;
await this.applicationManifestMigrationService.syncPreInstallLogicFunctionFromManifest(
{
manifest,
workspaceId,
ownerFlatApplication,
},
);
this.logger.log('Pre-install sync from manifest completed');
}
private async syncApplication({
workspaceId,
manifest,
@@ -18,6 +18,8 @@ export enum ApplicationExceptionCode {
PACKAGE_RESOLUTION_FAILED = 'PACKAGE_RESOLUTION_FAILED',
TARBALL_EXTRACTION_FAILED = 'TARBALL_EXTRACTION_FAILED',
UPGRADE_FAILED = 'UPGRADE_FAILED',
PRE_INSTALL_ERROR = 'PRE_INSTALL_ERROR',
POST_INSTALL_ERROR = 'POST_INSTALL_ERROR',
APP_ALREADY_INSTALLED = 'APP_ALREADY_INSTALLED',
CANNOT_DOWNGRADE_APPLICATION = 'CANNOT_DOWNGRADE_APPLICATION',
}
@@ -52,6 +54,10 @@ const getApplicationExceptionUserFriendlyMessage = (
return msg`Failed to extract tarball.`;
case ApplicationExceptionCode.UPGRADE_FAILED:
return msg`Application upgrade failed.`;
case ApplicationExceptionCode.PRE_INSTALL_ERROR:
return msg`Application pre-install logic function failed.`;
case ApplicationExceptionCode.POST_INSTALL_ERROR:
return msg`Application post-install logic function failed.`;
case ApplicationExceptionCode.APP_ALREADY_INSTALLED:
return msg`This version of the application is already installed in this workspace.`;
case ApplicationExceptionCode.CANNOT_DOWNGRADE_APPLICATION:
@@ -552,6 +552,16 @@ msgstr "Die toepassing is nie in hierdie werkruimte geïnstalleer nie. Installee
msgid "Application not found."
msgstr "Toepassing nie gevind nie."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "التطبيق غير مثبت في مساحة العمل هذه. قم
msgid "Application not found."
msgstr "التطبيق غير موجود."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "L'aplicació no està instal·lada en aquest espai de treball. Instal·l
msgid "Application not found."
msgstr "Aplicació no trobada."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Aplikace není v tomto pracovním prostoru nainstalována. Nejprve ji na
msgid "Application not found."
msgstr "Aplikace nenalezena."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Applikationen er ikke installeret i dette arbejdsområde. Installer den
msgid "Application not found."
msgstr "Applikation ikke fundet."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Die Anwendung ist in diesem Arbeitsbereich nicht installiert. Installier
msgid "Application not found."
msgstr "Anwendung nicht gefunden."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Η εφαρμογή δεν είναι εγκατεστημένη σε α
msgid "Application not found."
msgstr "Η εφαρμογή δεν βρέθηκε."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -547,6 +547,16 @@ msgstr "Application is not installed in this workspace. Install it first."
msgid "Application not found."
msgstr "Application not found."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr "Application post-install logic function failed."
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr "Application pre-install logic function failed."
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "La aplicación no está instalada en este espacio de trabajo. Instálala
msgid "Application not found."
msgstr "Aplicación no encontrada."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Sovellusta ei ole asennettu tähän työtilaan. Asenna se ensin."
msgid "Application not found."
msgstr "Sovellusta ei löytynyt."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "L'application n'est pas installée dans cet espace de travail. Installez
msgid "Application not found."
msgstr "Application introuvable."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -552,6 +552,16 @@ msgstr "היישום אינו מותקן בסביבת עבודה זו. התקן
msgid "Application not found."
msgstr "היישום לא נמצא."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Az alkalmazás nincs telepítve ezen a munkaterületen. Először telep
msgid "Application not found."
msgstr "Alkalmazás nem található."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "L'applicazione non è installata in questo spazio di lavoro. Installala
msgid "Application not found."
msgstr "Applicazione non trovata."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "このワークスペースにはアプリケーションがインスト
msgid "Application not found."
msgstr "アプリケーションが見つかりません。"
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr ""
msgid "Application not found."
msgstr "애플리케이션을 찾을 수 없음."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "De applicatie is niet geïnstalleerd in deze werkruimte. Installeer deze
msgid "Application not found."
msgstr "Applicatie niet gevonden."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Applikasjonen er ikke installert i dette arbeidsområdet. Installer den
msgid "Application not found."
msgstr "Applikasjon ikke funnet."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Aplikacja nie jest zainstalowana w tym obszarze roboczym. Najpierw ją z
msgid "Application not found."
msgstr "Nie znaleziono aplikacji."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -547,6 +547,16 @@ msgstr ""
msgid "Application not found."
msgstr ""
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr ""
msgid "Application not found."
msgstr "Aplicação não encontrada."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "O aplicativo não está instalado neste espaço de trabalho. Instale-o p
msgid "Application not found."
msgstr "Aplicação não encontrada."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Aplicația nu este instalată în acest spațiu de lucru. Instalează-o
msgid "Application not found."
msgstr "Aplicația nu a fost găsită."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr ""
msgid "Application not found."
msgstr "Приложение не найдено."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Апликација није инсталирана у овом рад
msgid "Application not found."
msgstr "Апликација није пронађена."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Applikationen är inte installerad i den här arbetsytan. Installera den
msgid "Application not found."
msgstr "Applikation hittades inte."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Uygulama bu çalışma alanında kurulu değil. Önce kurun."
msgid "Application not found."
msgstr "Uygulama bulunamadı."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "Застосунок не встановлено у цьому робо
msgid "Application not found."
msgstr "Застосунок не знайдено."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr ""
msgid "Application not found."
msgstr "Không tìm thấy ứng dụng."
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "此工作区未安装该应用。请先安装它。"
msgid "Application not found."
msgstr "未找到应用程序。"
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -552,6 +552,16 @@ msgstr "此工作區未安裝應用程式。請先安裝。"
msgid "Application not found."
msgstr "找不到應用程式。"
#. js-lingui-id: kuNcoR
#: src/engine/core-modules/application/application.exception.ts
msgid "Application post-install logic function failed."
msgstr ""
#. js-lingui-id: UTxpa1
#: src/engine/core-modules/application/application.exception.ts
msgid "Application pre-install logic function failed."
msgstr ""
#. js-lingui-id: DE/icK
#: src/engine/core-modules/application/application-registration/application-registration.exception.ts
msgid "Application registration not found."
@@ -1,4 +1,7 @@
import { type ExtendedUIMessagePart } from 'twenty-shared/ai';
import {
type ExtendedFileUIPart,
type ExtendedUIMessagePart,
} from 'twenty-shared/ai';
import { type AgentMessagePartEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity';
@@ -24,12 +27,11 @@ export const mapDBPartToUIMessagePart = (
case 'file':
return {
type: 'file',
mediaType: part.fileFilename?.endsWith('.png')
? 'image/png'
: 'application/octet-stream',
mediaType: part.file?.mimeType ?? 'application/octet-stream',
filename: part.fileFilename ?? '',
url: '',
};
fileId: part.fileId ?? '',
} as ExtendedFileUIPart;
case 'source-url':
return {
type: 'source-url',

Some files were not shown because too many files have changed in this diff Show More