Add defineApplicationRole method (#20314)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`).
|
||||
</Note>
|
||||
|
||||
@@ -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',
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
|
||||
"createValidationResult",
|
||||
"defineAgent",
|
||||
"defineApplication",
|
||||
"defineApplicationRole",
|
||||
"defineCommandMenuItem",
|
||||
"defineConnectionProvider",
|
||||
"defineField",
|
||||
|
||||
+9
@@ -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';
|
||||
|
||||
@@ -47,6 +47,6 @@ export const buildAndValidateManifest = async (
|
||||
success: true,
|
||||
manifest: result.manifest,
|
||||
filePaths: result.filePaths,
|
||||
warnings: validation.warnings,
|
||||
warnings: [...result.warnings, ...validation.warnings],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<string[]> => {
|
||||
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 = <T extends { universalIdentifier: string }>(a: T, b: T) =>
|
||||
a.universalIdentifier.localeCompare(b.universalIdentifier);
|
||||
|
||||
const byPath = <T extends { filePath: string }>(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 };
|
||||
};
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -6,4 +6,10 @@ export type ApplicationConfig = Omit<
|
||||
| 'yarnLockChecksum'
|
||||
| 'postInstallLogicFunction'
|
||||
| 'preInstallLogicFunction'
|
||||
>;
|
||||
| 'defaultRoleUniversalIdentifier'
|
||||
> & {
|
||||
/**
|
||||
* @deprecated Use `defineApplicationRole()` in your role file instead.
|
||||
*/
|
||||
defaultRoleUniversalIdentifier?: string;
|
||||
};
|
||||
|
||||
@@ -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<ApplicationConfig> = (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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ export type ValidationResult<T> = {
|
||||
success: boolean;
|
||||
config: T;
|
||||
errors: string[];
|
||||
warnings?: string[];
|
||||
};
|
||||
|
||||
export type DefinableEntity =
|
||||
|
||||
@@ -3,11 +3,14 @@ import { type ValidationResult } from '@/sdk/define/common/types/define-entity.t
|
||||
export const createValidationResult = <T>({
|
||||
config,
|
||||
errors = [],
|
||||
warnings = [],
|
||||
}: {
|
||||
config: T;
|
||||
errors: string[];
|
||||
warnings?: string[];
|
||||
}): ValidationResult<T> => ({
|
||||
success: errors.length === 0,
|
||||
config,
|
||||
errors,
|
||||
warnings,
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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<RoleConfig> = (config) =>
|
||||
defineRole(config);
|
||||
Reference in New Issue
Block a user