Publish twenty packages (#17676)
- removes code editor in settings - update readmes and docs
This commit is contained in:
@@ -61,15 +61,19 @@ yarn app:uninstall
|
||||
```
|
||||
|
||||
## What gets scaffolded
|
||||
- A minimal app structure ready for Twenty
|
||||
- A minimal app structure ready for Twenty with example files:
|
||||
- `application-config.ts` - Application metadata configuration
|
||||
- `roles/default-role.ts` - Default role for logic functions
|
||||
- `logic-functions/hello-world.ts` - Example logic function with HTTP trigger
|
||||
- `front-components/hello-world.tsx` - Example front component
|
||||
- TypeScript configuration
|
||||
- Prewired scripts that wrap the `twenty` CLI from twenty-sdk
|
||||
- Example placeholders to help you add entities, actions, and sync logic
|
||||
|
||||
## Next steps
|
||||
- Explore the generated project and add your first entity with `yarn entity:add` (functions, front components, objects, roles).
|
||||
- Keep your types up‑to‑date using `yarn app:generate`.
|
||||
- Use `yarn auth:login` to authenticate with your Twenty workspace.
|
||||
- Explore the generated project and add your first entity with `yarn entity:add` (logic functions, front components, objects, roles).
|
||||
- Use `yarn app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- Keep your types up‑to‑date using `yarn app:generate`.
|
||||
|
||||
|
||||
## Publish your application
|
||||
|
||||
@@ -12,6 +12,9 @@ jest.mock('fs-extra', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const APPLICATION_FILE_NAME = 'application-config.ts';
|
||||
const DEFAULT_ROLE_FILE_NAME = 'default-role.ts';
|
||||
|
||||
describe('copyBaseApplicationProject', () => {
|
||||
let testAppDirectory: string;
|
||||
|
||||
@@ -40,16 +43,16 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDirectory: testAppDirectory,
|
||||
});
|
||||
|
||||
// Verify src/app/ folder exists
|
||||
// Verify src/ folder exists
|
||||
const srcAppPath = join(testAppDirectory, 'src');
|
||||
expect(await fs.pathExists(srcAppPath)).toBe(true);
|
||||
|
||||
// Verify application.config.ts exists in src/app/
|
||||
const appConfigPath = join(srcAppPath, 'application.config.ts');
|
||||
// Verify application-config.ts exists in src/
|
||||
const appConfigPath = join(srcAppPath, APPLICATION_FILE_NAME);
|
||||
expect(await fs.pathExists(appConfigPath)).toBe(true);
|
||||
|
||||
// Verify default.role.ts exists in src/app/
|
||||
const roleConfigPath = join(srcAppPath, 'default.role.ts');
|
||||
// Verify default-role.ts exists in src/
|
||||
const roleConfigPath = join(srcAppPath, 'roles', DEFAULT_ROLE_FILE_NAME);
|
||||
expect(await fs.pathExists(roleConfigPath)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -102,7 +105,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
expect(yarnLockContent).toContain('yarn lockfile v1');
|
||||
});
|
||||
|
||||
it('should create application.config.ts with defineApplication 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',
|
||||
@@ -110,11 +113,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDirectory: testAppDirectory,
|
||||
});
|
||||
|
||||
const appConfigPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'application.config.ts',
|
||||
);
|
||||
const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME);
|
||||
const appConfigContent = await fs.readFile(appConfigPath, 'utf8');
|
||||
|
||||
// Verify it uses defineApplication
|
||||
@@ -125,7 +124,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
// Verify it imports the role identifier
|
||||
expect(appConfigContent).toContain(
|
||||
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role'",
|
||||
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'",
|
||||
);
|
||||
|
||||
// Verify display name and description
|
||||
@@ -143,7 +142,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should create default.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,7 +150,12 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDirectory: testAppDirectory,
|
||||
});
|
||||
|
||||
const roleConfigPath = join(testAppDirectory, 'src', 'default.role.ts');
|
||||
const roleConfigPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'roles',
|
||||
DEFAULT_ROLE_FILE_NAME,
|
||||
);
|
||||
const roleConfigContent = await fs.readFile(roleConfigPath, 'utf8');
|
||||
|
||||
// Verify it uses defineRole
|
||||
@@ -206,11 +210,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDirectory: testAppDirectory,
|
||||
});
|
||||
|
||||
const appConfigPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'application.config.ts',
|
||||
);
|
||||
const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME);
|
||||
const appConfigContent = await fs.readFile(appConfigPath, 'utf8');
|
||||
|
||||
expect(appConfigContent).toContain("description: ''");
|
||||
@@ -239,11 +239,11 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
// Read both app configs
|
||||
const firstAppConfig = await fs.readFile(
|
||||
join(firstAppDir, 'src', 'application.config.ts'),
|
||||
join(firstAppDir, 'src', APPLICATION_FILE_NAME),
|
||||
'utf8',
|
||||
);
|
||||
const secondAppConfig = await fs.readFile(
|
||||
join(secondAppDir, 'src', 'application.config.ts'),
|
||||
join(secondAppDir, 'src', APPLICATION_FILE_NAME),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
@@ -280,12 +280,12 @@ describe('copyBaseApplicationProject', () => {
|
||||
});
|
||||
|
||||
const firstRoleConfig = await fs.readFile(
|
||||
join(firstAppDir, 'src', 'default.role.ts'),
|
||||
join(firstAppDir, 'src', 'roles', DEFAULT_ROLE_FILE_NAME),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const secondRoleConfig = await fs.readFile(
|
||||
join(secondAppDir, 'src', 'default.role.ts'),
|
||||
join(secondAppDir, 'src', 'roles', DEFAULT_ROLE_FILE_NAME),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
|
||||
@@ -33,20 +33,27 @@ export const copyBaseApplicationProject = async ({
|
||||
await createDefaultRoleConfig({
|
||||
displayName: appDisplayName,
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'roles',
|
||||
fileName: 'default-role.ts',
|
||||
});
|
||||
|
||||
await createDefaultFrontComponent({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'front-components',
|
||||
fileName: 'hello-world.tsx',
|
||||
});
|
||||
|
||||
await createDefaultFunction({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'logic-functions',
|
||||
fileName: 'hello-world.ts',
|
||||
});
|
||||
|
||||
await createApplicationConfig({
|
||||
displayName: appDisplayName,
|
||||
description: appDescription,
|
||||
appDirectory: sourceFolderPath,
|
||||
fileName: 'application-config.ts',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -108,9 +115,13 @@ yarn-error.log*
|
||||
const createDefaultRoleConfig = async ({
|
||||
displayName,
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
displayName: string;
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
@@ -130,13 +141,18 @@ export default defineRole({
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'default.role.ts'), content);
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createDefaultFrontComponent = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
@@ -159,16 +175,18 @@ export default defineFrontComponent({
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.writeFile(
|
||||
join(appDirectory, 'hello-world.front-component.tsx'),
|
||||
content,
|
||||
);
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createDefaultFunction = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
const triggerUniversalIdentifier = v4();
|
||||
@@ -198,23 +216,25 @@ export default defineLogicFunction({
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.writeFile(
|
||||
join(appDirectory, 'hello-world.logic-function.ts'),
|
||||
content,
|
||||
);
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createApplicationConfig = async ({
|
||||
displayName,
|
||||
description,
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
displayName: string;
|
||||
description?: string;
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const content = `import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '${v4()}',
|
||||
@@ -224,7 +244,8 @@ export default defineApplication({
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'application.config.ts'), content);
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createPackageJson = async ({
|
||||
@@ -266,8 +287,8 @@ const createPackageJson = async ({
|
||||
devDependencies: {
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^19.0.0',
|
||||
react: '^19.0.0',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
eslint: '^9.32.0',
|
||||
'typescript-eslint': '^8.50.0',
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ const config: ApplicationConfig = {
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
functionRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -16,9 +16,6 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
|
||||
- Build logic functions with custom triggers
|
||||
- Deploy the same app across multiple workspaces
|
||||
|
||||
**Coming soon:**
|
||||
- Custom UI layouts and components
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24+ and Yarn 4
|
||||
@@ -93,78 +90,53 @@ my-twenty-app/
|
||||
README.md
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
src/
|
||||
application.config.ts # Required - main application configuration
|
||||
default-function.role.ts # Default role for serverless functions
|
||||
hello-world.function.ts # Example serverless function
|
||||
hello-world.front-component.tsx # Example front component
|
||||
// your entities (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
```
|
||||
|
||||
### Convention-over-configuration
|
||||
|
||||
Applications use a **convention-over-configuration** approach where entities are detected by their file suffix. This allows flexible organization within the `src/app/` folder:
|
||||
|
||||
| File suffix | Entity type |
|
||||
|-------------|-------------|
|
||||
| `*.object.ts` | Custom object definitions |
|
||||
| `*.function.ts` | Serverless function definitions |
|
||||
| `*.front-component.tsx` | Front component definitions |
|
||||
| `*.role.ts` | Role definitions |
|
||||
|
||||
### Supported folder organizations
|
||||
|
||||
You can organize your entities in any of these patterns:
|
||||
|
||||
**Traditional (by type):**
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**Feature-based:**
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Flat:**
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # Example logic function
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Example front component
|
||||
```
|
||||
|
||||
At a high level:
|
||||
|
||||
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and `auth:login` that delegate to the local `twenty` CLI.
|
||||
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and authentication commands that delegate to the local `twenty` CLI.
|
||||
- **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `generated/` (typed client), `dist/`, `build/`, coverage folders, log files, and `.env*` files.
|
||||
- **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Lock and configure the Yarn 4 toolchain used by the project.
|
||||
- **.nvmrc**: Pins the Node.js version expected by the project.
|
||||
- **eslint.config.mjs** and **tsconfig.json**: Provide linting and TypeScript configuration for your app's TypeScript sources.
|
||||
- **README.md**: A short README in the app root with basic instructions.
|
||||
- **public/**: A folder for storing public assets (images, fonts, static files) that will be served with your application. Files placed here are uploaded during sync and accessible at runtime.
|
||||
- **src/**: The main place where you define your application-as-code:
|
||||
- `application.config.ts`: Global configuration for your app (metadata and runtime wiring). See "Application config" below.
|
||||
- `*.role.ts`: Role definitions used by your logic functions. See "Default function role" below.
|
||||
- `*.object.ts`: Custom object definitions.
|
||||
- `*.function.ts`: Logic function definitions.
|
||||
- `*.front-component.tsx`: Front component definitions.
|
||||
- **src/**: The main place where you define your application-as-code
|
||||
|
||||
### Entity detection
|
||||
|
||||
The SDK detects entities by parsing your TypeScript files for **`export default define<Entity>({...})`** calls. Each entity type has a corresponding helper function exported from `twenty-sdk`:
|
||||
|
||||
| Helper function | Entity type |
|
||||
|-----------------|-------------|
|
||||
| `defineObject()` | Custom object definitions |
|
||||
| `defineLogicFunction()` | Logic function definitions |
|
||||
| `defineFrontComponent()` | Front component definitions |
|
||||
| `defineRole()` | Role definitions |
|
||||
| `defineField()` | Field extensions for existing objects |
|
||||
|
||||
<Note>
|
||||
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement.
|
||||
</Note>
|
||||
|
||||
Example of a detected entity:
|
||||
```typescript
|
||||
// This file can be named anything and placed anywhere in src/
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '...',
|
||||
nameSingular: 'postCard',
|
||||
// ... rest of config
|
||||
});
|
||||
```
|
||||
|
||||
Later commands will add more files and folders:
|
||||
|
||||
@@ -210,16 +182,18 @@ The twenty-sdk provides typed building blocks and helper functions you use insid
|
||||
|
||||
### Helper functions
|
||||
|
||||
The SDK provides four helper functions with built-in validation for defining your app entities:
|
||||
The SDK provides helper functions for defining your app entities. As described in [Entity detection](#entity-detection), you must use `export default define<Entity>({...})` for your entities to be detected:
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `defineApplication()` | Configure application metadata |
|
||||
| `defineApplication()` | Configure application metadata (required, one per app) |
|
||||
| `defineObject()` | Define custom objects with fields |
|
||||
| `defineFunction()` | Define logic functions with handlers |
|
||||
| `defineLogicFunction()` | Define logic functions with handlers |
|
||||
| `defineFrontComponent()` | Define front components for custom UI |
|
||||
| `defineRole()` | Configure role permissions and object access |
|
||||
| `defineField()` | Extend existing objects with additional fields |
|
||||
|
||||
These functions validate your configuration at runtime and provide better IDE autocompletion and type safety.
|
||||
These functions validate your configuration at build time and provide IDE autocompletion and type safety.
|
||||
|
||||
### Defining objects
|
||||
|
||||
@@ -307,9 +281,9 @@ Key points:
|
||||
</Note>
|
||||
|
||||
|
||||
### Application config (application.config.ts)
|
||||
### Application config (application-config.ts)
|
||||
|
||||
Every app has a single `application.config.ts` file that describes:
|
||||
Every app has a single `application-config.ts` file that describes:
|
||||
|
||||
- **Who the app is**: identifiers, display name, and description.
|
||||
- **How its functions run**: which role they use for permissions.
|
||||
@@ -318,9 +292,9 @@ Every app has a single `application.config.ts` file that describes:
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
|
||||
```typescript
|
||||
// src/app/application.config.ts
|
||||
// src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -335,18 +309,18 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
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 (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
- `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
|
||||
- `defaultRoleUniversalIdentifier` must match the role file (see below).
|
||||
|
||||
#### Roles and permissions
|
||||
|
||||
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.
|
||||
Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `defaultRoleUniversalIdentifier` 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.
|
||||
@@ -357,7 +331,7 @@ Applications can define roles that encapsulate permissions on your workspace's o
|
||||
When you scaffold a new app, the CLI also creates a default role file. Use `defineRole()` to define roles with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/app/default-function.role.ts
|
||||
// src/roles/default-role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
@@ -396,10 +370,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
The `universalIdentifier` of this role is then referenced in `application.config.ts` as `roleUniversalIdentifier`. In other words:
|
||||
The `universalIdentifier` of this role is then referenced in `application-config.ts` as `defaultRoleUniversalIdentifier`. 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.
|
||||
- **application-config.ts** points to that role so your functions inherit its permissions.
|
||||
|
||||
Notes:
|
||||
- Start from the scaffolded role, then progressively restrict it following least‑privilege.
|
||||
@@ -409,11 +383,11 @@ Notes:
|
||||
|
||||
### Logic function config and entrypoint
|
||||
|
||||
Each function file uses `defineFunction()` to export a configuration with a handler and optional triggers. Use the `*.function.ts` file suffix for automatic detection.
|
||||
Each function file uses `defineLogicFunction()` to export a configuration with a handler and optional triggers.
|
||||
|
||||
```typescript
|
||||
// src/app/createPostCard.function.ts
|
||||
import { defineFunction } from 'twenty-sdk';
|
||||
// src/app/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
|
||||
import Twenty, { type Person } from '~/generated';
|
||||
|
||||
@@ -433,7 +407,7 @@ const handler = async (params: RoutePayload) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineFunction({
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
@@ -502,7 +476,7 @@ const handler = async (event: RoutePayload) => {
|
||||
When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the AWS HTTP API v2 format. Import the type from `twenty-sdk`:
|
||||
|
||||
```typescript
|
||||
import { defineFunction, type RoutePayload } from 'twenty-sdk';
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Access request data
|
||||
@@ -532,7 +506,7 @@ The `RoutePayload` type has the following structure:
|
||||
By default, HTTP headers from incoming requests are **not** passed to your logic function for security reasons. To access specific headers, explicitly list them in the `forwardedRequestHeaders` array:
|
||||
|
||||
```typescript
|
||||
export default defineFunction({
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -567,8 +541,44 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
You can create new functions in two ways:
|
||||
|
||||
- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new function. This generates a starter file with a handler and config.
|
||||
- **Manual**: Create a new `*.function.ts` file and use `defineFunction()`, following the same pattern.
|
||||
- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new logic function. This generates a starter file with a handler and config.
|
||||
- **Manual**: Create a new `*.logic-function.ts` file and use `defineLogicFunction()`, following the same pattern.
|
||||
|
||||
### Front components
|
||||
|
||||
Front components let you build custom React components that render within Twenty's UI. Use `defineFrontComponent()` to define components with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/my-widget.front-component.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
const MyWidget = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>My Custom Widget</h1>
|
||||
<p>This is a custom front component for Twenty.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'my-widget',
|
||||
description: 'A custom widget component',
|
||||
component: MyWidget,
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Front components are React components that render in isolated contexts within Twenty.
|
||||
- Use the `*.front-component.tsx` file suffix for automatic detection.
|
||||
- The `component` field references your React component.
|
||||
- Components are built and synced automatically during `yarn app:dev`.
|
||||
|
||||
You can create new front components in two ways:
|
||||
|
||||
- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new front component.
|
||||
- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`.
|
||||
|
||||
### Generated typed client
|
||||
|
||||
@@ -592,13 +602,13 @@ 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 `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.
|
||||
- The API key's permissions are determined by the role referenced in your `application-config.ts` via `defaultRoleUniversalIdentifier`. 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 `defaultRoleUniversalIdentifier` to that role's universal identifier.
|
||||
|
||||
|
||||
### Hello World example
|
||||
|
||||
Explore a minimal, end-to-end example that demonstrates objects, functions, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
Explore a minimal, end-to-end example that demonstrates objects, logic functions, front components, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Manual setup (without the scaffolder)
|
||||
|
||||
|
||||
+2
-72
@@ -1,11 +1,6 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useTestLogicFunction } from '@/logic-functions/hooks/useTestLogicFunction';
|
||||
import { computeNewSources } from '@/logic-functions/utils/computeNewSources';
|
||||
import { flattenSources } from '@/logic-functions/utils/flattenSources';
|
||||
import { getToolInputSchemaFromSourceCode } from '@/logic-functions/utils/getToolInputSchemaFromSourceCode';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsLogicFunctionLabelContainer } from '@/settings/logic-functions/components/SettingsLogicFunctionLabelContainer';
|
||||
import { SettingsLogicFunctionCodeEditorTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionCodeEditorTab';
|
||||
import { SettingsLogicFunctionSettingsTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionSettingsTab';
|
||||
import { SettingsLogicFunctionTestTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionTestTab';
|
||||
import { SettingsLogicFunctionTriggersTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionTriggersTab';
|
||||
@@ -20,15 +15,9 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconBolt,
|
||||
IconCode,
|
||||
IconSettings,
|
||||
IconTestPipe,
|
||||
} from 'twenty-ui/display';
|
||||
import { IconBolt, IconSettings, IconTestPipe } from 'twenty-ui/display';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useFindOneApplicationQuery } from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -38,7 +27,6 @@ export const SettingsLogicFunctionDetail = () => {
|
||||
const { logicFunctionId = '', applicationId = '' } = useParams();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const { data, loading: applicationLoading } = useFindOneApplicationQuery({
|
||||
variables: { id: applicationId },
|
||||
@@ -47,15 +35,6 @@ export const SettingsLogicFunctionDetail = () => {
|
||||
|
||||
const applicationName = data?.findOneApplication?.name;
|
||||
|
||||
// A logic function is "managed" if it belongs to an application
|
||||
// other than the workspace's custom application
|
||||
const workspaceCustomApplicationId =
|
||||
currentWorkspace?.workspaceCustomApplication?.id;
|
||||
const isManaged =
|
||||
isDefined(applicationId) &&
|
||||
applicationId !== '' &&
|
||||
applicationId !== workspaceCustomApplicationId;
|
||||
|
||||
const instanceId = `${LOGIC_FUNCTION_DETAIL_ID}-${logicFunctionId}`;
|
||||
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
@@ -98,57 +77,17 @@ export const SettingsLogicFunctionDetail = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const onCodeChange = async (filePath: string, value: string) => {
|
||||
setFormValues((prevState: LogicFunctionFormValues) => {
|
||||
return {
|
||||
...prevState,
|
||||
code: computeNewSources({
|
||||
previousCode: prevState['code'],
|
||||
filePath,
|
||||
value,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// Parse and save schema if editing the handler file
|
||||
let toolInputSchema: object | null | undefined;
|
||||
|
||||
if (filePath === logicFunction?.sourceHandlerPath) {
|
||||
toolInputSchema = await getToolInputSchemaFromSourceCode(value);
|
||||
}
|
||||
|
||||
await handleSave(toolInputSchema);
|
||||
};
|
||||
|
||||
const handleTestFunction = async () => {
|
||||
navigate('#test');
|
||||
await testLogicFunction();
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'editor', title: t`Editor`, Icon: IconCode },
|
||||
{ id: 'settings', title: t`Settings`, Icon: IconSettings },
|
||||
{ id: 'triggers', title: t`Triggers`, Icon: IconBolt },
|
||||
{ id: 'test', title: t`Test`, Icon: IconTestPipe },
|
||||
{ id: 'settings', title: t`Settings`, Icon: IconSettings },
|
||||
];
|
||||
|
||||
const flattenedCode = flattenSources(formValues.code);
|
||||
|
||||
const files = flattenedCode
|
||||
.map((file) => ({
|
||||
path: file.path,
|
||||
language: 'typescript',
|
||||
content: file.content,
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
a.path === logicFunction?.sourceHandlerPath
|
||||
? -1
|
||||
: b.path === logicFunction?.sourceHandlerPath
|
||||
? 1
|
||||
: 0,
|
||||
);
|
||||
|
||||
const isEditorTab = activeTabId === 'editor';
|
||||
const isTriggersTab = activeTabId === 'triggers';
|
||||
const isTestTab = activeTabId === 'test';
|
||||
const isSettingsTab = activeTabId === 'settings';
|
||||
@@ -203,15 +142,6 @@ export const SettingsLogicFunctionDetail = () => {
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<TabList tabs={tabs} componentInstanceId={instanceId} />
|
||||
{isEditorTab && (
|
||||
<SettingsLogicFunctionCodeEditorTab
|
||||
files={files}
|
||||
handleExecute={handleTestFunction}
|
||||
onChange={onCodeChange}
|
||||
isTesting={isTesting}
|
||||
isManaged={isManaged}
|
||||
/>
|
||||
)}
|
||||
{isTriggersTab && logicFunction && (
|
||||
<SettingsLogicFunctionTriggersTab logicFunction={logicFunction} />
|
||||
)}
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ export const Default: Story = {
|
||||
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await sleep(100);
|
||||
await canvas.findByText('Code your function', undefined, {
|
||||
await canvas.findByText('Name and describe your function', undefined, {
|
||||
timeout: 3000,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ Commands:
|
||||
app:uninstall Uninstall application from Twenty
|
||||
entity:add Add a new entity to your application
|
||||
function:logs Watch application function logs
|
||||
function:execute Execute a serverless function with a JSON payload
|
||||
function:execute Execute a logic function with a JSON payload
|
||||
help [command] display help for command
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"application": {
|
||||
"universalIdentifier": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"functionRoleUniversalIdentifier": "00000000-0000-0000-0000-000000000000",
|
||||
"defaultRoleUniversalIdentifier": "00000000-0000-0000-0000-000000000000",
|
||||
"displayName": "Data Enrichment",
|
||||
"description": "Enrich your data easily. Choose your provider.",
|
||||
"icon": "IconSparkles",
|
||||
|
||||
Reference in New Issue
Block a user