From 4aca4d11433e94ede7b8f78595e0aeb9000892da Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Fri, 8 May 2026 00:25:13 +0530 Subject: [PATCH] Add defineApplicationRole method (#20314) Co-authored-by: Cursor --- .../template/src/application-config.ts | 2 - .../constants/template/src/default-role.ts | 4 +- .../extend/apps/config/application.mdx | 7 +- .../developers/extend/apps/config/roles.mdx | 16 ++-- .../extend/apps/logic/connections.mdx | 1 - .../extend/apps/logic/logic-functions.mdx | 2 +- .../extend/apps/operations/publishing.mdx | 1 - ...stub-twenty-sdk-define.plugin.spec.ts.snap | 1 + .../__tests__/manifest-extract-config.spec.ts | 9 ++ .../manifest/build-and-validate-manifest.ts | 2 +- .../build/manifest/manifest-build.ts | 87 ++++++++++++++----- .../build/manifest/manifest-extract-config.ts | 2 + .../application/__tests__/define-app.spec.ts | 32 +++++++ .../define/application/application-config.ts | 8 +- .../define/application/define-application.ts | 16 ++-- .../define/common/types/define-entity.type.ts | 1 + .../common/utils/create-validation-result.ts | 3 + packages/twenty-sdk/src/sdk/define/index.ts | 1 + .../__tests__/define-application-role.spec.ts | 86 ++++++++++++++++++ .../define/roles/define-application-role.ts | 6 ++ 20 files changed, 237 insertions(+), 50 deletions(-) create mode 100644 packages/twenty-sdk/src/sdk/define/roles/__tests__/define-application-role.spec.ts create mode 100644 packages/twenty-sdk/src/sdk/define/roles/define-application-role.ts diff --git a/packages/create-twenty-app/src/constants/template/src/application-config.ts b/packages/create-twenty-app/src/constants/template/src/application-config.ts index 3278c7b2b7..088f9d1ea9 100644 --- a/packages/create-twenty-app/src/constants/template/src/application-config.ts +++ b/packages/create-twenty-app/src/constants/template/src/application-config.ts @@ -4,12 +4,10 @@ import { APP_DESCRIPTION, APP_DISPLAY_NAME, APPLICATION_UNIVERSAL_IDENTIFIER, - DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, } from 'src/constants/universal-identifiers'; export default defineApplication({ universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, displayName: APP_DISPLAY_NAME, description: APP_DESCRIPTION, - defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, }); diff --git a/packages/create-twenty-app/src/constants/template/src/default-role.ts b/packages/create-twenty-app/src/constants/template/src/default-role.ts index e0338e0948..a6abf65445 100644 --- a/packages/create-twenty-app/src/constants/template/src/default-role.ts +++ b/packages/create-twenty-app/src/constants/template/src/default-role.ts @@ -1,11 +1,11 @@ -import { defineRole } from 'twenty-sdk/define'; +import { defineApplicationRole } from 'twenty-sdk/define'; import { APP_DISPLAY_NAME, DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, } from 'src/constants/universal-identifiers'; -export default defineRole({ +export default defineApplicationRole({ universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: `${APP_DISPLAY_NAME} default function role`, description: `${APP_DISPLAY_NAME} default function role`, diff --git a/packages/twenty-docs/developers/extend/apps/config/application.mdx b/packages/twenty-docs/developers/extend/apps/config/application.mdx index 2ae98f34cf..c5825c51d9 100644 --- a/packages/twenty-docs/developers/extend/apps/config/application.mdx +++ b/packages/twenty-docs/developers/extend/apps/config/application.mdx @@ -13,7 +13,6 @@ Every app must have exactly one `defineApplication` call. It declares: ```ts src/application-config.ts import { defineApplication } from 'twenty-sdk/define'; -import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; export default defineApplication({ universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', @@ -27,19 +26,19 @@ export default defineApplication({ isSecret: false, }, }, - defaultRoleUniversalIdentifier: 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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). -- `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/developers/extend/apps/config/roles). +- The default role is detected automatically from the role file marked with [`defineApplicationRole()`](/developers/extend/apps/config/roles) — you do not need to reference it from `defineApplication()`. - Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. +- Passing `defaultRoleUniversalIdentifier` explicitly is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`. ## Default function role -The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: +The role declared with [`defineApplicationRole()`](/developers/extend/apps/config/roles) controls what the app's logic functions and front components can access: - The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. - The typed API client is restricted to the permissions granted to that role. diff --git a/packages/twenty-docs/developers/extend/apps/config/roles.mdx b/packages/twenty-docs/developers/extend/apps/config/roles.mdx index bae6d40be4..814c03b3d7 100644 --- a/packages/twenty-docs/developers/extend/apps/config/roles.mdx +++ b/packages/twenty-docs/developers/extend/apps/config/roles.mdx @@ -4,7 +4,7 @@ description: Declare what objects and fields your app's logic functions and fron icon: "shield-halved" --- -A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role declared as `defaultRoleUniversalIdentifier` in [`defineApplication`](/developers/extend/apps/config/application). +A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role marked with `defineApplicationRole()` (see [The default function role](#the-default-function-role) below). ```ts src/roles/restricted-company-role.ts import { @@ -51,15 +51,15 @@ export default defineRole({ ## The default function role -When you scaffold a new app, the CLI creates a default role file: +When you scaffold a new app, the CLI creates a default role file declared with `defineApplicationRole()`: ```ts src/roles/default-role.ts -import { defineRole, PermissionFlag } from 'twenty-sdk/define'; +import { defineApplicationRole, PermissionFlag } from 'twenty-sdk/define'; export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b648f87b-1d26-4961-b974-0908fd991061'; -export default defineRole({ +export default defineApplicationRole({ universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, label: 'Default function role', description: 'Default role for function Twenty client', @@ -77,10 +77,12 @@ export default defineRole({ }); ``` -This role's `universalIdentifier` is referenced from `application-config.ts` as `defaultRoleUniversalIdentifier`: +`defineApplicationRole()` is a thin wrapper around `defineRole()` that flags **the** role used as your application's default at install time. Validation is identical to `defineRole`, but the build pipeline auto-wires its `universalIdentifier` into the application manifest's `defaultRoleUniversalIdentifier` — so you do not need to reference it from [`defineApplication`](/developers/extend/apps/config/application) yourself. -- **`*.role.ts`** declares what the role can do. -- **`application-config.ts`** points to that role so your functions inherit its permissions. +Notes: +- Exactly **one** `defineApplicationRole(...)` is allowed per app — the manifest build will fail if it finds more than one. +- Use `defineRole()` (not `defineApplicationRole()`) for any **additional** roles your app ships. +- Setting `defaultRoleUniversalIdentifier` explicitly on `defineApplication()` is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`. ## Best practices diff --git a/packages/twenty-docs/developers/extend/apps/logic/connections.mdx b/packages/twenty-docs/developers/extend/apps/logic/connections.mdx index 41960e885b..060ebdd17e 100644 --- a/packages/twenty-docs/developers/extend/apps/logic/connections.mdx +++ b/packages/twenty-docs/developers/extend/apps/logic/connections.mdx @@ -52,7 +52,6 @@ export default defineApplication({ universalIdentifier: '...', displayName: 'Linear', description: 'Connect Linear to Twenty.', - defaultRoleUniversalIdentifier: '...', // OAuth client credentials live on the app registration (one OAuth app per // Twenty server, configured by the admin) — not per-workspace. Declare them // as serverVariables so the admin can fill them in once for all installs. diff --git a/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx b/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx index f2712aad20..6edf3a9530 100644 --- a/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx +++ b/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx @@ -372,5 +372,5 @@ Key points: - `TWENTY_API_URL` — Base URL of the Twenty API - `TWENTY_APP_ACCESS_TOKEN` — Short-lived key scoped to your application's default function role - You do **not** need to pass these to the clients — they read from `process.env` automatically. The API key's permissions are determined by the role referenced in `defaultRoleUniversalIdentifier` in your `application-config.ts`. + You do **not** need to pass these to the clients — they read from `process.env` automatically. The API key's permissions are determined by the role declared with `defineApplicationRole()` (or referenced via `defaultRoleUniversalIdentifier` in `application-config.ts`). diff --git a/packages/twenty-docs/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/developers/extend/apps/operations/publishing.mdx index c5e15d5c4f..e8612608e8 100644 --- a/packages/twenty-docs/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/developers/extend/apps/operations/publishing.mdx @@ -187,7 +187,6 @@ export default defineApplication({ universalIdentifier: '...', displayName: 'My App', description: 'A great app', - defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, logoUrl: 'public/logo.png', screenshots: [ 'public/screenshot-1.png', diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap b/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap index 2f3f0a98a3..f88423b56b 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap +++ b/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap @@ -33,6 +33,7 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1 "createValidationResult", "defineAgent", "defineApplication", + "defineApplicationRole", "defineCommandMenuItem", "defineConnectionProvider", "defineField", 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 index 27abac4a7f..8cb18d09d2 100644 --- 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 @@ -46,6 +46,15 @@ describe('extractDefineEntity', () => { expect(result).toBe('defineRole'); }); + it('should detect defineApplicationRole in default export', () => { + const fileContent = ` + import { defineApplicationRole } from 'twenty-sdk/define'; + export default defineApplicationRole({ universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', label: 'Default function role' }); + `; + const result = extractDefineEntity(fileContent); + expect(result).toBe('defineApplicationRole'); + }); + it('should detect defineFrontComponent in default export', () => { const fileContent = ` import { defineFrontComponent } from 'twenty-sdk/define'; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/build-and-validate-manifest.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/build-and-validate-manifest.ts index 19d5e7c257..4b5965b658 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/build-and-validate-manifest.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/build-and-validate-manifest.ts @@ -47,6 +47,6 @@ export const buildAndValidateManifest = async ( success: true, manifest: result.manifest, filePaths: result.filePaths, - warnings: validation.warnings, + warnings: [...result.warnings, ...validation.warnings], }; }; 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 541900674d..bb9d6a930d 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 @@ -6,13 +6,18 @@ import { TargetFunction, } from '@/cli/utilities/build/manifest/manifest-extract-config'; import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file'; +import { addMissingFieldOptionIds } from '@/cli/utilities/build/manifest/utils/add-missing-field-option-ids'; +import { fromRoleConfigToRoleManifest } from '@/cli/utilities/build/manifest/utils/from-role-config-to-role-manifest'; import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields'; import { type ApplicationConfig, type LogicFunctionConfig } from '@/sdk/define'; import { type CommandMenuItemConfig } from '@/sdk/define/command-menu-items/command-menu-item-config'; import { type FrontComponentConfig } from '@/sdk/define/front-component/front-component-config'; +import { type PostInstallLogicFunctionConfig } from '@/sdk/define/logic-functions/post-install-logic-function-config'; +import { type PreInstallLogicFunctionConfig } from '@/sdk/define/logic-functions/pre-install-logic-function-config'; import { type ObjectConfig } from '@/sdk/define/objects/object-config'; import { type PageLayoutConfig } from '@/sdk/define/page-layouts/page-layout-config'; import { type PageLayoutTabConfig } from '@/sdk/define/page-layouts/page-layout-tab-config'; +import { type RoleConfig } from '@/sdk/define/roles/role-config'; import { type ViewConfig } from '@/sdk/define/views/view-config'; import { readFile } from 'node:fs/promises'; import { basename, extname, relative } from 'path'; @@ -43,11 +48,6 @@ import { jsonSchemaToInputSchema, } from 'twenty-shared/logic-function'; import { assertUnreachable } from 'twenty-shared/utils'; -import { addMissingFieldOptionIds } from '@/cli/utilities/build/manifest/utils/add-missing-field-option-ids'; -import { type PostInstallLogicFunctionConfig } from '@/sdk/define/logic-functions/post-install-logic-function-config'; -import { type PreInstallLogicFunctionConfig } from '@/sdk/define/logic-functions/pre-install-logic-function-config'; -import { fromRoleConfigToRoleManifest } from '@/cli/utilities/build/manifest/utils/from-role-config-to-role-manifest'; -import { type RoleConfig } from '@/sdk/define/roles/role-config'; const loadSources = async (appPath: string): Promise => { return await glob(['**/*.ts', '**/*.tsx'], { @@ -72,11 +72,13 @@ export const buildManifest = async ( manifest: Manifest | null; filePaths: EntityFilePaths; errors: string[]; + warnings: string[]; }> => { const filePaths = await loadSources(appPath); const errors: string[] = []; + const warnings: string[] = []; - let application: ApplicationManifest | undefined; + let applicationConfig: ApplicationConfig | undefined; const objects: ObjectManifest[] = []; const fields: FieldManifest[] = []; const roles: RoleManifest[] = []; @@ -95,6 +97,7 @@ export const buildManifest = async ( []; const preInstallLogicFunctions: PreInstallLogicFunctionApplicationManifest[] = []; + const applicationRoleUniversalIdentifiers: string[] = []; const applicationFilePaths: string[] = []; const objectsFilePaths: string[] = []; const fieldsFilePaths: string[] = []; @@ -130,12 +133,9 @@ export const buildManifest = async ( filePath, }); - application = { - ...extract.config, - yarnLockChecksum: null, - packageJsonChecksum: null, - }; + applicationConfig = extract.config; errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); applicationFilePaths.push(relativePath); break; } @@ -172,6 +172,7 @@ export const buildManifest = async ( fields.push(...reverseRelationFields); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); objectsFilePaths.push(relativePath); break; } @@ -183,6 +184,7 @@ export const buildManifest = async ( const fieldConfig = addMissingFieldOptionIds(extract.config); fields.push(fieldConfig); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); fieldsFilePaths.push(relativePath); break; } @@ -194,7 +196,15 @@ export const buildManifest = async ( const roleConfig = fromRoleConfigToRoleManifest(extract.config); roles.push(roleConfig); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); rolesFilePaths.push(relativePath); + + if (targetFunctionName === TargetFunction.DefineApplicationRole) { + applicationRoleUniversalIdentifiers.push( + extract.config.universalIdentifier, + ); + } + break; } case ManifestEntityKey.Skills: { @@ -204,6 +214,7 @@ export const buildManifest = async ( }); skills.push(extract.config); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); skillsFilePaths.push(relativePath); break; } @@ -214,6 +225,7 @@ export const buildManifest = async ( }); agents.push(extract.config); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); agentsFilePaths.push(relativePath); break; } @@ -225,6 +237,7 @@ export const buildManifest = async ( }); connectionProviders.push(extract.config); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); connectionProvidersFilePaths.push(relativePath); break; } @@ -235,6 +248,7 @@ export const buildManifest = async ( }); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); const { handler: _, ...rest } = extract.config; @@ -323,6 +337,7 @@ export const buildManifest = async ( }); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); const { component, ...rest } = extract.config; @@ -354,6 +369,7 @@ export const buildManifest = async ( views.push(viewManifest); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); viewsFilePaths.push(relativePath); break; } @@ -365,6 +381,7 @@ export const buildManifest = async ( }); navigationMenuItems.push(extract.config); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); navigationMenuItemsFilePaths.push(relativePath); break; } @@ -380,6 +397,7 @@ export const buildManifest = async ( pageLayouts.push(pageLayoutManifest); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); pageLayoutsFilePaths.push(relativePath); break; } @@ -395,6 +413,7 @@ export const buildManifest = async ( pageLayoutTabs.push(pageLayoutTabManifest); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); pageLayoutTabsFilePaths.push(relativePath); break; } @@ -408,6 +427,7 @@ export const buildManifest = async ( extract.config as unknown as CommandMenuItemManifest, ); errors.push(...extract.errors); + warnings.push(...(extract.warnings ?? [])); commandMenuItemsFilePaths.push(relativePath); break; } @@ -434,7 +454,7 @@ export const buildManifest = async ( publicAssetsFilePaths.push(relativePath); } - if (!application) { + if (!applicationConfig) { errors.push( 'Cannot build application, please export default defineApplication() to define an application', ); @@ -452,27 +472,46 @@ export const buildManifest = async ( ); } - if (application && postInstallLogicFunctions.length >= 1) { - application = { - ...application, - postInstallLogicFunction: postInstallLogicFunctions[0], - }; + if (applicationRoleUniversalIdentifiers.length > 1) { + errors.push('Only one defineApplicationRole is allowed per application'); } - if (application && preInstallLogicFunctions.length >= 1) { - application = { - ...application, - preInstallLogicFunction: preInstallLogicFunctions[0], - }; + const resolvedDefaultRoleUniversalIdentifier = + applicationConfig?.defaultRoleUniversalIdentifier ?? + (applicationRoleUniversalIdentifiers.length === 1 + ? applicationRoleUniversalIdentifiers[0] + : undefined); + + if (applicationConfig && !resolvedDefaultRoleUniversalIdentifier) { + errors.push( + 'Application must declare a default role: either pass `defaultRoleUniversalIdentifier` to defineApplication() or mark a role file with defineApplicationRole()', + ); } + const application: ApplicationManifest | undefined = + applicationConfig && resolvedDefaultRoleUniversalIdentifier + ? { + ...applicationConfig, + defaultRoleUniversalIdentifier: + resolvedDefaultRoleUniversalIdentifier, + yarnLockChecksum: null, + packageJsonChecksum: null, + ...(postInstallLogicFunctions.length >= 1 + ? { postInstallLogicFunction: postInstallLogicFunctions[0] } + : {}), + ...(preInstallLogicFunctions.length >= 1 + ? { preInstallLogicFunction: preInstallLogicFunctions[0] } + : {}), + } + : undefined; + const byId = (a: T, b: T) => a.universalIdentifier.localeCompare(b.universalIdentifier); const byPath = (a: T, b: T) => a.filePath.localeCompare(b.filePath); - const manifest = !application + const manifest: Manifest | null = !application ? null : { application, @@ -510,5 +549,5 @@ export const buildManifest = async ( commandMenuItems: commandMenuItemsFilePaths, }; - return { manifest, filePaths: entityFilePaths, errors }; + return { manifest, filePaths: entityFilePaths, errors, warnings }; }; 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 index fdfda01f81..25a9596120 100644 --- 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 @@ -2,6 +2,7 @@ import * as ts from 'typescript'; export enum TargetFunction { DefineApplication = 'defineApplication', + DefineApplicationRole = 'defineApplicationRole', DefineField = 'defineField', DefineLogicFunction = 'defineLogicFunction', DefinePostInstallLogicFunction = 'definePostInstallLogicFunction', @@ -44,6 +45,7 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record< ManifestEntityKey > = { [TargetFunction.DefineApplication]: ManifestEntityKey.Application, + [TargetFunction.DefineApplicationRole]: ManifestEntityKey.Roles, [TargetFunction.DefineField]: ManifestEntityKey.Fields, [TargetFunction.DefineLogicFunction]: ManifestEntityKey.LogicFunctions, [TargetFunction.DefinePostInstallLogicFunction]: diff --git a/packages/twenty-sdk/src/sdk/define/application/__tests__/define-app.spec.ts b/packages/twenty-sdk/src/sdk/define/application/__tests__/define-app.spec.ts index 5c1df74833..1b99e860d1 100644 --- a/packages/twenty-sdk/src/sdk/define/application/__tests__/define-app.spec.ts +++ b/packages/twenty-sdk/src/sdk/define/application/__tests__/define-app.spec.ts @@ -41,6 +41,38 @@ describe('defineApplication', () => { ); }); + it('should accept config without defaultRoleUniversalIdentifier (auto-wired by defineApplicationRole)', () => { + const config = { + universalIdentifier: 'a9faf5f8-cf7e-4f24-9d37-fd523c30febe', + displayName: 'My App', + description: 'My app description', + }; + + const result = defineApplication(config); + + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + expect(result.warnings).toEqual([]); + expect(result.config?.defaultRoleUniversalIdentifier).toBeUndefined(); + }); + + it('should warn that defaultRoleUniversalIdentifier is deprecated when provided', () => { + const result = defineApplication({ + universalIdentifier: 'a9faf5f8-cf7e-4f24-9d37-fd523c30febe', + displayName: 'My App', + description: 'My app description', + defaultRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2', + }); + + const warnings = result.warnings ?? []; + + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toMatch(/deprecated/i); + expect(warnings[0]).toMatch(/defineApplicationRole/); + }); + it('should return error when universalIdentifier is missing', () => { const config = { displayName: 'My App', diff --git a/packages/twenty-sdk/src/sdk/define/application/application-config.ts b/packages/twenty-sdk/src/sdk/define/application/application-config.ts index 21957fc6da..deff44bd23 100644 --- a/packages/twenty-sdk/src/sdk/define/application/application-config.ts +++ b/packages/twenty-sdk/src/sdk/define/application/application-config.ts @@ -6,4 +6,10 @@ export type ApplicationConfig = Omit< | 'yarnLockChecksum' | 'postInstallLogicFunction' | 'preInstallLogicFunction' ->; + | 'defaultRoleUniversalIdentifier' +> & { + /** + * @deprecated Use `defineApplicationRole()` in your role file instead. + */ + defaultRoleUniversalIdentifier?: string; +}; diff --git a/packages/twenty-sdk/src/sdk/define/application/define-application.ts b/packages/twenty-sdk/src/sdk/define/application/define-application.ts index 72221f6133..e41b8b7660 100644 --- a/packages/twenty-sdk/src/sdk/define/application/define-application.ts +++ b/packages/twenty-sdk/src/sdk/define/application/define-application.ts @@ -1,24 +1,28 @@ -import { createValidationResult } from '@/sdk/define/common/utils/create-validation-result'; -import { type DefineEntity } from '@/sdk/define/common/types/define-entity.type'; import { type ApplicationConfig } from '@/sdk/define/application/application-config'; +import { type DefineEntity } from '@/sdk/define/common/types/define-entity.type'; +import { createValidationResult } from '@/sdk/define/common/utils/create-validation-result'; export const defineApplication: DefineEntity = (config) => { const errors = []; + const warnings = []; if (!config.universalIdentifier) { errors.push('Application must have a universalIdentifier'); } - if (!config.defaultRoleUniversalIdentifier) { - errors.push('Application must have a defaultRoleUniversalIdentifier'); - } - if (!config.displayName || config.displayName.length === 0) { errors.push('Application must have a non empty display name'); } + if (config.defaultRoleUniversalIdentifier) { + warnings.push( + '`defaultRoleUniversalIdentifier` on defineApplication() is deprecated. Use defineApplicationRole() in your role file instead.', + ); + } + return createValidationResult({ config, errors, + warnings, }); }; diff --git a/packages/twenty-sdk/src/sdk/define/common/types/define-entity.type.ts b/packages/twenty-sdk/src/sdk/define/common/types/define-entity.type.ts index caf65e64cd..37650b534f 100644 --- a/packages/twenty-sdk/src/sdk/define/common/types/define-entity.type.ts +++ b/packages/twenty-sdk/src/sdk/define/common/types/define-entity.type.ts @@ -21,6 +21,7 @@ export type ValidationResult = { success: boolean; config: T; errors: string[]; + warnings?: string[]; }; export type DefinableEntity = diff --git a/packages/twenty-sdk/src/sdk/define/common/utils/create-validation-result.ts b/packages/twenty-sdk/src/sdk/define/common/utils/create-validation-result.ts index 1a32266c1b..b0faceba3d 100644 --- a/packages/twenty-sdk/src/sdk/define/common/utils/create-validation-result.ts +++ b/packages/twenty-sdk/src/sdk/define/common/utils/create-validation-result.ts @@ -3,11 +3,14 @@ import { type ValidationResult } from '@/sdk/define/common/types/define-entity.t export const createValidationResult = ({ config, errors = [], + warnings = [], }: { config: T; errors: string[]; + warnings?: string[]; }): ValidationResult => ({ success: errors.length === 0, config, errors, + warnings, }); diff --git a/packages/twenty-sdk/src/sdk/define/index.ts b/packages/twenty-sdk/src/sdk/define/index.ts index e9f792d5ed..3efbe4b038 100644 --- a/packages/twenty-sdk/src/sdk/define/index.ts +++ b/packages/twenty-sdk/src/sdk/define/index.ts @@ -84,6 +84,7 @@ export type { PageLayoutWidgetManifest, } from 'twenty-shared/application'; +export { defineApplicationRole } from '@/sdk/define/roles/define-application-role'; export { defineRole } from '@/sdk/define/roles/define-role'; export { PermissionFlag } from '@/sdk/define/roles/permission-flag-type'; diff --git a/packages/twenty-sdk/src/sdk/define/roles/__tests__/define-application-role.spec.ts b/packages/twenty-sdk/src/sdk/define/roles/__tests__/define-application-role.spec.ts new file mode 100644 index 0000000000..1f8e65d730 --- /dev/null +++ b/packages/twenty-sdk/src/sdk/define/roles/__tests__/define-application-role.spec.ts @@ -0,0 +1,86 @@ +import { defineApplicationRole } from '@/sdk/define'; + +describe('defineApplicationRole', () => { + const validConfig = { + universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', + label: 'Default function role', + description: 'Default role for function Twenty client', + }; + + it('should return successful validation result when valid', () => { + const result = defineApplicationRole(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: 'IconShield', + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + }; + + const result = defineApplicationRole(config); + + expect(result.success).toBe(true); + expect(result.config?.icon).toBe('IconShield'); + expect(result.config?.canReadAllObjectRecords).toBe(true); + }); + + it('should accept permissionFlags', () => { + const config = { + ...validConfig, + permissionFlags: ['UPLOAD_FILE'], + }; + + const result = defineApplicationRole(config as any); + + expect(result.success).toBe(true); + expect(result.config?.permissionFlags).toHaveLength(1); + }); + + it('should return error when universalIdentifier is missing', () => { + const config = { + label: 'Default function role', + }; + + const result = defineApplicationRole(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 = defineApplicationRole(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 = defineApplicationRole(config as any); + + expect(result.success).toBe(false); + expect(result.errors).toContain( + 'Object permission must have an objectUniversalIdentifier', + ); + }); +}); diff --git a/packages/twenty-sdk/src/sdk/define/roles/define-application-role.ts b/packages/twenty-sdk/src/sdk/define/roles/define-application-role.ts new file mode 100644 index 0000000000..02fc329e75 --- /dev/null +++ b/packages/twenty-sdk/src/sdk/define/roles/define-application-role.ts @@ -0,0 +1,6 @@ +import { type DefineEntity } from '@/sdk/define/common/types/define-entity.type'; +import { defineRole } from '@/sdk/define/roles/define-role'; +import { type RoleConfig } from '@/sdk/define/roles/role-config'; + +export const defineApplicationRole: DefineEntity = (config) => + defineRole(config);