diff --git a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts index db9ebe37e0c..636eab9d19c 100644 --- a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts +++ b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts @@ -32,7 +32,7 @@ describe('copyBaseApplicationProject', () => { } }); - it('should create the correct folder structure with src/app/', async () => { + it('should create the correct folder structure with src/', async () => { await copyBaseApplicationProject({ appName: 'my-test-app', appDisplayName: 'My Test App', @@ -48,8 +48,8 @@ describe('copyBaseApplicationProject', () => { const appConfigPath = join(srcAppPath, 'application.config.ts'); expect(await fs.pathExists(appConfigPath)).toBe(true); - // Verify default-function.role.ts exists in src/app/ - const roleConfigPath = join(srcAppPath, 'default-function.role.ts'); + // Verify default.role.ts exists in src/app/ + const roleConfigPath = join(srcAppPath, 'default.role.ts'); expect(await fs.pathExists(roleConfigPath)).toBe(true); }); @@ -102,7 +102,7 @@ describe('copyBaseApplicationProject', () => { expect(yarnLockContent).toContain('yarn lockfile v1'); }); - it('should create application.config.ts with defineApp and correct values', async () => { + it('should create application.config.ts with defineApplication and correct values', async () => { await copyBaseApplicationProject({ appName: 'my-test-app', appDisplayName: 'My Test App', @@ -117,15 +117,15 @@ describe('copyBaseApplicationProject', () => { ); const appConfigContent = await fs.readFile(appConfigPath, 'utf8'); - // Verify it uses defineApp + // Verify it uses defineApplication expect(appConfigContent).toContain( - "import { defineApp } from 'twenty-sdk'", + "import { defineApplication } from 'twenty-sdk'", ); - expect(appConfigContent).toContain('export default defineApp({'); + expect(appConfigContent).toContain('export default defineApplication({'); // Verify it imports the role identifier expect(appConfigContent).toContain( - "import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default-function.role'", + "import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role'", ); // Verify display name and description @@ -139,11 +139,11 @@ describe('copyBaseApplicationProject', () => { // Verify it references the role expect(appConfigContent).toContain( - 'functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER', + 'defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER', ); }); - it('should create default-function.role.ts with defineRole and correct values', async () => { + it('should create default.role.ts with defineRole and correct values', async () => { await copyBaseApplicationProject({ appName: 'my-test-app', appDisplayName: 'My Test App', @@ -151,11 +151,7 @@ describe('copyBaseApplicationProject', () => { appDirectory: testAppDirectory, }); - const roleConfigPath = join( - testAppDirectory, - 'src', - 'default-function.role.ts', - ); + const roleConfigPath = join(testAppDirectory, 'src', 'default.role.ts'); const roleConfigContent = await fs.readFile(roleConfigPath, 'utf8'); // Verify it uses defineRole @@ -166,7 +162,7 @@ describe('copyBaseApplicationProject', () => { // Verify it exports the universal identifier constant expect(roleConfigContent).toContain( - 'export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER', + 'export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER', ); // Verify role label includes app name @@ -182,7 +178,7 @@ describe('copyBaseApplicationProject', () => { // Verify it has a universalIdentifier (UUID format) expect(roleConfigContent).toMatch( - /universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER/, + /universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER/, ); }); @@ -283,19 +279,19 @@ describe('copyBaseApplicationProject', () => { appDirectory: secondAppDir, }); - // Read both role configs const firstRoleConfig = await fs.readFile( - join(firstAppDir, 'src', 'default-function.role.ts'), + join(firstAppDir, 'src', 'default.role.ts'), 'utf8', ); + const secondRoleConfig = await fs.readFile( - join(secondAppDir, 'src', 'default-function.role.ts'), + join(secondAppDir, 'src', 'default.role.ts'), 'utf8', ); // Extract UUIDs using regex const uuidRegex = - /DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/; + /DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/; const firstUuid = firstRoleConfig.match(uuidRegex)?.[1]; const secondUuid = secondRoleConfig.match(uuidRegex)?.[1]; diff --git a/packages/create-twenty-app/src/utils/app-template.ts b/packages/create-twenty-app/src/utils/app-template.ts index 8277fb827c6..b5c3314e658 100644 --- a/packages/create-twenty-app/src/utils/app-template.ts +++ b/packages/create-twenty-app/src/utils/app-template.ts @@ -3,7 +3,7 @@ import { join } from 'path'; import { v4 } from 'uuid'; import { ASSETS_DIR } from 'twenty-shared/application'; -const APP_FOLDER = 'src'; +const SRC_FOLDER = 'src'; export const copyBaseApplicationProject = async ({ appName, @@ -26,27 +26,27 @@ export const copyBaseApplicationProject = async ({ await createYarnLock(appDirectory); - const appFolderPath = join(appDirectory, APP_FOLDER); + const sourceFolderPath = join(appDirectory, SRC_FOLDER); - await fs.ensureDir(appFolderPath); + await fs.ensureDir(sourceFolderPath); - await createDefaultServerlessFunctionRoleConfig({ + await createDefaultRoleConfig({ displayName: appDisplayName, - appDirectory: appFolderPath, + appDirectory: sourceFolderPath, }); await createDefaultFrontComponent({ - appDirectory: appFolderPath, + appDirectory: sourceFolderPath, }); await createDefaultFunction({ - appDirectory: appFolderPath, + appDirectory: sourceFolderPath, }); await createApplicationConfig({ displayName: appDisplayName, description: appDescription, - appDirectory: appFolderPath, + appDirectory: sourceFolderPath, }); }; @@ -103,7 +103,7 @@ yarn-error.log* await fs.writeFile(join(appDirectory, '.gitignore'), gitignoreContent); }; -const createDefaultServerlessFunctionRoleConfig = async ({ +const createDefaultRoleConfig = async ({ displayName, appDirectory, }: { @@ -114,11 +114,11 @@ const createDefaultServerlessFunctionRoleConfig = async ({ const content = `import { defineRole } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = '${universalIdentifier}'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: '${displayName} default function role', description: '${displayName} default function role', canReadAllObjectRecords: true, @@ -128,7 +128,7 @@ export default defineRole({ }); `; - await fs.writeFile(join(appDirectory, 'default-function.role.ts'), content); + await fs.writeFile(join(appDirectory, 'default.role.ts'), content); }; const createDefaultFrontComponent = async ({ @@ -171,23 +171,24 @@ const createDefaultFunction = async ({ const universalIdentifier = v4(); const triggerUniversalIdentifier = v4(); - const content = `import { defineFunction } from 'twenty-sdk'; + const content = `import { defineLogicFunction } from 'twenty-sdk'; const handler = async (): Promise<{ message: string }> => { return { message: 'Hello, World!' }; }; -export default defineFunction({ +// Logic function handler - rename and implement your logic +export default defineLogicFunction({ universalIdentifier: '${universalIdentifier}', - name: 'hello-world-function', - description: 'A sample serverless function', + name: 'hello-world-logic-function', + description: 'A simple logic function', timeoutSeconds: 5, handler, triggers: [ { universalIdentifier: '${triggerUniversalIdentifier}', type: 'route', - path: '/hello-world-function', + path: '/hello-world-logic-function', httpMethod: 'GET', isAuthRequired: false, }, @@ -195,7 +196,10 @@ export default defineFunction({ }); `; - await fs.writeFile(join(appDirectory, 'hello-world.function.ts'), content); + await fs.writeFile( + join(appDirectory, 'hello-world.logic-function.ts'), + content, + ); }; const createApplicationConfig = async ({ @@ -207,14 +211,14 @@ const createApplicationConfig = async ({ description?: string; appDirectory: string; }) => { - const content = `import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default-function.role'; + const content = `import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '${v4()}', displayName: '${displayName}', description: '${description ?? ''}', - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); `; diff --git a/packages/twenty-apps/hello-world/src/roles/function-role.ts b/packages/twenty-apps/hello-world/src/roles/function-role.ts index 5336828b1f9..c06e8c6f9c8 100644 --- a/packages/twenty-apps/hello-world/src/roles/function-role.ts +++ b/packages/twenty-apps/hello-world/src/roles/function-role.ts @@ -14,7 +14,7 @@ export const functionRole: RoleConfig = { canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -23,8 +23,8 @@ export const functionRole: RoleConfig = { ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, diff --git a/packages/twenty-docs/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/developers/extend/capabilities/apps.mdx index c2700ac3ee3..779e62b8f22 100644 --- a/packages/twenty-docs/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/developers/extend/capabilities/apps.mdx @@ -214,7 +214,7 @@ The SDK provides four helper functions with built-in validation for defining you | Function | Purpose | |----------|---------| -| `defineApp()` | Configure application metadata | +| `defineApplication()` | Configure application metadata | | `defineObject()` | Define custom objects with fields | | `defineFunction()` | Define logic functions with handlers | | `defineRole()` | Configure role permissions and object access | @@ -315,14 +315,14 @@ Every app has a single `application.config.ts` file that describes: - **How its functions run**: which role they use for permissions. - **(Optional) variables**: key–value pairs exposed to your functions as environment variables. -Use `defineApp()` to define your application configuration: +Use `defineApplication()` to define your application configuration: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -335,18 +335,18 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` Notes: - `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs. - `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). -- `functionRoleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below). +- `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below). #### Roles and permissions -Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `functionRoleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions. +Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions. - The runtime API key injected as `TWENTY_API_KEY` is derived from this default function role. - The typed client will be restricted to the permissions granted to that role. @@ -360,11 +360,11 @@ When you scaffold a new app, the CLI also creates a default role file. Use `defi // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -377,7 +377,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -386,8 +386,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -396,7 +396,7 @@ export default defineRole({ }); ``` -The `universalIdentifier` of this role is then referenced in `application.config.ts` as `functionRoleUniversalIdentifier`. In other words: +The `universalIdentifier` of this role is then referenced in `application.config.ts` as `roleUniversalIdentifier`. In other words: - **\*.role.ts** defines what the default function role can do. - **application.config.ts** points to that role so your functions inherit its permissions. @@ -592,8 +592,8 @@ When your function runs on Twenty, the platform injects credentials as environme Notes: - You do not need to pass URL or API key to the generated client. It reads `TWENTY_API_URL` and `TWENTY_API_KEY` from process.env at runtime. -- The API key's permissions are determined by the role referenced in your `application.config.ts` via `functionRoleUniversalIdentifier`. This is the default role used by logic functions of your application. -- Applications can define roles to follow least‑privilege. Grant only the permissions your functions need, then point `functionRoleUniversalIdentifier` to that role's universal identifier. +- The API key's permissions are determined by the role referenced in your `application.config.ts` via `roleUniversalIdentifier`. This is the default role used by logic functions of your application. +- Applications can define roles to follow least‑privilege. Grant only the permissions your functions need, then point `roleUniversalIdentifier` to that role's universal identifier. ### Hello World example diff --git a/packages/twenty-docs/l/ar/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/ar/developers/extend/capabilities/apps.mdx index 4dc8eb8a3f4..6b897f5a5f3 100644 --- a/packages/twenty-docs/l/ar/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/ar/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ Once you've switched workspaces with `auth:switch`, all subsequent commands will | دالة | الغرض | | ------------------ | ---------------------------------------- | -| `defineApp()` | تهيئة بيانات التطبيق الوصفية | +| `defineApplication()` | تهيئة بيانات التطبيق الوصفية | | `defineObject()` | تعريف كائنات مخصصة مع حقول | | `defineFunction()` | تعريف وظائف منطقية مع معالجات | | `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات | @@ -319,14 +319,14 @@ export default defineObject({ * **كيفية تشغيل وظائفه**: الدور الذي تستخدمه للأذونات. * **متغيرات (اختياري)**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة. -استخدم `defineApp()` لتعريف تهيئة تطبيقك: +استخدم `defineApplication()` لتعريف تهيئة تطبيقك: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ export default defineApp({ * حقول `universalIdentifier` هي معرّفات حتمية تخصك؛ أنشئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة. * `applicationVariables` تصبح متغيرات بيئة لوظائفك (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`). -* `functionRoleUniversalIdentifier` يجب أن يطابق الدور الذي تعرّفه في ملف `*.role.ts` (انظر أدناه). +* `defaultRoleUniversalIdentifier` يجب أن يطابق الدور الذي تعرّفه في ملف `*.role.ts` (انظر أدناه). #### الأدوار والصلاحيات -يمكن للتطبيقات تعريف أدوار تُغلّف الصلاحيات على كائنات وإجراءات مساحة العمل لديك. يعين الحقل `functionRoleUniversalIdentifier` في `application.config.ts` الدور الافتراضي الذي تستخدمه وظائف المنطق في تطبيقك. +يمكن للتطبيقات تعريف أدوار تُغلّف الصلاحيات على كائنات وإجراءات مساحة العمل لديك. يعين الحقل `defaultRoleUniversalIdentifier` في `application.config.ts` الدور الافتراضي الذي تستخدمه وظائف المنطق في تطبيقك. * مفتاح واجهة البرمجة في وقت التشغيل المحقون باسم `TWENTY_API_KEY` مستمد من دور الوظيفة الافتراضي هذا. * سيُقيَّد العميل مضبوط الأنواع بالأذونات الممنوحة لذلك الدور. @@ -365,11 +365,11 @@ export default defineApp({ // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -يُشار بعد ذلك إلى `universalIdentifier` لهذا الدور في `application.config.ts` باسم `functionRoleUniversalIdentifier`. بعبارة أخرى: +يُشار بعد ذلك إلى `universalIdentifier` لهذا الدور في `application.config.ts` باسم `defaultRoleUniversalIdentifier`. بعبارة أخرى: * **\\*.role.ts** يحدد ما يمكن أن يفعله الدور الافتراضي للوظيفة. * **application.config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك صلاحياته. @@ -606,8 +606,8 @@ const { me } = await client.query({ me: { id: true, displayName: true } }); الملاحظات: * لا تحتاج إلى تمرير عنوان URL أو مفتاح واجهة برمجة التطبيقات إلى العميل المُولَّد. يقوم بقراءة `TWENTY_API_URL` و`TWENTY_API_KEY` من process.env وقت التشغيل. -* تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `application.config.ts` عبر `functionRoleUniversalIdentifier`. هذا هو الدور الافتراضي الذي تستخدمه الوظائف المنطقية في تطبيقك. -* يمكن للتطبيقات تعريف أدوار لاتباع مبدأ أقل الامتياز. امنح فقط الأذونات التي تحتاجها وظائفك، ثم وجّه `functionRoleUniversalIdentifier` إلى المعرّف الشامل لذلك الدور. +* تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `application.config.ts` عبر `defaultRoleUniversalIdentifier`. هذا هو الدور الافتراضي الذي تستخدمه الوظائف المنطقية في تطبيقك. +* يمكن للتطبيقات تعريف أدوار لاتباع مبدأ أقل الامتياز. امنح فقط الأذونات التي تحتاجها وظائفك، ثم وجّه `defaultRoleUniversalIdentifier` إلى المعرّف الشامل لذلك الدور. ### مثال Hello World diff --git a/packages/twenty-docs/l/cs/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/cs/developers/extend/capabilities/apps.mdx index 60d50651557..d2d1c0077be 100644 --- a/packages/twenty-docs/l/cs/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/cs/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ SDK poskytuje čtyři pomocné funkce s vestavěnou validací pro definování e | Funkce | Účel | | ------------------ | ------------------------------------------------ | -| `defineApp()` | Konfigurace metadat aplikace | +| `defineApplication()` | Konfigurace metadat aplikace | | `defineObject()` | Definice vlastních objektů s poli | | `defineFunction()` | Definice logických funkcí s obslužnými funkcemi | | `defineRole()` | Konfigurace oprávnění rolí a přístupu k objektům | @@ -319,14 +319,14 @@ Každá aplikace má jeden soubor `application.config.ts`, který popisuje: * **Jak běží její funkce**: kterou roli používají pro oprávnění. * **(Volitelné) proměnné**: dvojice klíč–hodnota zpřístupněné vašim funkcím jako proměnné prostředí. -K definování konfigurace aplikace použijte `defineApp()`: +K definování konfigurace aplikace použijte `defineApplication()`: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ Poznámky: * Pole `universalIdentifier` jsou deterministická ID, která vlastníte; vygenerujte je jednou a udržujte je stabilní napříč synchronizacemi. * `applicationVariables` se stanou proměnnými prostředí pro vaše funkce (například `DEFAULT_RECIPIENT_NAME` je dostupné jako `process.env.DEFAULT_RECIPIENT_NAME`). -* `functionRoleUniversalIdentifier` se musí shodovat s rolí, kterou definujete ve svém souboru `*.role.ts` (viz níže). +* `defaultRoleUniversalIdentifier` se musí shodovat s rolí, kterou definujete ve svém souboru `*.role.ts` (viz níže). #### Role a oprávnění -Aplikace mohou definovat role, které zapouzdřují oprávnění k objektům a akcím ve vašem pracovním prostoru. Pole `functionRoleUniversalIdentifier` v `application.config.ts` určuje výchozí roli používanou logickými funkcemi vaší aplikace. +Aplikace mohou definovat role, které zapouzdřují oprávnění k objektům a akcím ve vašem pracovním prostoru. Pole `defaultRoleUniversalIdentifier` v `application.config.ts` určuje výchozí roli používanou logickými funkcemi vaší aplikace. * Běhový klíč API vložený jako `TWENTY_API_KEY` je odvozen z této výchozí role funkcí. * Typovaný klient bude omezen oprávněními udělenými této roli. @@ -365,11 +365,11 @@ Když vygenerujete novou aplikaci, CLI také vytvoří výchozí soubor role. K // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -Na `universalIdentifier` této role se poté odkazuje v `application.config.ts` jako na `functionRoleUniversalIdentifier`. Jinými slovy: +Na `universalIdentifier` této role se poté odkazuje v `application.config.ts` jako na `defaultRoleUniversalIdentifier`. Jinými slovy: * **\*.role.ts** definuje, co může výchozí role funkce dělat. * **application.config.ts** ukazuje na tuto roli, aby vaše funkce zdědily její oprávnění. @@ -606,8 +606,8 @@ Když vaše funkce běží na Twenty, platforma před spuštěním kódu vloží Poznámky: * Není nutné předávat URL ani klíč API vygenerovanému klientovi. Za běhu čte `TWENTY_API_URL` a `TWENTY_API_KEY` z process.env. -* Oprávnění API klíče jsou určena rolí odkazovanou v `application.config.ts` prostřednictvím `functionRoleUniversalIdentifier`. Toto je výchozí role používaná logickými funkcemi vaší aplikace. -* Aplikace mohou definovat role podle principu nejmenších oprávnění. Udělte pouze oprávnění, která vaše funkce potřebují, a poté nastavte `functionRoleUniversalIdentifier` na univerzální identifikátor této role. +* Oprávnění API klíče jsou určena rolí odkazovanou v `application.config.ts` prostřednictvím `defaultRoleUniversalIdentifier`. Toto je výchozí role používaná logickými funkcemi vaší aplikace. +* Aplikace mohou definovat role podle principu nejmenších oprávnění. Udělte pouze oprávnění, která vaše funkce potřebují, a poté nastavte `defaultRoleUniversalIdentifier` na univerzální identifikátor této role. ### Příklad Hello World diff --git a/packages/twenty-docs/l/de/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/de/developers/extend/capabilities/apps.mdx index 5e414d644bf..5de7e653c76 100644 --- a/packages/twenty-docs/l/de/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/de/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ Das SDK stellt vier Hilfsfunktionen mit eingebauter Validierung bereit, um Ihre | Funktion | Zweck | | ------------------ | ---------------------------------------------------- | -| `defineApp()` | Anwendungsmetadaten konfigurieren | +| `defineApplication()` | Anwendungsmetadaten konfigurieren | | `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren | | `defineFunction()` | Logikfunktionen mit Handlern definieren | | `defineRole()` | Rollenberechtigungen und Objektzugriff konfigurieren | @@ -319,14 +319,14 @@ Jede App hat eine einzelne Datei `application.config.ts`, die Folgendes beschrei * **Wie ihre Funktionen ausgeführt werden**: welche Rolle sie für Berechtigungen verwenden. * **(Optional) Variablen**: Schlüssel–Wert-Paare, die Ihren Funktionen als Umgebungsvariablen zur Verfügung gestellt werden. -Verwenden Sie `defineApp()`, um Ihre Anwendungskonfiguration zu definieren: +Verwenden Sie `defineApplication()`, um Ihre Anwendungskonfiguration zu definieren: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ Notizen: * `universalIdentifier`-Felder sind deterministische IDs, die Sie besitzen; generieren Sie sie einmal und halten Sie sie über Synchronisierungen hinweg stabil. * `applicationVariables` werden zu Umgebungsvariablen für Ihre Funktionen (zum Beispiel ist `DEFAULT_RECIPIENT_NAME` als `process.env.DEFAULT_RECIPIENT_NAME` verfügbar). -* `functionRoleUniversalIdentifier` muss mit der Rolle übereinstimmen, die Sie in Ihrer `*.role.ts`-Datei definieren (siehe unten). +* `defaultRoleUniversalIdentifier` muss mit der Rolle übereinstimmen, die Sie in Ihrer `*.role.ts`-Datei definieren (siehe unten). #### Rollen und Berechtigungen -Anwendungen können Rollen definieren, die Berechtigungen für die Objekte und Aktionen Ihres Workspaces kapseln. Das Feld `functionRoleUniversalIdentifier` in `application.config.ts` legt die Standardrolle fest, die von den Logikfunktionen Ihrer App verwendet wird. +Anwendungen können Rollen definieren, die Berechtigungen für die Objekte und Aktionen Ihres Workspaces kapseln. Das Feld `defaultRoleUniversalIdentifier` in `application.config.ts` legt die Standardrolle fest, die von den Logikfunktionen Ihrer App verwendet wird. * Der zur Laufzeit als `TWENTY_API_KEY` injizierte API-Schlüssel wird von dieser Standard-Funktionsrolle abgeleitet. * Der typisierte Client ist auf die dieser Rolle gewährten Berechtigungen beschränkt. @@ -365,11 +365,11 @@ Wenn Sie eine neue App erzeugen, erstellt die CLI auch eine Standard-Rolldatei. // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -Der `universalIdentifier` dieser Rolle wird anschließend in `application.config.ts` als `functionRoleUniversalIdentifier` referenziert. Anders ausgedrückt: +Der `universalIdentifier` dieser Rolle wird anschließend in `application.config.ts` als `defaultRoleUniversalIdentifier` referenziert. Anders ausgedrückt: * **\*.role.ts** definiert, was die Standard-Funktionsrolle darf. * **application.config.ts** verweist auf diese Rolle, sodass Ihre Funktionen deren Berechtigungen erben. @@ -606,8 +606,8 @@ Wenn Ihre Funktion auf Twenty läuft, injiziert die Plattform vor der Ausführun Notizen: * Sie müssen dem generierten Client weder URL noch API-Schlüssel übergeben. Er liest `TWENTY_API_URL` und `TWENTY_API_KEY` zur Laufzeit aus process.env. -* Die Berechtigungen des API-Schlüssels werden durch die Rolle bestimmt, auf die in Ihrer `application.config.ts` über `functionRoleUniversalIdentifier` verwiesen wird. Dies ist die Standardrolle, die von den Logikfunktionen Ihrer Anwendung verwendet wird. -* Anwendungen können Rollen definieren, um das Least-Privilege-Prinzip einzuhalten. Gewähren Sie nur die Berechtigungen, die Ihre Funktionen benötigen, und verweisen Sie dann mit `functionRoleUniversalIdentifier` auf den universellen Bezeichner dieser Rolle. +* Die Berechtigungen des API-Schlüssels werden durch die Rolle bestimmt, auf die in Ihrer `application.config.ts` über `defaultRoleUniversalIdentifier` verwiesen wird. Dies ist die Standardrolle, die von den Logikfunktionen Ihrer Anwendung verwendet wird. +* Anwendungen können Rollen definieren, um das Least-Privilege-Prinzip einzuhalten. Gewähren Sie nur die Berechtigungen, die Ihre Funktionen benötigen, und verweisen Sie dann mit `defaultRoleUniversalIdentifier` auf den universellen Bezeichner dieser Rolle. ### Hello-World-Beispiel diff --git a/packages/twenty-docs/l/es/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/es/developers/extend/capabilities/apps.mdx index 9388640fac7..0a2a1f3c540 100644 --- a/packages/twenty-docs/l/es/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/es/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ El SDK proporciona cuatro funciones auxiliares con validación incorporada para | Función | Propósito | | ------------------ | ---------------------------------------------- | -| `defineApp()` | Configura los metadatos de la aplicación | +| `defineApplication()` | Configura los metadatos de la aplicación | | `defineObject()` | Define objetos personalizados con campos | | `defineFunction()` | Define funciones de lógica con controladores | | `defineRole()` | Configura permisos de roles y acceso a objetos | @@ -319,14 +319,14 @@ Cada aplicación tiene un único archivo `application.config.ts` que describe: * **Cómo se ejecutan sus funciones**: qué rol usan para permisos. * **Variables (opcionales)**: pares clave–valor expuestos a tus funciones como variables de entorno. -Usa `defineApp()` para definir la configuración de tu aplicación: +Usa `defineApplication()` para definir la configuración de tu aplicación: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ Notas: * Los campos `universalIdentifier` son ID deterministas bajo tu control; genéralos una vez y mantenlos estables entre sincronizaciones. * Las `applicationVariables` se convierten en variables de entorno para tus funciones (por ejemplo, `DEFAULT_RECIPIENT_NAME` está disponible como `process.env.DEFAULT_RECIPIENT_NAME`). -* `functionRoleUniversalIdentifier` debe coincidir con el rol que defines en tu archivo `*.role.ts` (ver abajo). +* `defaultRoleUniversalIdentifier` debe coincidir con el rol que defines en tu archivo `*.role.ts` (ver abajo). #### Roles y permisos -Las aplicaciones pueden definir roles que encapsulan permisos sobre los objetos y acciones de tu espacio de trabajo. El campo `functionRoleUniversalIdentifier` en `application.config.ts` designa el rol predeterminado que usan las funciones de lógica de tu aplicación. +Las aplicaciones pueden definir roles que encapsulan permisos sobre los objetos y acciones de tu espacio de trabajo. El campo `defaultRoleUniversalIdentifier` en `application.config.ts` designa el rol predeterminado que usan las funciones de lógica de tu aplicación. * La clave de API en tiempo de ejecución inyectada como `TWENTY_API_KEY` se deriva de este rol de función predeterminado. * El cliente tipado estará restringido a los permisos otorgados a ese rol. @@ -365,11 +365,11 @@ Cuando generas una nueva aplicación, la CLI también crea un archivo de rol pre // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -El `universalIdentifier` de este rol se referencia luego en `application.config.ts` como `functionRoleUniversalIdentifier`. En otras palabras: +El `universalIdentifier` de este rol se referencia luego en `application.config.ts` como `defaultRoleUniversalIdentifier`. En otras palabras: * **\*.role.ts** define lo que puede hacer el rol de función predeterminado. * **application.config.ts** apunta a ese rol para que tus funciones hereden sus permisos. @@ -606,8 +606,8 @@ Cuando tu función se ejecuta en Twenty, la plataforma inyecta credenciales como Notas: * No necesitas pasar la URL ni la clave de API al cliente generado. Lee `TWENTY_API_URL` y `TWENTY_API_KEY` de process.env en tiempo de ejecución. -* Los permisos de la clave de API están determinados por el rol referenciado en tu `application.config.ts` mediante `functionRoleUniversalIdentifier`. Este es el rol predeterminado que usan las funciones de lógica de tu aplicación. -* Las aplicaciones pueden definir roles para seguir el principio de mínimo privilegio. Concede solo los permisos que necesitan tus funciones y después apunta `functionRoleUniversalIdentifier` al identificador universal de ese rol. +* Los permisos de la clave de API están determinados por el rol referenciado en tu `application.config.ts` mediante `defaultRoleUniversalIdentifier`. Este es el rol predeterminado que usan las funciones de lógica de tu aplicación. +* Las aplicaciones pueden definir roles para seguir el principio de mínimo privilegio. Concede solo los permisos que necesitan tus funciones y después apunta `defaultRoleUniversalIdentifier` al identificador universal de ese rol. ### Ejemplo Hello World diff --git a/packages/twenty-docs/l/fr/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/fr/developers/extend/capabilities/apps.mdx index 269102a6a25..1f98c1397d1 100644 --- a/packages/twenty-docs/l/fr/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/fr/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ Le SDK fournit quatre fonctions utilitaires avec validation intégrée pour déf | Fonction | Objectif | | ------------------ | ---------------------------------------------------------- | -| `defineApp()` | Configurer les métadonnées de l’application | +| `defineApplication()` | Configurer les métadonnées de l’application | | `defineObject()` | Définir des objets personnalisés avec des champs | | `defineFunction()` | Définir des fonctions logiques avec des gestionnaires | | `defineRole()` | Configurer les autorisations de rôle et l’accès aux objets | @@ -319,14 +319,14 @@ Chaque application dispose d’un seul fichier `application.config.ts` qui décr * **Exécution des fonctions** : le rôle utilisé pour les autorisations. * **Variables (facultatif)** : paires clé–valeur exposées à vos fonctions en tant que variables d’environnement. -Utilisez `defineApp()` pour définir la configuration de votre application : +Utilisez `defineApplication()` pour définir la configuration de votre application : ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ Notes : * Les champs `universalIdentifier` sont des identifiants déterministes que vous possédez ; générez-les une fois et conservez-les stables entre les synchronisations. * `applicationVariables` deviennent des variables d’environnement pour vos fonctions (par exemple, `DEFAULT_RECIPIENT_NAME` est disponible sous `process.env.DEFAULT_RECIPIENT_NAME`). -* `functionRoleUniversalIdentifier` doit correspondre au rôle que vous définissez dans votre fichier `*.role.ts` (voir ci-dessous). +* `defaultRoleUniversalIdentifier` doit correspondre au rôle que vous définissez dans votre fichier `*.role.ts` (voir ci-dessous). #### Rôles et autorisations -Les applications peuvent définir des rôles qui encapsulent des autorisations sur les objets et actions de votre espace de travail. Le champ `functionRoleUniversalIdentifier` dans `application.config.ts` désigne le rôle par défaut utilisé par les fonctions logiques de votre application. +Les applications peuvent définir des rôles qui encapsulent des autorisations sur les objets et actions de votre espace de travail. Le champ `defaultRoleUniversalIdentifier` dans `application.config.ts` désigne le rôle par défaut utilisé par les fonctions logiques de votre application. * La clé API d’exécution injectée sous `TWENTY_API_KEY` est dérivée de ce rôle de fonction par défaut. * Le client typé sera limité aux autorisations accordées à ce rôle. @@ -365,11 +365,11 @@ Lorsque vous générez une nouvelle application, la CLI crée également un fich // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -Le `universalIdentifier` de ce rôle est ensuite référencé dans `application.config.ts` en tant que `functionRoleUniversalIdentifier`. En d’autres termes : +Le `universalIdentifier` de ce rôle est ensuite référencé dans `application.config.ts` en tant que `defaultRoleUniversalIdentifier`. En d’autres termes : * **\*.role.ts** définit ce que le rôle de fonction par défaut peut faire. * **application.config.ts** pointe vers ce rôle afin que vos fonctions héritent de ses autorisations. @@ -606,8 +606,8 @@ Lorsque votre fonction s’exécute sur Twenty, la plateforme injecte des identi Notes: * Vous n’avez pas besoin de passer l’URL ou la clé API au client généré. Il lit `TWENTY_API_URL` et `TWENTY_API_KEY` depuis process.env à l’exécution. -* Les autorisations de la clé API sont déterminées par le rôle référencé dans votre `application.config.ts` via `functionRoleUniversalIdentifier`. Il s’agit du rôle par défaut utilisé par les fonctions logiques de votre application. -* Les applications peuvent définir des rôles pour appliquer le principe du moindre privilège. N’accordez que les autorisations dont vos fonctions ont besoin, puis faites pointer `functionRoleUniversalIdentifier` vers l’identifiant universel de ce rôle. +* Les autorisations de la clé API sont déterminées par le rôle référencé dans votre `application.config.ts` via `defaultRoleUniversalIdentifier`. Il s’agit du rôle par défaut utilisé par les fonctions logiques de votre application. +* Les applications peuvent définir des rôles pour appliquer le principe du moindre privilège. N’accordez que les autorisations dont vos fonctions ont besoin, puis faites pointer `defaultRoleUniversalIdentifier` vers l’identifiant universel de ce rôle. ### Exemple Hello World diff --git a/packages/twenty-docs/l/it/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/it/developers/extend/capabilities/apps.mdx index e0d602f618d..e78b6a0beef 100644 --- a/packages/twenty-docs/l/it/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/it/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ L'SDK fornisce quattro funzioni helper con convalida integrata per definire le e | Funzione | Scopo | | ------------------ | ------------------------------------------------------- | -| `defineApp()` | Configura i metadati dell'applicazione | +| `defineApplication()` | Configura i metadati dell'applicazione | | `defineObject()` | Definisci oggetti personalizzati con campi | | `defineFunction()` | Definisci funzioni logiche con handler | | `defineRole()` | Configura i permessi dei ruoli e l'accesso agli oggetti | @@ -319,14 +319,14 @@ Ogni app ha un singolo file `application.config.ts` che descrive: * **Come vengono eseguite le sue funzioni**: quale ruolo usano per i permessi. * **Variabili (opzionali)**: coppie chiave–valore esposte alle funzioni come variabili d'ambiente. -Usa `defineApp()` per definire la configurazione della tua applicazione: +Usa `defineApplication()` per definire la configurazione della tua applicazione: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ Note: * I campi `universalIdentifier` sono ID deterministici sotto il tuo controllo; generali una volta e mantienili stabili tra le sincronizzazioni. * `applicationVariables` diventano variabili d'ambiente per le tue funzioni (ad esempio, `DEFAULT_RECIPIENT_NAME` è disponibile come `process.env.DEFAULT_RECIPIENT_NAME`). -* `functionRoleUniversalIdentifier` deve corrispondere al ruolo che definisci nel tuo file `*.role.ts` (vedi sotto). +* `defaultRoleUniversalIdentifier` deve corrispondere al ruolo che definisci nel tuo file `*.role.ts` (vedi sotto). #### Ruoli e permessi -Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro. Il campo `functionRoleUniversalIdentifier` in `application.config.ts` designa il ruolo predefinito utilizzato dalle funzioni logiche della tua app. +Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro. Il campo `defaultRoleUniversalIdentifier` in `application.config.ts` designa il ruolo predefinito utilizzato dalle funzioni logiche della tua app. * La chiave API di runtime iniettata come `TWENTY_API_KEY` è derivata da questo ruolo funzione predefinito. * Il client tipizzato sarà limitato ai permessi concessi a quel ruolo. @@ -365,11 +365,11 @@ Quando generi una nuova app con lo scaffolder, la CLI crea anche un file di ruol // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -L'`universalIdentifier` di questo ruolo viene quindi referenziato in `application.config.ts` come `functionRoleUniversalIdentifier`. In altre parole: +L'`universalIdentifier` di questo ruolo viene quindi referenziato in `application.config.ts` come `defaultRoleUniversalIdentifier`. In altre parole: * **\*.role.ts** definisce ciò che il ruolo funzione predefinito può fare. * **application.config.ts** punta a quel ruolo in modo che le tue funzioni ne ereditino i permessi. @@ -606,8 +606,8 @@ Quando la tua funzione viene eseguita su Twenty, la piattaforma inietta le crede Note: * Non è necessario passare URL o chiave API al client generato. Legge `TWENTY_API_URL` e `TWENTY_API_KEY` da process.env in fase di esecuzione. -* I permessi della chiave API sono determinati dal ruolo referenziato in `application.config.ts` tramite `functionRoleUniversalIdentifier`. Questo è il ruolo predefinito utilizzato dalle funzioni logiche della tua applicazione. -* Le applicazioni possono definire ruoli per seguire il principio del privilegio minimo. Concedi solo i permessi necessari alle tue funzioni, quindi punta `functionRoleUniversalIdentifier` all'identificatore universale di quel ruolo. +* I permessi della chiave API sono determinati dal ruolo referenziato in `application.config.ts` tramite `defaultRoleUniversalIdentifier`. Questo è il ruolo predefinito utilizzato dalle funzioni logiche della tua applicazione. +* Le applicazioni possono definire ruoli per seguire il principio del privilegio minimo. Concedi solo i permessi necessari alle tue funzioni, quindi punta `defaultRoleUniversalIdentifier` all'identificatore universale di quel ruolo. ### Esempio Hello World diff --git a/packages/twenty-docs/l/ja/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/ja/developers/extend/capabilities/apps.mdx index 69529138048..d858172d5ee 100644 --- a/packages/twenty-docs/l/ja/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/ja/developers/extend/capabilities/apps.mdx @@ -217,7 +217,7 @@ twenty-sdk は、アプリ内で使用する型付きのビルディングブロ | 関数 | 目的 | | ------------------ | ------------------------------------ | -| `defineApp()` | アプリケーションのメタデータを構成 | +| `defineApplication()` | アプリケーションのメタデータを構成 | | `defineObject()` | フィールド付きのカスタムオブジェクトを定義 | | `defineFunction()` | Define logic functions with handlers | | `defineRole()` | ロールの権限とオブジェクトアクセスを構成 | @@ -317,14 +317,14 @@ export default defineObject({ * **関数の実行方法**: 権限に使用するロール。 * **(任意)変数**: 関数に環境変数として公開されるキーと値のペア。 -アプリケーション設定を定義するには `defineApp()` を使用します: +アプリケーション設定を定義するには `defineApplication()` を使用します: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -337,7 +337,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -345,11 +345,11 @@ export default defineApp({ * `universalIdentifier` フィールドは、あなたが管理する決定的な ID です。一度生成し、同期をまたいで安定したままにしてください。 * `applicationVariables` は関数の環境変数になります(例:`DEFAULT_RECIPIENT_NAME` は `process.env.DEFAULT_RECIPIENT_NAME` として利用可能)。 -* `functionRoleUniversalIdentifier` は、`*.role.ts` ファイルで定義するロールと一致している必要があります(下記参照)。 +* `defaultRoleUniversalIdentifier` は、`*.role.ts` ファイルで定義するロールと一致している必要があります(下記参照)。 #### ロールと権限 -アプリケーションは、ワークスペース内のオブジェクトやアクションに対する権限をカプセル化するロールを定義できます。 The field `functionRoleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions. +アプリケーションは、ワークスペース内のオブジェクトやアクションに対する権限をカプセル化するロールを定義できます。 The field `defaultRoleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions. * `TWENTY_API_KEY` として注入される実行時の API キーは、このデフォルトの関数ロールから派生します。 * 型付きクライアントの権限は、そのロールに付与された権限に制限されます。 @@ -363,11 +363,11 @@ export default defineApp({ // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -380,7 +380,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -389,8 +389,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -399,7 +399,7 @@ export default defineRole({ }); ``` -このロールの `universalIdentifier` は、`application.config.ts` で `functionRoleUniversalIdentifier` として参照されます。 言い換えると: +このロールの `universalIdentifier` は、`application.config.ts` で `defaultRoleUniversalIdentifier` として参照されます。 言い換えると: * **\*.role.ts** は、デフォルトの関数ロールで可能な操作を定義します。 * **application.config.ts** でそのロールを指定することで、関数はその権限を継承します。 @@ -604,8 +604,8 @@ const { me } = await client.query({ me: { id: true, displayName: true } }); ノート: * 生成されたクライアントに URL や API キーを渡す必要はありません。 実行時に process.env から `TWENTY_API_URL` と `TWENTY_API_KEY` を読み取ります。 -* API キーの権限は、`application.config.ts` で `functionRoleUniversalIdentifier` によって参照されるロールによって決まります。 This is the default role used by logic functions of your application. -* アプリケーションは、最小権限の原則に従うロールを定義できます。 関数に必要な権限のみを付与し、`functionRoleUniversalIdentifier` をそのロールのユニバーサル識別子に指定してください。 +* API キーの権限は、`application.config.ts` で `defaultRoleUniversalIdentifier` によって参照されるロールによって決まります。 This is the default role used by logic functions of your application. +* アプリケーションは、最小権限の原則に従うロールを定義できます。 関数に必要な権限のみを付与し、`defaultRoleUniversalIdentifier` をそのロールのユニバーサル識別子に指定してください。 ### Hello World の例 diff --git a/packages/twenty-docs/l/ko/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/ko/developers/extend/capabilities/apps.mdx index 6924c687677..e279c1b50dd 100644 --- a/packages/twenty-docs/l/ko/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/ko/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ SDK는 앱 엔티티를 정의할 때 사용할 수 있는, 내장 검증이 포 | 함수 | 목적 | | ------------------ | ------------------- | -| `defineApp()` | 애플리케이션 메타데이터 구성 | +| `defineApplication()` | 애플리케이션 메타데이터 구성 | | `defineObject()` | 필드가 있는 사용자 정의 객체 정의 | | `defineFunction()` | 핸들러가 있는 로직 함수 정의 | | `defineRole()` | 역할 권한과 객체 접근 구성 | @@ -319,14 +319,14 @@ export default defineObject({ * **함수가 실행되는 방식**: 권한을 위해 사용하는 역할. * **(선택 사항) 변수**: 함수에 환경 변수로 노출되는 키–값 쌍. -`defineApp()`을 사용해 애플리케이션 구성을 정의하세요: +`defineApplication()`을 사용해 애플리케이션 구성을 정의하세요: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ export default defineApp({ * `universalIdentifier` 필드는 고유하고 결정적인 ID입니다. 한 번 생성한 후 동기화 전반에 걸쳐 안정적으로 유지하세요. * `applicationVariables`는 함수의 환경 변수가 됩니다(예: `DEFAULT_RECIPIENT_NAME`는 `process.env.DEFAULT_RECIPIENT_NAME`로 사용 가능). -* `functionRoleUniversalIdentifier`는 `*.role.ts` 파일에서 정의한 역할과 일치해야 합니다(아래 참조). +* `defaultRoleUniversalIdentifier`는 `*.role.ts` 파일에서 정의한 역할과 일치해야 합니다(아래 참조). #### 역할 및 권한 -애플리케이션은 워크스페이스의 객체와 작업에 대한 권한을 캡슐화하는 역할을 정의할 수 있습니다. `application.config.ts`의 `functionRoleUniversalIdentifier` 필드는 앱의 로직 함수가 사용하는 기본 역할을 지정합니다. +애플리케이션은 워크스페이스의 객체와 작업에 대한 권한을 캡슐화하는 역할을 정의할 수 있습니다. `application.config.ts`의 `defaultRoleUniversalIdentifier` 필드는 앱의 로직 함수가 사용하는 기본 역할을 지정합니다. * `TWENTY_API_KEY`로 주입되는 런타임 API 키는 이 기본 함수 역할에서 파생됩니다. * 타입드 클라이언트는 해당 역할에 부여된 권한으로 제한됩니다. @@ -365,11 +365,11 @@ export default defineApp({ // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -이 역할의 `universalIdentifier`는 `application.config.ts`에서 `functionRoleUniversalIdentifier`로 참조됩니다. 다시 말해: +이 역할의 `universalIdentifier`는 `application.config.ts`에서 `defaultRoleUniversalIdentifier`로 참조됩니다. 다시 말해: * **\*.role.ts**는 기본 함수 역할이 수행할 수 있는 작업을 정의합니다. * **application.config.ts**는 해당 역할을 가리키므로, 함수는 그 권한을 상속받습니다. @@ -606,8 +606,8 @@ const { me } = await client.query({ me: { id: true, displayName: true } }); 노트: * 생성된 클라이언트에 URL이나 API 키를 전달할 필요가 없습니다. 런타임에 process.env에서 `TWENTY_API_URL`과 `TWENTY_API_KEY`를 읽습니다. -* API 키의 권한은 `application.config.ts`에서 `functionRoleUniversalIdentifier`를 통해 참조된 역할에 의해 결정됩니다. 이는 애플리케이션의 로직 함수에서 사용하는 기본 역할입니다. -* 애플리케이션은 최소 권한 원칙을 따르도록 역할을 정의할 수 있습니다. 함수에 필요한 권한만 부여하고, `functionRoleUniversalIdentifier`를 해당 역할의 universal identifier로 지정하세요. +* API 키의 권한은 `application.config.ts`에서 `defaultRoleUniversalIdentifier`를 통해 참조된 역할에 의해 결정됩니다. 이는 애플리케이션의 로직 함수에서 사용하는 기본 역할입니다. +* 애플리케이션은 최소 권한 원칙을 따르도록 역할을 정의할 수 있습니다. 함수에 필요한 권한만 부여하고, `defaultRoleUniversalIdentifier`를 해당 역할의 universal identifier로 지정하세요. ### Hello World 예제 diff --git a/packages/twenty-docs/l/pt/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/pt/developers/extend/capabilities/apps.mdx index 950e6f85358..baa81f8a9f1 100644 --- a/packages/twenty-docs/l/pt/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/pt/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ O SDK fornece quatro funções utilitárias com validação integrada para defin | Função | Finalidade | | ------------------ | ------------------------------------------------- | -| `defineApp()` | Configura os metadados do aplicativo | +| `defineApplication()` | Configura os metadados do aplicativo | | `defineObject()` | Define objetos personalizados com campos | | `defineFunction()` | Defina funções de lógica com handlers | | `defineRole()` | Configura permissões de papéis e acesso a objetos | @@ -319,14 +319,14 @@ Todo aplicativo tem um único arquivo `application.config.ts` que descreve: * **Como suas funções são executadas**: qual papel usam para permissões. * **Variáveis (opcional)**: pares chave–valor expostos às suas funções como variáveis de ambiente. -Use `defineApp()` para definir a configuração do seu aplicativo: +Use `defineApplication()` para definir a configuração do seu aplicativo: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ Notas: * `universalIdentifier` são IDs determinísticos que você controla; gere-os uma vez e mantenha-os estáveis entre sincronizações. * `applicationVariables` tornam-se variáveis de ambiente para suas funções (por exemplo, `DEFAULT_RECIPIENT_NAME` fica disponível como `process.env.DEFAULT_RECIPIENT_NAME`). -* `functionRoleUniversalIdentifier` deve corresponder ao papel que você define no seu arquivo `*.role.ts` (veja abaixo). +* `defaultRoleUniversalIdentifier` deve corresponder ao papel que você define no seu arquivo `*.role.ts` (veja abaixo). #### Papéis e permissões -Os aplicativos podem definir papéis que encapsulam permissões sobre os objetos e ações do seu espaço de trabalho. O campo `functionRoleUniversalIdentifier` em `application.config.ts` designa o papel padrão usado pelas funções de lógica do seu app. +Os aplicativos podem definir papéis que encapsulam permissões sobre os objetos e ações do seu espaço de trabalho. O campo `defaultRoleUniversalIdentifier` em `application.config.ts` designa o papel padrão usado pelas funções de lógica do seu app. * A chave de API em tempo de execução, injetada como `TWENTY_API_KEY`, é derivada desse papel padrão de função. * O cliente tipado ficará restrito às permissões concedidas a esse papel. @@ -365,11 +365,11 @@ Ao criar um novo aplicativo com o scaffold, a CLI também cria um arquivo de pap // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -O `universalIdentifier` desse papel é então referenciado em `application.config.ts` como `functionRoleUniversalIdentifier`. Em outras palavras: +O `universalIdentifier` desse papel é então referenciado em `application.config.ts` como `defaultRoleUniversalIdentifier`. Em outras palavras: * **\*.role.ts** define o que o papel de função padrão pode fazer. * **application.config.ts** aponta para esse papel para que suas funções herdem suas permissões. @@ -606,8 +606,8 @@ Quando sua função é executada no Twenty, a plataforma injeta credenciais como Notas: * Você não precisa passar a URL ou a chave de API para o cliente gerado. Ele lê `TWENTY_API_URL` e `TWENTY_API_KEY` de process.env em tempo de execução. -* As permissões da chave de API são determinadas pelo papel referenciado no seu `application.config.ts` via `functionRoleUniversalIdentifier`. Este é o papel padrão usado pelas funções de lógica do seu app. -* Os aplicativos podem definir papéis para seguir o princípio do menor privilégio. Conceda apenas as permissões de que suas funções precisam e, em seguida, aponte `functionRoleUniversalIdentifier` para o identificador universal desse papel. +* As permissões da chave de API são determinadas pelo papel referenciado no seu `application.config.ts` via `defaultRoleUniversalIdentifier`. Este é o papel padrão usado pelas funções de lógica do seu app. +* Os aplicativos podem definir papéis para seguir o princípio do menor privilégio. Conceda apenas as permissões de que suas funções precisam e, em seguida, aponte `defaultRoleUniversalIdentifier` para o identificador universal desse papel. ### Exemplo Hello World diff --git a/packages/twenty-docs/l/ro/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/ro/developers/extend/capabilities/apps.mdx index bcc51f23b08..248e1910d6f 100644 --- a/packages/twenty-docs/l/ro/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/ro/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ SDK-ul oferă patru funcții ajutătoare cu validare încorporată pentru defini | Funcție | Scop | | ------------------ | -------------------------------------------------------- | -| `defineApp()` | Configurați metadatele aplicației | +| `defineApplication()` | Configurați metadatele aplicației | | `defineObject()` | Definiți obiecte personalizate cu câmpuri | | `defineFunction()` | Definiți funcții de logică cu handleri | | `defineRole()` | Configurați permisiunile rolurilor și accesul la obiecte | @@ -319,14 +319,14 @@ Fiecare aplicație are un singur fișier `application.config.ts` care descrie: * **Cum rulează funcțiile**: ce rol folosesc pentru permisiuni. * **(Opțional) variabile**: perechi cheie–valoare expuse funcțiilor ca variabile de mediu. -Folosiți `defineApp()` pentru a defini configurația aplicației: +Folosiți `defineApplication()` pentru a defini configurația aplicației: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ Notițe: * Câmpurile `universalIdentifier` sunt ID-uri deterministe pe care le dețineți; generați-le o singură dată și păstrați-le stabile între sincronizări. * `applicationVariables` devin variabile de mediu pentru funcțiile dvs. (de exemplu, `DEFAULT_RECIPIENT_NAME` este disponibil ca `process.env.DEFAULT_RECIPIENT_NAME`). -* `functionRoleUniversalIdentifier` trebuie să corespundă rolului pe care îl definiți în fișierul `*.role.ts` (vedeți mai jos). +* `defaultRoleUniversalIdentifier` trebuie să corespundă rolului pe care îl definiți în fișierul `*.role.ts` (vedeți mai jos). #### Roluri și permisiuni -Aplicațiile pot defini roluri care încapsulează permisiuni asupra obiectelor și acțiunilor din spațiul dvs. de lucru. Câmpul `functionRoleUniversalIdentifier` din `application.config.ts` desemnează rolul implicit folosit de funcțiile de logică ale aplicației. +Aplicațiile pot defini roluri care încapsulează permisiuni asupra obiectelor și acțiunilor din spațiul dvs. de lucru. Câmpul `defaultRoleUniversalIdentifier` din `application.config.ts` desemnează rolul implicit folosit de funcțiile de logică ale aplicației. * Cheia API de runtime injectată ca `TWENTY_API_KEY` este derivată din acest rol implicit pentru funcții. * Clientul tipizat va fi restricționat la permisiunile acordate acelui rol. @@ -365,11 +365,11 @@ Când generați o aplicație nouă, CLI creează și un fișier de rol implicit. // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -`universalIdentifier` al acestui rol este apoi referențiat în `application.config.ts` ca `functionRoleUniversalIdentifier`. Cu alte cuvinte: +`universalIdentifier` al acestui rol este apoi referențiat în `application.config.ts` ca `defaultRoleUniversalIdentifier`. Cu alte cuvinte: * **\*.role.ts** definește ce poate face rolul implicit pentru funcții. * **application.config.ts** indică acel rol, astfel încât funcțiile moștenesc permisiunile lui. @@ -606,8 +606,8 @@ Când funcția rulează pe Twenty, platforma injectează acreditări ca variabil Notițe: * Nu trebuie să transmiteți URL-ul sau cheia API către clientul generat. Acesta citește `TWENTY_API_URL` și `TWENTY_API_KEY` din process.env la runtime. -* Permisiunile cheii API sunt determinate de rolul referențiat în `application.config.ts` prin `functionRoleUniversalIdentifier`. Acesta este rolul implicit folosit de funcțiile de logică ale aplicației. -* Aplicațiile pot defini roluri pentru a urma principiul celui mai mic privilegiu. Acordați doar permisiunile de care au nevoie funcțiile, apoi setați `functionRoleUniversalIdentifier` la identificatorul universal al acelui rol. +* Permisiunile cheii API sunt determinate de rolul referențiat în `application.config.ts` prin `defaultRoleUniversalIdentifier`. Acesta este rolul implicit folosit de funcțiile de logică ale aplicației. +* Aplicațiile pot defini roluri pentru a urma principiul celui mai mic privilegiu. Acordați doar permisiunile de care au nevoie funcțiile, apoi setați `defaultRoleUniversalIdentifier` la identificatorul universal al acelui rol. ### Exemplu Hello World diff --git a/packages/twenty-docs/l/ru/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/ru/developers/extend/capabilities/apps.mdx index 19a9e9f6526..f8bd7782dca 100644 --- a/packages/twenty-docs/l/ru/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/ru/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ SDK предоставляет четыре вспомогательных фу | Функция | Назначение | | ------------------ | ---------------------------------------------- | -| `defineApp()` | Настраивает метаданные приложения | +| `defineApplication()` | Настраивает метаданные приложения | | `defineObject()` | Определяет пользовательские объекты с полями | | `defineFunction()` | Определение логических функций с обработчиками | | `defineRole()` | Настраивает права роли и доступ к объектам | @@ -319,14 +319,14 @@ export default defineObject({ * **Как запускаются его функции**: какую роль они используют для прав доступа. * **(Необязательно) переменные**: пары ключ-значение, предоставляемые вашим функциям как переменные окружения. -Используйте `defineApp()` для определения конфигурации вашего приложения: +Используйте `defineApplication()` для определения конфигурации вашего приложения: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ export default defineApp({ * `universalIdentifier` — это детерминированные идентификаторы, которыми вы управляете; сгенерируйте их один раз и сохраняйте стабильными между синхронизациями. * `applicationVariables` становятся переменными окружения для ваших функций (например, `DEFAULT_RECIPIENT_NAME` доступна как `process.env.DEFAULT_RECIPIENT_NAME`). -* `functionRoleUniversalIdentifier` должен соответствовать роли, которую вы определяете в файле `*.role.ts` (см. ниже). +* `defaultRoleUniversalIdentifier` должен соответствовать роли, которую вы определяете в файле `*.role.ts` (см. ниже). #### Роли и разрешения -Приложения могут определять роли, инкапсулирующие права на объекты и действия в вашем рабочем пространстве. Поле `functionRoleUniversalIdentifier` в `application.config.ts` обозначает роль по умолчанию, используемую логическими функциями вашего приложения. +Приложения могут определять роли, инкапсулирующие права на объекты и действия в вашем рабочем пространстве. Поле `defaultRoleUniversalIdentifier` в `application.config.ts` обозначает роль по умолчанию, используемую логическими функциями вашего приложения. * Ключ API во время выполнения, подставляемый как `TWENTY_API_KEY`, получается из этой роли функции по умолчанию. * Типизированный клиент будет ограничен правами, предоставленными этой ролью. @@ -365,11 +365,11 @@ export default defineApp({ // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -`universalIdentifier` этой роли затем указывается в `application.config.ts` как `functionRoleUniversalIdentifier`. Иными словами: +`universalIdentifier` этой роли затем указывается в `application.config.ts` как `defaultRoleUniversalIdentifier`. Иными словами: * **\*.role.ts** определяет, что может делать роль функции по умолчанию. * **application.config.ts** указывает на эту роль, чтобы ваши функции наследовали её права. @@ -606,8 +606,8 @@ const { me } = await client.query({ me: { id: true, displayName: true } }); Заметки: * Вам не нужно передавать URL или ключ API сгенерированному клиенту. Он читает `TWENTY_API_URL` и `TWENTY_API_KEY` из process.env во время выполнения. -* Права ключа API определяются ролью, на которую ссылается ваш `application.config.ts` через `functionRoleUniversalIdentifier`. Это роль по умолчанию, используемая логическими функциями вашего приложения. -* Приложения могут определять роли, чтобы следовать принципу наименьших привилегий. Выдавайте только те права, которые нужны вашим функциям, затем укажите в `functionRoleUniversalIdentifier` универсальный идентификатор этой роли. +* Права ключа API определяются ролью, на которую ссылается ваш `application.config.ts` через `defaultRoleUniversalIdentifier`. Это роль по умолчанию, используемая логическими функциями вашего приложения. +* Приложения могут определять роли, чтобы следовать принципу наименьших привилегий. Выдавайте только те права, которые нужны вашим функциям, затем укажите в `defaultRoleUniversalIdentifier` универсальный идентификатор этой роли. ### Пример Hello World diff --git a/packages/twenty-docs/l/tr/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/tr/developers/extend/capabilities/apps.mdx index feb98882fee..c18c44f9381 100644 --- a/packages/twenty-docs/l/tr/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/tr/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ SDK, uygulama varlıklarınızı tanımlamak için yerleşik doğrulamaya sahip | Fonksiyon | Amaç | | ------------------ | ---------------------------------------------- | -| `defineApp()` | Uygulama meta verilerini yapılandırın | +| `defineApplication()` | Uygulama meta verilerini yapılandırın | | `defineObject()` | Alanlara sahip özel nesneler tanımlayın | | `defineFunction()` | İşleyicilerle mantık fonksiyonları tanımlayın | | `defineRole()` | Rol izinlerini ve nesne erişimini yapılandırın | @@ -319,14 +319,14 @@ Her uygulamanın aşağıdakileri açıklayan tek bir `application.config.ts` do * **Fonksiyonlarının nasıl çalıştığı**: izinler için hangi rolü kullandıkları. * **(İsteğe bağlı) değişkenler**: fonksiyonlarınıza ortam değişkenleri olarak sunulan anahtar–değer çiftleri. -Uygulama yapılandırmanızı tanımlamak için `defineApp()` kullanın: +Uygulama yapılandırmanızı tanımlamak için `defineApplication()` kullanın: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ Notlar: * `universalIdentifier` alanları size ait belirleyici kimliklerdir; bunları bir kez oluşturun ve eşitlemeler boyunca kararlı tutun. * `applicationVariables`, fonksiyonlarınız için ortam değişkenlerine dönüşür (örneğin, `DEFAULT_RECIPIENT_NAME` değeri `process.env.DEFAULT_RECIPIENT_NAME` olarak kullanılabilir). -* `functionRoleUniversalIdentifier`, `*.role.ts` dosyanızda tanımladığınız rolle eşleşmelidir (aşağıya bakın). +* `defaultRoleUniversalIdentifier`, `*.role.ts` dosyanızda tanımladığınız rolle eşleşmelidir (aşağıya bakın). #### Roller ve izinler -Uygulamalar, çalışma alanınızdaki nesneler ve eylemler üzerindeki izinleri kapsülleyen roller tanımlayabilir. `application.config.ts` içindeki `functionRoleUniversalIdentifier` alanı, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan rolü belirtir. +Uygulamalar, çalışma alanınızdaki nesneler ve eylemler üzerindeki izinleri kapsülleyen roller tanımlayabilir. `application.config.ts` içindeki `defaultRoleUniversalIdentifier` alanı, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan rolü belirtir. * `TWENTY_API_KEY` olarak enjekte edilen çalışma zamanı API anahtarı bu varsayılan fonksiyon rolünden türetilir. * Türlendirilmiş istemci, o role tanınan izinlerle sınırlandırılır. @@ -365,11 +365,11 @@ Yeni bir uygulama oluşturduğunuzda CLI ayrıca varsayılan bir rol dosyası da // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -Bu rolün `universalIdentifier` değeri, `application.config.ts` içinde `functionRoleUniversalIdentifier` olarak referans verilir. Başka bir deyişle: +Bu rolün `universalIdentifier` değeri, `application.config.ts` içinde `defaultRoleUniversalIdentifier` olarak referans verilir. Başka bir deyişle: * **\*.role.ts**, varsayılan fonksiyon rolünün neler yapabileceğini tanımlar. * **application.config.ts** bu role işaret eder, böylece fonksiyonlarınız onun izinlerini devralır. @@ -606,8 +606,8 @@ Fonksiyonunuz Twenty üzerinde çalıştığında, platform kodunuz yürütülme Notlar: * Oluşturulan istemciye URL veya API anahtarı geçirmeniz gerekmez. Çalışma zamanında `TWENTY_API_URL` ve `TWENTY_API_KEY` değerlerini process.env üzerinden okur. -* API anahtarının izinleri, `application.config.ts` içinde `functionRoleUniversalIdentifier` aracılığıyla referans verilen role göre belirlenir. Bu, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan roldür. -* Uygulamalar, en az ayrıcalık ilkesini izlemek için roller tanımlayabilir. Yalnızca fonksiyonlarınızın ihtiyaç duyduğu izinleri verin ve ardından `functionRoleUniversalIdentifier` değerini o rolün evrensel tanımlayıcısına yönlendirin. +* API anahtarının izinleri, `application.config.ts` içinde `defaultRoleUniversalIdentifier` aracılığıyla referans verilen role göre belirlenir. Bu, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan roldür. +* Uygulamalar, en az ayrıcalık ilkesini izlemek için roller tanımlayabilir. Yalnızca fonksiyonlarınızın ihtiyaç duyduğu izinleri verin ve ardından `defaultRoleUniversalIdentifier` değerini o rolün evrensel tanımlayıcısına yönlendirin. ### Hello World örneği diff --git a/packages/twenty-docs/l/zh/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/l/zh/developers/extend/capabilities/apps.mdx index 98a793506e4..5669a27e05d 100644 --- a/packages/twenty-docs/l/zh/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/l/zh/developers/extend/capabilities/apps.mdx @@ -219,7 +219,7 @@ twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以 | 函数 | 目的 | | ------------------ | ------------ | -| `defineApp()` | 配置应用元数据 | +| `defineApplication()` | 配置应用元数据 | | `defineObject()` | 定义带字段的自定义对象 | | `defineFunction()` | 定义带处理程序的逻辑函数 | | `defineRole()` | 配置角色权限和对象访问 | @@ -319,14 +319,14 @@ export default defineObject({ * **函数如何运行**:它们用于权限的角色。 * **(可选)变量**:以环境变量形式提供给函数的键值对。 -使用 `defineApp()` 定义你的应用配置: +使用 `defineApplication()` 定义你的应用配置: ```typescript // src/app/application.config.ts -import { defineApp } from 'twenty-sdk'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { defineApplication } from 'twenty-sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'My Twenty App', description: 'My first Twenty app', @@ -339,7 +339,7 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); ``` @@ -347,11 +347,11 @@ export default defineApp({ * `universalIdentifier` 字段是你拥有的确定性 ID;生成一次并在多次同步中保持稳定。 * `applicationVariables` 会变成函数可用的环境变量(例如,`DEFAULT_RECIPIENT_NAME` 可作为 `process.env.DEFAULT_RECIPIENT_NAME` 使用)。 -* `functionRoleUniversalIdentifier` 必须与在 `*.role.ts` 文件中定义的角色一致(见下文)。 +* `roleUniversalIdentifier` 必须与在 `*.role.ts` 文件中定义的角色一致(见下文)。 #### 角色和权限 -应用可以定义角色,以封装对工作空间对象与操作的权限。 `application.config.ts` 中的 `functionRoleUniversalIdentifier` 字段指定你的应用逻辑函数所使用的默认角色。 +应用可以定义角色,以封装对工作空间对象与操作的权限。 `application.config.ts` 中的 `roleUniversalIdentifier` 字段指定你的应用逻辑函数所使用的默认角色。 * 作为 `TWENTY_API_KEY` 注入的运行时 API 密钥源自该默认函数角色。 * 类型化客户端将受限于该角色授予的权限。 @@ -365,11 +365,11 @@ export default defineApp({ // src/app/default-function.role.ts import { defineRole, PermissionFlag } from 'twenty-sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -382,7 +382,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -391,8 +391,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, @@ -401,7 +401,7 @@ export default defineRole({ }); ``` -随后,该角色的 `universalIdentifier` 会在 `application.config.ts` 中以 `functionRoleUniversalIdentifier` 引用。 换句话说: +随后,该角色的 `universalIdentifier` 会在 `application.config.ts` 中以 `roleUniversalIdentifier` 引用。 换句话说: * **\*.role.ts** 定义默认函数角色可以执行的操作。 * **application.config.ts** 指向该角色,使你的函数继承其权限。 @@ -606,8 +606,8 @@ const { me } = await client.query({ me: { id: true, displayName: true } }); 备注: * 你无需向生成的客户端传递 URL 或 API 密钥。 它会在运行时从 process.env 读取 `TWENTY_API_URL` 和 `TWENTY_API_KEY`。 -* API 密钥的权限由 `application.config.ts` 中通过 `functionRoleUniversalIdentifier` 引用的角色决定。 这是你的应用逻辑函数使用的默认角色。 -* 应用可以定义角色以遵循最小权限原则。 仅授予函数所需的权限,然后将 `functionRoleUniversalIdentifier` 指向该角色的通用标识符。 +* API 密钥的权限由 `application.config.ts` 中通过 `roleUniversalIdentifier` 引用的角色决定。 这是你的应用逻辑函数使用的默认角色。 +* 应用可以定义角色以遵循最小权限原则。 仅授予函数所需的权限,然后将 `roleUniversalIdentifier` 指向该角色的通用标识符。 ### Hello World 示例 diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 73ffc600199..83fe92a8c90 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -271,7 +271,7 @@ export type Application = { applicationVariables: Array; canBeUninstalled: Scalars['Boolean']; defaultLogicFunctionRole?: Maybe; - defaultLogicFunctionRoleId?: Maybe; + defaultRoleId?: Maybe; description: Scalars['String']; id: Scalars['UUID']; logicFunctions: Array; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 7531213f88e..6204357217c 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -271,7 +271,7 @@ export type Application = { applicationVariables: Array; canBeUninstalled: Scalars['Boolean']; defaultLogicFunctionRole?: Maybe; - defaultLogicFunctionRoleId?: Maybe; + defaultRoleId?: Maybe; description: Scalars['String']; id: Scalars['UUID']; logicFunctions: Array; diff --git a/packages/twenty-sdk/scripts/generateBarrels.ts b/packages/twenty-sdk/scripts/generateBarrels.ts index 166e09dde4e..7b27f61e006 100644 --- a/packages/twenty-sdk/scripts/generateBarrels.ts +++ b/packages/twenty-sdk/scripts/generateBarrels.ts @@ -33,7 +33,7 @@ const EXCLUDED_DIRECTORIES = [ '**/internal/**', '**/cli/**', ] as const; -const ROOT_DIRECTORIES = ['application']; +const ROOT_DIRECTORIES = ['sdk']; const prettierConfigFile = prettier.resolveConfigFile(); if (prettierConfigFile == null) { diff --git a/packages/twenty-sdk/src/application/__tests__/define-app.spec.ts b/packages/twenty-sdk/src/application/__tests__/define-app.spec.ts deleted file mode 100644 index e542a31fdad..00000000000 --- a/packages/twenty-sdk/src/application/__tests__/define-app.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { defineApp } from '@/application'; - -describe('defineApp', () => { - it('should return the config when valid', () => { - const config = { - universalIdentifier: 'a9faf5f8-cf7e-4f24-9d37-fd523c30febe', - displayName: 'My App', - description: 'My app description', - icon: 'IconWorld', - functionRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2', - }; - - const result = defineApp(config); - - expect(result).toEqual(config); - }); - - it('should pass through all optional fields', () => { - const config = { - universalIdentifier: 'a9faf5f8-cf7e-4f24-9d37-fd523c30febe', - displayName: 'My App', - description: 'My app description', - icon: 'IconWorld', - applicationVariables: { - API_KEY: { - universalIdentifier: '3a327392-3a0f-4605-9223-0633f063eaf6', - description: 'API Key', - isSecret: true, - }, - }, - functionRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2', - }; - - const result = defineApp(config); - - expect(result).toEqual(config); - expect(result.applicationVariables).toBeDefined(); - expect(result.functionRoleUniversalIdentifier).toBe( - '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2', - ); - }); - - it('should throw error when universalIdentifier is missing', () => { - const config = { - displayName: 'My App', - }; - - expect(() => defineApp(config as any)).toThrow( - 'App must have a universalIdentifier', - ); - }); - - it('should throw error when universalIdentifier is empty string', () => { - const config = { - universalIdentifier: '', - displayName: 'My App', - }; - - expect(() => defineApp(config as any)).toThrow( - 'App must have a universalIdentifier', - ); - }); -}); diff --git a/packages/twenty-sdk/src/application/__tests__/define-front-component.spec.ts b/packages/twenty-sdk/src/application/__tests__/define-front-component.spec.ts deleted file mode 100644 index 7b2480b37ca..00000000000 --- a/packages/twenty-sdk/src/application/__tests__/define-front-component.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { defineFrontComponent } from '@/application'; - -// Mock component for testing -const MockComponent = () => null; - -describe('defineFrontComponent', () => { - const validConfig = { - universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', - name: 'My Component', - component: MockComponent, - }; - - it('should return the config when valid', () => { - const result = defineFrontComponent(validConfig); - - expect(result).toEqual(validConfig); - }); - - it('should pass through optional fields', () => { - const config = { - ...validConfig, - description: 'A sample front component', - }; - - const result = defineFrontComponent(config); - - expect(result.description).toBe('A sample front component'); - }); - - it('should throw error when universalIdentifier is missing', () => { - const config = { - name: 'My Component', - component: MockComponent, - }; - - expect(() => defineFrontComponent(config as any)).toThrow( - 'FrontComponent must have a universalIdentifier', - ); - }); - - it('should throw error when component is missing', () => { - const config = { - universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', - name: 'My Component', - }; - - expect(() => defineFrontComponent(config as any)).toThrow( - 'FrontComponent must have a component', - ); - }); - - it('should throw error when component is not a function', () => { - const config = { - universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', - name: 'My Component', - component: 'not-a-function', - }; - - expect(() => defineFrontComponent(config as any)).toThrow( - 'FrontComponent must have a component', - ); - }); - - it('should accept config without name', () => { - const config = { - universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', - component: MockComponent, - }; - - const result = defineFrontComponent(config as any); - - expect(result.name).toBeUndefined(); - }); -}); diff --git a/packages/twenty-sdk/src/application/__tests__/define-role.spec.ts b/packages/twenty-sdk/src/application/__tests__/define-role.spec.ts deleted file mode 100644 index 0c1a61f9fc1..00000000000 --- a/packages/twenty-sdk/src/application/__tests__/define-role.spec.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { defineRole } from '@/application'; - -describe('defineRole', () => { - const validConfig = { - universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', - label: 'App User', - description: 'Standard user role', - }; - - it('should return the config when valid', () => { - const result = defineRole(validConfig); - - expect(result).toEqual(validConfig); - }); - - it('should pass through all optional fields', () => { - const config = { - ...validConfig, - icon: 'IconUser', - canReadAllObjectRecords: true, - canUpdateAllObjectRecords: false, - canSoftDeleteAllObjectRecords: false, - canDestroyAllObjectRecords: false, - }; - - const result = defineRole(config); - - expect(result.icon).toBe('IconUser'); - expect(result.canReadAllObjectRecords).toBe(true); - }); - - it('should accept objectPermissions with objectNameSingular', () => { - const config = { - ...validConfig, - objectPermissions: [ - { - objectNameSingular: 'postCard', - canReadObjectRecords: true, - canUpdateObjectRecords: true, - }, - ], - }; - - const result = defineRole(config); - - expect(result.objectPermissions).toHaveLength(1); - expect(result.objectPermissions![0].objectNameSingular).toBe('postCard'); - }); - - it('should accept objectPermissions with objectUniversalIdentifier', () => { - const config = { - ...validConfig, - objectPermissions: [ - { - objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', - canReadObjectRecords: true, - }, - ], - }; - - const result = defineRole(config); - - expect(result.objectPermissions![0].objectUniversalIdentifier).toBe( - '54b589ca-eeed-4950-a176-358418b85c05', - ); - }); - - it('should accept fieldPermissions with fieldName', () => { - const config = { - ...validConfig, - fieldPermissions: [ - { - objectNameSingular: 'postCard', - fieldName: 'content', - canReadFieldValue: true, - canUpdateFieldValue: false, - }, - ], - }; - - const result = defineRole(config); - - expect(result.fieldPermissions).toHaveLength(1); - expect(result.fieldPermissions![0].fieldName).toBe('content'); - }); - - it('should accept fieldPermissions with fieldUniversalIdentifier', () => { - const config = { - ...validConfig, - fieldPermissions: [ - { - objectNameSingular: 'postCard', - fieldUniversalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b', - canReadFieldValue: true, - }, - ], - }; - - const result = defineRole(config); - - expect(result.fieldPermissions![0].fieldUniversalIdentifier).toBe( - '58a0a314-d7ea-4865-9850-7fb84e72f30b', - ); - }); - - it('should accept permissionFlags', () => { - const config = { - ...validConfig, - permissionFlags: ['UPLOAD_FILE', 'DOWNLOAD_FILE'], - }; - - const result = defineRole(config as any); - - expect(result.permissionFlags).toHaveLength(2); - }); - - it('should throw error when universalIdentifier is missing', () => { - const config = { - label: 'App User', - }; - - expect(() => defineRole(config as any)).toThrow( - 'Role must have a universalIdentifier', - ); - }); - - it('should throw error when label is missing', () => { - const config = { - universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', - }; - - expect(() => defineRole(config as any)).toThrow('Role must have a label'); - }); - - it('should throw error when objectPermission has neither objectNameSingular nor objectUniversalIdentifier', () => { - const config = { - ...validConfig, - objectPermissions: [ - { - canReadObjectRecords: true, - }, - ], - }; - - expect(() => defineRole(config as any)).toThrow( - 'Object permission must have either objectNameSingular or objectUniversalIdentifier', - ); - }); - - it('should throw error when fieldPermission has neither objectNameSingular nor objectUniversalIdentifier', () => { - const config = { - ...validConfig, - fieldPermissions: [ - { - fieldName: 'content', - canReadFieldValue: true, - }, - ], - }; - - expect(() => defineRole(config as any)).toThrow( - 'Field permission must have either objectNameSingular or objectUniversalIdentifier', - ); - }); - - it('should throw error when fieldPermission has neither fieldName nor fieldUniversalIdentifier', () => { - const config = { - ...validConfig, - fieldPermissions: [ - { - objectNameSingular: 'postCard', - canReadFieldValue: true, - }, - ], - }; - - expect(() => defineRole(config as any)).toThrow( - 'Field permission must have either fieldName or fieldUniversalIdentifier', - ); - }); -}); diff --git a/packages/twenty-sdk/src/application/__tests__/extend-object.spec.ts b/packages/twenty-sdk/src/application/__tests__/extend-object.spec.ts deleted file mode 100644 index e20a92aaed5..00000000000 --- a/packages/twenty-sdk/src/application/__tests__/extend-object.spec.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { extendObject } from '@/application/objects/extend-object'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { type ObjectExtensionManifest } from 'twenty-shared/application'; - -describe('extendObject', () => { - const validConfigByName: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', - type: FieldMetadataType.NUMBER, - name: 'healthScore', - label: 'Health Score', - }, - ], - }; - - const validConfigByUniversalId: ObjectExtensionManifest = { - targetObject: { - universalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440002', - type: FieldMetadataType.TEXT, - name: 'customNote', - label: 'Custom Note', - }, - ], - }; - - describe('valid configurations', () => { - it('should return the config when targeting by nameSingular', () => { - const result = extendObject(validConfigByName); - - expect(result).toEqual(validConfigByName); - }); - - it('should return the config when targeting by universalIdentifier', () => { - const result = extendObject(validConfigByUniversalId); - - expect(result).toEqual(validConfigByUniversalId); - }); - - it('should pass through optional field properties', () => { - const config: ObjectExtensionManifest = { - ...validConfigByName, - fields: [ - { - ...validConfigByName.fields[0], - description: 'A health score from 0-100', - icon: 'IconHeart', - }, - ], - }; - - const result = extendObject(config); - - expect(result.fields[0].description).toBe('A health score from 0-100'); - expect(result.fields[0].icon).toBe('IconHeart'); - }); - - it('should accept SELECT field with options', () => { - const config: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440003', - type: FieldMetadataType.SELECT, - name: 'churnRisk', - label: 'Churn Risk', - options: [ - { value: 'low', label: 'Low', color: 'green', position: 0 }, - { - value: 'medium', - label: 'Medium', - color: 'yellow', - position: 1, - }, - { value: 'high', label: 'High', color: 'red', position: 2 }, - ], - }, - ], - }; - - const result = extendObject(config); - - expect(result.fields[0].label).toBe('Churn Risk'); - }); - - it('should accept multiple fields', () => { - const config: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'person', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440004', - type: FieldMetadataType.NUMBER, - name: 'score', - label: 'Score', - }, - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440005', - type: FieldMetadataType.TEXT, - name: 'notes', - label: 'Notes', - }, - ], - }; - - const result = extendObject(config); - - expect(result.fields).toHaveLength(2); - }); - }); - - describe('targetObject validation', () => { - it('should throw error when targetObject is missing', () => { - const config = { - fields: validConfigByName.fields, - }; - - expect(() => extendObject(config as any)).toThrow( - 'Object extension must have a targetObject', - ); - }); - - it('should throw error when targetObject has neither nameSingular nor universalIdentifier', () => { - const config = { - targetObject: {}, - fields: validConfigByName.fields, - }; - - expect(() => extendObject(config as any)).toThrow( - 'targetObject must have either nameSingular or universalIdentifier', - ); - }); - - it('should throw error when targetObject has both nameSingular and universalIdentifier', () => { - const config = { - targetObject: { - nameSingular: 'company', - universalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', - }, - fields: validConfigByName.fields, - }; - - expect(() => extendObject(config as any)).toThrow( - 'targetObject cannot have both nameSingular and universalIdentifier', - ); - }); - }); - - describe('fields validation', () => { - it('should throw error when fields is missing', () => { - const config = { - targetObject: { - nameSingular: 'company', - }, - }; - - expect(() => extendObject(config as any)).toThrow( - 'Object extension must have at least one field', - ); - }); - - it('should throw error when fields is empty', () => { - const config = { - targetObject: { - nameSingular: 'company', - }, - fields: [], - }; - - expect(() => extendObject(config as any)).toThrow( - 'Object extension must have at least one field', - ); - }); - - it('should throw error when field is missing label', () => { - const config = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', - type: FieldMetadataType.NUMBER, - name: 'healthScore', - }, - ], - }; - - expect(() => extendObject(config as any)).toThrow( - 'Field must have a label', - ); - }); - - it('should throw error when field is missing name', () => { - const config = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', - type: FieldMetadataType.NUMBER, - label: 'Health Score', - }, - ], - }; - - expect(() => extendObject(config as any)).toThrow( - 'Field "Health Score" must have a name', - ); - }); - - it('should throw error when field is missing universalIdentifier', () => { - const config = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - type: FieldMetadataType.NUMBER, - name: 'healthScore', - label: 'Health Score', - }, - ], - }; - - expect(() => extendObject(config as any)).toThrow( - 'Field "Health Score" must have a universalIdentifier', - ); - }); - - it('should throw error when SELECT field has no options', () => { - const config = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', - type: FieldMetadataType.SELECT, - name: 'status', - label: 'Status', - }, - ], - }; - - expect(() => extendObject(config as any)).toThrow( - 'Field "Status" is a SELECT/MULTI_SELECT type and must have options', - ); - }); - - it('should throw error when MULTI_SELECT field has no options', () => { - const config = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', - type: FieldMetadataType.MULTI_SELECT, - name: 'tags', - label: 'Tags', - }, - ], - }; - - expect(() => extendObject(config as any)).toThrow( - 'Field "Tags" is a SELECT/MULTI_SELECT type and must have options', - ); - }); - - it('should throw error when SELECT field has empty options array', () => { - const config = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', - type: FieldMetadataType.SELECT, - name: 'status', - label: 'Status', - options: [], - }, - ], - }; - - expect(() => extendObject(config as any)).toThrow( - 'Field "Status" is a SELECT/MULTI_SELECT type and must have options', - ); - }); - }); -}); diff --git a/packages/twenty-sdk/src/application/application-config.ts b/packages/twenty-sdk/src/application/application-config.ts deleted file mode 100644 index 670199b88da..00000000000 --- a/packages/twenty-sdk/src/application/application-config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { type Application } from 'twenty-shared/application'; - -export type ApplicationConfig = Application; diff --git a/packages/twenty-sdk/src/application/define-app.ts b/packages/twenty-sdk/src/application/define-app.ts deleted file mode 100644 index 6e35b4e9747..00000000000 --- a/packages/twenty-sdk/src/application/define-app.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { type Application } from 'twenty-shared/application'; - -/** - * Define an application configuration with validation. - * - * @example - * ```typescript - * import { defineApp } from 'twenty-sdk'; - * import { APP_ID } from '../src/constants'; - * - * export default defineApp({ - * universalIdentifier: APP_ID, - * displayName: 'My App', - * description: 'My app description', - * icon: 'IconWorld', - * }); - * ``` - */ -export const defineApp = (config: T): T => { - if (!config.universalIdentifier) { - throw new Error('App must have a universalIdentifier'); - } - - return config; -}; diff --git a/packages/twenty-sdk/src/application/fields/field.decorator.ts b/packages/twenty-sdk/src/application/fields/field.decorator.ts deleted file mode 100644 index 917b12a711a..00000000000 --- a/packages/twenty-sdk/src/application/fields/field.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type FieldMetadataType } from 'twenty-shared/types'; -import { type FieldManifest } from 'twenty-shared/application'; - -export const Field = ( - _: FieldManifest, -): PropertyDecorator => { - return () => {}; -}; diff --git a/packages/twenty-sdk/src/application/fields/relation.decorator.ts b/packages/twenty-sdk/src/application/fields/relation.decorator.ts deleted file mode 100644 index 37fef1d9ad0..00000000000 --- a/packages/twenty-sdk/src/application/fields/relation.decorator.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { type SyncableEntityOptions } from '@/application/syncable-entity-options.type'; - -import { - type RelationOnDeleteAction, - type RelationType, -} from 'twenty-shared/types'; - -interface WorkspaceRelationMinimumBaseOptions { - label: string; - description?: string; - icon?: string; - inverseSideTargetUniversalIdentifier: string; - inverseSideFieldKey?: keyof TClass; - onDelete?: RelationOnDeleteAction; -} - -interface WorkspaceRegularRelationBaseOptions - extends WorkspaceRelationMinimumBaseOptions { - isMorphRelation?: false; -} - -interface WorkspaceMorphRelationBaseOptions - extends WorkspaceRelationMinimumBaseOptions { - isMorphRelation: true; - morphId: string; -} - -type WorkspaceRelationBaseOptions = - | WorkspaceRegularRelationBaseOptions - | WorkspaceMorphRelationBaseOptions; - -type WorkspaceOtherRelationOptions = - WorkspaceRelationBaseOptions & { - type: RelationType.ONE_TO_MANY; - }; - -type WorkspaceManyToOneRelationOptions = - WorkspaceRelationBaseOptions & { - type: RelationType.MANY_TO_ONE; - inverseSideFieldKey: keyof TClass; - }; - -type RelationOptions = SyncableEntityOptions & - (WorkspaceOtherRelationOptions | WorkspaceManyToOneRelationOptions); - -export const Relation = ( - _: RelationOptions, -): PropertyDecorator => { - return () => {}; -}; diff --git a/packages/twenty-sdk/src/application/front-components/define-front-component.ts b/packages/twenty-sdk/src/application/front-components/define-front-component.ts deleted file mode 100644 index d82bd736998..00000000000 --- a/packages/twenty-sdk/src/application/front-components/define-front-component.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { type FrontComponentConfig } from '@/application/front-components/front-component-config'; - -/** - * Define a front component configuration with validation. - * - * @example - * ```typescript - * import { defineFrontComponent } from 'twenty-sdk'; - * - * const MyComponent = () => { - * return
Hello World
; - * }; - * - * export default defineFrontComponent({ - * universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', - * name: 'My Component', - * description: 'A sample front component', - * component: MyComponent, - * }); - * ``` - */ -export const defineFrontComponent = ( - config: T, -): T => { - if (!config.universalIdentifier) { - throw new Error('FrontComponent must have a universalIdentifier'); - } - - if (typeof config.component !== 'function') { - throw new Error('FrontComponent must have a component'); - } - - return config; -}; diff --git a/packages/twenty-sdk/src/application/functions/define-function.ts b/packages/twenty-sdk/src/application/functions/define-function.ts deleted file mode 100644 index a51fec0202d..00000000000 --- a/packages/twenty-sdk/src/application/functions/define-function.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { type FunctionConfig } from '@/application/functions/function-config'; - -/** - * Define a logic function configuration with validation. - * - * @example - * ```typescript - * import { defineFunction } from 'twenty-sdk'; - * import { sendPostcard } from '../src/handlers/send-postcard'; - * - * export const config = defineFunction({ - * universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', - * name: 'Send Postcard', - * description: 'Send a postcard to a contact', - * timeoutSeconds: 30, - * handler: sendPostcard, - * triggers: [ - * { - * universalIdentifier: 'c9f84c8d-...', - * type: 'route', - * path: '/postcards/send', - * httpMethod: 'POST', - * isAuthRequired: true, - * }, - * ], - * }); - * ``` - */ -export const defineFunction = (config: T): T => { - if (!config.universalIdentifier) { - throw new Error('Function must have a universalIdentifier'); - } - - if (typeof config.handler !== 'function') { - throw new Error('Function must have a handler'); - } - - // Validate each trigger - for (const trigger of config.triggers ?? []) { - if (!trigger.universalIdentifier) { - throw new Error('Each trigger must have a universalIdentifier'); - } - - if (!trigger.type) { - throw new Error('Each trigger must have a type'); - } - - switch (trigger.type) { - case 'route': - if (!trigger.path) { - throw new Error('Route trigger must have a path'); - } - if (!trigger.httpMethod) { - throw new Error('Route trigger must have an httpMethod'); - } - break; - - case 'cron': - if (!trigger.pattern) { - throw new Error('Cron trigger must have a pattern'); - } - break; - - case 'databaseEvent': - if (!trigger.eventName) { - throw new Error('Database event trigger must have an eventName'); - } - break; - - default: - throw new Error( - `Unknown trigger type: ${(trigger as { type: string }).type}`, - ); - } - } - - return config; -}; diff --git a/packages/twenty-sdk/src/application/objects/define-object.ts b/packages/twenty-sdk/src/application/objects/define-object.ts deleted file mode 100644 index 66fca225c9a..00000000000 --- a/packages/twenty-sdk/src/application/objects/define-object.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { isNonEmptyString } from '@sniptt/guards'; -import { type ObjectManifest } from 'twenty-shared/application'; -import { validateFieldsOrThrow } from './validate-fields'; - -/** - * Define an object configuration with validation. - * - * @example - * ```typescript - * import { defineObject, FieldType } from 'twenty-sdk'; - * import { POST_CARD_ID, STATUS_OPTIONS } from '../../src/constants'; - * - * export default defineObject({ - * universalIdentifier: POST_CARD_ID, - * nameSingular: 'postCard', - * namePlural: 'postCards', - * labelSingular: 'Post Card', - * labelPlural: 'Post Cards', - * icon: 'IconMail', - * fields: [ - * { - * universalIdentifier: '...', - * name: 'content', - * type: FieldType.TEXT, - * label: 'Content', - * }, - * { - * universalIdentifier: '...', - * name: 'status', - * type: FieldType.SELECT, - * label: 'Status', - * options: STATUS_OPTIONS, - * }, - * ], - * }); - * ``` - */ -export const defineObject = (config: T): T => { - if (!isNonEmptyString(config.universalIdentifier)) { - throw new Error('Object must have a universalIdentifier'); - } - - if (!isNonEmptyString(config.nameSingular)) { - throw new Error('Object must have a nameSingular'); - } - - if (!isNonEmptyString(config.namePlural)) { - throw new Error('Object must have a namePlural'); - } - - if (!isNonEmptyString(config.labelSingular)) { - throw new Error('Object must have a labelSingular'); - } - - if (!isNonEmptyString(config.labelPlural)) { - throw new Error('Object must have a labelPlural'); - } - - validateFieldsOrThrow(config.fields); - - return config; -}; diff --git a/packages/twenty-sdk/src/application/objects/extend-object.ts b/packages/twenty-sdk/src/application/objects/extend-object.ts deleted file mode 100644 index 5908aee64de..00000000000 --- a/packages/twenty-sdk/src/application/objects/extend-object.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { type ObjectExtensionManifest } from 'twenty-shared/application'; -import { isNonEmptyString } from '@sniptt/guards'; -import { validateFieldsOrThrow } from './validate-fields'; - -/** - * Extend an existing object with additional fields. - * - * @example - * ```typescript - * import { extendObject, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk'; - * - * // Extend by object name - * export const companyExtension = extendObject({ - * targetObject: { - * nameSingular: 'company', - * }, - * fields: [ - * { - * universalIdentifier: '...', - * name: 'healthScore', - * type: FieldType.NUMBER, - * label: 'Health Score', - * description: 'Calculated customer health metric', - * }, - * { - * universalIdentifier: '...', - * name: 'churnRisk', - * type: FieldType.SELECT, - * label: 'Churn Risk', - * options: [ - * { label: 'Low', value: 'low', color: 'green' }, - * { label: 'Medium', value: 'medium', color: 'yellow' }, - * { label: 'High', value: 'high', color: 'red' }, - * ], - * }, - * ], - * }); - * - * // Or extend by universal identifier - * export const companyExtensionById = extendObject({ - * targetObject: { - * universalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company, - * }, - * fields: [...], - * }); - * ``` - */ -export const extendObject = ( - config: T, -): T => { - if (!config.targetObject) { - throw new Error('Object extension must have a targetObject'); - } - - const { nameSingular, universalIdentifier } = config.targetObject; - - if ( - !isNonEmptyString(nameSingular) && - !isNonEmptyString(universalIdentifier) - ) { - throw new Error( - 'targetObject must have either nameSingular or universalIdentifier', - ); - } - - if (isNonEmptyString(nameSingular) && isNonEmptyString(universalIdentifier)) { - throw new Error( - 'targetObject cannot have both nameSingular and universalIdentifier - they are mutually exclusive', - ); - } - - if (!config.fields || config.fields.length === 0) { - throw new Error('Object extension must have at least one field'); - } - - validateFieldsOrThrow(config.fields); - - return config; -}; diff --git a/packages/twenty-sdk/src/application/objects/object.decorator.ts b/packages/twenty-sdk/src/application/objects/object.decorator.ts deleted file mode 100644 index 49f2da656e5..00000000000 --- a/packages/twenty-sdk/src/application/objects/object.decorator.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type ObjectManifest } from 'twenty-shared/application'; - -type ObjectMetadataOptions = Omit; - -export const Object = (_: ObjectMetadataOptions): ClassDecorator => { - return () => {}; -}; diff --git a/packages/twenty-sdk/src/application/role-config.ts b/packages/twenty-sdk/src/application/role-config.ts deleted file mode 100644 index ef02c7b3055..00000000000 --- a/packages/twenty-sdk/src/application/role-config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { RoleManifest } from 'twenty-shared/application'; - -export type RoleConfig = RoleManifest; diff --git a/packages/twenty-sdk/src/application/roles/define-role.ts b/packages/twenty-sdk/src/application/roles/define-role.ts deleted file mode 100644 index 430b486a2ab..00000000000 --- a/packages/twenty-sdk/src/application/roles/define-role.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { type RoleConfig } from '@/application/role-config'; - -/** - * Define a role configuration with validation. - * - * @example - * ```typescript - * import { defineRole, PermissionFlag } from 'twenty-sdk'; - * - * export default defineRole({ - * universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', - * label: 'App User', - * description: 'Standard user role for the app', - * icon: 'IconUser', - * canReadAllObjectRecords: false, - * objectPermissions: [ - * { - * objectNameSingular: 'postCard', - * canReadObjectRecords: true, - * canUpdateObjectRecords: true, - * }, - * ], - * permissionFlags: [PermissionFlag.UPLOAD_FILE], - * }); - * ``` - */ -export const defineRole = (config: T): T => { - if (!config.universalIdentifier) { - throw new Error('Role must have a universalIdentifier'); - } - - if (!config.label) { - throw new Error('Role must have a label'); - } - - // Validate object permissions if provided - if (config.objectPermissions) { - for (const permission of config.objectPermissions) { - if ( - !permission.objectNameSingular && - !permission.objectUniversalIdentifier - ) { - throw new Error( - 'Object permission must have either objectNameSingular or objectUniversalIdentifier', - ); - } - } - } - - // Validate field permissions if provided - if (config.fieldPermissions) { - for (const permission of config.fieldPermissions) { - if ( - !permission.objectNameSingular && - !permission.objectUniversalIdentifier - ) { - throw new Error( - 'Field permission must have either objectNameSingular or objectUniversalIdentifier', - ); - } - if (!permission.fieldName && !permission.fieldUniversalIdentifier) { - throw new Error( - 'Field permission must have either fieldName or fieldUniversalIdentifier', - ); - } - } - } - - return config; -}; diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/application.config.ts b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/application.config.ts index 90377a673bc..c6d5c7e8fd4 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/application.config.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/application.config.ts @@ -1,9 +1,9 @@ -import { defineApp } from '@/application/define-app'; +import { defineApplication } from '@/sdk'; -export default defineApp({ +export default defineApplication({ universalIdentifier: 'invalid-app-0000-0000-0000-000000000001', displayName: 'Invalid App', description: 'An app with duplicate IDs for testing validation', icon: 'IconAlertTriangle', - functionRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002', + defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002', }); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/first.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/first.object.ts index 8f6fed6f004..2a1ca954c00 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/first.object.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/first.object.ts @@ -1,5 +1,4 @@ -import { defineObject } from '@/application/objects/define-object'; -import { FieldType } from '@/application/fields/field-type'; +import { defineObject, FieldType } from '@/sdk'; const DUPLICATE_ID = 'duplicate-id-0000-0000-000000000001'; diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/second.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/second.object.ts index acac2432ae7..bc0e3d9d716 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/second.object.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/second.object.ts @@ -1,5 +1,4 @@ -import { defineObject } from '@/application/objects/define-object'; -import { FieldType } from '@/application/fields/field-type'; +import { defineObject, FieldType } from '@/sdk'; const DUPLICATE_ID = 'duplicate-id-0000-0000-000000000001'; diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts index 2bb1f526ae1..d202d4757b2 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts @@ -1,8 +1,8 @@ -import { FieldType } from '@/application'; -import type { ApplicationManifest } from 'twenty-shared/application'; +import { FieldType } from '@/sdk'; +import type { Manifest } from 'twenty-shared/application'; import { PermissionFlagType } from 'twenty-shared/constants'; -export const EXPECTED_MANIFEST: ApplicationManifest = { +export const EXPECTED_MANIFEST: Manifest = { sources: {}, publicAssets: [ { @@ -24,7 +24,7 @@ export const EXPECTED_MANIFEST: ApplicationManifest = { }, description: 'A simple hello world app', displayName: 'Hello World', - functionRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', + defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', icon: 'IconWorld', universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', }, @@ -66,47 +66,43 @@ export const EXPECTED_MANIFEST: ApplicationManifest = { universalIdentifier: 'f1234567-abcd-4000-8000-000000000001', }, ], - objectExtensions: [ + + fields: [ { - fields: [ + objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', + description: 'Priority level for the post card (1-10)', + label: 'Priority', + name: 'priority', + type: FieldType.NUMBER, + universalIdentifier: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d', + }, + { + objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', + description: 'Post card category', + label: 'Category', + name: 'category', + options: [ { - description: 'Priority level for the post card (1-10)', - label: 'Priority', - name: 'priority', - type: FieldType.NUMBER, - universalIdentifier: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d', + color: 'blue', + label: 'Personal', + position: 0, + value: 'PERSONAL', }, { - description: 'Post card category', - label: 'Category', - name: 'category', - options: [ - { - color: 'blue', - label: 'Personal', - position: 0, - value: 'PERSONAL', - }, - { - color: 'green', - label: 'Business', - position: 1, - value: 'BUSINESS', - }, - { - color: 'orange', - label: 'Promotional', - position: 2, - value: 'PROMOTIONAL', - }, - ], - type: FieldType.SELECT, - universalIdentifier: '8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e', + color: 'green', + label: 'Business', + position: 1, + value: 'BUSINESS', + }, + { + color: 'orange', + label: 'Promotional', + position: 2, + value: 'PROMOTIONAL', }, ], - targetObject: { - nameSingular: 'postCard', - }, + type: FieldType.SELECT, + universalIdentifier: '8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e', }, ], objects: [ @@ -210,44 +206,6 @@ export const EXPECTED_MANIFEST: ApplicationManifest = { universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', }, ], - packageJson: { - name: 'rich-app', - version: '0.1.0', - license: 'MIT', - engines: { - node: '^24.5.0', - npm: 'please-use-yarn', - yarn: '>=4.0.2', - }, - packageManager: 'yarn@4.9.2', - scripts: { - 'auth:login': 'twenty auth:login', - 'auth:logout': 'twenty auth:logout', - 'auth:status': 'twenty auth:status', - 'auth:switch': 'twenty auth:switch', - 'auth:list': 'twenty auth:list', - 'app:dev': 'twenty app:dev', - 'entity:add': 'twenty entity:add', - 'app:generate': 'twenty app:generate', - 'function:logs': 'twenty function:logs', - 'function:execute': 'twenty function:execute', - 'app:uninstall': 'twenty app:uninstall', - help: 'twenty help', - lint: 'eslint', - 'lint:fix': 'eslint --fix', - }, - dependencies: { - 'twenty-sdk': 'latest', - }, - devDependencies: { - typescript: '^5.9.3', - '@types/node': '^24.7.2', - '@types/react': '^19.0.2', - react: '^19.0.2', - eslint: '^9.32.0', - 'typescript-eslint': '^8.50.0', - }, - }, roles: [ { canBeAssignedToAgents: false, @@ -276,8 +234,8 @@ export const EXPECTED_MANIFEST: ApplicationManifest = { { canReadFieldValue: false, canUpdateFieldValue: false, - fieldName: 'content', - objectNameSingular: 'postCard', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', }, ], label: 'Default function role', @@ -287,14 +245,14 @@ export const EXPECTED_MANIFEST: ApplicationManifest = { canReadObjectRecords: true, canSoftDeleteObjectRecords: false, canUpdateObjectRecords: true, - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', }, ], permissionFlags: [PermissionFlagType.APPLICATIONS], universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', }, ], - functions: [ + logicFunctions: [ { builtHandlerChecksum: '[checksum]', builtHandlerPath: 'src/root.function.mjs', @@ -377,4 +335,42 @@ export const EXPECTED_MANIFEST: ApplicationManifest = { universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', }, ], + packageJson: { + name: 'rich-app', + version: '0.1.0', + license: 'MIT', + engines: { + node: '^24.5.0', + npm: 'please-use-yarn', + yarn: '>=4.0.2', + }, + packageManager: 'yarn@4.9.2', + scripts: { + 'auth:login': 'twenty auth:login', + 'auth:logout': 'twenty auth:logout', + 'auth:status': 'twenty auth:status', + 'auth:switch': 'twenty auth:switch', + 'auth:list': 'twenty auth:list', + 'app:dev': 'twenty app:dev', + 'entity:add': 'twenty entity:add', + 'app:generate': 'twenty app:generate', + 'function:logs': 'twenty function:logs', + 'function:execute': 'twenty function:execute', + 'app:uninstall': 'twenty app:uninstall', + help: 'twenty help', + lint: 'eslint', + 'lint:fix': 'eslint --fix', + }, + dependencies: { + 'twenty-sdk': 'latest', + }, + devDependencies: { + typescript: '^5.9.3', + '@types/node': '^24.7.2', + '@types/react': '^19.0.2', + react: '^19.0.2', + eslint: '^9.32.0', + 'typescript-eslint': '^8.50.0', + }, + }, }; diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/entities.tests.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/entities.tests.ts index 1dce0c4041d..014e81b2177 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/entities.tests.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/entities.tests.ts @@ -4,8 +4,8 @@ import { OUTPUT_DIR } from 'twenty-shared/application'; export const defineEntitiesTests = (appPath: string): void => { const outputDir = join(appPath, OUTPUT_DIR); - describe('functions', () => { - it('should have built functions preserving source path structure', async () => { + describe('logicFunctions', () => { + it('should have built logicFunctions preserving source path structure', async () => { const files = await fs.readdir(outputDir, { recursive: true }); const sortedFiles = files.map((f) => f.toString()).sort(); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts index f6b63cb5e07..36b0abaea55 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/tests/manifest.tests.ts @@ -19,13 +19,13 @@ export const defineManifestTests = (appPath: string): void => { normalizeManifestForComparison(EXPECTED_MANIFEST), ); - for (const fn of manifest.functions) { + for (const fn of manifest.entities.logicFunctions) { expect(fn.builtHandlerChecksum).toBeDefined(); expect(fn.builtHandlerChecksum).not.toBeNull(); expect(typeof fn.builtHandlerChecksum).toBe('string'); } - for (const component of manifest.frontComponents ?? []) { + for (const component of manifest.entities.frontComponents ?? []) { expect(component.builtComponentChecksum).toBeDefined(); expect(component.builtComponentChecksum).not.toBeNull(); expect(typeof component.builtComponentChecksum).toBe('string'); @@ -44,11 +44,11 @@ export const defineManifestTests = (appPath: string): void => { it('should load all entity types', async () => { const manifest = await fs.readJson(manifestOutputPath); - expect(manifest?.objects).toHaveLength(2); - expect(manifest?.functions).toHaveLength(4); - expect(manifest?.frontComponents).toHaveLength(4); - expect(manifest?.roles).toHaveLength(2); - expect(manifest?.objectExtensions).toHaveLength(1); + expect(manifest?.entities.objects).toHaveLength(2); + expect(manifest?.entities.logicFunctions).toHaveLength(4); + expect(manifest?.entities.frontComponents).toHaveLength(4); + expect(manifest?.entities.roles).toHaveLength(2); + expect(manifest?.entities.objectExtensions).toHaveLength(1); }); }); }; diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/application.config.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/application.config.ts index a5dfbbc6322..9d328c2327e 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/application.config.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/application.config.ts @@ -1,7 +1,7 @@ -import { defineApp } from '@/application/define-app'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './src/roles/default-function.role'; +import { defineApplication } from '@/sdk'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './src/roles/default-function.role'; -export default defineApp({ +export default defineApplication({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', displayName: 'Hello World', description: 'A simple hello world app', @@ -14,5 +14,5 @@ export default defineApp({ isSecret: false, }, }, - functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/card.front-component.tsx b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/card.front-component.tsx index d1250659fcd..afc292f472e 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/card.front-component.tsx +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/card.front-component.tsx @@ -1,4 +1,4 @@ -import { defineFrontComponent } from '@/application/front-components/define-front-component'; +import { defineFrontComponent } from '@/sdk'; import { CardDisplay } from '../utils/card-display.component'; export default defineFrontComponent({ diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/greeting.front-component.tsx b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/greeting.front-component.tsx index 6478ffdb6b6..ed148e89eef 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/greeting.front-component.tsx +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/greeting.front-component.tsx @@ -1,4 +1,4 @@ -import { defineFrontComponent } from '@/application/front-components/define-front-component'; +import { defineFrontComponent } from '@/sdk'; import { DEFAULT_NAME, formatGreeting } from '../utils/greeting.util'; const GreetingComponent = () => { diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/test.front-component.tsx b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/test.front-component.tsx index 2c584d4be04..5bef8ed3817 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/test.front-component.tsx +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/test.front-component.tsx @@ -1,4 +1,4 @@ -import { defineFrontComponent } from '@/application/front-components/define-front-component'; +import { defineFrontComponent } from '@/sdk'; export const TestComponent = () => { return ( diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/fields/postCardCategoryField.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/fields/postCardCategoryField.ts new file mode 100644 index 00000000000..d96ef0ee80e --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/fields/postCardCategoryField.ts @@ -0,0 +1,22 @@ +import { defineField, FieldType } from '@/sdk'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/postCard.object'; +import { POST_CARD_EXTENSION_CATEGORY_FIELD_ID } from '@/cli/__tests__/apps/rich-app/src/fields/postCardNumber.field'; + +export default defineField({ + objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + universalIdentifier: POST_CARD_EXTENSION_CATEGORY_FIELD_ID, + type: FieldType.SELECT, + name: 'category', + label: 'Category', + description: 'Post card category', + options: [ + { value: 'PERSONAL', label: 'Personal', color: 'blue', position: 0 }, + { value: 'BUSINESS', label: 'Business', color: 'green', position: 1 }, + { + value: 'PROMOTIONAL', + label: 'Promotional', + color: 'orange', + position: 2, + }, + ], +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/fields/postCardNumber.field.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/fields/postCardNumber.field.ts new file mode 100644 index 00000000000..c471a7d175f --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/fields/postCardNumber.field.ts @@ -0,0 +1,17 @@ +import { defineField, FieldType } from '@/sdk'; +import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/postCard.object'; + +export const POST_CARD_EXTENSION_PRIORITY_FIELD_ID = + '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d'; + +export const POST_CARD_EXTENSION_CATEGORY_FIELD_ID = + '8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e'; + +export default defineField({ + objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, + universalIdentifier: POST_CARD_EXTENSION_PRIORITY_FIELD_ID, + type: FieldType.NUMBER, + name: 'priority', + label: 'Priority', + description: 'Priority level for the post card (1-10)', +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/greeting.function.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/logic-functions/greeting.function.ts similarity index 82% rename from packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/greeting.function.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/logic-functions/greeting.function.ts index 8c35f8132a2..12b4f305634 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/greeting.function.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/logic-functions/greeting.function.ts @@ -1,11 +1,11 @@ -import { defineFunction } from '@/application/functions/define-function'; +import { defineLogicFunction } from '@/sdk'; import { DEFAULT_NAME, formatGreeting } from '../utils/greeting.util'; const greetingHandler = () => { return formatGreeting(DEFAULT_NAME); }; -export default defineFunction({ +export default defineLogicFunction({ universalIdentifier: '9d412d9e-2caf-487c-8b66-d1585883dd4e', name: 'greeting-function', timeoutSeconds: 5, diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function-2.function.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/logic-functions/test-function-2.function.ts similarity index 75% rename from packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function-2.function.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/logic-functions/test-function-2.function.ts index a41752329bd..100ec80d59f 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function-2.function.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/logic-functions/test-function-2.function.ts @@ -1,7 +1,7 @@ -import { defineFunction } from '@/application/functions/define-function'; +import { defineLogicFunction } from '@/sdk'; import { testFunction2 } from '../utils/test-function-2.util'; -export const config = defineFunction({ +export const config = defineLogicFunction({ universalIdentifier: 'eb3ffc98-88ec-45d4-9b4a-56833b219ccb', name: 'test-function-2', timeoutSeconds: 2, diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function.function.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/logic-functions/test-function.function.ts similarity index 87% rename from packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function.function.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/logic-functions/test-function.function.ts index bd1dfe85991..2e8dea036a4 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function.function.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/logic-functions/test-function.function.ts @@ -1,11 +1,11 @@ -import { defineFunction } from '@/application/functions/define-function'; +import { defineLogicFunction } from '@/sdk'; import { formatFarewell } from '../utils/greeting.util'; const handler = () => { return formatFarewell('test'); }; -export default defineFunction({ +export default defineLogicFunction({ universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'test-function', timeoutSeconds: 2, diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object-extension.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object-extension.ts deleted file mode 100644 index e93f468c4ec..00000000000 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object-extension.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { extendObject } from '@/application/objects/extend-object'; -import { FieldType } from '@/application/fields/field-type'; - -export const POST_CARD_EXTENSION_PRIORITY_FIELD_ID = - '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d'; - -export const POST_CARD_EXTENSION_CATEGORY_FIELD_ID = - '8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e'; - -export default extendObject({ - targetObject: { - nameSingular: 'postCard', - }, - fields: [ - { - universalIdentifier: POST_CARD_EXTENSION_PRIORITY_FIELD_ID, - type: FieldType.NUMBER, - name: 'priority', - label: 'Priority', - description: 'Priority level for the post card (1-10)', - }, - { - universalIdentifier: POST_CARD_EXTENSION_CATEGORY_FIELD_ID, - type: FieldType.SELECT, - name: 'category', - label: 'Category', - description: 'Post card category', - options: [ - { value: 'PERSONAL', label: 'Personal', color: 'blue', position: 0 }, - { value: 'BUSINESS', label: 'Business', color: 'green', position: 1 }, - { - value: 'PROMOTIONAL', - label: 'Promotional', - color: 'orange', - position: 2, - }, - ], - }, - ], -}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object.ts index 23b1374a8d6..ea80b3a4875 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object.ts @@ -1,5 +1,4 @@ -import { defineObject } from '@/application/objects/define-object'; -import { FieldType } from '@/application/fields/field-type'; +import { defineObject, FieldType } from '@/sdk'; enum PostCardStatus { DRAFT = 'DRAFT', @@ -8,8 +7,11 @@ enum PostCardStatus { RETURNED = 'RETURNED', } +export const POST_CARD_UNIVERSAL_IDENTIFIER = + '54b589ca-eeed-4950-a176-358418b85c05'; + export default defineObject({ - universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05', + universalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER, nameSingular: 'postCard', namePlural: 'postCards', labelSingular: 'Post card', diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/roles/default-function.role.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/roles/default-function.role.ts index 9596245039f..6a27de17d93 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/roles/default-function.role.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/roles/default-function.role.ts @@ -1,11 +1,10 @@ -import { PermissionFlag } from '@/application/permission-flag-type'; -import { defineRole } from '@/application/roles/define-role'; +import { PermissionFlag, defineRole } from '@/sdk'; -export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER = +export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; export default defineRole({ - universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER, + universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', canReadAllObjectRecords: false, @@ -18,7 +17,7 @@ export default defineRole({ canBeAssignedToApiKeys: false, objectPermissions: [ { - objectNameSingular: 'postCard', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', canReadObjectRecords: true, canUpdateObjectRecords: true, canSoftDeleteObjectRecords: false, @@ -27,8 +26,8 @@ export default defineRole({ ], fieldPermissions: [ { - objectNameSingular: 'postCard', - fieldName: 'content', + objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050', + fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff', canReadFieldValue: false, canUpdateFieldValue: false, }, diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.front-component.tsx b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.front-component.tsx index 71f2d649dc7..da55710ec0f 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.front-component.tsx +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.front-component.tsx @@ -1,4 +1,4 @@ -import { defineFrontComponent } from '@/application/front-components/define-front-component'; +import { defineFrontComponent } from '@/sdk'; export const RootComponent = () => { return ( diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.function.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.function.ts index 86312c90c4a..504aad593c6 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.function.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.function.ts @@ -1,10 +1,10 @@ -import { defineFunction } from '@/application/functions/define-function'; +import { defineLogicFunction } from '@/sdk'; const rootHandler = () => { return 'root-function-result'; }; -export default defineFunction({ +export default defineLogicFunction({ universalIdentifier: 'f0f1f2f3-f4f5-4000-8000-000000000001', name: 'root-function', timeoutSeconds: 5, diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.object.ts index 5dcc0f737b6..780d123fdb2 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.object.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.object.ts @@ -1,5 +1,4 @@ -import { defineObject } from '@/application/objects/define-object'; -import { FieldType } from '@/application/fields/field-type'; +import { defineObject, FieldType } from '@/sdk'; export default defineObject({ universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000001', diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.role.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.role.ts index c2ecb8ca267..70a83fe0c4c 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.role.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.role.ts @@ -1,4 +1,4 @@ -import { defineRole } from '@/application/roles/define-role'; +import { defineRole } from '@/sdk'; export default defineRole({ universalIdentifier: 'c0c1c2c3-c4c5-4000-8000-000000000001', diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/app-dev.integration.spec.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/app-dev.integration.spec.ts index 37c729acf04..212f028a9ba 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/app-dev.integration.spec.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/app-dev.integration.spec.ts @@ -4,7 +4,7 @@ import { runAppDev } from '@/cli/__tests__/integration/utils/run-app-dev.util'; import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util'; import { defineConsoleOutputTests } from './tests/console-output.tests'; import { defineFrontComponentsTests } from './tests/front-components.tests'; -import { defineFunctionsTests } from './tests/functions.tests'; +import { defineLogicFunctionsTests } from './tests/logic-functions.tests'; import { defineManifestTests } from './tests/manifest.tests'; const APP_PATH = join(__dirname, '../..'); @@ -22,6 +22,6 @@ describe('root-app app:dev', () => { defineConsoleOutputTests(() => result); defineManifestTests(APP_PATH); - defineFunctionsTests(APP_PATH); + defineLogicFunctionsTests(APP_PATH); defineFrontComponentsTests(APP_PATH); }); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts index 62102bfe032..c3ba28efa8c 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts @@ -1,7 +1,7 @@ -import { FieldType } from '@/application'; -import type { ApplicationManifest } from 'twenty-shared/application'; +import { FieldType } from '@/sdk'; +import type { Manifest } from 'twenty-shared/application'; -export const EXPECTED_MANIFEST: ApplicationManifest = { +export const EXPECTED_MANIFEST: Manifest = { sources: {}, yarnLock: '', packageJson: { @@ -47,8 +47,10 @@ export const EXPECTED_MANIFEST: ApplicationManifest = { displayName: 'Root App', description: 'An app with all entities at root level', icon: 'IconFolder', - functionRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002', + defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002', }, + publicAssets: [], + fields: [], objects: [ { universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000030', @@ -68,7 +70,7 @@ export const EXPECTED_MANIFEST: ApplicationManifest = { ], }, ], - functions: [ + logicFunctions: [ { universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000010', name: 'my-function', diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/functions.tests.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/logic-functions.tests.ts similarity index 70% rename from packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/functions.tests.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/logic-functions.tests.ts index d99a87505e3..3156d56ea3a 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/functions.tests.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/logic-functions.tests.ts @@ -1,9 +1,9 @@ import * as fs from 'fs-extra'; import { join } from 'path'; -export const defineFunctionsTests = (appPath: string): void => { - describe('functions', () => { - it('should have built functions at root level', async () => { +export const defineLogicFunctionsTests = (appPath: string): void => { + describe('logicFunctions', () => { + it('should have built logicFunctions at root level', async () => { const outputDir = join(appPath, '.twenty/output'); const files = await fs.readdir(outputDir, { recursive: true }); const functionFiles = files diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts index 57af6e942b6..45e220ee0f7 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/tests/manifest.tests.ts @@ -1,6 +1,6 @@ import * as fs from 'fs-extra'; import { join } from 'path'; -import { type ApplicationManifest } from 'twenty-shared/application'; +import { type Manifest } from 'twenty-shared/application'; import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util'; import { EXPECTED_MANIFEST } from '@/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest'; @@ -16,21 +16,22 @@ export const defineManifestTests = (appPath: string): void => { it('should have correct manifest content', async () => { const manifestPath = join(appPath, '.twenty/output/manifest.json'); - const manifest: ApplicationManifest = await fs.readJSON(manifestPath); + const manifest: Manifest = await fs.readJSON(manifestPath); expect(manifest.application).toEqual(EXPECTED_MANIFEST.application); expect(manifest.objects).toEqual(EXPECTED_MANIFEST.objects); expect( - normalizeManifestForComparison({ functions: manifest.functions }) - .functions, + normalizeManifestForComparison({ + logicFunctions: manifest.logicFunctions, + }).logicFunctions, ).toEqual( normalizeManifestForComparison({ - functions: EXPECTED_MANIFEST.functions, - }).functions, + logicFunctions: EXPECTED_MANIFEST.logicFunctions, + }).logicFunctions, ); - for (const fn of manifest.functions) { + for (const fn of manifest.logicFunctions) { expect(fn.builtHandlerChecksum).toBeDefined(); expect(fn.builtHandlerChecksum).not.toBeNull(); expect(typeof fn.builtHandlerChecksum).toBe('string'); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/application.config.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/application.config.ts index 6315b9e7ded..cede064231c 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/application.config.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/application.config.ts @@ -1,9 +1,9 @@ -import { defineApp } from '@/application/define-app'; +import { defineApplication } from '@/sdk'; -export default defineApp({ +export default defineApplication({ universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001', displayName: 'Root App', description: 'An app with all entities at root level', icon: 'IconFolder', - functionRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002', + defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002', }); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.front-component.tsx b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.front-component.tsx index c35f5b87660..6806684e8b4 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.front-component.tsx +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.front-component.tsx @@ -1,4 +1,4 @@ -import { defineFrontComponent } from '@/application/front-components/define-front-component'; +import { defineFrontComponent } from '@/sdk'; export const MyComponent = () => { return ( diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.function.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.function.ts index ba55ca945c1..974ae0a28e9 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.function.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.function.ts @@ -1,10 +1,10 @@ -import { defineFunction } from '@/application/functions/define-function'; +import { defineLogicFunction } from '@/sdk'; const myHandler = () => { return 'my-function-result'; }; -export default defineFunction({ +export default defineLogicFunction({ universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000010', name: 'my-function', timeoutSeconds: 5, diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.object.ts index 179de5bc07e..21670d3bd55 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.object.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.object.ts @@ -1,5 +1,4 @@ -import { FieldType } from '@/application/fields/field-type'; -import { defineObject } from '@/application/objects/define-object'; +import { FieldType, defineObject } from '@/sdk'; export default defineObject({ universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000030', diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.role.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.role.ts index 1c0c667027f..2a7634e3b58 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.role.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/my.role.ts @@ -1,4 +1,4 @@ -import { defineRole } from '@/application/roles/define-role'; +import { defineRole } from '@/sdk'; export default defineRole({ universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000040', diff --git a/packages/twenty-sdk/src/cli/__tests__/integration/utils/get-output-by-prefix.util.ts b/packages/twenty-sdk/src/cli/__tests__/integration/utils/get-output-by-prefix.util.ts index 2232131c49b..5d38b202a0d 100644 --- a/packages/twenty-sdk/src/cli/__tests__/integration/utils/get-output-by-prefix.util.ts +++ b/packages/twenty-sdk/src/cli/__tests__/integration/utils/get-output-by-prefix.util.ts @@ -2,7 +2,7 @@ export type LogPrefix = | 'init' | 'dev-mode' | 'manifest-watch' - | 'functions-watch' + | 'logicFunctions-watch' | 'front-components-watch'; export const getOutputByPrefix = ( diff --git a/packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts b/packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts index e463ec8b897..76519a6a23e 100644 --- a/packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts +++ b/packages/twenty-sdk/src/cli/__tests__/integration/utils/normalize-manifest.util.ts @@ -1,6 +1,6 @@ // Loose type for JSON manifest imports where enum values are inferred as strings type JsonManifestInput = { - functions?: Array<{ + logicFunctions?: Array<{ builtHandlerChecksum?: string | null; [key: string]: unknown; }>; @@ -16,7 +16,7 @@ export const normalizeManifestForComparison = ( manifest: T, ): T => ({ ...manifest, - functions: manifest.functions?.map((fn) => ({ + logicFunctions: manifest.logicFunctions?.map((fn) => ({ ...fn, builtHandlerChecksum: fn.builtHandlerChecksum ? '[checksum]' : null, })), diff --git a/packages/twenty-sdk/src/cli/commands/app-command.ts b/packages/twenty-sdk/src/cli/commands/app-command.ts index f2392077753..ca040d50f0f 100644 --- a/packages/twenty-sdk/src/cli/commands/app-command.ts +++ b/packages/twenty-sdk/src/cli/commands/app-command.ts @@ -8,14 +8,11 @@ import { AuthListCommand } from './auth/auth-list'; import { AuthLoginCommand } from './auth/auth-login'; import { AuthLogoutCommand } from './auth/auth-logout'; import { AuthStatusCommand } from './auth/auth-status'; -import { FunctionExecuteCommand } from './function/function-execute'; -import { FunctionLogsCommand } from './function/function-logs'; +import { LogicFunctionExecuteCommand } from './logic-function/logic-function-execute'; +import { LogicFunctionLogsCommand } from './logic-function/logic-function-logs'; import { AuthSwitchCommand } from './auth/auth-switch'; -import { - EntityAddCommand, - isSyncableEntity, - SyncableEntity, -} from './entity/entity-add'; +import { EntityAddCommand } from './entity/entity-add'; +import { SyncableEntity } from 'twenty-shared/application'; export const registerCommands = (program: Command): void => { // Auth commands @@ -67,8 +64,8 @@ export const registerCommands = (program: Command): void => { const uninstallCommand = new AppUninstallCommand(); const addCommand = new EntityAddCommand(); const generateCommand = new AppGenerateCommand(); - const logsCommand = new FunctionLogsCommand(); - const executeCommand = new FunctionExecuteCommand(); + const logsCommand = new LogicFunctionLogsCommand(); + const executeCommand = new LogicFunctionExecuteCommand(); program .command('app:dev [appPath]') @@ -101,14 +98,6 @@ export const registerCommands = (program: Command): void => { `Add a new entity to your application (${Object.values(SyncableEntity).join('|')})`, ) .action(async (entityType?: string, options?: { path?: string }) => { - if (entityType && !isSyncableEntity(entityType)) { - console.error( - chalk.red( - `Invalid entity type "${entityType}". Must be one of: ${Object.values(SyncableEntity).join('|')}`, - ), - ); - process.exit(1); - } await addCommand.execute(entityType as SyncableEntity, options?.path); }); diff --git a/packages/twenty-sdk/src/cli/commands/app/app-dev.ts b/packages/twenty-sdk/src/cli/commands/app/app-dev.ts index 791d5213e1b..2129d899bd4 100644 --- a/packages/twenty-sdk/src/cli/commands/app/app-dev.ts +++ b/packages/twenty-sdk/src/cli/commands/app/app-dev.ts @@ -1,10 +1,10 @@ import { AssetWatcher } from '@/cli/utilities/build/common/asset-watcher'; import { createFrontComponentsWatcher, - createFunctionsWatcher, + createLogicFunctionsWatcher, type EsbuildWatcher, } from '@/cli/utilities/build/common/esbuild-watcher'; -import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-build'; +import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/update-manifest-checksums'; import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher'; import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory'; import { DevModeOrchestrator } from '@/cli/utilities/dev/dev-mode-orchestrator'; @@ -22,7 +22,7 @@ export class AppDevCommand { private appPath = ''; private orchestrator: DevModeOrchestrator | null = null; private manifestWatcher: ManifestWatcher | null = null; - private functionsWatcher: EsbuildWatcher | null = null; + private logicFunctionsWatcher: EsbuildWatcher | null = null; private frontComponentsWatcher: EsbuildWatcher | null = null; private assetWatcher: AssetWatcher | null = null; private watchersStarted = false; @@ -71,16 +71,16 @@ export class AppDevCommand { } private async handleWatcherRestarts(result: ManifestBuildResult) { - const { functions, frontComponents } = result.filePaths; + const { logicFunctions, frontComponents } = result.filePaths; if (!this.watchersStarted) { this.watchersStarted = true; - await this.startFileWatchers(functions, frontComponents); + await this.startFileWatchers(logicFunctions, frontComponents); return; } - if (this.functionsWatcher?.shouldRestart(functions)) { - await this.functionsWatcher.restart(functions); + if (this.logicFunctionsWatcher?.shouldRestart(logicFunctions)) { + await this.logicFunctionsWatcher.restart(logicFunctions); } if (this.frontComponentsWatcher?.shouldRestart(frontComponents)) { @@ -89,18 +89,20 @@ export class AppDevCommand { } private async startFileWatchers( - functions: string[], + logicFunctions: string[], frontComponents: string[], ): Promise { await Promise.all([ - this.startFunctionsWatcher(functions), + this.startLogicFunctionsWatcher(logicFunctions), this.startFrontComponentsWatcher(frontComponents), this.startAssetWatcher(), ]); } - private async startFunctionsWatcher(sourcePaths: string[]): Promise { - this.functionsWatcher = createFunctionsWatcher({ + private async startLogicFunctionsWatcher( + sourcePaths: string[], + ): Promise { + this.logicFunctionsWatcher = createLogicFunctionsWatcher({ appPath: this.appPath, sourcePaths, handleBuildError: this.orchestrator!.handleFileBuildError.bind( @@ -111,7 +113,7 @@ export class AppDevCommand { ), }); - await this.functionsWatcher.start(); + await this.logicFunctionsWatcher.start(); } private async startFrontComponentsWatcher( @@ -148,7 +150,7 @@ export class AppDevCommand { await Promise.all([ this.manifestWatcher?.close(), - this.functionsWatcher?.close(), + this.logicFunctionsWatcher?.close(), this.frontComponentsWatcher?.close(), this.assetWatcher?.close(), ]); diff --git a/packages/twenty-sdk/src/cli/commands/app/app-uninstall.ts b/packages/twenty-sdk/src/cli/commands/app/app-uninstall.ts index c6f6fa8c365..ae28ed8a704 100644 --- a/packages/twenty-sdk/src/cli/commands/app/app-uninstall.ts +++ b/packages/twenty-sdk/src/cli/commands/app/app-uninstall.ts @@ -1,9 +1,9 @@ import { ApiService } from '@/cli/utilities/api/api-service'; import { type ApiResponse } from '@/cli/utilities/api/api-response-type'; -import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build'; import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory'; import chalk from 'chalk'; import inquirer from 'inquirer'; +import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build'; export class AppUninstallCommand { private apiService = new ApiService(); @@ -25,7 +25,7 @@ export class AppUninstallCommand { process.exit(1); } - const { manifest } = await runManifestBuild(appPath); + const { manifest } = await buildManifest(appPath); if (!manifest) { return { success: false, error: 'Build failed' }; diff --git a/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts b/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts index f683ba1658a..ccb9f7e0274 100644 --- a/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts +++ b/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts @@ -1,132 +1,51 @@ import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory'; import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-component-template'; -import { getFunctionBaseFile } from '@/cli/utilities/entity/entity-function-template'; +import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-function-template'; import { convertToLabel } from '@/cli/utilities/entity/entity-label'; import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template'; import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template'; import chalk from 'chalk'; import * as fs from 'fs-extra'; import inquirer from 'inquirer'; -import camelcase from 'lodash.camelcase'; import kebabcase from 'lodash.kebabcase'; -import { join } from 'path'; +import { join, relative } from 'path'; +import { SyncableEntity } from 'twenty-shared/application'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { assertUnreachable } from 'twenty-shared/utils'; +import { getFieldBaseFile } from '@/cli/utilities/entity/entity-field-template'; const APP_FOLDER = 'src'; -export enum SyncableEntity { - AGENT = 'agent', - OBJECT = 'object', - FUNCTION = 'function', - FRONT_COMPONENT = 'front-component', - ROLE = 'role', -} - -export const isSyncableEntity = (value: string): value is SyncableEntity => { - return Object.values(SyncableEntity).includes(value as SyncableEntity); -}; - export class EntityAddCommand { async execute(entityType?: SyncableEntity, path?: string): Promise { try { - // Default to src/ folder, allow override with path parameter + const entity = entityType ?? (await this.getEntity()); + + const entityName = this.getFolderName(entity); + const appPath = path ? join(CURRENT_EXECUTION_DIRECTORY, path) - : join(CURRENT_EXECUTION_DIRECTORY, APP_FOLDER); + : join(CURRENT_EXECUTION_DIRECTORY, APP_FOLDER, entityName); await fs.ensureDir(appPath); - const entity = entityType ?? (await this.getEntity()); + const { name, file } = await this.getEntityData(entity); - if (entity === SyncableEntity.OBJECT) { - const entityData = await this.getObjectData(); + const filePath = join(appPath, this.getFileName(name, entity)); - const name = entityData.nameSingular; - - // Use *.object.ts naming convention - const objectFileName = `${camelcase(name)}.object.ts`; - - const decoratedObject = getObjectBaseFile({ - data: entityData, - name, - }); - - const filePath = join(appPath, objectFileName); - - await fs.writeFile(filePath, decoratedObject); - - console.log( - chalk.green(`✓ Created object:`), - chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')), - ); - - return; + if (await fs.pathExists(filePath)) { + const { overwrite } = await this.handleFileExist(); + if (!overwrite) { + return; + } } - if (entity === SyncableEntity.FUNCTION) { - const entityName = await this.getEntityName(entity); + await fs.writeFile(filePath, file); - // Use *.function.ts naming convention - const functionFileName = `${kebabcase(entityName)}.function.ts`; - - const decoratedLogicFunction = getFunctionBaseFile({ - name: entityName, - }); - - const filePath = join(appPath, functionFileName); - - await fs.writeFile(filePath, decoratedLogicFunction); - - console.log( - chalk.green(`✓ Created function:`), - chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')), - ); - - return; - } - - if (entity === SyncableEntity.FRONT_COMPONENT) { - const entityName = await this.getEntityName(entity); - - // Use *.front-component.tsx naming convention - const frontComponentFileName = `${kebabcase(entityName)}.front-component.tsx`; - - const decoratedFrontComponent = getFrontComponentBaseFile({ - name: entityName, - }); - - const filePath = join(appPath, frontComponentFileName); - - await fs.writeFile(filePath, decoratedFrontComponent); - - console.log( - chalk.green(`✓ Created front component:`), - chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')), - ); - - return; - } - - if (entity === SyncableEntity.ROLE) { - const entityName = await this.getEntityName(entity); - - // Use *.role.ts naming convention - const roleFileName = `${kebabcase(entityName)}.role.ts`; - - const roleFileContent = getRoleBaseFile({ - name: entityName, - }); - - const filePath = join(appPath, roleFileName); - - await fs.writeFile(filePath, roleFileContent); - - console.log( - chalk.green(`✓ Created role:`), - chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')), - ); - - return; - } + console.log( + chalk.green(`✓ Created ${entityName}:`), + chalk.cyan(relative(CURRENT_EXECUTION_DIRECTORY, filePath)), + ); } catch (error) { console.error( chalk.red(`Add new entity failed:`), @@ -136,6 +55,69 @@ export class EntityAddCommand { } } + private async getEntityData(entity: SyncableEntity) { + switch (entity) { + case SyncableEntity.Object: { + const entityData = await this.getObjectData(); + + const name = entityData.nameSingular; + + const file = getObjectBaseFile({ + data: entityData, + name, + }); + + return { name, file }; + } + + case SyncableEntity.Field: { + const entityData = await this.getFieldData(); + + const name = entityData.name; + + const file = getFieldBaseFile({ + data: entityData, + name, + }); + + return { name, file }; + } + + case SyncableEntity.LogicFunction: { + const name = await this.getEntityName(entity); + + const file = getLogicFunctionBaseFile({ + name, + }); + + return { name, file }; + } + + case SyncableEntity.FrontComponent: { + const name = await this.getEntityName(entity); + + const file = getFrontComponentBaseFile({ + name, + }); + + return { name, file }; + } + + case SyncableEntity.Role: { + const name = await this.getEntityName(entity); + + const file = getRoleBaseFile({ + name, + }); + + return { name, file }; + } + + default: + assertUnreachable(entity); + } + } + private async getEntity() { const { entity } = await inquirer.prompt<{ entity: SyncableEntity }>([ { @@ -143,18 +125,24 @@ export class EntityAddCommand { name: 'entity', message: `What entity do you want to create?`, default: '', - choices: [ - SyncableEntity.FUNCTION, - SyncableEntity.FRONT_COMPONENT, - SyncableEntity.OBJECT, - SyncableEntity.ROLE, - ], + choices: Object.values(SyncableEntity), }, ]); return entity; } + private async handleFileExist() { + return await inquirer.prompt<{ overwrite: boolean }>([ + { + type: 'confirm', + name: 'overwrite', + message: `File already exists. Do you want to overwrite it?`, + default: false, + }, + ]); + } + private async getEntityName(entity: SyncableEntity) { const { name } = await inquirer.prompt<{ name: string }>([ { @@ -167,10 +155,6 @@ export class EntityAddCommand { return `${entity} name is required`; } - if (!/^[a-z0-9-]+$/.test(input)) { - return 'Name must contain only lowercase letters, numbers, and hyphens'; - } - return true; }, }, @@ -179,8 +163,76 @@ export class EntityAddCommand { return name; } + private async getFieldData() { + return inquirer.prompt<{ + name: string; + label: string; + type: FieldMetadataType; + objectUniversalIdentifier: string; + description: string; + }>([ + { + type: 'input', + name: 'name', + message: 'Enter a name for your field:', + default: '', + validate: (input: string) => { + if (!input || input.trim().length === 0) { + return 'Please enter a non empty string'; + } + return true; + }, + }, + { + type: 'input', + name: 'label', + message: 'Enter a label for your field:', + default: (answers: any) => { + return convertToLabel(answers.name); + }, + validate: (input: string) => { + if (!input || input.trim().length === 0) { + return 'Please enter a non empty string'; + } + return true; + }, + }, + { + type: 'select', + name: 'type', + message: 'Select the field type:', + choices: Object.values(FieldMetadataType), + default: FieldMetadataType.TEXT, + }, + { + type: 'input', + name: 'objectUniversalIdentifier', + message: + 'Enter the universalIdentifier of the object this field belongs to:', + default: 'fill-later', + validate: (input: string) => { + if (!input || input.trim().length === 0) { + return 'Please enter a non empty string'; + } + return true; + }, + }, + { + type: 'input', + name: 'description', + message: 'Enter a description for your field (optional):', + default: '', + }, + ]); + } + private async getObjectData() { - return inquirer.prompt([ + return inquirer.prompt<{ + nameSingular: string; + namePlural: string; + labelSingular: string; + labelPlural: string; + }>([ { type: 'input', name: 'nameSingular', @@ -238,4 +290,19 @@ export class EntityAddCommand { }, ]); } + + getFolderName(entity: SyncableEntity) { + return `${kebabcase(entity)}s`; + } + + getFileName(name: string, entity: SyncableEntity) { + switch (entity) { + case SyncableEntity.FrontComponent: { + return `${kebabcase(name)}.tsx`; + } + default: { + return `${kebabcase(name)}.ts`; + } + } + } } diff --git a/packages/twenty-sdk/src/cli/commands/function/function-execute.ts b/packages/twenty-sdk/src/cli/commands/logic-function/logic-function-execute.ts similarity index 93% rename from packages/twenty-sdk/src/cli/commands/function/function-execute.ts rename to packages/twenty-sdk/src/cli/commands/logic-function/logic-function-execute.ts index b17765f8366..eeb8c0643f1 100644 --- a/packages/twenty-sdk/src/cli/commands/function/function-execute.ts +++ b/packages/twenty-sdk/src/cli/commands/logic-function/logic-function-execute.ts @@ -1,11 +1,11 @@ import { ApiService } from '@/cli/utilities/api/api-service'; -import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build'; import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory'; import chalk from 'chalk'; -import { type ApplicationManifest } from 'twenty-shared/application'; +import { type Manifest } from 'twenty-shared/application'; import { isDefined } from 'twenty-shared/utils'; +import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build'; -export class FunctionExecuteCommand { +export class LogicFunctionExecuteCommand { private apiService = new ApiService(); async execute({ @@ -30,7 +30,7 @@ export class FunctionExecuteCommand { process.exit(1); } - const { manifest } = await runManifestBuild(appPath); + const { manifest } = await buildManifest(appPath); if (!manifest) { console.error(chalk.red('Failed to build manifest.')); @@ -162,9 +162,9 @@ export class FunctionExecuteCommand { private belongsToApplication( fn: { universalIdentifier: string; applicationId: string | null }, - manifest: ApplicationManifest, + manifest: Manifest, ): boolean { - return manifest.functions.some( + return manifest.logicFunctions.some( (manifestFn) => manifestFn.universalIdentifier === fn.universalIdentifier, ); } diff --git a/packages/twenty-sdk/src/cli/commands/function/function-logs.ts b/packages/twenty-sdk/src/cli/commands/logic-function/logic-function-logs.ts similarity index 90% rename from packages/twenty-sdk/src/cli/commands/function/function-logs.ts rename to packages/twenty-sdk/src/cli/commands/logic-function/logic-function-logs.ts index 57ca8e79078..86cbcb8a779 100644 --- a/packages/twenty-sdk/src/cli/commands/function/function-logs.ts +++ b/packages/twenty-sdk/src/cli/commands/logic-function/logic-function-logs.ts @@ -1,9 +1,9 @@ import { ApiService } from '@/cli/utilities/api/api-service'; -import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build'; import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory'; import chalk from 'chalk'; +import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build'; -export class FunctionLogsCommand { +export class LogicFunctionLogsCommand { private apiService = new ApiService(); async execute({ @@ -16,7 +16,7 @@ export class FunctionLogsCommand { functionName?: string; }): Promise { try { - const { manifest } = await runManifestBuild(appPath); + const { manifest } = await buildManifest(appPath); if (!manifest) { process.exit(1); diff --git a/packages/twenty-sdk/src/cli/utilities/api/api-service.ts b/packages/twenty-sdk/src/cli/utilities/api/api-service.ts index 86aa158ed8a..030e4f23c42 100644 --- a/packages/twenty-sdk/src/cli/utilities/api/api-service.ts +++ b/packages/twenty-sdk/src/cli/utilities/api/api-service.ts @@ -9,7 +9,7 @@ import { printSchema, } from 'graphql/index'; import * as path from 'path'; -import { type ApplicationManifest } from 'twenty-shared/application'; +import { type Manifest } from 'twenty-shared/application'; import { type FileFolder } from 'twenty-shared/types'; import { type ApiResponse } from '@/cli/utilities/api/api-response-type'; import { pascalCase } from 'twenty-shared/utils'; @@ -99,7 +99,7 @@ export class ApiService { } } - async syncApplication(manifest: ApplicationManifest): Promise { + async syncApplication(manifest: Manifest): Promise { try { const mutation = ` mutation SyncApplication($manifest: JSON!, $packageJson: JSON!, $yarnLock: String!) { diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts b/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts index 1146fd084b5..5ef299865c5 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/common/esbuild-watcher.ts @@ -13,7 +13,7 @@ import path from 'path'; import { OUTPUT_DIR } from 'twenty-shared/application'; import { FileFolder } from 'twenty-shared/types'; -export const FUNCTION_EXTERNAL_MODULES: string[] = [ +export const LOGIC_FUNCTION_EXTERNAL_MODULES: string[] = [ 'path', 'fs', 'crypto', @@ -204,14 +204,14 @@ const externalPatternsPlugin: esbuild.Plugin = { }, }; -export const createFunctionsWatcher = ( +export const createLogicFunctionsWatcher = ( options: RestartableWatcherOptions, ): EsbuildWatcher => new EsbuildWatcher({ ...options, config: { - externalModules: FUNCTION_EXTERNAL_MODULES, - fileFolder: FileFolder.BuiltFunction, + externalModules: LOGIC_FUNCTION_EXTERNAL_MODULES, + fileFolder: FileFolder.BuiltLogicFunction, platform: 'node', extraPlugins: [externalPatternsPlugin], }, diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-extract-config.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-extract-config.spec.ts new file mode 100644 index 00000000000..d7b250f1ee1 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-extract-config.spec.ts @@ -0,0 +1,84 @@ +import { extractDefineEntity } from '@/cli/utilities/build/manifest/manifest-extract-config'; + +describe('extractDefineEntity', () => { + it('should detect defineApplication in default export', () => { + const fileContent = ` + import { defineApplication } from 'twenty-sdk'; + export default defineApplication({ name: 'MyApp' }); + `; + const result = extractDefineEntity(fileContent); + expect(result).toBe('defineApplication'); + }); + + it('should detect defineField in default export', () => { + const fileContent = ` + import { defineField } from 'twenty-sdk'; + export default defineField({ name: 'myField' }); + `; + const result = extractDefineEntity(fileContent); + expect(result).toBe('defineField'); + }); + + it('should detect defineLogicFunction in default export', () => { + const fileContent = ` + import { defineLogicFunction } from 'twenty-sdk'; + export default defineLogicFunction({ name: 'myFunction' }); + `; + const result = extractDefineEntity(fileContent); + expect(result).toBe('defineLogicFunction'); + }); + + it('should detect defineObject in default export', () => { + const fileContent = ` + import { defineObject } from 'twenty-sdk'; + export default defineObject({ name: 'myObject' }); + `; + const result = extractDefineEntity(fileContent); + expect(result).toBe('defineObject'); + }); + + it('should detect defineRole in default export', () => { + const fileContent = ` + import { defineRole } from 'twenty-sdk'; + export default defineRole({ name: 'myRole' }); + `; + const result = extractDefineEntity(fileContent); + expect(result).toBe('defineRole'); + }); + + it('should detect defineFrontComponent in default export', () => { + const fileContent = ` + import { defineFrontComponent } from 'twenty-sdk'; + export default defineFrontComponent({ name: 'myComponent' }); + `; + const result = extractDefineEntity(fileContent); + expect(result).toBe('defineFrontComponent'); + }); + + it('should not detect non-target function in default export', () => { + const fileContent = ` + import { someOtherFunction } from 'twenty-sdk'; + export default someOtherFunction({ name: 'myFunction' }); + `; + const result = extractDefineEntity(fileContent); + expect(result).toBeUndefined(); + }); + + it('should handle files without default export', () => { + const fileContent = ` + import { defineApplication } from 'twenty-sdk'; + const app = defineApplication({ name: 'MyApp' }); + `; + const result = extractDefineEntity(fileContent); + expect(result).toBeUndefined(); + }); + + it('should handle files with non-call default export', () => { + const fileContent = ` + import { defineApplication } from 'twenty-sdk'; + export default { name: 'MyApp' }; + `; + const result = extractDefineEntity(fileContent); + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/validate-manifest.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/validate-manifest.spec.ts index 417b14f7b8a..39a32f6db03 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/validate-manifest.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/validate-manifest.spec.ts @@ -1,40 +1,46 @@ -import { validateManifest } from '@/cli/utilities/build/manifest/manifest-validate'; import { - type Application, - type ObjectExtensionManifest, + type ApplicationManifest, + type Manifest, + type PackageJson, + type FieldManifest, } from 'twenty-shared/application'; import { FieldMetadataType } from 'twenty-shared/types'; +import { validateManifest } from '@/cli/utilities/build/manifest/validate-manifest'; + +const validApplication: ApplicationManifest = { + universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', + displayName: 'Test App', + defaultRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2', +}; + +const validField: FieldManifest = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + + universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', + type: FieldMetadataType.NUMBER, + name: 'healthScore', + label: 'Health Score', +}; + +const validManifest: Manifest = { + application: validApplication, + objects: [], + frontComponents: [], + fields: [], + logicFunctions: [], + roles: [], + publicAssets: [], + sources: {}, + packageJson: {} as PackageJson, + yarnLock: '', +}; describe('validateManifest - objectExtensions', () => { - const validApplication: Application = { - universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', - displayName: 'Test App', - functionRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2', - }; - - const validObjectExtension: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', - type: FieldMetadataType.NUMBER, - name: 'healthScore', - label: 'Health Score', - }, - ], - }; - describe('valid object extensions', () => { it('should pass validation with valid object extension by nameSingular', () => { const result = validateManifest({ - application: validApplication, - objects: [], - frontComponents: [], - objectExtensions: [validObjectExtension], - functions: [], - roles: [], + ...validManifest, + fields: [validField], }); expect(result.isValid).toBe(true); @@ -42,27 +48,17 @@ describe('validateManifest - objectExtensions', () => { }); it('should pass validation with valid object extension by universalIdentifier', () => { - const extensionByUuid: ObjectExtensionManifest = { - targetObject: { - universalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440002', - type: FieldMetadataType.TEXT, - name: 'customNote', - label: 'Custom Note', - }, - ], + const extensionByUuid: FieldManifest = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + universalIdentifier: '550e8400-e29b-41d4-a716-446655440002', + type: FieldMetadataType.TEXT, + name: 'customNote', + label: 'Custom Note', }; const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [extensionByUuid], - functions: [], - frontComponents: [], - roles: [], + ...validManifest, + fields: [extensionByUuid], }); expect(result.isValid).toBe(true); @@ -70,27 +66,17 @@ describe('validateManifest - objectExtensions', () => { }); it('should pass validation with multiple object extensions', () => { - const anotherExtension: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'person', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440003', - type: FieldMetadataType.TEXT, - name: 'nickname', - label: 'Nickname', - }, - ], + const anotherExtension: FieldManifest = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + universalIdentifier: '550e8400-e29b-41d4-a716-446655440003', + type: FieldMetadataType.TEXT, + name: 'nickname', + label: 'Nickname', }; const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [validObjectExtension, anotherExtension], - functions: [], - frontComponents: [], - roles: [], + ...validManifest, + fields: [validField, anotherExtension], }); expect(result.isValid).toBe(true); @@ -98,36 +84,26 @@ describe('validateManifest - objectExtensions', () => { }); it('should pass validation with SELECT field having options', () => { - const extensionWithSelect: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', - }, - fields: [ + const extensionWithSelect: FieldManifest = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + + universalIdentifier: '550e8400-e29b-41d4-a716-446655440004', + type: FieldMetadataType.SELECT, + name: 'status', + label: 'Status', + options: [ + { value: 'active', label: 'Active', color: 'green', position: 0 }, { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440004', - type: FieldMetadataType.SELECT, - name: 'status', - label: 'Status', - options: [ - { value: 'active', label: 'Active', color: 'green', position: 0 }, - { - value: 'inactive', - label: 'Inactive', - color: 'red', - position: 1, - }, - ], + value: 'inactive', + label: 'Inactive', + color: 'red', + position: 1, }, ], }; - const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [extensionWithSelect], - functions: [], - frontComponents: [], - roles: [], + ...validManifest, + fields: [extensionWithSelect], }); expect(result.isValid).toBe(true); @@ -135,312 +111,46 @@ describe('validateManifest - objectExtensions', () => { }); }); - describe('targetObject validation', () => { - it('should fail when targetObject is missing', () => { - const invalidExtension = { - fields: validObjectExtension.fields, - } as ObjectExtensionManifest; - - const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [invalidExtension], - functions: [], - frontComponents: [], - roles: [], - }); - - expect(result.isValid).toBe(false); - expect(result.errors).toContainEqual( - expect.objectContaining({ - message: 'Object extension must have a targetObject', - }), - ); - }); - - it('should fail when targetObject has neither nameSingular nor universalIdentifier', () => { - const invalidExtension: ObjectExtensionManifest = { - targetObject: {} as any, - fields: validObjectExtension.fields, - }; - - const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [invalidExtension], - functions: [], - frontComponents: [], - roles: [], - }); - - expect(result.isValid).toBe(false); - expect(result.errors).toContainEqual( - expect.objectContaining({ - message: - 'Object extension targetObject must have either nameSingular or universalIdentifier', - }), - ); - }); - - it('should fail when targetObject has both nameSingular and universalIdentifier', () => { - const invalidExtension: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', - universalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', - } as any, - fields: validObjectExtension.fields, - }; - - const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [invalidExtension], - functions: [], - frontComponents: [], - roles: [], - }); - - expect(result.isValid).toBe(false); - expect(result.errors).toContainEqual( - expect.objectContaining({ - message: - 'Object extension targetObject cannot have both nameSingular and universalIdentifier', - }), - ); - }); - }); - - describe('fields validation', () => { - it('should fail when fields array is empty', () => { - const invalidExtension: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', - }, - fields: [], - }; - - const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [invalidExtension], - functions: [], - frontComponents: [], - roles: [], - }); - - expect(result.isValid).toBe(false); - expect(result.errors).toContainEqual( - expect.objectContaining({ - message: 'Object extension must have at least one field', - }), - ); - }); - - it('should fail when field is missing universalIdentifier', () => { - const invalidExtension: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - type: FieldMetadataType.NUMBER, - name: 'healthScore', - label: 'Health Score', - } as any, - ], - }; - - const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [invalidExtension], - functions: [], - frontComponents: [], - roles: [], - }); - - expect(result.isValid).toBe(false); - expect(result.errors).toContainEqual( - expect.objectContaining({ - message: 'Field must have a universalIdentifier', - }), - ); - }); - - it('should fail when field is missing type', () => { - const invalidExtension: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', - name: 'healthScore', - label: 'Health Score', - } as any, - ], - }; - - const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [invalidExtension], - functions: [], - frontComponents: [], - roles: [], - }); - - expect(result.isValid).toBe(false); - expect(result.errors).toContainEqual( - expect.objectContaining({ - message: 'Field must have a type', - }), - ); - }); - - it('should fail when field is missing label', () => { - const invalidExtension: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', - type: FieldMetadataType.NUMBER, - name: 'healthScore', - } as any, - ], - }; - - const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [invalidExtension], - functions: [], - frontComponents: [], - roles: [], - }); - - expect(result.isValid).toBe(false); - expect(result.errors).toContainEqual( - expect.objectContaining({ - message: 'Field must have a label', - }), - ); - }); - - it('should fail when SELECT field has no options', () => { - const invalidExtension: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', - type: FieldMetadataType.SELECT, - name: 'status', - label: 'Status', - } as any, - ], - }; - - const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [invalidExtension], - functions: [], - frontComponents: [], - roles: [], - }); - - expect(result.isValid).toBe(false); - expect(result.errors).toContainEqual( - expect.objectContaining({ - message: 'SELECT/MULTI_SELECT field must have options', - }), - ); - }); - - it('should fail when MULTI_SELECT field has empty options', () => { - const invalidExtension: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', - }, - fields: [ - { - universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', - type: FieldMetadataType.MULTI_SELECT, - name: 'tags', - label: 'Tags', - options: [], - } as any, - ], - }; - - const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [invalidExtension], - functions: [], - frontComponents: [], - roles: [], - }); - - expect(result.isValid).toBe(false); - expect(result.errors).toContainEqual( - expect.objectContaining({ - message: 'SELECT/MULTI_SELECT field must have options', - }), - ); - }); - }); - describe('duplicate universalIdentifier detection', () => { it('should fail when extension field has duplicate universalIdentifier', () => { const duplicateId = '550e8400-e29b-41d4-a716-446655440001'; - const extensionWithDuplicates: ObjectExtensionManifest = { - targetObject: { - nameSingular: 'company', + const fieldsWithDuplicates: FieldManifest[] = [ + { + objectUniversalIdentifier: '91c5848c-36dc-4e7e-b9ee-aa78caeff5a8', + universalIdentifier: duplicateId, + type: FieldMetadataType.NUMBER, + name: 'field1', + label: 'Field 1', }, - fields: [ - { - universalIdentifier: duplicateId, - type: FieldMetadataType.NUMBER, - name: 'field1', - label: 'Field 1', - }, - { - universalIdentifier: duplicateId, // Same ID! - type: FieldMetadataType.TEXT, - name: 'field2', - label: 'Field 2', - }, - ], - }; + { + objectUniversalIdentifier: '97931020-123c-435b-ad97-9e19a5b38f1f', + universalIdentifier: duplicateId, + type: FieldMetadataType.TEXT, + name: 'field2', + label: 'Field 2', + }, + ]; const result = validateManifest({ - application: validApplication, - objects: [], - objectExtensions: [extensionWithDuplicates], - functions: [], - frontComponents: [], - roles: [], + ...validManifest, + fields: fieldsWithDuplicates, }); expect(result.isValid).toBe(false); - expect(result.errors).toContainEqual( - expect.objectContaining({ - message: expect.stringContaining('Duplicate universalIdentifier'), - }), + expect(result.errors).toContain( + 'Duplicate universal identifiers: 550e8400-e29b-41d4-a716-446655440001', ); + expect(result.warnings).toContain('No object defined'); + expect(result.warnings).toContain('No logic function defined'); + expect(result.warnings).toContain('No front component defined'); }); it('should fail when extension field ID conflicts with object field ID', () => { const sharedId = '550e8400-e29b-41d4-a716-446655440001'; const result = validateManifest({ - application: validApplication, + ...validManifest, objects: [ { universalIdentifier: 'obj-uuid', @@ -458,30 +168,24 @@ describe('validateManifest - objectExtensions', () => { ], }, ], - objectExtensions: [ + fields: [ { - targetObject: { nameSingular: 'company' }, - fields: [ - { - universalIdentifier: sharedId, // Same as object field! - type: FieldMetadataType.NUMBER, - name: 'extensionField', - label: 'Extension Field', - }, - ], + objectUniversalIdentifier: '91c5848c-36dc-4e7e-b9ee-aa78caeff5a8', + universalIdentifier: sharedId, + type: FieldMetadataType.NUMBER, + name: 'extensionField', + label: 'Extension Field', }, ], - functions: [], - frontComponents: [], - roles: [], }); expect(result.isValid).toBe(false); - expect(result.errors).toContainEqual( - expect.objectContaining({ - message: expect.stringContaining('Duplicate universalIdentifier'), - }), + expect(result.errors).toContain( + 'Duplicate universal identifiers: 550e8400-e29b-41d4-a716-446655440001', ); + expect(result.warnings).not.toContain('No object defined'); + expect(result.warnings).toContain('No logic function defined'); + expect(result.warnings).toContain('No front component defined'); }); }); }); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/application.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/application.ts deleted file mode 100644 index 73dba997a78..00000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/application.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { glob } from 'fast-glob'; -import path from 'path'; -import { - type Application, - type ApplicationVariables, -} from 'twenty-shared/application'; -import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server'; -import { - type EntityBuildResult, - type EntityIdWithLocation, - type ManifestEntityBuilder, - type ManifestWithoutSources, -} from '@/cli/utilities/build/manifest/entities/entity-interface'; -import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types'; - -const findApplicationConfigPath = async (appPath: string): Promise => { - const files = await glob('**/application.config.ts', { - cwd: appPath, - ignore: ['**/node_modules/**', '**/.twenty/**', '**/dist/**'], - }); - - if (files.length === 0) { - throw new Error('Missing application.config.ts in your app'); - } - - if (files.length > 1) { - throw new Error( - `Multiple application.config.ts files found: ${files.join(', ')}. Only one is allowed.`, - ); - } - - return path.join(appPath, files[0]); -}; - -export class ApplicationEntityBuilder - implements ManifestEntityBuilder -{ - async build(appPath: string): Promise> { - const applicationConfigPath = await findApplicationConfigPath(appPath); - const { manifest: application } = - await manifestExtractFromFileServer.extractManifestFromFile( - applicationConfigPath, - ); - const relativePath = path.relative(appPath, applicationConfigPath); - - return { manifests: [application], filePaths: [relativePath] }; - } - - validate(applications: Application[], errors: ValidationError[]): void { - const application = applications[0]; - - if (!application) { - errors.push({ - path: 'application', - message: 'Application config is required', - }); - return; - } - - if (!application.universalIdentifier) { - errors.push({ - path: 'application', - message: 'Application must have a universalIdentifier', - }); - } - } - - findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { - const seen = new Map(); - const application = manifest.application; - - if (application?.universalIdentifier) { - seen.set(application.universalIdentifier, ['application']); - } - - if (application?.applicationVariables) { - for (const [name, variable] of Object.entries( - application.applicationVariables, - ) as [string, ApplicationVariables[string]][]) { - if (variable.universalIdentifier) { - const locations = seen.get(variable.universalIdentifier) ?? []; - locations.push(`application.variables.${name}`); - seen.set(variable.universalIdentifier, locations); - } - } - } - - return Array.from(seen.entries()) - .filter(([_, locations]) => locations.length > 1) - .map(([id, locations]) => ({ id, locations })); - } -} - -export const applicationEntityBuilder = new ApplicationEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/asset.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/asset.ts deleted file mode 100644 index 32e7fe0f07c..00000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/asset.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { glob } from 'fast-glob'; -import path from 'path'; -import { type AssetManifest, ASSETS_DIR } from 'twenty-shared/application'; - -import { type EntityBuildResult } from '@/cli/utilities/build/manifest/entities/entity-interface'; - -export class AssetEntityBuilder { - async build(appPath: string): Promise> { - const assetFiles = await glob([`${ASSETS_DIR}/**/*`], { - cwd: appPath, - onlyFiles: true, - }); - - const manifests: AssetManifest[] = assetFiles.map((filePath) => ({ - filePath, - fileName: path.basename(filePath), - fileType: path.extname(filePath).replace(/^\./, ''), - checksum: null, - })); - - return { manifests, filePaths: assetFiles }; - } -} - -export const assetEntityBuilder = new AssetEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/entity-interface.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/entity-interface.ts deleted file mode 100644 index 97747af7894..00000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/entity-interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { type ApplicationManifest } from 'twenty-shared/application'; -import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types'; - -export type EntityIdWithLocation = { - id: string; - locations: string[]; -}; - -export type ManifestWithoutSources = Omit< - ApplicationManifest, - 'sources' | 'packageJson' | 'yarnLock' ->; - -export type EntityBuildResult = { - manifests: TManifest[]; - filePaths: string[]; -}; - -export type ManifestEntityBuilder = { - build(appPath: string): Promise>; - validate(data: EntityManifest[], errors: ValidationError[]): void; - findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[]; -}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/front-component.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/front-component.ts deleted file mode 100644 index f05208dee6f..00000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/front-component.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { glob } from 'fast-glob'; -import { type FrontComponentManifest } from 'twenty-shared/application'; - -import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server'; -import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types'; -import { - type EntityBuildResult, - type EntityIdWithLocation, - type ManifestEntityBuilder, - type ManifestWithoutSources, -} from '@/cli/utilities/build/manifest/entities/entity-interface'; - -type FrontComponentConfig = Omit< - FrontComponentManifest, - | 'sourceComponentPath' - | 'builtComponentPath' - | 'builtComponentChecksum' - | 'componentName' -> & { - component: { name: string }; -}; - -export class FrontComponentEntityBuilder - implements ManifestEntityBuilder -{ - async build( - appPath: string, - ): Promise> { - const componentFiles = await glob(['**/*.front-component.tsx'], { - cwd: appPath, - ignore: [ - '**/node_modules/**', - '**/*.d.ts', - '**/dist/**', - '**/.twenty/**', - ], - }); - - const manifests: FrontComponentManifest[] = []; - - for (const filePath of componentFiles) { - try { - const absolutePath = `${appPath}/${filePath}`; - const { manifest: config } = - await manifestExtractFromFileServer.extractManifestFromFile( - absolutePath, - ); - - const { component, ...rest } = config; - const builtComponentPath = this.computeBuiltComponentPath(filePath); - - manifests.push({ - ...rest, - componentName: component.name, - sourceComponentPath: filePath, - builtComponentPath, - builtComponentChecksum: null, - }); - } catch (error) { - throw new Error( - `Failed to load front component from ${filePath}: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - return { manifests, filePaths: componentFiles }; - } - - private computeBuiltComponentPath(sourceComponentPath: string): string { - return sourceComponentPath.replace(/\.tsx?$/, '.mjs'); - } - - validate( - components: FrontComponentManifest[], - errors: ValidationError[], - ): void { - for (const component of components) { - const componentPath = `front-components/${component.name ?? component.componentName ?? 'unknown'}`; - - if (!component.universalIdentifier) { - errors.push({ - path: componentPath, - message: 'Front component must have a universalIdentifier', - }); - } - } - } - - findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { - const seen = new Map(); - const components = manifest.frontComponents ?? []; - - for (const component of components) { - if (component.universalIdentifier) { - const location = `front-components/${component.name ?? component.componentName}`; - const locations = seen.get(component.universalIdentifier) ?? []; - locations.push(location); - seen.set(component.universalIdentifier, locations); - } - } - - return Array.from(seen.entries()) - .filter(([_, locations]) => locations.length > 1) - .map(([id, locations]) => ({ id, locations })); - } -} - -export const frontComponentEntityBuilder = new FrontComponentEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/function.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/function.ts deleted file mode 100644 index c75572275a9..00000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/function.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { glob } from 'fast-glob'; -import { type LogicFunctionManifest } from 'twenty-shared/application'; - -import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server'; -import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types'; -import { - type EntityBuildResult, - type EntityIdWithLocation, - type ManifestEntityBuilder, - type ManifestWithoutSources, -} from '@/cli/utilities/build/manifest/entities/entity-interface'; - -type ExtractedFunctionManifest = Omit< - LogicFunctionManifest, - 'sourceHandlerPath' | 'builtHandlerPath' | 'builtHandlerChecksum' -> & { - handler: string; -}; - -export class FunctionEntityBuilder - implements ManifestEntityBuilder -{ - async build( - appPath: string, - ): Promise> { - const functionFiles = await glob(['**/*.function.ts'], { - cwd: appPath, - ignore: [ - '**/node_modules/**', - '**/*.d.ts', - '**/dist/**', - '**/.twenty/**', - ], - }); - - const manifests: LogicFunctionManifest[] = []; - - for (const filePath of functionFiles) { - try { - const absolutePath = `${appPath}/${filePath}`; - - const { manifest, exportName } = - await manifestExtractFromFileServer.extractManifestFromFile( - absolutePath, - ); - - const { handler: _, ...rest } = manifest; - // builtHandlerPath is computed from filePath (the .function.ts file) - // since that's what esbuild actually builds, not handlerPath - const builtHandlerPath = this.computeBuiltHandlerPath(filePath); - - // For default exports, use 'default.handler' - // For named exports like 'export const anyName = ...', use 'anyName.handler' - const handlerName = - exportName !== null ? `${exportName}.handler` : 'default.handler'; - - manifests.push({ - ...rest, - handlerName, - sourceHandlerPath: filePath, - builtHandlerPath, - builtHandlerChecksum: null, - }); - } catch (error) { - throw new Error( - `Failed to load function from ${filePath}: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - return { manifests, filePaths: functionFiles }; - } - - private computeBuiltHandlerPath(sourceHandlerPath: string): string { - return sourceHandlerPath.replace(/\.tsx?$/, '.mjs'); - } - - validate( - functions: LogicFunctionManifest[], - errors: ValidationError[], - ): void { - for (const fn of functions) { - const fnPath = `functions/${fn.name ?? fn.handlerName ?? 'unknown'}`; - - if (!fn.universalIdentifier) { - errors.push({ - path: fnPath, - message: 'Function must have a universalIdentifier', - }); - } - - for (const trigger of fn.triggers ?? []) { - const triggerPath = `${fnPath}.triggers.${trigger.type ?? 'unknown'}`; - - if (!trigger.universalIdentifier) { - errors.push({ - path: triggerPath, - message: 'Trigger must have a universalIdentifier', - }); - } - - if (!trigger.type) { - errors.push({ - path: triggerPath, - message: 'Trigger must have a type', - }); - continue; - } - - switch (trigger.type) { - case 'route': - if (!trigger.path) { - errors.push({ - path: triggerPath, - message: 'Route trigger must have a path', - }); - } - if (!trigger.httpMethod) { - errors.push({ - path: triggerPath, - message: 'Route trigger must have an httpMethod', - }); - } - break; - - case 'cron': - if (!trigger.pattern) { - errors.push({ - path: triggerPath, - message: 'Cron trigger must have a pattern', - }); - } - break; - - case 'databaseEvent': - if (!trigger.eventName) { - errors.push({ - path: triggerPath, - message: 'Database event trigger must have an eventName', - }); - } - break; - } - } - } - } - - findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { - const seen = new Map(); - const functions = manifest.functions ?? []; - - for (const fn of functions) { - if (fn.universalIdentifier) { - const location = `functions/${fn.name ?? fn.handlerName}`; - const locations = seen.get(fn.universalIdentifier) ?? []; - locations.push(location); - seen.set(fn.universalIdentifier, locations); - } - for (const trigger of fn.triggers ?? []) { - if (trigger.universalIdentifier) { - const location = `functions/${fn.name ?? fn.handlerName}.triggers.${trigger.type}`; - const locations = seen.get(trigger.universalIdentifier) ?? []; - locations.push(location); - seen.set(trigger.universalIdentifier, locations); - } - } - } - - return Array.from(seen.entries()) - .filter(([_, locations]) => locations.length > 1) - .map(([id, locations]) => ({ id, locations })); - } -} - -export const functionEntityBuilder = new FunctionEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object-extension.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object-extension.ts deleted file mode 100644 index 5f8a4321ad8..00000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object-extension.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { glob } from 'fast-glob'; -import { type ObjectExtensionManifest } from 'twenty-shared/application'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { isNonEmptyArray } from 'twenty-shared/utils'; -import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server'; -import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types'; -import { - type EntityBuildResult, - type EntityIdWithLocation, - type ManifestEntityBuilder, - type ManifestWithoutSources, -} from '@/cli/utilities/build/manifest/entities/entity-interface'; - -export class ObjectExtensionEntityBuilder - implements ManifestEntityBuilder -{ - async build( - appPath: string, - ): Promise> { - const extensionFiles = await glob(['**/*.object-extension.ts'], { - cwd: appPath, - ignore: [ - '**/node_modules/**', - '**/*.d.ts', - '**/dist/**', - '**/.twenty/**', - ], - }); - - const manifests: ObjectExtensionManifest[] = []; - - for (const filePath of extensionFiles) { - try { - const absolutePath = `${appPath}/${filePath}`; - - const { manifest } = - await manifestExtractFromFileServer.extractManifestFromFile( - absolutePath, - ); - - manifests.push(manifest); - } catch (error) { - throw new Error( - `Failed to load object extension from ${filePath}: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - return { manifests, filePaths: extensionFiles }; - } - - validate( - extensions: ObjectExtensionManifest[], - errors: ValidationError[], - ): void { - for (const ext of extensions) { - const targetName = - ext.targetObject?.nameSingular ?? - ext.targetObject?.universalIdentifier ?? - 'unknown'; - const extPath = `object-extensions/${targetName}`; - - if (!ext.targetObject) { - errors.push({ - path: extPath, - message: 'Object extension must have a targetObject', - }); - continue; - } - - const { nameSingular, universalIdentifier } = ext.targetObject; - - if (!nameSingular && !universalIdentifier) { - errors.push({ - path: extPath, - message: - 'Object extension targetObject must have either nameSingular or universalIdentifier', - }); - } - - if (nameSingular && universalIdentifier) { - errors.push({ - path: extPath, - message: - 'Object extension targetObject cannot have both nameSingular and universalIdentifier', - }); - } - - if (!isNonEmptyArray(ext.fields)) { - errors.push({ - path: extPath, - message: 'Object extension must have at least one field', - }); - } - - for (const field of ext.fields ?? []) { - const fieldPath = `${extPath}.fields.${field.label ?? 'unknown'}`; - - if (!field.universalIdentifier) { - errors.push({ - path: fieldPath, - message: 'Field must have a universalIdentifier', - }); - } - - if (!field.type) { - errors.push({ - path: fieldPath, - message: 'Field must have a type', - }); - } - - if (!field.label) { - errors.push({ - path: fieldPath, - message: 'Field must have a label', - }); - } - - if ( - (field.type === FieldMetadataType.SELECT || - field.type === FieldMetadataType.MULTI_SELECT) && - !isNonEmptyArray(field.options) - ) { - errors.push({ - path: fieldPath, - message: 'SELECT/MULTI_SELECT field must have options', - }); - } - } - } - } - - findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { - const extensions = manifest.objectExtensions ?? []; - const objects = manifest.objects ?? []; - - const objectFieldIds = new Map(); - for (const obj of objects) { - for (const field of obj.fields ?? []) { - if (field.universalIdentifier) { - const location = `objects/${obj.nameSingular}.fields.${field.label}`; - objectFieldIds.set(field.universalIdentifier, location); - } - } - } - - const extensionFieldLocations = new Map(); - for (const ext of extensions) { - const targetName = - ext.targetObject?.nameSingular ?? - ext.targetObject?.universalIdentifier ?? - 'unknown'; - for (const field of ext.fields ?? []) { - if (field.universalIdentifier) { - const location = `object-extensions/${targetName}.fields.${field.label}`; - const locations = - extensionFieldLocations.get(field.universalIdentifier) ?? []; - locations.push(location); - extensionFieldLocations.set(field.universalIdentifier, locations); - } - } - } - - const duplicates: EntityIdWithLocation[] = []; - - for (const [id, extLocations] of extensionFieldLocations.entries()) { - const objectLocation = objectFieldIds.get(id); - - if (extLocations.length > 1 || objectLocation) { - const allLocations = objectLocation - ? [objectLocation, ...extLocations] - : extLocations; - duplicates.push({ id, locations: allLocations }); - } - } - - return duplicates; - } -} - -export const objectExtensionEntityBuilder = new ObjectExtensionEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object.ts deleted file mode 100644 index fb8e3675796..00000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { glob } from 'fast-glob'; -import { type ObjectManifest } from 'twenty-shared/application'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { isNonEmptyArray } from 'twenty-shared/utils'; -import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server'; -import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types'; -import { - type EntityBuildResult, - type EntityIdWithLocation, - type ManifestEntityBuilder, - type ManifestWithoutSources, -} from '@/cli/utilities/build/manifest/entities/entity-interface'; - -export class ObjectEntityBuilder - implements ManifestEntityBuilder -{ - async build(appPath: string): Promise> { - const objectFiles = await glob(['**/*.object.ts'], { - cwd: appPath, - ignore: [ - '**/node_modules/**', - '**/*.d.ts', - '**/dist/**', - '**/.twenty/**', - ], - }); - - const manifests: ObjectManifest[] = []; - - for (const filePath of objectFiles) { - try { - const absolutePath = `${appPath}/${filePath}`; - - const { manifest } = - await manifestExtractFromFileServer.extractManifestFromFile( - absolutePath, - ); - - manifests.push(manifest); - } catch (error) { - throw new Error( - `Failed to load object from ${filePath}: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - return { manifests, filePaths: objectFiles }; - } - - validate(objects: ObjectManifest[], errors: ValidationError[]): void { - for (const obj of objects) { - const objPath = `objects/${obj.nameSingular ?? 'unknown'}`; - - if (!obj.universalIdentifier) { - errors.push({ - path: objPath, - message: 'Object must have a universalIdentifier', - }); - } - - if (!obj.nameSingular) { - errors.push({ - path: objPath, - message: 'Object must have a nameSingular', - }); - } - - if (!obj.namePlural) { - errors.push({ - path: objPath, - message: 'Object must have a namePlural', - }); - } - - for (const field of obj.fields ?? []) { - const fieldPath = `${objPath}.fields.${field.label ?? 'unknown'}`; - - if (!field.universalIdentifier) { - errors.push({ - path: fieldPath, - message: 'Field must have a universalIdentifier', - }); - } - - if (!field.type) { - errors.push({ - path: fieldPath, - message: 'Field must have a type', - }); - } - - if (!field.label) { - errors.push({ - path: fieldPath, - message: 'Field must have a label', - }); - } - - if ( - (field.type === FieldMetadataType.SELECT || - field.type === FieldMetadataType.MULTI_SELECT) && - !isNonEmptyArray(field.options) - ) { - errors.push({ - path: fieldPath, - message: 'SELECT/MULTI_SELECT field must have options', - }); - } - } - } - } - - findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { - const seen = new Map(); - const objects = manifest.objects ?? []; - - for (const obj of objects) { - if (obj.universalIdentifier) { - const location = `objects/${obj.nameSingular}`; - const locations = seen.get(obj.universalIdentifier) ?? []; - locations.push(location); - seen.set(obj.universalIdentifier, locations); - } - for (const field of obj.fields ?? []) { - if (field.universalIdentifier) { - const location = `objects/${obj.nameSingular}.fields.${field.label}`; - const locations = seen.get(field.universalIdentifier) ?? []; - locations.push(location); - seen.set(field.universalIdentifier, locations); - } - } - } - - return Array.from(seen.entries()) - .filter(([_, locations]) => locations.length > 1) - .map(([id, locations]) => ({ id, locations })); - } -} - -export const objectEntityBuilder = new ObjectEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/role.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/role.ts deleted file mode 100644 index 8a22bd560b1..00000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/role.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { glob } from 'fast-glob'; -import { type RoleManifest } from 'twenty-shared/application'; -import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server'; -import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types'; -import { - type EntityBuildResult, - type EntityIdWithLocation, - type ManifestEntityBuilder, - type ManifestWithoutSources, -} from '@/cli/utilities/build/manifest/entities/entity-interface'; - -export class RoleEntityBuilder implements ManifestEntityBuilder { - async build(appPath: string): Promise> { - const roleFiles = await glob(['**/*.role.ts'], { - cwd: appPath, - ignore: [ - '**/node_modules/**', - '**/*.d.ts', - '**/dist/**', - '**/.twenty/**', - ], - }); - - const manifests: RoleManifest[] = []; - - for (const filePath of roleFiles) { - try { - const absolutePath = `${appPath}/${filePath}`; - - const { manifest } = - await manifestExtractFromFileServer.extractManifestFromFile( - absolutePath, - ); - - manifests.push(manifest); - } catch (error) { - throw new Error( - `Failed to load role from ${filePath}: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - return { manifests, filePaths: roleFiles }; - } - - validate(roles: RoleManifest[], errors: ValidationError[]): void { - for (const role of roles) { - const rolePath = `roles/${role.label ?? 'unknown'}`; - - if (!role.universalIdentifier) { - errors.push({ - path: rolePath, - message: 'Role must have a universalIdentifier', - }); - } - - if (!role.label) { - errors.push({ - path: rolePath, - message: 'Role must have a label', - }); - } - } - } - - findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] { - const seen = new Map(); - const roles = manifest.roles ?? []; - - for (const role of roles) { - if (role.universalIdentifier) { - const location = `roles/${role.label}`; - const locations = seen.get(role.universalIdentifier) ?? []; - locations.push(location); - seen.set(role.universalIdentifier, locations); - } - } - - return Array.from(seen.entries()) - .filter(([_, locations]) => locations.length > 1) - .map(([id, locations]) => ({ id, locations })); - } -} - -export const roleEntityBuilder = new RoleEntityBuilder(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts index f7756ab6d0a..893e0cc3d85 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts @@ -1,44 +1,54 @@ -import { findPathFile } from '@/cli/utilities/file/file-find'; -import { parseJsoncFile } from '@/cli/utilities/file/file-jsonc'; -import { glob } from 'fast-glob'; -import * as fs from 'fs-extra'; +import { basename, extname, relative, sep } from 'path'; import { readFile } from 'fs-extra'; -import { relative, sep } from 'path'; +import { glob } from 'fast-glob'; +import { + type EntityFilePaths, + extractDefineEntity, + ManifestEntityKey, + TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING, +} from '@/cli/utilities/build/manifest/manifest-extract-config'; +import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file'; import { type ApplicationManifest, - OUTPUT_DIR, + type Manifest, + type AssetManifest, + ASSETS_DIR, + type FieldManifest, + type FrontComponentManifest, + type LogicFunctionManifest, + type ObjectManifest, + type RoleManifest, } from 'twenty-shared/application'; -import { FileFolder, type Sources } from 'twenty-shared/types'; -import { applicationEntityBuilder } from '@/cli/utilities/build/manifest/entities/application'; -import { assetEntityBuilder } from '@/cli/utilities/build/manifest/entities/asset'; -import { frontComponentEntityBuilder } from '@/cli/utilities/build/manifest/entities/front-component'; -import { functionEntityBuilder } from '@/cli/utilities/build/manifest/entities/function'; -import { objectEntityBuilder } from '@/cli/utilities/build/manifest/entities/object'; -import { objectExtensionEntityBuilder } from '@/cli/utilities/build/manifest/entities/object-extension'; -import { roleEntityBuilder } from '@/cli/utilities/build/manifest/entities/role'; +import { parseJsoncFile } from '@/cli/utilities/file/file-jsonc'; +import { findPathFile } from '@/cli/utilities/file/file-find'; +import { assertUnreachable } from 'twenty-shared/utils'; +import { type FrontComponentConfig, type LogicFunctionConfig } from '@/sdk'; +import type { Sources } from 'twenty-shared/types'; +import * as fs from 'fs-extra'; -import { manifestExtractFromFileServer } from './manifest-extract-from-file-server'; - -export type EntityFilePaths = { - application: string[]; - objects: string[]; - objectExtensions: string[]; - functions: string[]; - frontComponents: string[]; - roles: string[]; - assets: string[]; -}; - -const loadSources = async (appPath: string): Promise => { - const sources: Sources = {}; - - const tsFiles = await glob(['**/*.ts', '**/*.tsx'], { +const loadSources = async (appPath: string): Promise => { + return await glob(['**/*.ts', '**/*.tsx'], { cwd: appPath, absolute: true, ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**', '**/.twenty/**'], + onlyFiles: true, }); +}; - for (const filepath of tsFiles) { +const loadAssets = async (appPath: string) => { + return await glob([`${ASSETS_DIR}/**/*`], { + cwd: appPath, + onlyFiles: true, + }); +}; + +const computeSources = async ( + appPath: string, + sourceFilePaths: string[], +): Promise => { + const sources: Sources = {}; + + for (const filepath of sourceFilePaths) { const relPath = relative(appPath, filepath); const parts = relPath.split(sep); const content = await fs.readFile(filepath, 'utf8'); @@ -58,165 +68,190 @@ const loadSources = async (appPath: string): Promise => { return sources; }; -export const EMPTY_FILE_PATHS: EntityFilePaths = { - application: [], - objects: [], - objectExtensions: [], - functions: [], - frontComponents: [], - roles: [], - assets: [], -}; - -export type ManifestBuildResult = { - manifest: ApplicationManifest | null; +export const buildManifest = async ( + appPath: string, +): Promise<{ + manifest: Manifest | null; filePaths: EntityFilePaths; - error?: string; -}; + errors: string[]; +}> => { + const filePaths = await loadSources(appPath); + const errors: string[] = []; -export type UpdateManifestChecksumParams = { - manifest: ApplicationManifest; - builtFileInfos: Map< - string, - { checksum: string; builtPath: string; fileFolder: FileFolder } - >; -}; + let application: ApplicationManifest | undefined; + const objects: ObjectManifest[] = []; + const fields: FieldManifest[] = []; + const roles: RoleManifest[] = []; + const logicFunctions: LogicFunctionManifest[] = []; + const frontComponents: FrontComponentManifest[] = []; + const publicAssets: AssetManifest[] = []; -export const updateManifestChecksum = ({ - manifest, - builtFileInfos, -}: UpdateManifestChecksumParams): ApplicationManifest => { - let result = structuredClone(manifest); - for (const [ - builtPath, - { fileFolder, checksum }, - ] of builtFileInfos.entries()) { - const rootBuiltPath = relative(OUTPUT_DIR, builtPath); - if (fileFolder === FileFolder.BuiltFunction) { - const functions = result.functions ?? []; - const fnIndex = functions.findIndex( - (f) => f.builtHandlerPath === rootBuiltPath, - ); - if (fnIndex === -1) { - continue; - } - result = { - ...result, - functions: functions.map((fn, index) => - index === fnIndex ? { ...fn, builtHandlerChecksum: checksum } : fn, - ), - }; - } + const applicationFilePaths: string[] = []; + const objectsFilePaths: string[] = []; + const fieldsFilePaths: string[] = []; + const rolesFilePaths: string[] = []; + const logicFunctionsFilePaths: string[] = []; + const frontComponentsFilePaths: string[] = []; + const publicAssetsFilePaths: string[] = []; - if (fileFolder === FileFolder.PublicAsset) { - const assets = result.publicAssets ?? []; - const assetIndex = assets.findIndex((a) => a.filePath === rootBuiltPath); - if (assetIndex === -1) { - continue; - } - result = { - ...result, - publicAssets: assets.map((asset, index) => - index === assetIndex ? { ...asset, checksum } : asset, - ), - }; + for (const filePath of filePaths) { + const fileContent = await readFile(filePath, 'utf-8'); + const relativePath = relative(appPath, filePath); + + const targetFunctionName = extractDefineEntity(fileContent); + + if (!targetFunctionName) { continue; } - if (fileFolder === FileFolder.BuiltFrontComponent) { - const frontComponents = result.frontComponents ?? []; - const componentIndex = - frontComponents.findIndex( - (c) => c.builtComponentPath === rootBuiltPath, - ) ?? -1; - if (componentIndex === -1) { - continue; + const entity = TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING[targetFunctionName]; + + switch (entity) { + case ManifestEntityKey.Application: { + const extract = await extractManifestFromFile({ + appPath, + filePath, + }); + application = extract.config; + errors.push(...extract.errors); + applicationFilePaths.push(relativePath); + break; + } + case ManifestEntityKey.Objects: { + const extract = await extractManifestFromFile({ + appPath, + filePath, + }); + objects.push(extract.config); + errors.push(...extract.errors); + objectsFilePaths.push(relativePath); + break; + } + case ManifestEntityKey.Fields: { + const extract = await extractManifestFromFile({ + appPath, + filePath, + }); + fields.push(extract.config); + errors.push(...extract.errors); + fieldsFilePaths.push(relativePath); + break; + } + case ManifestEntityKey.Roles: { + const extract = await extractManifestFromFile({ + appPath, + filePath, + }); + roles.push(extract.config); + errors.push(...extract.errors); + rolesFilePaths.push(relativePath); + break; + } + case ManifestEntityKey.LogicFunctions: { + const extract = await extractManifestFromFile({ + appPath, + filePath, + }); + + errors.push(...extract.errors); + + const { handler: _, ...rest } = extract.config; + + const config: LogicFunctionManifest = { + ...rest, + handlerName: 'default.handler', + sourceHandlerPath: filePath, + builtHandlerPath: filePath.replace(/\.tsx?$/, '.mjs'), + builtHandlerChecksum: null, + }; + + logicFunctions.push(config); + logicFunctionsFilePaths.push(relativePath); + break; + } + case ManifestEntityKey.FrontComponents: { + const extract = await extractManifestFromFile({ + appPath, + filePath, + }); + + errors.push(...extract.errors); + + const { component, ...rest } = extract.config; + + const config: FrontComponentManifest = { + ...rest, + componentName: component.name, + sourceComponentPath: filePath, + builtComponentPath: filePath.replace(/\.tsx?$/, '.mjs'), + builtComponentChecksum: null, + }; + + frontComponents.push(config); + frontComponentsFilePaths.push(relativePath); + break; + } + case ManifestEntityKey.PublicAssets: { + // Public assets are handled below + break; + } + default: { + assertUnreachable(entity); } - result = { - ...result, - frontComponents: frontComponents.map((component, index) => - index === componentIndex - ? { ...component, builtComponentChecksum: checksum } - : component, - ), - }; } } - return result; -}; -export const runManifestBuild = async ( - appPath: string, -): Promise => { - try { - manifestExtractFromFileServer.init(appPath); + const assetFiles = await loadAssets(appPath); - const packageJson = await parseJsoncFile( - await findPathFile(appPath, 'package.json'), - ); - - const yarnLock = await readFile( - await findPathFile(appPath, 'yarn.lock'), - 'utf8', - ); - - const [ - applicationBuildResult, - objectBuildResult, - objectExtensionBuildResult, - functionBuildResult, - frontComponentBuildResult, - roleBuildResult, - assetBuildResult, - sources, - ] = await Promise.all([ - applicationEntityBuilder.build(appPath), - objectEntityBuilder.build(appPath), - objectExtensionEntityBuilder.build(appPath), - functionEntityBuilder.build(appPath), - frontComponentEntityBuilder.build(appPath), - roleEntityBuilder.build(appPath), - assetEntityBuilder.build(appPath), - loadSources(appPath), - ]); - - const application = applicationBuildResult.manifests[0]; - const objectManifests = objectBuildResult.manifests; - const objectExtensionManifests = objectExtensionBuildResult.manifests; - const functionManifests = functionBuildResult.manifests; - const frontComponentManifests = frontComponentBuildResult.manifests; - const roleManifests = roleBuildResult.manifests; - const assetManifests = assetBuildResult.manifests; - - const filePaths: EntityFilePaths = { - application: applicationBuildResult.filePaths, - objects: objectBuildResult.filePaths, - objectExtensions: objectExtensionBuildResult.filePaths, - functions: functionBuildResult.filePaths, - frontComponents: frontComponentBuildResult.filePaths, - roles: roleBuildResult.filePaths, - assets: assetBuildResult.filePaths, - }; - - const manifest: ApplicationManifest = { - application, - objects: objectManifests, - objectExtensions: objectExtensionManifests, - functions: functionManifests, - frontComponents: frontComponentManifests, - roles: roleManifests, - publicAssets: assetManifests, - sources, - packageJson, - yarnLock, - }; - - return { manifest, filePaths }; - } catch (error) { - return { - manifest: null, - filePaths: EMPTY_FILE_PATHS, - error: error instanceof Error ? error.message : `${error}`, - }; + for (const assetFile of assetFiles) { + publicAssets.push({ + filePath: assetFile, + fileName: basename(assetFile), + fileType: extname(assetFile).replace(/^\./, ''), + checksum: null, + }); + publicAssetsFilePaths.push(relative(appPath, assetFile)); } + + if (!application) { + errors.push( + 'Cannot build application, please export default defineApplication() to define an application', + ); + } + + const packageJson = await parseJsoncFile( + await findPathFile(appPath, 'package.json'), + ); + + const yarnLock = await readFile( + await findPathFile(appPath, 'yarn.lock'), + 'utf8', + ); + + const manifest = !application + ? null + : { + application, + objects, + fields, + roles, + logicFunctions, + frontComponents, + publicAssets, + sources: await computeSources(appPath, filePaths), + packageJson, + yarnLock, + }; + + const entityFilePaths: EntityFilePaths = { + application: applicationFilePaths, + objects: objectsFilePaths, + fields: fieldsFilePaths, + roles: rolesFilePaths, + logicFunctions: logicFunctionsFilePaths, + frontComponents: frontComponentsFilePaths, + publicAssets: publicAssetsFilePaths, + }; + + return { manifest, filePaths: entityFilePaths, errors }; }; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config-from-file.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config-from-file.ts new file mode 100644 index 00000000000..767cf95dda6 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config-from-file.ts @@ -0,0 +1,84 @@ +import path from 'path'; +import * as fs from 'fs-extra'; +import { createRequire } from 'module'; +import * as esbuild from 'esbuild'; +import os from 'os'; +import { isDefined, isPlainObject } from 'twenty-shared/utils'; +import { type ValidationResult } from '@/sdk'; + +export const extractManifestFromFile = async ({ + filePath, + appPath, +}: { + filePath: string; + appPath: string; +}): Promise> => { + const module = await loadModule({ filePath, appPath }); + + return extractDefaultConfigFromModuleOrThrow(module, filePath); +}; + +const loadModule = async ({ + filePath, + appPath, +}: { + filePath: string; + appPath: string; +}): Promise> => { + const tsconfigPath = path.join(appPath, 'tsconfig.json'); + const hasTsconfig = await fs.pathExists(tsconfigPath); + + // Resolve react from the app's node_modules for the alias + const appRequire = createRequire(path.join(appPath, 'package.json')); + let reactPath: string | undefined; + let reactDomPath: string | undefined; + + try { + reactPath = path.dirname(appRequire.resolve('react/package.json')); + reactDomPath = path.dirname(appRequire.resolve('react-dom/package.json')); + } catch { + // React not installed in app, will be bundled if used + } + + const result = await esbuild.build({ + entryPoints: [filePath], + bundle: true, + write: false, + format: 'cjs', + platform: 'node', + target: 'node18', + jsx: 'automatic', + tsconfig: hasTsconfig ? tsconfigPath : undefined, + alias: { + ...(reactPath && { react: reactPath }), + ...(reactDomPath && { 'react-dom': reactDomPath }), + }, + logLevel: 'silent', + }); + + const code = result.outputFiles[0].text; + + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'twenty-manifest-')); + const tempFile = path.join(tempDir, 'module.cjs'); + + try { + await fs.writeFile(tempFile, code); + + return require(tempFile) as Record; + } finally { + await fs.remove(tempDir); + } +}; + +const extractDefaultConfigFromModuleOrThrow = ( + module: Record, + filePath: string, +): ValidationResult => { + if (isDefined(module.default) && isPlainObject(module.default)) { + return module.default as ValidationResult; + } + + throw new Error( + `Config file ${filePath} must export a config object default export`, + ); +}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config.ts new file mode 100644 index 00000000000..012dccfce23 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config.ts @@ -0,0 +1,82 @@ +import * as ts from 'typescript'; + +export enum TargetFunction { + DefineApplication = 'defineApplication', + DefineField = 'defineField', + DefineLogicFunction = 'defineLogicFunction', + DefineObject = 'defineObject', + DefineRole = 'defineRole', + DefineFrontComponent = 'defineFrontComponent', +} + +export enum ManifestEntityKey { + Application = 'application', + Fields = 'fields', + LogicFunctions = 'logicFunctions', + Objects = 'objects', + Roles = 'roles', + FrontComponents = 'frontComponents', + PublicAssets = 'publicAssets', +} + +export type EntityFilePaths = Record; + +export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record< + TargetFunction, + ManifestEntityKey +> = { + [TargetFunction.DefineApplication]: ManifestEntityKey.Application, + [TargetFunction.DefineField]: ManifestEntityKey.Fields, + [TargetFunction.DefineLogicFunction]: ManifestEntityKey.LogicFunctions, + [TargetFunction.DefineObject]: ManifestEntityKey.Objects, + [TargetFunction.DefineRole]: ManifestEntityKey.Roles, + [TargetFunction.DefineFrontComponent]: ManifestEntityKey.FrontComponents, +}; + +const computeIsTargetFunctionCall = (node: ts.Node): string | undefined => { + if (!ts.isCallExpression(node)) { + return undefined; + } + + const expression = node.expression; + if (ts.isIdentifier(expression)) { + if ((Object.values(TargetFunction) as string[]).includes(expression.text)) { + return expression.text; + } + } + + return undefined; +}; + +export const extractDefineEntity = ( + fileContent: string, +): TargetFunction | undefined => { + const sourceFile = ts.createSourceFile( + 'temp.ts', + fileContent, + ts.ScriptTarget.Latest, + true, + ); + + const children: ts.Node[] = []; + + ts.forEachChild(sourceFile, (node) => { + children.push(node); + }); + + for (const node of children) { + if (ts.isExportAssignment(node)) { + if (node.isExportEquals || !node.expression) { + return; + } + + const targetFunction = computeIsTargetFunctionCall(node.expression); + + if (targetFunction) { + return targetFunction as TargetFunction; + } + } + } + + return; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-from-file-server.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-from-file-server.ts deleted file mode 100644 index 1bed0d4377d..00000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-from-file-server.ts +++ /dev/null @@ -1,115 +0,0 @@ -import * as esbuild from 'esbuild'; -import * as fs from 'fs-extra'; -import { createRequire } from 'module'; -import * as os from 'os'; -import path from 'path'; -import { isDefined, isPlainObject } from 'twenty-shared/utils'; - -export type ExtractedManifest = { - manifest: TManifest; - exportName: string | null; -}; - -export class ManifestExtractFromFileServer { - private appPath: string | null = null; - - init(appPath: string): void { - this.appPath = appPath; - } - - async extractManifestFromFile( - filepath: string, - ): Promise> { - if (!this.appPath) { - throw new Error( - 'ManifestExtractFromFileServer not initialized. Call init(appPath) first.', - ); - } - - const module = await this.loadModule(filepath); - - const result = this.extractConfigFromModule(module); - - if (!result) { - throw new Error( - `Config file ${filepath} must export a config object (default export or any named object export)`, - ); - } - - return result; - } - - private async loadModule(filepath: string): Promise> { - if (!this.appPath) { - throw new Error( - 'ManifestExtractFromFileServer not initialized. Call init(appPath) first.', - ); - } - - const tsconfigPath = path.join(this.appPath, 'tsconfig.json'); - const hasTsconfig = await fs.pathExists(tsconfigPath); - - // Resolve react from the app's node_modules for the alias - const appRequire = createRequire(path.join(this.appPath, 'package.json')); - let reactPath: string | undefined; - let reactDomPath: string | undefined; - - try { - reactPath = path.dirname(appRequire.resolve('react/package.json')); - reactDomPath = path.dirname(appRequire.resolve('react-dom/package.json')); - } catch { - // React not installed in app, will be bundled if used - } - - const result = await esbuild.build({ - entryPoints: [filepath], - bundle: true, - write: false, - format: 'cjs', - platform: 'node', - target: 'node18', - jsx: 'automatic', - tsconfig: hasTsconfig ? tsconfigPath : undefined, - // Use alias to resolve react from app's node_modules - alias: { - ...(reactPath && { react: reactPath }), - ...(reactDomPath && { 'react-dom': reactDomPath }), - }, - logLevel: 'silent', - }); - - const code = result.outputFiles[0].text; - - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'twenty-manifest-'), - ); - const tempFile = path.join(tempDir, 'module.cjs'); - - try { - await fs.writeFile(tempFile, code); - - return require(tempFile) as Record; - } finally { - await fs.remove(tempDir); - } - } - - private extractConfigFromModule( - module: Record, - ): ExtractedManifest | undefined { - if (isDefined(module.default) && isPlainObject(module.default)) { - return { manifest: module.default as T, exportName: null }; - } - - for (const [key, value] of Object.entries(module)) { - if (isPlainObject(value)) { - return { manifest: value as T, exportName: key }; - } - } - - return undefined; - } -} - -export const manifestExtractFromFileServer = - new ManifestExtractFromFileServer(); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-types.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-types.ts index b4093d76b89..a675cefd2fe 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-types.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-types.ts @@ -2,14 +2,3 @@ export type ValidationError = { path: string; message: string; }; - -export type ValidationWarning = { - path?: string; - message: string; -}; - -export type ValidationResult = { - isValid: boolean; - errors: ValidationError[]; - warnings: ValidationWarning[]; -}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts deleted file mode 100644 index 7be25123c4a..00000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { isNonEmptyArray } from 'twenty-shared/utils'; -import { applicationEntityBuilder } from './entities/application'; -import { - type EntityIdWithLocation, - type ManifestWithoutSources, -} from '@/cli/utilities/build/manifest/entities/entity-interface'; -import { frontComponentEntityBuilder } from './entities/front-component'; -import { functionEntityBuilder } from './entities/function'; -import { objectEntityBuilder } from './entities/object'; -import { objectExtensionEntityBuilder } from './entities/object-extension'; -import { roleEntityBuilder } from './entities/role'; -import { - type ValidationError, - type ValidationResult, - type ValidationWarning, -} from '@/cli/utilities/build/manifest/manifest-types'; - -const collectAllDuplicates = ( - manifest: ManifestWithoutSources, -): EntityIdWithLocation[] => { - return [ - ...applicationEntityBuilder.findDuplicates(manifest), - ...objectEntityBuilder.findDuplicates(manifest), - ...objectExtensionEntityBuilder.findDuplicates(manifest), - ...functionEntityBuilder.findDuplicates(manifest), - ...roleEntityBuilder.findDuplicates(manifest), - ...frontComponentEntityBuilder.findDuplicates(manifest), - ]; -}; - -export const validateManifest = ( - manifest: ManifestWithoutSources, -): ValidationResult => { - const errors: ValidationError[] = []; - const warnings: ValidationWarning[] = []; - - applicationEntityBuilder.validate( - manifest.application ? [manifest.application] : [], - errors, - ); - objectEntityBuilder.validate(manifest.objects ?? [], errors); - objectExtensionEntityBuilder.validate( - manifest.objectExtensions ?? [], - errors, - ); - functionEntityBuilder.validate(manifest.functions ?? [], errors); - roleEntityBuilder.validate(manifest.roles ?? [], errors); - frontComponentEntityBuilder.validate(manifest.frontComponents ?? [], errors); - - const duplicates = collectAllDuplicates(manifest); - for (const dup of duplicates) { - errors.push({ - path: dup.locations.join(', '), - message: `Duplicate universalIdentifier: ${dup.id}`, - }); - } - - if (!isNonEmptyArray(manifest.objects)) { - warnings.push({ - message: 'No objects defined', - }); - } - - if (!isNonEmptyArray(manifest.functions)) { - warnings.push({ - message: 'No functions defined', - }); - } - - return { - isValid: errors.length === 0, - errors, - warnings, - }; -}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-writer.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-writer.ts index 60c68f1ce46..579b474c085 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-writer.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-writer.ts @@ -1,13 +1,10 @@ import * as fs from 'fs-extra'; import path from 'path'; -import { - type ApplicationManifest, - OUTPUT_DIR, -} from 'twenty-shared/application'; +import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application'; export const writeManifestToOutput = async ( appPath: string, - manifest: ApplicationManifest, + manifest: Manifest, ): Promise => { const outputDir = path.join(appPath, OUTPUT_DIR); await fs.ensureDir(outputDir); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/update-manifest-checksums.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/update-manifest-checksums.ts new file mode 100644 index 00000000000..f014db64205 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/update-manifest-checksums.ts @@ -0,0 +1,82 @@ +import { relative } from 'path'; +import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application'; +import { FileFolder } from 'twenty-shared/types'; + +import type { EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config'; + +export type ManifestBuildResult = { + manifest: Manifest | null; + filePaths: EntityFilePaths; + error?: string; +}; + +export type UpdateManifestChecksumParams = { + manifest: Manifest; + builtFileInfos: Map< + string, + { checksum: string; builtPath: string; fileFolder: FileFolder } + >; +}; + +export const updateManifestChecksums = ({ + manifest, + builtFileInfos, +}: UpdateManifestChecksumParams): Manifest => { + let result = structuredClone(manifest); + for (const [ + builtPath, + { fileFolder, checksum }, + ] of builtFileInfos.entries()) { + const rootBuiltPath = relative(OUTPUT_DIR, builtPath); + if (fileFolder === FileFolder.BuiltLogicFunction) { + const logicFunctions = result.logicFunctions; + const fnIndex = logicFunctions.findIndex( + (f) => f.builtHandlerPath === rootBuiltPath, + ); + if (fnIndex === -1) { + continue; + } + result = { + ...result, + logicFunctions: logicFunctions.map((fn, index) => + index === fnIndex ? { ...fn, builtHandlerChecksum: checksum } : fn, + ), + }; + } + + if (fileFolder === FileFolder.PublicAsset) { + const assets = result.publicAssets; + const assetIndex = assets.findIndex((a) => a.filePath === rootBuiltPath); + if (assetIndex === -1) { + continue; + } + result = { + ...result, + publicAssets: assets.map((asset, index) => + index === assetIndex ? { ...asset, checksum } : asset, + ), + }; + continue; + } + + if (fileFolder === FileFolder.BuiltFrontComponent) { + const frontComponents = result.frontComponents; + const componentIndex = + frontComponents.findIndex( + (c) => c.builtComponentPath === rootBuiltPath, + ) ?? -1; + if (componentIndex === -1) { + continue; + } + result = { + ...result, + frontComponents: frontComponents.map((component, index) => + index === componentIndex + ? { ...component, builtComponentChecksum: checksum } + : component, + ), + }; + } + } + return result; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/validate-manifest.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/validate-manifest.ts new file mode 100644 index 00000000000..2f92a284ce5 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/validate-manifest.ts @@ -0,0 +1,60 @@ +import { type Manifest } from 'twenty-shared/application'; +import { isNonEmptyArray } from 'twenty-shared/utils'; + +const extractDuplicates = (values: string[]): string[] => { + const seen = new Set(); + const duplicates = new Set(); + + for (const value of values) { + if (seen.has(value)) { + duplicates.add(value); + } else { + seen.add(value); + } + } + + return Array.from(duplicates); +}; + +const findUniversalIdentifiers = (obj: object): string[] => { + const universalIdentifiers: string[] = []; + + if (!obj) { + return []; + } + + for (const [key, val] of Object.entries(obj)) { + if (key === 'universalIdentifier' && typeof val === 'string') { + universalIdentifiers.push(val); + } + if (typeof val === 'object') { + universalIdentifiers.push(...findUniversalIdentifiers(val)); + } + } + + return universalIdentifiers; +}; +export const validateManifest = (manifest: Manifest) => { + const errors: string[] = []; + const warnings: string[] = []; + + const duplicates = extractDuplicates(findUniversalIdentifiers(manifest)); + + if (duplicates.length > 0) { + errors.push(`Duplicate universal identifiers: ${duplicates.join(', ')}`); + } + + if (!isNonEmptyArray(manifest.objects)) { + warnings.push('No object defined'); + } + + if (!isNonEmptyArray(manifest.logicFunctions)) { + warnings.push('No logic function defined'); + } + + if (!isNonEmptyArray(manifest.frontComponents)) { + warnings.push('No front component defined'); + } + + return { errors, warnings, isValid: errors.length === 0 }; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/dev/dev-mode-orchestrator.ts b/packages/twenty-sdk/src/cli/utilities/dev/dev-mode-orchestrator.ts index dd8bdf78206..fee5f4047a1 100644 --- a/packages/twenty-sdk/src/cli/utilities/dev/dev-mode-orchestrator.ts +++ b/packages/twenty-sdk/src/cli/utilities/dev/dev-mode-orchestrator.ts @@ -1,16 +1,16 @@ import { type ManifestBuildResult, - runManifestBuild, - updateManifestChecksum, -} from '@/cli/utilities/build/manifest/manifest-build'; + updateManifestChecksums, +} from '@/cli/utilities/build/manifest/update-manifest-checksums'; import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer'; import { ApiService } from '@/cli/utilities/api/api-service'; import { FileUploader } from '@/cli/utilities/file/file-uploader'; import { type FileFolder } from 'twenty-shared/types'; -import { validateManifest } from '@/cli/utilities/build/manifest/manifest-validate'; import type { Location } from 'esbuild'; import { type DevUiStateManager } from '@/cli/utilities/dev/dev-ui-state-manager'; import { type EventName } from 'chokidar/handler.js'; +import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build'; +import { validateManifest } from '@/cli/utilities/build/manifest/validate-manifest'; export type DevModeOrchestratorOptions = { appPath: string; @@ -232,21 +232,38 @@ export class DevModeOrchestrator { manifestStatus: 'building', }); - const result = await runManifestBuild(this.appPath); + const result = await buildManifest(this.appPath); - if (result.error || !result.manifest) { + if (result.errors.length > 0 || !result.manifest) { + for (const error of result.errors) { + this.uiStateManager.addEvent({ + message: error, + status: 'error', + }); + } this.uiStateManager.updateManifestState({ manifestStatus: 'error', - }); - this.uiStateManager.addEvent({ - message: result.error ?? 'Unknown error', - status: 'error', + error: result.errors[result.errors.length - 1], }); return; } const validation = validateManifest(result.manifest); + if (!validation.isValid) { + for (const e of validation.errors) { + this.uiStateManager.addEvent({ + message: e, + status: 'error', + }); + this.uiStateManager.updateManifestState({ + manifestStatus: 'error', + error: e, + }); + } + return; + } + this.uiStateManager.updateManifestState({ appName: result.manifest.application.displayName, }); @@ -255,25 +272,10 @@ export class DevModeOrchestrator { manifestFilePaths: result.filePaths, }); - if (!validation.isValid) { - for (const e of validation.errors) { - this.uiStateManager.addEvent({ - message: `${e.path}: ${e.message}`, - status: 'error', - }); - this.uiStateManager.updateManifestState({ - manifestStatus: 'error', - }); - } - - return; - } - if (validation.warnings.length > 0) { for (const warning of validation.warnings) { - const path = warning.path ? `${warning.path}: ` : ''; this.uiStateManager.addEvent({ - message: `⚠ ${path}${warning.message}`, + message: `⚠ ${warning}`, status: 'warning', }); } @@ -304,7 +306,7 @@ export class DevModeOrchestrator { await Promise.all(this.activeUploads); } - const manifest = updateManifestChecksum({ + const manifest = updateManifestChecksums({ manifest: result.manifest, builtFileInfos: this.builtFileInfos, }); @@ -344,7 +346,7 @@ export class DevModeOrchestrator { }); } else { this.uiStateManager.addEvent({ - message: `Sync failed: ${JSON.stringify(syncResult.error, null, 2)}`, + message: `Sync failed with error ${JSON.stringify(syncResult.error, null, 2)}`, status: 'error', }); this.uiStateManager.updateManifestState({ @@ -353,7 +355,7 @@ export class DevModeOrchestrator { } } catch (error) { this.uiStateManager.addEvent({ - message: `Sync failed: ${JSON.stringify(error, null, 2)}`, + message: `Sync failed with error ${JSON.stringify(error, null, 2)}`, status: 'error', }); this.uiStateManager.updateManifestState({ diff --git a/packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state-manager.ts b/packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state-manager.ts index e66f8d75c1f..bc4e72c8953 100644 --- a/packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state-manager.ts +++ b/packages/twenty-sdk/src/cli/utilities/dev/dev-ui-state-manager.ts @@ -6,7 +6,7 @@ import { type UiEvent, type DevUiState, } from '@/cli/utilities/dev/dev-ui-state'; -import { type EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-build'; +import type { EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config'; const MAX_EVENT_NUMBER = 200; @@ -92,7 +92,7 @@ export class DevUiStateManager { ...this.state, ...(manifestStatus ? { manifestStatus } : {}), ...(appName ? { appName } : {}), - ...(error ? { error } : {}), + ...(error ? { error } : { error: undefined }), }; this.notify(); @@ -104,16 +104,14 @@ export class DevUiStateManager { switch (entityType) { case 'objects': return SyncableEntity.Object; - case 'objectExtensions': - return SyncableEntity.ObjectExtension; - case 'functions': - return SyncableEntity.Function; + case 'fields': + return SyncableEntity.Field; + case 'logicFunctions': + return SyncableEntity.LogicFunction; case 'frontComponents': return SyncableEntity.FrontComponent; case 'roles': return SyncableEntity.Role; - case 'assets': - return SyncableEntity.PublicAsset; default: return; } diff --git a/packages/twenty-sdk/src/cli/utilities/dev/dev-ui.tsx b/packages/twenty-sdk/src/cli/utilities/dev/dev-ui.tsx index 48104c12917..575a49cee99 100644 --- a/packages/twenty-sdk/src/cli/utilities/dev/dev-ui.tsx +++ b/packages/twenty-sdk/src/cli/utilities/dev/dev-ui.tsx @@ -26,11 +26,10 @@ const STATUS_COLORS: Record = { const ENTITY_LABELS: Record = { [SyncableEntity.Object]: 'Objects', - [SyncableEntity.ObjectExtension]: 'Object Extensions', - [SyncableEntity.Function]: 'Functions', - [SyncableEntity.FrontComponent]: 'Front Components', + [SyncableEntity.Field]: 'Fields', + [SyncableEntity.LogicFunction]: 'Logic functions', + [SyncableEntity.FrontComponent]: 'Front components', [SyncableEntity.Role]: 'Roles', - [SyncableEntity.PublicAsset]: 'Public Assets', }; const ENTITY_ORDER = Object.keys(ENTITY_LABELS) as SyncableEntity[]; @@ -194,7 +193,7 @@ export const renderDevUI = async ( return ( - {icon ?? ''} + {icon ? `${icon} ` : ''} {config.text} {snapshot.error && `: ${snapshot.error}`} diff --git a/packages/twenty-sdk/src/cli/utilities/entity/__tests__/get-function-base-file.spec.ts b/packages/twenty-sdk/src/cli/utilities/entity/__tests__/get-function-base-file.spec.ts index e5413efa7e1..f5157f5e86e 100644 --- a/packages/twenty-sdk/src/cli/utilities/entity/__tests__/get-function-base-file.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/entity/__tests__/get-function-base-file.spec.ts @@ -1,14 +1,16 @@ -import { getFunctionBaseFile } from '@/cli/utilities/entity/entity-function-template'; +import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-function-template'; describe('getFunctionBaseFile', () => { it('should render proper file using defineFunction', () => { - const result = getFunctionBaseFile({ + const result = getLogicFunctionBaseFile({ name: 'my-function', universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4', }); - expect(result).toContain("import { defineFunction } from 'twenty-sdk'"); - expect(result).toContain('export default defineFunction({'); + expect(result).toContain( + "import { defineLogicFunction } from 'twenty-sdk'", + ); + expect(result).toContain('export default defineLogicFunction({'); expect(result).toContain( "universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4'", @@ -21,12 +23,12 @@ describe('getFunctionBaseFile', () => { expect(result).toContain('const handler = async'); expect(result).toContain( - "description: 'Add a description for your function'", + "description: 'Add a description for your logic function'", ); }); it('should generate unique UUID when not provided', () => { - const result = getFunctionBaseFile({ + const result = getLogicFunctionBaseFile({ name: 'auto-uuid-function', }); @@ -36,7 +38,7 @@ describe('getFunctionBaseFile', () => { }); it('should use kebab-case for function name', () => { - const result = getFunctionBaseFile({ + const result = getLogicFunctionBaseFile({ name: 'my-awesome-function', }); @@ -45,7 +47,7 @@ describe('getFunctionBaseFile', () => { }); it('should include trigger examples as comments', () => { - const result = getFunctionBaseFile({ + const result = getLogicFunctionBaseFile({ name: 'example-function', }); diff --git a/packages/twenty-sdk/src/cli/utilities/entity/entity-field-template.ts b/packages/twenty-sdk/src/cli/utilities/entity/entity-field-template.ts new file mode 100644 index 00000000000..2cd4c78156f --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/entity/entity-field-template.ts @@ -0,0 +1,31 @@ +import { type FieldMetadataType } from 'twenty-shared/types'; +import { v4 } from 'uuid'; + +export const getFieldBaseFile = ({ + data, +}: { + data: { + name: string; + label: string; + type: FieldMetadataType; + objectUniversalIdentifier: string; + description?: string; + }; + name: string; +}) => { + const universalIdentifier = v4(); + const descriptionLine = data.description + ? `\n description: '${data.description}',` + : ''; + + return `import { defineField, FieldType } from 'twenty-sdk'; + +export default defineField({ + universalIdentifier: '${universalIdentifier}', + name: '${data.name}', + label: '${data.label}', + type: FieldMetadataType.${data.type}, + objectUniversalIdentifier: '${data.objectUniversalIdentifier}',${descriptionLine} +}); +`; +}; diff --git a/packages/twenty-sdk/src/cli/utilities/entity/entity-function-template.ts b/packages/twenty-sdk/src/cli/utilities/entity/entity-logic-function-template.ts similarity index 82% rename from packages/twenty-sdk/src/cli/utilities/entity/entity-function-template.ts rename to packages/twenty-sdk/src/cli/utilities/entity/entity-logic-function-template.ts index a6d8695f1de..1a72b9f916c 100644 --- a/packages/twenty-sdk/src/cli/utilities/entity/entity-function-template.ts +++ b/packages/twenty-sdk/src/cli/utilities/entity/entity-logic-function-template.ts @@ -1,7 +1,7 @@ import kebabCase from 'lodash.kebabcase'; import { v4 } from 'uuid'; -export const getFunctionBaseFile = ({ +export const getLogicFunctionBaseFile = ({ name, universalIdentifier = v4(), }: { @@ -11,9 +11,9 @@ export const getFunctionBaseFile = ({ const kebabCaseName = kebabCase(name); const triggerUniversalIdentifier = v4(); - return `import { defineFunction } from 'twenty-sdk'; + return `import { defineLogicFunction } from 'twenty-sdk'; -// Handler function - rename and implement your logic +// Logic function handler - rename and implement your logic const handler = async (params: { a: string; b: number; @@ -26,10 +26,10 @@ const handler = async (params: { return { message }; }; -export default defineFunction({ +export default defineLogicFunction({ universalIdentifier: '${universalIdentifier}', name: '${kebabCaseName}', - description: 'Add a description for your function', + description: 'Add a description for your logic function', timeoutSeconds: 5, handler, triggers: [ diff --git a/packages/twenty-sdk/src/index.ts b/packages/twenty-sdk/src/index.ts index 0027553248e..c404b4b9504 100644 --- a/packages/twenty-sdk/src/index.ts +++ b/packages/twenty-sdk/src/index.ts @@ -7,4 +7,4 @@ * |___/ */ -export * from './application'; +export * from './sdk'; diff --git a/packages/twenty-sdk/src/sdk/application/__tests__/define-app.spec.ts b/packages/twenty-sdk/src/sdk/application/__tests__/define-app.spec.ts new file mode 100644 index 00000000000..bf7cdd1589c --- /dev/null +++ b/packages/twenty-sdk/src/sdk/application/__tests__/define-app.spec.ts @@ -0,0 +1,72 @@ +import { defineApplication } from '@/sdk'; + +describe('defineApplication', () => { + it('should return successful validation result when valid', () => { + const config = { + universalIdentifier: 'a9faf5f8-cf7e-4f24-9d37-fd523c30febe', + displayName: 'My App', + description: 'My app description', + icon: 'IconWorld', + defaultRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2', + }; + + const result = defineApplication(config); + + expect(result.success).toBe(true); + expect(result.config).toEqual(config); + expect(result.errors).toEqual([]); + }); + + it('should pass through all optional fields', () => { + const config = { + universalIdentifier: 'a9faf5f8-cf7e-4f24-9d37-fd523c30febe', + displayName: 'My App', + description: 'My app description', + icon: 'IconWorld', + applicationVariables: { + API_KEY: { + universalIdentifier: '3a327392-3a0f-4605-9223-0633f063eaf6', + description: 'API Key', + isSecret: true, + }, + }, + defaultRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2', + }; + + const result = defineApplication(config); + + expect(result.success).toBe(true); + expect(result.config).toEqual(config); + expect(result.config?.applicationVariables).toBeDefined(); + expect(result.config?.defaultRoleUniversalIdentifier).toBe( + '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2', + ); + }); + + it('should return error when universalIdentifier is missing', () => { + const config = { + displayName: 'My App', + }; + + const result = defineApplication(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Application must have a universalIdentifier', + ); + }); + + it('should return error when universalIdentifier is empty string', () => { + const config = { + universalIdentifier: '', + displayName: 'My App', + }; + + const result = defineApplication(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Application must have a universalIdentifier', + ); + }); +}); diff --git a/packages/twenty-sdk/src/sdk/application/define-application.ts b/packages/twenty-sdk/src/sdk/application/define-application.ts new file mode 100644 index 00000000000..252948f7a15 --- /dev/null +++ b/packages/twenty-sdk/src/sdk/application/define-application.ts @@ -0,0 +1,22 @@ +import { type ApplicationManifest } from 'twenty-shared/application'; +import { createValidationResult } from '@/sdk/common/utils/create-validation-result'; +import { type DefineEntity } from '@/sdk/common/types/define-entity.type'; + +export const defineApplication: DefineEntity = ( + config, +) => { + const errors = []; + + if (!config.universalIdentifier) { + errors.push('Application must have a universalIdentifier'); + } + + if (!config.defaultRoleUniversalIdentifier) { + errors.push('Application must have a defaultRoleUniversalIdentifier'); + } + + return createValidationResult({ + config, + errors, + }); +}; diff --git a/packages/twenty-sdk/src/sdk/common/types/define-entity.type.ts b/packages/twenty-sdk/src/sdk/common/types/define-entity.type.ts new file mode 100644 index 00000000000..754b0dba8c0 --- /dev/null +++ b/packages/twenty-sdk/src/sdk/common/types/define-entity.type.ts @@ -0,0 +1,28 @@ +import { + type ApplicationManifest, + type FieldManifest, + type ObjectManifest, + type RoleManifest, +} from 'twenty-shared/application'; +import { type FrontComponentConfig } from '@/sdk/front-components/front-component-config'; +import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config'; + +export type ValidationResult = { + success: boolean; + config: T; + errors: string[]; +}; + +export type DefinableEntity = + | ApplicationManifest + | ObjectManifest + | FieldManifest + | FrontComponentConfig + | LogicFunctionConfig + | RoleManifest; + +export type DefineEntity = < + T extends C, +>( + config: T, +) => ValidationResult; diff --git a/packages/twenty-sdk/src/application/syncable-entity-options.type.ts b/packages/twenty-sdk/src/sdk/common/types/syncable-entity-options.type.ts similarity index 100% rename from packages/twenty-sdk/src/application/syncable-entity-options.type.ts rename to packages/twenty-sdk/src/sdk/common/types/syncable-entity-options.type.ts diff --git a/packages/twenty-sdk/src/sdk/common/utils/create-validation-result.ts b/packages/twenty-sdk/src/sdk/common/utils/create-validation-result.ts new file mode 100644 index 00000000000..3aa4546213f --- /dev/null +++ b/packages/twenty-sdk/src/sdk/common/utils/create-validation-result.ts @@ -0,0 +1,13 @@ +import { type ValidationResult } from '@/sdk/common/types/define-entity.type'; + +export const createValidationResult = ({ + config, + errors = [], +}: { + config: T; + errors: string[]; +}): ValidationResult => ({ + success: errors.length === 0, + config, + errors, +}); diff --git a/packages/twenty-sdk/src/sdk/fields/__tests__/define-field.spec.ts b/packages/twenty-sdk/src/sdk/fields/__tests__/define-field.spec.ts new file mode 100644 index 00000000000..963e507f5ab --- /dev/null +++ b/packages/twenty-sdk/src/sdk/fields/__tests__/define-field.spec.ts @@ -0,0 +1,179 @@ +import { defineField } from '@/sdk'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { type FieldManifest } from 'twenty-shared/application'; + +const validConfig: FieldManifest = { + objectUniversalIdentifier: '45e8ae95-0ed8-4087-9f59-3ac85144f86d', + universalIdentifier: '73068741-1638-4d40-b30c-3431a8733094', + type: FieldMetadataType.TEXT, + name: 'customNote', + label: 'Custom Note', +}; + +describe('defineField', () => { + describe('valid configurations', () => { + it('should return successful validation result when targeting by nameSingular', () => { + const result = defineField(validConfig); + + expect(result.success).toBe(true); + expect(result.config).toEqual(validConfig); + expect(result.errors).toEqual([]); + }); + + it('should pass through optional field properties', () => { + const config: FieldManifest = { + ...validConfig, + + description: 'A health score from 0-100', + icon: 'IconHeart', + }; + + const result = defineField(config); + + expect(result.success).toBe(true); + expect(result.config?.description).toBe('A health score from 0-100'); + expect(result.config?.icon).toBe('IconHeart'); + }); + + it('should accept SELECT field with options', () => { + const config: FieldManifest = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + universalIdentifier: '550e8400-e29b-41d4-a716-446655440003', + type: FieldMetadataType.SELECT, + name: 'churnRisk', + label: 'Churn Risk', + options: [ + { value: 'low', label: 'Low', color: 'green', position: 0 }, + { + value: 'medium', + label: 'Medium', + color: 'yellow', + position: 1, + }, + { value: 'high', label: 'High', color: 'red', position: 2 }, + ], + }; + + const result = defineField(config); + + expect(result.success).toBe(true); + expect(result.config?.label).toBe('Churn Risk'); + }); + }); + + it('should return error when objectUniversalIdentifier is missing', () => { + const { objectUniversalIdentifier: _, ...validConfigWithoutTargetObject } = + validConfig; + + const result = defineField(validConfigWithoutTargetObject as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Field must have an objectUniversalIdentifier', + ); + }); + + describe('fields validation', () => { + it('should return error when field is missing label', () => { + const config = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + + universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', + type: FieldMetadataType.NUMBER, + name: 'healthScore', + }; + + const result = defineField(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Field must have a label'); + }); + + it('should return error when field is missing name', () => { + const config = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + + universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', + type: FieldMetadataType.NUMBER, + label: 'Health Score', + }; + + const result = defineField(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Field "Health Score" must have a name'); + }); + + it('should return error when field is missing universalIdentifier', () => { + const config = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + + type: FieldMetadataType.NUMBER, + name: 'healthScore', + label: 'Health Score', + }; + + const result = defineField(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Field "Health Score" must have a universalIdentifier', + ); + }); + + it('should return error when SELECT field has no options', () => { + const config = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + + universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', + type: FieldMetadataType.SELECT, + name: 'status', + label: 'Status', + }; + + const result = defineField(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Field "Status" is a SELECT/MULTI_SELECT type and must have options', + ); + }); + + it('should return error when MULTI_SELECT field has no options', () => { + const config = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + + universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', + type: FieldMetadataType.MULTI_SELECT, + name: 'tags', + label: 'Tags', + }; + + const result = defineField(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Field "Tags" is a SELECT/MULTI_SELECT type and must have options', + ); + }); + + it('should return error when SELECT field has empty options array', () => { + const config = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + + universalIdentifier: '550e8400-e29b-41d4-a716-446655440001', + type: FieldMetadataType.SELECT, + name: 'status', + label: 'Status', + options: [], + }; + + const result = defineField(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Field "Status" is a SELECT/MULTI_SELECT type and must have options', + ); + }); + }); +}); diff --git a/packages/twenty-sdk/src/application/fields/composite-fields.ts b/packages/twenty-sdk/src/sdk/fields/composite-fields.ts similarity index 100% rename from packages/twenty-sdk/src/application/fields/composite-fields.ts rename to packages/twenty-sdk/src/sdk/fields/composite-fields.ts diff --git a/packages/twenty-sdk/src/sdk/fields/define-field.ts b/packages/twenty-sdk/src/sdk/fields/define-field.ts new file mode 100644 index 00000000000..b515ae749af --- /dev/null +++ b/packages/twenty-sdk/src/sdk/fields/define-field.ts @@ -0,0 +1,22 @@ +import { type FieldManifest } from 'twenty-shared/application'; +import { validateFields } from '@/sdk/fields/validate-fields'; + +import { type DefineEntity } from '@/sdk/common/types/define-entity.type'; +import { createValidationResult } from '@/sdk/common/utils/create-validation-result'; + +export const defineField: DefineEntity = (config) => { + const errors = []; + + if (!config.objectUniversalIdentifier) { + errors.push('Field must have an objectUniversalIdentifier'); + } + + const fieldErrors = validateFields([config]); + + errors.push(...fieldErrors); + + return createValidationResult({ + config, + errors, + }); +}; diff --git a/packages/twenty-sdk/src/application/fields/field-type.ts b/packages/twenty-sdk/src/sdk/fields/field-type.ts similarity index 100% rename from packages/twenty-sdk/src/application/fields/field-type.ts rename to packages/twenty-sdk/src/sdk/fields/field-type.ts diff --git a/packages/twenty-sdk/src/application/fields/on-delete-action.ts b/packages/twenty-sdk/src/sdk/fields/on-delete-action.ts similarity index 100% rename from packages/twenty-sdk/src/application/fields/on-delete-action.ts rename to packages/twenty-sdk/src/sdk/fields/on-delete-action.ts diff --git a/packages/twenty-sdk/src/application/fields/relation-type.ts b/packages/twenty-sdk/src/sdk/fields/relation-type.ts similarity index 100% rename from packages/twenty-sdk/src/application/fields/relation-type.ts rename to packages/twenty-sdk/src/sdk/fields/relation-type.ts diff --git a/packages/twenty-sdk/src/application/objects/validate-fields.ts b/packages/twenty-sdk/src/sdk/fields/validate-fields.ts similarity index 51% rename from packages/twenty-sdk/src/application/objects/validate-fields.ts rename to packages/twenty-sdk/src/sdk/fields/validate-fields.ts index 4375ae9eb04..359708c460d 100644 --- a/packages/twenty-sdk/src/application/objects/validate-fields.ts +++ b/packages/twenty-sdk/src/sdk/fields/validate-fields.ts @@ -1,32 +1,29 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { - type FieldManifest, - type RelationFieldManifest, -} from 'twenty-shared/application'; + import { isNonEmptyString } from '@sniptt/guards'; -/** - * Validates an array of fields and throws an error if any field is invalid. - * This validation is shared between defineObject and extendObject. - */ -export const validateFieldsOrThrow = ( - fields: (FieldManifest | RelationFieldManifest)[] | undefined, -): void => { +import { type ObjectFieldManifest } from 'twenty-shared/application'; + +export const validateFields = ( + fields: ObjectFieldManifest[] | undefined, +): string[] => { if (!fields) { - return; + return []; } + const errors: string[] = []; + for (const field of fields) { if (!isNonEmptyString(field.label)) { - throw new Error('Field must have a label'); + errors.push('Field must have a label'); } if (!isNonEmptyString(field.name)) { - throw new Error(`Field "${field.label}" must have a name`); + errors.push(`Field "${field.label}" must have a name`); } if (!isNonEmptyString(field.universalIdentifier)) { - throw new Error(`Field "${field.label}" must have a universalIdentifier`); + errors.push(`Field "${field.label}" must have a universalIdentifier`); } if ( @@ -34,9 +31,11 @@ export const validateFieldsOrThrow = ( field.type === FieldMetadataType.MULTI_SELECT) && (!Array.isArray(field.options) || field.options.length === 0) ) { - throw new Error( + errors.push( `Field "${field.label}" is a SELECT/MULTI_SELECT type and must have options`, ); } } + + return errors; }; diff --git a/packages/twenty-sdk/src/sdk/front-components/__tests__/define-front-component.spec.ts b/packages/twenty-sdk/src/sdk/front-components/__tests__/define-front-component.spec.ts new file mode 100644 index 00000000000..46ca45a3f17 --- /dev/null +++ b/packages/twenty-sdk/src/sdk/front-components/__tests__/define-front-component.spec.ts @@ -0,0 +1,85 @@ +import { defineFrontComponent } from '@/sdk'; + +// Mock component for testing +const MockComponent = () => null; + +describe('defineFrontComponent', () => { + const validConfig = { + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'My Component', + component: MockComponent, + }; + + it('should return successful validation result when valid', () => { + const result = defineFrontComponent(validConfig); + + expect(result.success).toBe(true); + expect(result.config).toEqual(validConfig); + expect(result.errors).toEqual([]); + }); + + it('should pass through optional fields', () => { + const config = { + ...validConfig, + description: 'A sample front component', + }; + + const result = defineFrontComponent(config); + + expect(result.success).toBe(true); + expect(result.config?.description).toBe('A sample front component'); + }); + + it('should return error when universalIdentifier is missing', () => { + const config = { + name: 'My Component', + component: MockComponent, + }; + + const result = defineFrontComponent(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Front component must have a universalIdentifier', + ); + }); + + it('should return error when component is missing', () => { + const config = { + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'My Component', + }; + + const result = defineFrontComponent(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Front component must have a component'); + }); + + it('should return error when component is not a function', () => { + const config = { + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + name: 'My Component', + component: 'not-a-function', + }; + + const result = defineFrontComponent(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Front component component must be a React component', + ); + }); + + it('should accept config without name', () => { + const config = { + universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', + component: MockComponent, + }; + + const result = defineFrontComponent(config as any); + + expect(result.success).toBe(true); + expect(result.config?.name).toBeUndefined(); + }); +}); diff --git a/packages/twenty-sdk/src/sdk/front-components/define-front-component.ts b/packages/twenty-sdk/src/sdk/front-components/define-front-component.ts new file mode 100644 index 00000000000..a6c2c14129a --- /dev/null +++ b/packages/twenty-sdk/src/sdk/front-components/define-front-component.ts @@ -0,0 +1,26 @@ +import { type FrontComponentConfig } from '@/sdk/front-components/front-component-config'; +import type { DefineEntity } from '@/sdk/common/types/define-entity.type'; +import { createValidationResult } from '@/sdk'; + +export const defineFrontComponent: DefineEntity = ( + config, +) => { + const errors = []; + + if (!config.universalIdentifier) { + errors.push('Front component must have a universalIdentifier'); + } + + if (!config.component) { + errors.push('Front component must have a component'); + } + + if (typeof config.component !== 'function') { + errors.push('Front component component must be a React component'); + } + + return createValidationResult({ + config, + errors, + }); +}; diff --git a/packages/twenty-sdk/src/application/front-components/front-component-config.ts b/packages/twenty-sdk/src/sdk/front-components/front-component-config.ts similarity index 89% rename from packages/twenty-sdk/src/application/front-components/front-component-config.ts rename to packages/twenty-sdk/src/sdk/front-components/front-component-config.ts index 62ff75eb8b7..58482b957ae 100644 --- a/packages/twenty-sdk/src/application/front-components/front-component-config.ts +++ b/packages/twenty-sdk/src/sdk/front-components/front-component-config.ts @@ -9,7 +9,5 @@ export type FrontComponentConfig = Omit< | 'builtComponentChecksum' | 'componentName' > & { - name?: string; - description?: string; component: FrontComponentType; }; diff --git a/packages/twenty-sdk/src/application/index.ts b/packages/twenty-sdk/src/sdk/index.ts similarity index 57% rename from packages/twenty-sdk/src/application/index.ts rename to packages/twenty-sdk/src/sdk/index.ts index 512b2c85b05..1ec1bd8769a 100644 --- a/packages/twenty-sdk/src/application/index.ts +++ b/packages/twenty-sdk/src/sdk/index.ts @@ -7,8 +7,14 @@ * |___/ */ -export type { ApplicationConfig } from './application-config'; -export { defineApp } from './define-app'; +export { defineApplication } from './application/define-application'; +export type { + ValidationResult, + DefinableEntity, + DefineEntity, +} from './common/types/define-entity.type'; +export type { SyncableEntityOptions } from './common/types/syncable-entity-options.type'; +export { createValidationResult } from './common/utils/create-validation-result'; export type { ActorField, AddressField, @@ -19,22 +25,22 @@ export type { PhonesField, RichTextField, } from './fields/composite-fields'; +export { defineField } from './fields/define-field'; export { FieldType } from './fields/field-type'; -export { Field } from './fields/field.decorator'; export { OnDeleteAction } from './fields/on-delete-action'; export { RelationType } from './fields/relation-type'; -export { Relation } from './fields/relation.decorator'; +export { validateFields } from './fields/validate-fields'; export { defineFrontComponent } from './front-components/define-front-component'; export type { FrontComponentType, FrontComponentConfig, } from './front-components/front-component-config'; -export { defineFunction } from './functions/define-function'; +export { defineLogicFunction } from './logic-functions/define-logic-function'; export type { - FunctionHandler, - FunctionConfig, -} from './functions/function-config'; -export type { CronPayload } from './functions/triggers/cron-payload-type'; + LogicFunctionHandler, + LogicFunctionConfig, +} from './logic-functions/logic-function-config'; +export type { CronPayload } from './logic-functions/triggers/cron-payload-type'; export type { DatabaseEventPayload, ObjectRecordCreateEvent, @@ -45,14 +51,9 @@ export type { ObjectRecordBaseEvent, ObjectRecordRestoreEvent, ObjectRecordUpsertEvent, -} from './functions/triggers/database-event-payload-type'; -export type { RoutePayload } from './functions/triggers/route-payload-type'; +} from './logic-functions/triggers/database-event-payload-type'; +export type { RoutePayload } from './logic-functions/triggers/route-payload-type'; export { defineObject } from './objects/define-object'; -export { extendObject } from './objects/extend-object'; -export { Object } from './objects/object.decorator'; export { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from './objects/standard-object-ids'; -export { validateFieldsOrThrow } from './objects/validate-fields'; -export { PermissionFlag } from './permission-flag-type'; -export type { RoleConfig } from './role-config'; export { defineRole } from './roles/define-role'; -export type { SyncableEntityOptions } from './syncable-entity-options.type'; +export { PermissionFlag } from './roles/permission-flag-type'; diff --git a/packages/twenty-sdk/src/application/__tests__/define-function.spec.ts b/packages/twenty-sdk/src/sdk/logic-functions/__tests__/define-logic-function.spec.ts similarity index 63% rename from packages/twenty-sdk/src/application/__tests__/define-function.spec.ts rename to packages/twenty-sdk/src/sdk/logic-functions/__tests__/define-logic-function.spec.ts index c7c5e6dcaa7..ca7e928894d 100644 --- a/packages/twenty-sdk/src/application/__tests__/define-function.spec.ts +++ b/packages/twenty-sdk/src/sdk/logic-functions/__tests__/define-logic-function.spec.ts @@ -1,9 +1,8 @@ -import { defineFunction } from '@/application'; +import { defineLogicFunction } from '@/sdk'; -// Mock handler for testing const mockHandler = async () => ({ success: true }); -describe('defineFunction', () => { +describe('defineLogicFunction', () => { const validRouteConfig = { universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'Send Postcard', @@ -20,9 +19,9 @@ describe('defineFunction', () => { }; it('should return the config when valid with route trigger', () => { - const result = defineFunction(validRouteConfig); + const result = defineLogicFunction(validRouteConfig); - expect(result).toEqual(validRouteConfig); + expect(result.config).toEqual(validRouteConfig); }); it('should accept cron trigger', () => { @@ -39,9 +38,9 @@ describe('defineFunction', () => { ], }; - const result = defineFunction(config); + const result = defineLogicFunction(config); - expect(result.triggers[0].type).toBe('cron'); + expect(result.config.triggers[0].type).toBe('cron'); }); it('should accept databaseEvent trigger', () => { @@ -58,9 +57,9 @@ describe('defineFunction', () => { ], }; - const result = defineFunction(config); + const result = defineLogicFunction(config); - expect(result.triggers[0].type).toBe('databaseEvent'); + expect(result.config.triggers[0].type).toBe('databaseEvent'); }); it('should accept multiple triggers', () => { @@ -84,9 +83,9 @@ describe('defineFunction', () => { ], }; - const result = defineFunction(config); + const result = defineLogicFunction(config); - expect(result.triggers).toHaveLength(2); + expect(result.config.triggers).toHaveLength(2); }); it('should pass through optional fields', () => { @@ -96,37 +95,40 @@ describe('defineFunction', () => { timeoutSeconds: 30, }; - const result = defineFunction(config); + const result = defineLogicFunction(config); - expect(result.description).toBe('Send a postcard to a contact'); - expect(result.timeoutSeconds).toBe(30); + expect(result.config.description).toBe('Send a postcard to a contact'); + expect(result.config.timeoutSeconds).toBe(30); }); - it('should throw error when universalIdentifier is missing', () => { + it('should return error when universalIdentifier is missing', () => { const config = { name: 'Send Postcard', handler: mockHandler, triggers: validRouteConfig.triggers, }; + const result = defineLogicFunction(config as any); - expect(() => defineFunction(config as any)).toThrow( - 'Function must have a universalIdentifier', + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Logic function must have a universalIdentifier', ); }); - it('should throw error when handler is missing', () => { + it('should return error when handler is missing', () => { const config = { universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'Send Postcard', triggers: validRouteConfig.triggers, }; - expect(() => defineFunction(config as any)).toThrow( - 'Function must have a handler', - ); + const result = defineLogicFunction(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Logic function must have a handler'); }); - it('should throw error when handler is not a function', () => { + it('should return error when handler is not a function', () => { const config = { universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'Send Postcard', @@ -134,8 +136,11 @@ describe('defineFunction', () => { triggers: validRouteConfig.triggers, }; - expect(() => defineFunction(config as any)).toThrow( - 'Function must have a handler', + const result = defineLogicFunction(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Logic function handler must be a function', ); }); @@ -147,9 +152,9 @@ describe('defineFunction', () => { triggers: [], }; - const result = defineFunction(config as any); + const result = defineLogicFunction(config as any); - expect(result.triggers).toEqual([]); + expect(result.config.triggers).toEqual([]); }); it('should accept missing triggers', () => { @@ -159,12 +164,12 @@ describe('defineFunction', () => { handler: mockHandler, }; - const result = defineFunction(config as any); + const result = defineLogicFunction(config as any); - expect(result.triggers).toBeUndefined(); + expect(result.config.triggers).toBeUndefined(); }); - it('should throw error when trigger is missing universalIdentifier', () => { + it('should return error when trigger is missing universalIdentifier', () => { const config = { universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'Send Postcard', @@ -179,12 +184,15 @@ describe('defineFunction', () => { ], }; - expect(() => defineFunction(config as any)).toThrow( + const result = defineLogicFunction(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( 'Each trigger must have a universalIdentifier', ); }); - it('should throw error when trigger is missing type', () => { + it('should return error when trigger is missing type', () => { const config = { universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'Send Postcard', @@ -198,12 +206,13 @@ describe('defineFunction', () => { ], }; - expect(() => defineFunction(config as any)).toThrow( - 'Each trigger must have a type', - ); + const result = defineLogicFunction(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Each trigger must have a type'); }); - it('should throw error when route trigger is missing path', () => { + it('should return error when route trigger is missing path', () => { const config = { universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'Send Postcard', @@ -218,12 +227,13 @@ describe('defineFunction', () => { ], }; - expect(() => defineFunction(config as any)).toThrow( - 'Route trigger must have a path', - ); + const result = defineLogicFunction(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Route trigger must have a path'); }); - it('should throw error when route trigger is missing httpMethod', () => { + it('should return error when route trigger is missing httpMethod', () => { const config = { universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'Send Postcard', @@ -238,12 +248,13 @@ describe('defineFunction', () => { ], }; - expect(() => defineFunction(config as any)).toThrow( - 'Route trigger must have an httpMethod', - ); + const result = defineLogicFunction(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Route trigger must have an httpMethod'); }); - it('should throw error when cron trigger is missing pattern', () => { + it('should return error when cron trigger is missing pattern', () => { const config = { universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'Daily Report', @@ -256,12 +267,13 @@ describe('defineFunction', () => { ], }; - expect(() => defineFunction(config as any)).toThrow( - 'Cron trigger must have a pattern', - ); + const result = defineLogicFunction(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Cron trigger must have a pattern'); }); - it('should throw error when databaseEvent trigger is missing eventName', () => { + it('should return error when databaseEvent trigger is missing eventName', () => { const config = { universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'On Contact Created', @@ -274,12 +286,15 @@ describe('defineFunction', () => { ], }; - expect(() => defineFunction(config as any)).toThrow( + const result = defineLogicFunction(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( 'Database event trigger must have an eventName', ); }); - it('should throw error for unknown trigger type', () => { + it('should return error for unknown trigger type', () => { const config = { universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', name: 'Unknown Trigger', @@ -291,9 +306,9 @@ describe('defineFunction', () => { }, ], }; + const result = defineLogicFunction(config as any); - expect(() => defineFunction(config as any)).toThrow( - 'Unknown trigger type: unknown', - ); + expect(result.success).toBe(false); + expect(result.errors).toContain('Unknown trigger type: unknown'); }); }); diff --git a/packages/twenty-sdk/src/sdk/logic-functions/define-logic-function.ts b/packages/twenty-sdk/src/sdk/logic-functions/define-logic-function.ts new file mode 100644 index 00000000000..18f649ccf42 --- /dev/null +++ b/packages/twenty-sdk/src/sdk/logic-functions/define-logic-function.ts @@ -0,0 +1,64 @@ +import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config'; +import { createValidationResult } from '@/sdk/common/utils/create-validation-result'; +import type { DefineEntity } from '@/sdk/common/types/define-entity.type'; + +export const defineLogicFunction: DefineEntity = ( + config, +) => { + const errors = []; + + if (!config.universalIdentifier) { + errors.push('Logic function must have a universalIdentifier'); + } + + if (!config.handler) { + errors.push('Logic function must have a handler'); + } + + if (typeof config.handler !== 'function') { + errors.push('Logic function handler must be a function'); + } + + for (const trigger of config.triggers ?? []) { + if (!trigger.universalIdentifier) { + errors.push('Each trigger must have a universalIdentifier'); + } + + if (!trigger.type) { + errors.push('Each trigger must have a type'); + } + + switch (trigger.type) { + case 'route': + if (!trigger.path) { + errors.push('Route trigger must have a path'); + } + if (!trigger.httpMethod) { + errors.push('Route trigger must have an httpMethod'); + } + break; + + case 'cron': + if (!trigger.pattern) { + errors.push('Cron trigger must have a pattern'); + } + break; + + case 'databaseEvent': + if (!trigger.eventName) { + errors.push('Database event trigger must have an eventName'); + } + break; + + default: + errors.push( + `Unknown trigger type: ${(trigger as { type: string }).type}`, + ); + } + } + + return createValidationResult({ + config, + errors, + }); +}; diff --git a/packages/twenty-sdk/src/application/functions/function-config.ts b/packages/twenty-sdk/src/sdk/logic-functions/logic-function-config.ts similarity index 58% rename from packages/twenty-sdk/src/application/functions/function-config.ts rename to packages/twenty-sdk/src/sdk/logic-functions/logic-function-config.ts index 92dfb68a58c..82686a8122b 100644 --- a/packages/twenty-sdk/src/application/functions/function-config.ts +++ b/packages/twenty-sdk/src/sdk/logic-functions/logic-function-config.ts @@ -3,18 +3,15 @@ import { type LogicFunctionTriggerManifest, } from 'twenty-shared/application'; -export type FunctionHandler = (...args: any[]) => any | Promise; +export type LogicFunctionHandler = (...args: any[]) => any | Promise; -export type FunctionConfig = Omit< +export type LogicFunctionConfig = Omit< LogicFunctionManifest, | 'sourceHandlerPath' | 'builtHandlerPath' | 'builtHandlerChecksum' | 'handlerName' > & { - name?: string; - description?: string; - timeoutSeconds?: number; - handler: FunctionHandler; + handler: LogicFunctionHandler; triggers?: LogicFunctionTriggerManifest[]; }; diff --git a/packages/twenty-sdk/src/application/functions/triggers/cron-payload-type.ts b/packages/twenty-sdk/src/sdk/logic-functions/triggers/cron-payload-type.ts similarity index 100% rename from packages/twenty-sdk/src/application/functions/triggers/cron-payload-type.ts rename to packages/twenty-sdk/src/sdk/logic-functions/triggers/cron-payload-type.ts diff --git a/packages/twenty-sdk/src/application/functions/triggers/database-event-payload-type.ts b/packages/twenty-sdk/src/sdk/logic-functions/triggers/database-event-payload-type.ts similarity index 100% rename from packages/twenty-sdk/src/application/functions/triggers/database-event-payload-type.ts rename to packages/twenty-sdk/src/sdk/logic-functions/triggers/database-event-payload-type.ts diff --git a/packages/twenty-sdk/src/application/functions/triggers/route-payload-type.ts b/packages/twenty-sdk/src/sdk/logic-functions/triggers/route-payload-type.ts similarity index 100% rename from packages/twenty-sdk/src/application/functions/triggers/route-payload-type.ts rename to packages/twenty-sdk/src/sdk/logic-functions/triggers/route-payload-type.ts diff --git a/packages/twenty-sdk/src/application/__tests__/define-object.spec.ts b/packages/twenty-sdk/src/sdk/objects/__tests__/define-object.spec.ts similarity index 57% rename from packages/twenty-sdk/src/application/__tests__/define-object.spec.ts rename to packages/twenty-sdk/src/sdk/objects/__tests__/define-object.spec.ts index db4773eb7e7..086bf4c1e81 100644 --- a/packages/twenty-sdk/src/application/__tests__/define-object.spec.ts +++ b/packages/twenty-sdk/src/sdk/objects/__tests__/define-object.spec.ts @@ -1,4 +1,4 @@ -import { defineObject } from '@/application'; +import { defineObject } from '@/sdk'; import { FieldMetadataType } from 'twenty-shared/types'; import { type ObjectManifest } from 'twenty-shared/application'; @@ -20,10 +20,12 @@ describe('defineObject', () => { ], }; - it('should return the config when valid', () => { + it('should return successful validation result when valid', () => { const result = defineObject(validConfig); - expect(result).toEqual(validConfig); + expect(result.success).toBe(true); + expect(result.config).toEqual(validConfig); + expect(result.errors).toEqual([]); }); it('should pass through all optional fields', () => { @@ -34,62 +36,68 @@ describe('defineObject', () => { const result = defineObject(config); - expect(result.description).toBe('A post card object'); + expect(result.success).toBe(true); + expect(result.config?.description).toBe('A post card object'); }); - it('should throw error when universalIdentifier is missing', () => { + it('should return error when universalIdentifier is missing', () => { const config = { ...validConfig, universalIdentifier: undefined, }; - expect(() => defineObject(config as any)).toThrow( - 'Object must have a universalIdentifier', - ); + const result = defineObject(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Object must have a universalIdentifier'); }); - it('should throw error when nameSingular is missing', () => { + it('should return error when nameSingular is missing', () => { const config = { ...validConfig, nameSingular: undefined, }; - expect(() => defineObject(config as any)).toThrow( - 'Object must have a nameSingular', - ); + const result = defineObject(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Object must have a nameSingular'); }); - it('should throw error when namePlural is missing', () => { + it('should return error when namePlural is missing', () => { const config = { ...validConfig, namePlural: undefined, }; - expect(() => defineObject(config as any)).toThrow( - 'Object must have a namePlural', - ); + const result = defineObject(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Object must have a namePlural'); }); - it('should throw error when labelSingular is missing', () => { + it('should return error when labelSingular is missing', () => { const config = { ...validConfig, labelSingular: undefined, }; - expect(() => defineObject(config as any)).toThrow( - 'Object must have a labelSingular', - ); + const result = defineObject(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Object must have a labelSingular'); }); - it('should throw error when labelPlural is missing', () => { + it('should return error when labelPlural is missing', () => { const config = { ...validConfig, labelPlural: undefined, }; - expect(() => defineObject(config as any)).toThrow( - 'Object must have a labelPlural', - ); + const result = defineObject(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Object must have a labelPlural'); }); it('should accept empty fields array', () => { @@ -100,7 +108,7 @@ describe('defineObject', () => { const result = defineObject(config as any); - expect(result.fields).toEqual([]); + expect(result.config.fields).toEqual([]); }); it('should accept missing fields', () => { @@ -111,10 +119,10 @@ describe('defineObject', () => { const result = defineObject(config as any); - expect(result.fields).toBeUndefined(); + expect(result.config.fields).toBeUndefined(); }); - it('should throw error when field is missing label', () => { + it('should return error when field is missing label', () => { const config = { ...validConfig, fields: [ @@ -126,12 +134,13 @@ describe('defineObject', () => { ], }; - expect(() => defineObject(config as any)).toThrow( - 'Field must have a label', - ); + const result = defineObject(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Field must have a label'); }); - it('should throw error when field is missing name', () => { + it('should return error when field is missing name', () => { const config = { ...validConfig, fields: [ @@ -143,12 +152,13 @@ describe('defineObject', () => { ], }; - expect(() => defineObject(config as any)).toThrow( - 'Field "Content" must have a name', - ); + const result = defineObject(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Field "Content" must have a name'); }); - it('should throw error when field is missing universalIdentifier', () => { + it('should return error when field is missing universalIdentifier', () => { const config = { ...validConfig, fields: [ @@ -159,13 +169,15 @@ describe('defineObject', () => { }, ], }; + const result = defineObject(config as any); - expect(() => defineObject(config as any)).toThrow( + expect(result.success).toBe(false); + expect(result.errors).toContain( 'Field "Content" must have a universalIdentifier', ); }); - it('should throw error when SELECT field has no options', () => { + it('should return error when SELECT field has no options', () => { const config = { ...validConfig, fields: [ @@ -177,13 +189,15 @@ describe('defineObject', () => { }, ], }; + const result = defineObject(config as any); - expect(() => defineObject(config as any)).toThrow( + expect(result.success).toBe(false); + expect(result.errors).toContain( 'Field "Status" is a SELECT/MULTI_SELECT type and must have options', ); }); - it('should throw error when MULTI_SELECT field has no options', () => { + it('should return error when MULTI_SELECT field has no options', () => { const config = { ...validConfig, fields: [ @@ -196,7 +210,10 @@ describe('defineObject', () => { ], }; - expect(() => defineObject(config as any)).toThrow( + const result = defineObject(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( 'Field "Tags" is a SELECT/MULTI_SELECT type and must have options', ); }); @@ -220,6 +237,6 @@ describe('defineObject', () => { const result = defineObject(config as any); - expect(result.fields[0].options).toHaveLength(2); + expect(result.config.fields[0].options).toHaveLength(2); }); }); diff --git a/packages/twenty-sdk/src/sdk/objects/define-object.ts b/packages/twenty-sdk/src/sdk/objects/define-object.ts new file mode 100644 index 00000000000..c3ad960518d --- /dev/null +++ b/packages/twenty-sdk/src/sdk/objects/define-object.ts @@ -0,0 +1,38 @@ +import { type ObjectManifest } from 'twenty-shared/application'; + +import { createValidationResult } from '@/sdk/common/utils/create-validation-result'; +import { type DefineEntity } from '@/sdk/common/types/define-entity.type'; +import { validateFields } from '@/sdk/fields/validate-fields'; + +export const defineObject: DefineEntity = (config) => { + const errors = []; + + if (!config.universalIdentifier) { + errors.push('Object must have a universalIdentifier'); + } + + if (!config.nameSingular) { + errors.push('Object must have a nameSingular'); + } + + if (!config.namePlural) { + errors.push('Object must have a namePlural'); + } + + if (!config.labelSingular) { + errors.push('Object must have a labelSingular'); + } + + if (!config.labelPlural) { + errors.push('Object must have a labelPlural'); + } + + const fieldErrors = validateFields(config.fields); + + errors.push(...fieldErrors); + + return createValidationResult({ + config, + errors, + }); +}; diff --git a/packages/twenty-sdk/src/application/objects/standard-object-ids.ts b/packages/twenty-sdk/src/sdk/objects/standard-object-ids.ts similarity index 100% rename from packages/twenty-sdk/src/application/objects/standard-object-ids.ts rename to packages/twenty-sdk/src/sdk/objects/standard-object-ids.ts diff --git a/packages/twenty-sdk/src/sdk/roles/__tests__/define-role.spec.ts b/packages/twenty-sdk/src/sdk/roles/__tests__/define-role.spec.ts new file mode 100644 index 00000000000..51cdad01e5d --- /dev/null +++ b/packages/twenty-sdk/src/sdk/roles/__tests__/define-role.spec.ts @@ -0,0 +1,124 @@ +import { defineRole } from '@/sdk'; + +describe('defineRole', () => { + const validConfig = { + universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', + label: 'App User', + description: 'Standard user role', + }; + + it('should return successful validation result when valid', () => { + const result = defineRole(validConfig); + + expect(result.success).toBe(true); + expect(result.config).toEqual(validConfig); + expect(result.errors).toEqual([]); + }); + + it('should pass through all optional fields', () => { + const config = { + ...validConfig, + icon: 'IconUser', + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }; + + const result = defineRole(config); + + expect(result.success).toBe(true); + expect(result.config?.icon).toBe('IconUser'); + expect(result.config?.canReadAllObjectRecords).toBe(true); + }); + + it('should accept permissionFlags', () => { + const config = { + ...validConfig, + permissionFlags: ['UPLOAD_FILE', 'DOWNLOAD_FILE'], + }; + + const result = defineRole(config as any); + + expect(result.success).toBe(true); + expect(result.config?.permissionFlags).toHaveLength(2); + }); + + it('should return error when universalIdentifier is missing', () => { + const config = { + label: 'App User', + }; + + const result = defineRole(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Role must have a universalIdentifier'); + }); + + it('should return error when label is missing', () => { + const config = { + universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', + }; + + const result = defineRole(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain('Role must have a label'); + }); + + it('should return error when objectPermission has no objectUniversalIdentifier', () => { + const config = { + ...validConfig, + objectPermissions: [ + { + canReadObjectRecords: true, + }, + ], + }; + + const result = defineRole(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Object permission must have an objectUniversalIdentifier', + ); + }); + + it('should return error when fieldPermission has no objectUniversalIdentifier', () => { + const config = { + ...validConfig, + fieldPermissions: [ + { + fieldUniversalIdentifier: 'dd14cab4-0829-4475-a794-d0d4959161e6', + canReadFieldValue: true, + }, + ], + }; + + const result = defineRole(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Field permission must have an objectUniversalIdentifier', + ); + }); + + it('should return error when fieldPermission has no fieldUniversalIdentifier', () => { + const config = { + ...validConfig, + fieldPermissions: [ + { + objectUniversalIdentifier: '38339ab2-f00b-416c-8ee0-806b48caca18', + canReadFieldValue: true, + }, + ], + }; + + const result = defineRole(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Field permission must have a fieldUniversalIdentifier', + ); + }); +}); diff --git a/packages/twenty-sdk/src/sdk/roles/define-role.ts b/packages/twenty-sdk/src/sdk/roles/define-role.ts new file mode 100644 index 00000000000..e092c9faab9 --- /dev/null +++ b/packages/twenty-sdk/src/sdk/roles/define-role.ts @@ -0,0 +1,37 @@ +import { createValidationResult } from '@/sdk/common/utils/create-validation-result'; +import { type RoleManifest } from 'twenty-shared/application'; +import { type DefineEntity } from '@/sdk/common/types/define-entity.type'; + +export const defineRole: DefineEntity = (config) => { + const errors = []; + + if (!config.universalIdentifier) { + errors.push('Role must have a universalIdentifier'); + } + + if (!config.label) { + errors.push('Role must have a label'); + } + + if (config.objectPermissions) { + for (const permission of config.objectPermissions) { + if (!permission.objectUniversalIdentifier) { + errors.push('Object permission must have an objectUniversalIdentifier'); + } + } + } + + if (config.fieldPermissions) { + for (const permission of config.fieldPermissions) { + if (!permission.objectUniversalIdentifier) { + errors.push('Field permission must have an objectUniversalIdentifier'); + } + + if (!permission.fieldUniversalIdentifier) { + errors.push('Field permission must have a fieldUniversalIdentifier'); + } + } + } + + return createValidationResult({ config, errors }); +}; diff --git a/packages/twenty-sdk/src/application/permission-flag-type.ts b/packages/twenty-sdk/src/sdk/roles/permission-flag-type.ts similarity index 100% rename from packages/twenty-sdk/src/application/permission-flag-type.ts rename to packages/twenty-sdk/src/sdk/roles/permission-flag-type.ts diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1769685701443-updateColumnName.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1769685701443-updateColumnName.ts new file mode 100644 index 00000000000..675e04dc41f --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1769685701443-updateColumnName.ts @@ -0,0 +1,17 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class UpdateColumnName1769685701443 implements MigrationInterface { + name = 'UpdateColumnName1769685701443'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."application" RENAME COLUMN "defaultLogicFunctionRoleId" TO "defaultRoleId"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."application" RENAME COLUMN "defaultRoleId" TO "defaultLogicFunctionRoleId"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index f1f12b09339..7e16611f611 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -319,8 +319,8 @@ export abstract class CommonBaseQueryRunnerService< ); } - if (isDefined(authContext.application?.defaultLogicFunctionRoleId)) { - return authContext.application?.defaultLogicFunctionRoleId; + if (isDefined(authContext.application?.defaultRoleId)) { + return authContext.application?.defaultRoleId; } if (!isDefined(authContext.userWorkspaceId)) { diff --git a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts index b33fe981e06..082d084798e 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts @@ -3,14 +3,14 @@ import { Injectable, Logger } from '@nestjs/common'; import { parse } from 'path'; import { - ApplicationManifest, + Manifest, FieldManifest, - ObjectExtensionManifest, ObjectManifest, - RelationFieldManifest, RoleManifest, LogicFunctionManifest, LogicFunctionTriggerManifest, + ObjectFieldManifest, + RelationFieldManifest, } from 'twenty-shared/application'; import { FieldMetadataType, HTTPMethod, Sources } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -29,7 +29,6 @@ import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { findFlatEntitiesByApplicationId } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entities-by-application-id.util'; import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service'; import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service'; @@ -87,21 +86,21 @@ export class ApplicationSyncService { applicationId: application.id, }); - await this.syncRelations({ + await this.syncObjectRelations({ objectsToSync: manifest.objects, workspaceId, applicationId: application.id, }); - if (manifest.objectExtensions && manifest.objectExtensions.length > 0) { - await this.syncObjectExtensionsOrThrow({ - objectExtensionsToSync: manifest.objectExtensions, + if (manifest.fields.length > 0) { + await this.syncFieldsOrThrow({ + fieldsToSync: manifest.fields, workspaceId, applicationId: application.id, }); } - if (manifest.functions.length > 0) { + if (manifest.logicFunctions.length > 0) { if (!isDefined(application.logicFunctionLayerId)) { throw new ApplicationException( `Failed to sync logic function, could not find a logic function layer.`, @@ -110,7 +109,7 @@ export class ApplicationSyncService { } await this.syncLogicFunctions({ - logicFunctionsToSync: manifest.functions, + logicFunctionsToSync: manifest.logicFunctions, code: manifest.sources, workspaceId, applicationId: application.id, @@ -148,13 +147,13 @@ export class ApplicationSyncService { version: packageJson.version, sourcePath: 'cli-sync', // Placeholder for CLI-synced apps logicFunctionLayerId: null, - defaultLogicFunctionRoleId: null, + defaultRoleId: null, workspaceId, })); let logicFunctionLayerId = application.logicFunctionLayerId; - if (manifest.functions.length > 0) { + if (manifest.logicFunctions.length > 0) { if (!isDefined(logicFunctionLayerId)) { logicFunctionLayerId = ( await this.logicFunctionLayerService.create( @@ -190,7 +189,7 @@ export class ApplicationSyncService { description: manifest.application.description, version: packageJson.version, logicFunctionLayerId, - defaultLogicFunctionRoleId: null, + defaultRoleId: null, }); } @@ -199,13 +198,13 @@ export class ApplicationSyncService { workspaceId, applicationId, }: { - manifest: ApplicationManifest; + manifest: Manifest; workspaceId: string; applicationId: string; }) { - let defaultLogicFunctionRoleId: string | null = null; + let defaultRoleId: string | null = null; - for (const role of manifest.roles ?? []) { + for (const role of manifest.roles) { let existingRole = await this.roleService.getRoleByUniversalIdentifier({ universalIdentifier: role.universalIdentifier, workspaceId, @@ -235,15 +234,15 @@ export class ApplicationSyncService { if ( existingRole.universalIdentifier === - manifest.application.functionRoleUniversalIdentifier + manifest.application.defaultRoleUniversalIdentifier ) { - defaultLogicFunctionRoleId = existingRole.id; + defaultRoleId = existingRole.id; } } - if (isDefined(defaultLogicFunctionRoleId)) { + if (isDefined(defaultRoleId)) { await this.applicationService.update(applicationId, { - defaultLogicFunctionRoleId: defaultLogicFunctionRoleId, + defaultRoleId: defaultRoleId, }); } } @@ -269,19 +268,13 @@ export class ApplicationSyncService { }, ); - const { idByNameSingular: objectIdByNameSingular } = - buildObjectIdByNameMaps(flatObjectMetadataMaps); - const formattedObjectPermissions = role.objectPermissions ?.map((perm) => ({ ...perm, - objectMetadataId: isDefined(perm.objectNameSingular) - ? objectIdByNameSingular[perm.objectNameSingular] - : isDefined(perm.objectUniversalIdentifier) - ? flatObjectMetadataMaps.idByUniversalIdentifier[ - perm.objectUniversalIdentifier - ] - : undefined, + objectMetadataId: + flatObjectMetadataMaps.idByUniversalIdentifier[ + perm.objectUniversalIdentifier + ], })) .filter((perm): perm is typeof perm & { objectMetadataId: string } => isDefined(perm.objectMetadataId), @@ -299,32 +292,15 @@ export class ApplicationSyncService { const formattedFieldPermissions = role?.fieldPermissions ?.map((perm) => { - const objectMetadataId = isDefined(perm.objectNameSingular) - ? objectIdByNameSingular[perm.objectNameSingular] - : isDefined(perm.objectUniversalIdentifier) - ? flatObjectMetadataMaps.idByUniversalIdentifier[ - perm.objectUniversalIdentifier - ] - : undefined; + const objectMetadataId = + flatObjectMetadataMaps.idByUniversalIdentifier[ + perm.objectUniversalIdentifier + ]; - const fieldMetadataId = isDefined(objectMetadataId) - ? isDefined(perm.fieldName) - ? Object.values(flatFieldMetadataMaps.byId).find( - (flatField) => - isDefined(flatField) && - flatField.objectMetadataId === objectMetadataId && - flatField.name === perm.fieldName, - )?.id - : isDefined(perm.fieldUniversalIdentifier) - ? Object.values(flatFieldMetadataMaps.byId).find( - (flatField) => - isDefined(flatField) && - flatField.objectMetadataId === objectMetadataId && - flatField.universalIdentifier === - perm.fieldUniversalIdentifier, - )?.id - : undefined - : undefined; + const fieldMetadataId = + flatFieldMetadataMaps.idByUniversalIdentifier[ + perm.fieldUniversalIdentifier + ]; return { ...perm, @@ -365,7 +341,7 @@ export class ApplicationSyncService { } private isRelationFieldManifest( - field: FieldManifest | RelationFieldManifest, + field: ObjectFieldManifest, ): field is RelationFieldManifest { return this.isFieldTypeRelation(field.type); } @@ -374,26 +350,22 @@ export class ApplicationSyncService { return type === FieldMetadataType.RELATION; } - private async syncFieldsWithoutRelations({ + private async syncObjectFieldsWithoutRelations({ objectId, - fieldsToSync: allFieldsToSync, + fieldsToSync, workspaceId, applicationId, }: { objectId: string; workspaceId: string; applicationId: string; - fieldsToSync?: (FieldManifest | RelationFieldManifest)[]; + fieldsToSync: ObjectFieldManifest[]; }) { - if (!isDefined(allFieldsToSync)) { - return; - } - - const fieldsToSync = allFieldsToSync.filter( + const fieldsWithoutRelation = fieldsToSync.filter( (field): field is FieldManifest => !this.isRelationFieldManifest(field), ); - if (fieldsToSync.length === 0) { + if (fieldsWithoutRelation.length === 0) { return; } @@ -414,7 +386,7 @@ export class ApplicationSyncService { !this.isFieldTypeRelation(field.type), ) as FlatFieldMetadata[]; - const fieldsToSyncUniversalIds = fieldsToSync.map( + const fieldsToSyncUniversalIds = fieldsWithoutRelation.map( (field) => field.universalIdentifier, ); @@ -436,7 +408,7 @@ export class ApplicationSyncService { fieldsToSyncUniversalIds.includes(field.universalIdentifier), ); - const fieldsToCreate = fieldsToSync.filter( + const fieldsToCreate = fieldsWithoutRelation.filter( (fieldToSync) => !existingFieldsStandardIds.includes(fieldToSync.universalIdentifier), ); @@ -456,7 +428,7 @@ export class ApplicationSyncService { } for (const fieldToUpdate of fieldsToUpdate) { - const fieldToSync = fieldsToSync.find( + const fieldToSync = fieldsWithoutRelation.find( (field) => field.universalIdentifier === fieldToUpdate.universalIdentifier, ); @@ -512,21 +484,25 @@ export class ApplicationSyncService { } } - private async syncRelationFields({ + private async syncObjectFieldsRelationOnly({ objectId, - relationsToSync, + fieldsToSync, workspaceId, applicationId, flatObjectMetadataMaps, }: { objectId: string; - relationsToSync: RelationFieldManifest[]; + fieldsToSync: ObjectFieldManifest[]; workspaceId: string; applicationId: string; flatObjectMetadataMaps: { idByUniversalIdentifier: Partial>; }; }) { + const relationFields = fieldsToSync.filter((field) => + this.isRelationFieldManifest(field), + ); + const { flatFieldMetadataMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -535,7 +511,7 @@ export class ApplicationSyncService { }, ); - for (const relation of relationsToSync) { + for (const relation of relationFields) { const existingRelationField = Object.values( flatFieldMetadataMaps.byId, ).find( @@ -677,7 +653,7 @@ export class ApplicationSyncService { workspaceId, }); - await this.syncFieldsWithoutRelations({ + await this.syncObjectFieldsWithoutRelations({ fieldsToSync: objectToSync.fields, objectId: objectToUpdate.id, workspaceId, @@ -710,7 +686,7 @@ export class ApplicationSyncService { workspaceId, }); - await this.syncFieldsWithoutRelations({ + await this.syncObjectFieldsWithoutRelations({ fieldsToSync: objectToCreate.fields, objectId: createdObject.id, workspaceId, @@ -719,7 +695,7 @@ export class ApplicationSyncService { } } - private async syncRelations({ + private async syncObjectRelations({ objectsToSync, workspaceId, applicationId, @@ -737,15 +713,6 @@ export class ApplicationSyncService { ); for (const objectToSync of objectsToSync) { - const relationFields = objectToSync.fields.filter( - (field): field is RelationFieldManifest => - this.isRelationFieldManifest(field), - ); - - if (relationFields.length === 0) { - continue; - } - const sourceObjectId = flatObjectMetadataMaps.idByUniversalIdentifier[ objectToSync.universalIdentifier @@ -758,9 +725,9 @@ export class ApplicationSyncService { ); } - await this.syncRelationFields({ + await this.syncObjectFieldsRelationOnly({ objectId: sourceObjectId, - relationsToSync: relationFields, + fieldsToSync: objectToSync.fields, workspaceId, applicationId, flatObjectMetadataMaps, @@ -768,12 +735,12 @@ export class ApplicationSyncService { } } - private async syncObjectExtensionsOrThrow({ - objectExtensionsToSync, + private async syncFieldsOrThrow({ + fieldsToSync, workspaceId, applicationId, }: { - objectExtensionsToSync: ObjectExtensionManifest[]; + fieldsToSync: FieldManifest[]; workspaceId: string; applicationId: string; }) { @@ -785,36 +752,11 @@ export class ApplicationSyncService { }, ); - const { idByNameSingular: objectIdByNameSingular } = - buildObjectIdByNameMaps(flatObjectMetadataMaps); - - for (const objectExtension of objectExtensionsToSync) { - const { targetObject, fields } = objectExtension; - - let targetObjectId: string | undefined; - - if (isDefined(targetObject.nameSingular)) { - targetObjectId = objectIdByNameSingular[targetObject.nameSingular]; - - if (!isDefined(targetObjectId)) { - throw new ApplicationException( - `Failed to find target object with nameSingular "${targetObject.nameSingular}" for object extension`, - ApplicationExceptionCode.OBJECT_NOT_FOUND, - ); - } - } else if (isDefined(targetObject.universalIdentifier)) { - targetObjectId = - flatObjectMetadataMaps.idByUniversalIdentifier[ - targetObject.universalIdentifier - ]; - - if (!isDefined(targetObjectId)) { - throw new ApplicationException( - `Failed to find target object with universalIdentifier "${targetObject.universalIdentifier}" for object extension`, - ApplicationExceptionCode.OBJECT_NOT_FOUND, - ); - } - } + for (const fieldToSync of fieldsToSync) { + const targetObjectId = + flatObjectMetadataMaps.idByUniversalIdentifier[ + fieldToSync.objectUniversalIdentifier + ]; if (!isDefined(targetObjectId)) { throw new ApplicationException( @@ -823,29 +765,20 @@ export class ApplicationSyncService { ); } - // Sync regular fields for this extension - await this.syncFieldsWithoutRelations({ + await this.syncObjectFieldsWithoutRelations({ objectId: targetObjectId, - fieldsToSync: fields, + fieldsToSync: [fieldToSync], workspaceId, applicationId, }); - // Sync relation fields for this extension - const relationFields = fields.filter( - (field): field is RelationFieldManifest => - this.isRelationFieldManifest(field), - ); - - if (relationFields.length > 0) { - await this.syncRelationFields({ - objectId: targetObjectId, - relationsToSync: relationFields, - workspaceId, - applicationId, - flatObjectMetadataMaps, - }); - } + await this.syncObjectFieldsRelationOnly({ + objectId: targetObjectId, + fieldsToSync: [fieldToSync], + workspaceId, + applicationId, + flatObjectMetadataMaps, + }); } } diff --git a/packages/twenty-server/src/engine/core-modules/application/application.entity.ts b/packages/twenty-server/src/engine/core-modules/application/application.entity.ts index 292d697d595..1d6a86a18eb 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.entity.ts @@ -57,10 +57,10 @@ export class ApplicationEntity extends WorkspaceRelatedEntity { logicFunctionLayerId: string | null; @Column({ nullable: true, type: 'uuid' }) - defaultLogicFunctionRoleId: string | null; + defaultRoleId: string | null; @Field(() => RoleDTO, { nullable: true }) - defaultLogicFunctionRole: RoleDTO | null; + defaultRole: RoleDTO | null; @Column({ nullable: false, type: 'boolean', default: true }) canBeUninstalled: boolean; diff --git a/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts index 51a07d4b571..89174b9baad 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts @@ -7,8 +7,6 @@ import { import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { InjectRepository } from '@nestjs/typeorm'; -import path, { join } from 'path'; - import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import { PermissionFlagType } from 'twenty-shared/constants'; import { FileFolder } from 'twenty-shared/types'; @@ -164,33 +162,16 @@ export class ApplicationResolver { ); } - const stream = createReadStream(); - const buffer = await streamToBuffer(stream); + const buffer = await streamToBuffer(createReadStream()); - const dirname = path.dirname(filePath); - - const filename = path.basename(filePath); - - const folderPath = join( - `workspace-${workspaceId}`, - applicationUniversalIdentifier, - fileFolder, - dirname, - ); - - await this.fileStorageService.writeFile({ - file: buffer, - name: filename, - folder: folderPath, + return await this.fileStorageService.writeFile_v2({ + sourceFile: buffer, mimeType: mimetype, - }); - - const createdFile = this.fileRepository.create({ - path: join(folderPath, filename), - size: buffer.length, + fileFolder, + applicationUniversalIdentifier, workspaceId, + resourcePath: filePath, + settings: { isTemporaryFile: false, toDelete: false }, }); - - return await this.fileRepository.save(createdFile); } } diff --git a/packages/twenty-server/src/engine/core-modules/application/application.service.ts b/packages/twenty-server/src/engine/core-modules/application/application.service.ts index a6ee118aa0a..1b5912c50fc 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.service.ts @@ -33,17 +33,14 @@ export class ApplicationService { where: { id: applicationId, workspaceId }, }); - if ( - !isDefined(application) || - !isDefined(application.defaultLogicFunctionRoleId) - ) { + if (!isDefined(application) || !isDefined(application.defaultRoleId)) { throw new ApplicationException( `Could not find application ${applicationId}`, ApplicationExceptionCode.APPLICATION_NOT_FOUND, ); } - return application.defaultLogicFunctionRoleId; + return application.defaultRoleId; } async findWorkspaceTwentyStandardAndCustomApplicationOrThrow({ diff --git a/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts b/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts index f39bafcead1..c1c7b5c0be1 100644 --- a/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts @@ -47,7 +47,7 @@ export class ApplicationDTO { @IsOptional() @IsString() @Field({ nullable: true }) - defaultLogicFunctionRoleId?: string; + defaultRoleId?: string; @IsOptional() @Field(() => RoleDTO, { nullable: true }) diff --git a/packages/twenty-server/src/engine/core-modules/application/dtos/application.input.ts b/packages/twenty-server/src/engine/core-modules/application/dtos/application.input.ts index 8fb5ca11f86..3b27eb50d77 100644 --- a/packages/twenty-server/src/engine/core-modules/application/dtos/application.input.ts +++ b/packages/twenty-server/src/engine/core-modules/application/dtos/application.input.ts @@ -1,12 +1,12 @@ import { ArgsType, Field } from '@nestjs/graphql'; import GraphQLJSON from 'graphql-type-json'; -import { ApplicationManifest, PackageJson } from 'twenty-shared/application'; +import { Manifest, PackageJson } from 'twenty-shared/application'; @ArgsType() export class ApplicationInput { @Field(() => GraphQLJSON, { nullable: false }) - manifest: ApplicationManifest; + manifest: Manifest; @Field(() => GraphQLJSON, { nullable: false }) packageJson: PackageJson; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/common-api-context-builder.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/common-api-context-builder.service.ts index 7f26db9343f..dddf6e81226 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/common-api-context-builder.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/common-api-context-builder.service.ts @@ -126,9 +126,9 @@ export class CommonApiContextBuilderService { ); } else if ( isApplicationAuthContext(authContext) && - isDefined(authContext.application.defaultLogicFunctionRoleId) + isDefined(authContext.application.defaultRoleId) ) { - roleId = authContext.application.defaultLogicFunctionRoleId; + roleId = authContext.application.defaultRoleId; } else if (isUserAuthContext(authContext)) { const userWorkspaceRoleId = await this.userRoleService.getRoleIdForUserWorkspace({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts index dc9a6238efb..9c3b57ba26e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts @@ -29,8 +29,8 @@ const MOCK_FLAT_APPLICATION: FlatApplication = { sourceType: 'local', sourcePath: '', logicFunctionLayerId: null, - defaultLogicFunctionRoleId: null, - defaultLogicFunctionRole: null, + defaultRoleId: null, + defaultRole: null, canBeUninstalled: false, createdAt: new Date(), updatedAt: new Date(), diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts index fc0e95d4895..68b09fd8024 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts @@ -24,8 +24,8 @@ const MOCK_FLAT_APPLICATION: FlatApplication = { sourceType: 'local', sourcePath: '', logicFunctionLayerId: null, - defaultLogicFunctionRoleId: null, - defaultLogicFunctionRole: null, + defaultRoleId: null, + defaultRole: null, canBeUninstalled: false, createdAt: new Date(), updatedAt: new Date(), diff --git a/packages/twenty-server/src/engine/metadata-modules/function-build/function-build.module.ts b/packages/twenty-server/src/engine/metadata-modules/function-build/function-build.module.ts deleted file mode 100644 index 75374f01ae6..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/function-build/function-build.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service'; - -@Module({ - providers: [FunctionBuildService], - exports: [FunctionBuildService], -}) -export class FunctionBuildModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function-build/logic-function-build.module.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function-build/logic-function-build.module.ts new file mode 100644 index 00000000000..420fdbf6059 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function-build/logic-function-build.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { LogicFunctionBuildService } from 'src/engine/metadata-modules/logic-function-build/logic-function-build.service'; + +@Module({ + providers: [LogicFunctionBuildService], + exports: [LogicFunctionBuildService], +}) +export class LogicFunctionBuildModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/function-build/function-build.service.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function-build/logic-function-build.service.ts similarity index 98% rename from packages/twenty-server/src/engine/metadata-modules/function-build/function-build.service.ts rename to packages/twenty-server/src/engine/metadata-modules/logic-function-build/logic-function-build.service.ts index 1a2bfb388ab..01be8727d80 100644 --- a/packages/twenty-server/src/engine/metadata-modules/function-build/function-build.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function-build/logic-function-build.service.ts @@ -20,7 +20,7 @@ export type FunctionBuildParams = { }; @Injectable() -export class FunctionBuildService { +export class LogicFunctionBuildService { constructor(private readonly fileStorageService: FileStorageService) {} async isBuilt({ diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts index 5a0f5ca9bf3..e15ec576ba6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts @@ -14,7 +14,7 @@ import { FileModule } from 'src/engine/core-modules/file/file.module'; import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; -import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/function-build.module'; +import { LogicFunctionBuildModule } from 'src/engine/metadata-modules/logic-function-build/logic-function-build.module'; import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module'; import { LogicFunctionTriggerJob } from 'src/engine/metadata-modules/logic-function/jobs/logic-function-trigger.job'; import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; @@ -40,7 +40,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace PermissionsModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, WorkspaceMigrationModule, - FunctionBuildModule, + LogicFunctionBuildModule, LogicFunctionLayerModule, SubscriptionsModule, WorkspaceCacheModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.service.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.service.ts index b89304ba9d2..816a1e7a896 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.service.ts @@ -43,7 +43,7 @@ import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/works import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; import { cleanServerUrl } from 'src/utils/clean-server-url'; -import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service'; +import { LogicFunctionBuildService } from 'src/engine/metadata-modules/logic-function-build/logic-function-build.service'; const MIN_TOKEN_EXPIRATION_IN_SECONDS = 5; @@ -52,7 +52,7 @@ export class LogicFunctionService { constructor( private readonly fileStorageService: FileStorageService, private readonly logicFunctionExecutorService: LogicFunctionExecutorService, - private readonly functionBuildService: FunctionBuildService, + private readonly functionBuildService: LogicFunctionBuildService, private readonly logicFunctionLayerService: LogicFunctionLayerService, @InjectRepository(LogicFunctionEntity) private readonly logicFunctionRepository: Repository, diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts index de811606a9d..99d6fc46aae 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service.ts @@ -367,9 +367,9 @@ export class NavigationMenuItemService { if ( isApplicationAuthContext(authContext) && - isDefined(authContext.application.defaultLogicFunctionRoleId) + isDefined(authContext.application.defaultRoleId) ) { - return authContext.application.defaultLogicFunctionRoleId; + return authContext.application.defaultRoleId; } if (isUserAuthContext(authContext)) { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/create-logic-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/create-logic-function-action-handler.service.ts index 7eb1eb4fd18..2f141ee984e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/create-logic-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/create-logic-function-action-handler.service.ts @@ -24,7 +24,7 @@ import { import { CreateLogicFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type'; import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type'; -import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service'; +import { LogicFunctionBuildService } from 'src/engine/metadata-modules/logic-function-build/logic-function-build.service'; @Injectable() export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler( @@ -33,7 +33,7 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR ) { constructor( private readonly fileStorageService: FileStorageService, - private readonly functionBuildService: FunctionBuildService, + private readonly functionBuildService: LogicFunctionBuildService, @InjectRepository(ApplicationEntity) private readonly applicationRepository: Repository, ) { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/update-logic-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/update-logic-function-action-handler.service.ts index 5490ce851b8..34467f78852 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/update-logic-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/logic-function/services/update-logic-function-action-handler.service.ts @@ -16,7 +16,6 @@ import { FileStorageService } from 'src/engine/core-modules/file-storage/file-st import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function-executor/drivers/utils/lambda-build-directory-manager'; import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function-executor/logic-function-executor.service'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service'; import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; import { LogicFunctionException, @@ -27,6 +26,7 @@ import { getLogicFunctionBaseFolderPath } from 'src/engine/metadata-modules/logi import { UpdateLogicFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type'; import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity'; +import { LogicFunctionBuildService } from 'src/engine/metadata-modules/logic-function-build/logic-function-build.service'; @Injectable() export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler( @@ -36,7 +36,7 @@ export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationR constructor( private readonly fileStorageService: FileStorageService, private readonly logicFunctionExecutorService: LogicFunctionExecutorService, - private readonly functionBuildService: FunctionBuildService, + private readonly functionBuildService: LogicFunctionBuildService, @InjectRepository(ApplicationEntity) private readonly applicationRepository: Repository, ) { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts index afb53234dee..740e0323a79 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts @@ -69,13 +69,13 @@ import { UpdateViewGroupActionHandlerService } from 'src/engine/workspace-manage import { CreateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service'; import { DeleteViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/delete-view-action-handler.service'; import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/update-view-action-handler.service'; -import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/function-build.module'; +import { LogicFunctionBuildModule } from 'src/engine/metadata-modules/logic-function-build/logic-function-build.module'; @Module({ imports: [ TypeOrmModule.forFeature([ApplicationEntity]), WorkspaceSchemaManagerModule, - FunctionBuildModule, + LogicFunctionBuildModule, ], providers: [ CreateFieldActionHandlerService, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts index 3ba4b2c8e15..49a0aadfba0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts @@ -98,10 +98,10 @@ export class WorkflowExecutionContextService { // Use the application's role if set, otherwise fall back to admin role // In the future we should probably assign the Admin role to the Standard Application - let roleId = application.defaultLogicFunctionRoleId; + let roleId = application.defaultRoleId; if (!isDefined(roleId)) { - // Fallback: Look up admin role for existing workspaces without defaultLogicFunctionRoleId + // Fallback: Look up admin role for existing workspaces without defaultRoleId const adminRole = await this.roleService.getRoleByUniversalIdentifier({ universalIdentifier: ADMIN_ROLE.standardId, workspaceId, @@ -118,7 +118,7 @@ export class WorkflowExecutionContextService { workspace, application: { ...application, - defaultLogicFunctionRoleId: roleId, + defaultRoleId: roleId, }, }); diff --git a/packages/twenty-shared/src/application/applicationManifestType.ts b/packages/twenty-shared/src/application/applicationManifestType.ts deleted file mode 100644 index 310cf4b5b07..00000000000 --- a/packages/twenty-shared/src/application/applicationManifestType.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { - type PackageJson, - type Application, - type ObjectManifest, - type LogicFunctionManifest, -} from '@/application'; -import { type AssetManifest } from '@/application/assetManifestType'; -import { type FrontComponentManifest } from '@/application/frontComponentManifestType'; -import { type ObjectExtensionManifest } from '@/application/objectExtensionManifestType'; -import { type RoleManifest } from '@/application/roleManifestType'; -import { type Sources } from '@/types'; - -export type ApplicationManifest = { - application: Application; - objects: ObjectManifest[]; - objectExtensions?: ObjectExtensionManifest[]; - functions: LogicFunctionManifest[]; - frontComponents: FrontComponentManifest[]; - roles?: RoleManifest[]; - publicAssets?: AssetManifest[]; - sources: Sources; - packageJson: PackageJson; - yarnLock: string; -}; diff --git a/packages/twenty-shared/src/application/applicationType.ts b/packages/twenty-shared/src/application/applicationType.ts index 78fe3bfdddc..ed96465f216 100644 --- a/packages/twenty-shared/src/application/applicationType.ts +++ b/packages/twenty-shared/src/application/applicationType.ts @@ -1,8 +1,8 @@ -import { type ApplicationVariables } from '@/application'; +import { type ApplicationVariables } from '@/sdk'; import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; -export type Application = SyncableEntityOptions & { - functionRoleUniversalIdentifier: string; +export type ApplicationManifest = SyncableEntityOptions & { + defaultRoleUniversalIdentifier: string; displayName?: string; description?: string; icon?: string; diff --git a/packages/twenty-shared/src/application/enums/syncable-entities.enum.ts b/packages/twenty-shared/src/application/enums/syncable-entities.enum.ts index a02cb481d91..9b949edf8ba 100644 --- a/packages/twenty-shared/src/application/enums/syncable-entities.enum.ts +++ b/packages/twenty-shared/src/application/enums/syncable-entities.enum.ts @@ -1,8 +1,7 @@ export enum SyncableEntity { Object = 'object', - ObjectExtension = 'objectExtension', - Function = 'function', + Field = 'field', + LogicFunction = 'logicFunction', FrontComponent = 'frontComponent', Role = 'role', - PublicAsset = 'publicAsset', } diff --git a/packages/twenty-shared/src/application/fieldManifestType.ts b/packages/twenty-shared/src/application/fieldManifestType.ts index 4151d3fbf04..896fa367513 100644 --- a/packages/twenty-shared/src/application/fieldManifestType.ts +++ b/packages/twenty-shared/src/application/fieldManifestType.ts @@ -5,8 +5,10 @@ import { type FieldMetadataDefaultValue, } from '@/types'; import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; +import { type RelationOnDeleteAction } from '@/types/RelationOnDeleteAction.type'; +import { type RelationType } from '@/types/RelationType'; -export type FieldManifest< +export type RegularFieldManifest< T extends FieldMetadataType = Exclude< FieldMetadataType, FieldMetadataType.RELATION @@ -21,4 +23,21 @@ export type FieldManifest< options?: FieldMetadataOptions; settings?: FieldMetadataSettings; isNullable?: boolean; + objectUniversalIdentifier: string; }; + +export type RelationFieldManifest = + RegularFieldManifest & { + relationType: RelationType; + targetObjectUniversalIdentifier: string; + targetFieldLabel: string; + targetFieldIcon?: string; + onDelete?: RelationOnDeleteAction; + }; + +export type FieldManifest< + T extends FieldMetadataType = Exclude< + FieldMetadataType, + FieldMetadataType.RELATION + >, +> = RegularFieldManifest | RelationFieldManifest; diff --git a/packages/twenty-shared/src/application/index.ts b/packages/twenty-shared/src/application/index.ts index f80e531934d..20b0bef5e1a 100644 --- a/packages/twenty-shared/src/application/index.ts +++ b/packages/twenty-shared/src/application/index.ts @@ -7,8 +7,7 @@ * |___/ */ -export type { ApplicationManifest } from './applicationManifestType'; -export type { Application } from './applicationType'; +export type { ApplicationManifest } from './applicationType'; export type { ApplicationVariables } from './applicationVariablesType'; export type { AssetManifest } from './assetManifestType'; export { ASSETS_DIR } from './constants/AssetDirectory'; @@ -17,7 +16,11 @@ export { DEFAULT_API_URL_NAME } from './constants/DefaultApiUrlName'; export { GENERATED_DIR } from './constants/GeneratedDirectory'; export { OUTPUT_DIR } from './constants/OutputDirectory'; export { SyncableEntity } from './enums/syncable-entities.enum'; -export type { FieldManifest } from './fieldManifestType'; +export type { + RegularFieldManifest, + RelationFieldManifest, + FieldManifest, +} from './fieldManifestType'; export type { FrontComponentManifest } from './frontComponentManifestType'; export type { InputJsonSchema, @@ -27,9 +30,9 @@ export type { RouteTrigger, LogicFunctionTriggerManifest, } from './logicFunctionManifestType'; -export type { ObjectExtensionManifest } from './objectExtensionManifestType'; +export type { Manifest } from './manifestType'; +export type { ObjectFieldManifest } from './objectFieldManifest.type'; export type { ObjectManifest } from './objectManifestType'; export type { PackageJson } from './packageJsonType'; -export type { RelationFieldManifest } from './relationFieldManifestType'; export type { RoleManifest } from './roleManifestType'; export type { SyncableEntityOptions } from './syncableEntityOptionsType'; diff --git a/packages/twenty-shared/src/application/manifestType.ts b/packages/twenty-shared/src/application/manifestType.ts new file mode 100644 index 00000000000..23067ae6f4b --- /dev/null +++ b/packages/twenty-shared/src/application/manifestType.ts @@ -0,0 +1,24 @@ +import { + type PackageJson, + type ApplicationManifest, + type ObjectManifest, + type LogicFunctionManifest, + type AssetManifest, + type FrontComponentManifest, + type RoleManifest, + type FieldManifest, +} from '@/sdk'; +import { type Sources } from '@/types'; + +export type Manifest = { + application: ApplicationManifest; + objects: ObjectManifest[]; + fields: FieldManifest[]; + logicFunctions: LogicFunctionManifest[]; + frontComponents: FrontComponentManifest[]; + roles: RoleManifest[]; + publicAssets: AssetManifest[]; + sources: Sources; + packageJson: PackageJson; + yarnLock: string; +}; diff --git a/packages/twenty-shared/src/application/objectExtensionManifestType.ts b/packages/twenty-shared/src/application/objectExtensionManifestType.ts deleted file mode 100644 index 7ec350e7c03..00000000000 --- a/packages/twenty-shared/src/application/objectExtensionManifestType.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type FieldManifest } from '@/application/fieldManifestType'; -import { type RelationFieldManifest } from '@/application/relationFieldManifestType'; - -type TargetObjectByNameSingular = { - nameSingular: string; - universalIdentifier?: never; -}; - -type TargetObjectByUniversalIdentifier = { - nameSingular?: never; - universalIdentifier: string; -}; - -export type ObjectExtensionManifest = { - targetObject: TargetObjectByNameSingular | TargetObjectByUniversalIdentifier; - fields: (FieldManifest | RelationFieldManifest)[]; -}; diff --git a/packages/twenty-shared/src/application/objectFieldManifest.type.ts b/packages/twenty-shared/src/application/objectFieldManifest.type.ts new file mode 100644 index 00000000000..1d8c69ee1c7 --- /dev/null +++ b/packages/twenty-shared/src/application/objectFieldManifest.type.ts @@ -0,0 +1,9 @@ +import { type FieldManifest } from '@/application/fieldManifestType'; +import type { FieldMetadataType } from '@/types'; + +export type ObjectFieldManifest< + T extends FieldMetadataType = Exclude< + FieldMetadataType, + FieldMetadataType.RELATION + >, +> = Omit, 'objectUniversalIdentifier'>; diff --git a/packages/twenty-shared/src/application/objectManifestType.ts b/packages/twenty-shared/src/application/objectManifestType.ts index 501b50d1b34..b870b796301 100644 --- a/packages/twenty-shared/src/application/objectManifestType.ts +++ b/packages/twenty-shared/src/application/objectManifestType.ts @@ -1,6 +1,5 @@ -import { type FieldManifest } from '@/application'; -import { type RelationFieldManifest } from '@/application/relationFieldManifestType'; import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; +import { type ObjectFieldManifest } from '@/application/objectFieldManifest.type'; export type ObjectManifest = SyncableEntityOptions & { nameSingular: string; @@ -9,5 +8,5 @@ export type ObjectManifest = SyncableEntityOptions & { labelPlural: string; description?: string; icon?: string; - fields: (FieldManifest | RelationFieldManifest)[]; + fields: ObjectFieldManifest[]; }; diff --git a/packages/twenty-shared/src/application/relationFieldManifestType.ts b/packages/twenty-shared/src/application/relationFieldManifestType.ts deleted file mode 100644 index 7b3601878a8..00000000000 --- a/packages/twenty-shared/src/application/relationFieldManifestType.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type FieldManifest } from '@/application/fieldManifestType'; -import { type FieldMetadataType } from '@/types'; -import { type RelationOnDeleteAction } from '@/types/RelationOnDeleteAction.type'; -import { type RelationType } from '@/types/RelationType'; - -export type RelationFieldManifest = - FieldManifest & { - relationType: RelationType; - targetObjectUniversalIdentifier: string; - targetFieldLabel: string; - targetFieldIcon?: string; - onDelete?: RelationOnDeleteAction; - }; diff --git a/packages/twenty-shared/src/application/roleManifestType.ts b/packages/twenty-shared/src/application/roleManifestType.ts index 6c94c1c5da2..ac16863ca52 100644 --- a/packages/twenty-shared/src/application/roleManifestType.ts +++ b/packages/twenty-shared/src/application/roleManifestType.ts @@ -1,48 +1,21 @@ import { type PermissionFlagType } from '@/constants'; import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; -type WithObjectIdentifier = { +type ObjectPermission = { objectUniversalIdentifier: string; - objectNameSingular?: never; -}; - -type WithObjectName = { - objectNameSingular: string; - objectUniversalIdentifier?: never; -}; - -type BaseObjectPermission = { canReadObjectRecords?: boolean; canUpdateObjectRecords?: boolean; canSoftDeleteObjectRecords?: boolean; canDestroyObjectRecords?: boolean; }; -type ObjectPermission = - | (BaseObjectPermission & WithObjectIdentifier) - | (BaseObjectPermission & WithObjectName); - -type WithFieldIdentifier = { +type FieldPermission = { + objectUniversalIdentifier: string; fieldUniversalIdentifier: string; - fieldName?: never; -}; - -type WithFieldName = { - fieldName: string; - fieldUniversalIdentifier?: never; -}; - -type BaseFieldPermission = { canReadFieldValue?: boolean; canUpdateFieldValue?: boolean; }; -type FieldPermission = - | (BaseFieldPermission & WithObjectIdentifier & WithFieldIdentifier) - | (BaseFieldPermission & WithObjectIdentifier & WithFieldName) - | (BaseFieldPermission & WithObjectName & WithFieldIdentifier) - | (BaseFieldPermission & WithObjectName & WithFieldName); - export type RoleManifest = SyncableEntityOptions & { label: string; description?: string;