Update manifest structure (#17547)

Move all sync entities in an `entities` key. Rename functions to
logicFunctions

```json
{
  application: {
    ...
  },
  entities: {
    objects: [],
    logicFunctions: [],
    ...
  }
}
```
This commit is contained in:
martmull 2026-01-30 16:26:45 +01:00 committed by GitHub
parent bc022f82cb
commit f46da3eefd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
162 changed files with 2555 additions and 3778 deletions

View file

@ -32,7 +32,7 @@ describe('copyBaseApplicationProject', () => {
}
});
it('should create the correct folder structure with src/app/', async () => {
it('should create the correct folder structure with src/', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
@ -48,8 +48,8 @@ describe('copyBaseApplicationProject', () => {
const appConfigPath = join(srcAppPath, 'application.config.ts');
expect(await fs.pathExists(appConfigPath)).toBe(true);
// Verify default-function.role.ts exists in src/app/
const roleConfigPath = join(srcAppPath, 'default-function.role.ts');
// Verify default.role.ts exists in src/app/
const roleConfigPath = join(srcAppPath, 'default.role.ts');
expect(await fs.pathExists(roleConfigPath)).toBe(true);
});
@ -102,7 +102,7 @@ describe('copyBaseApplicationProject', () => {
expect(yarnLockContent).toContain('yarn lockfile v1');
});
it('should create application.config.ts with defineApp and correct values', async () => {
it('should create application.config.ts with defineApplication and correct values', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
@ -117,15 +117,15 @@ describe('copyBaseApplicationProject', () => {
);
const appConfigContent = await fs.readFile(appConfigPath, 'utf8');
// Verify it uses defineApp
// Verify it uses defineApplication
expect(appConfigContent).toContain(
"import { defineApp } from 'twenty-sdk'",
"import { defineApplication } from 'twenty-sdk'",
);
expect(appConfigContent).toContain('export default defineApp({');
expect(appConfigContent).toContain('export default defineApplication({');
// Verify it imports the role identifier
expect(appConfigContent).toContain(
"import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default-function.role'",
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role'",
);
// Verify display name and description
@ -139,11 +139,11 @@ describe('copyBaseApplicationProject', () => {
// Verify it references the role
expect(appConfigContent).toContain(
'functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER',
'defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER',
);
});
it('should create default-function.role.ts with defineRole and correct values', async () => {
it('should create default.role.ts with defineRole and correct values', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
@ -151,11 +151,7 @@ describe('copyBaseApplicationProject', () => {
appDirectory: testAppDirectory,
});
const roleConfigPath = join(
testAppDirectory,
'src',
'default-function.role.ts',
);
const roleConfigPath = join(testAppDirectory, 'src', 'default.role.ts');
const roleConfigContent = await fs.readFile(roleConfigPath, 'utf8');
// Verify it uses defineRole
@ -166,7 +162,7 @@ describe('copyBaseApplicationProject', () => {
// Verify it exports the universal identifier constant
expect(roleConfigContent).toContain(
'export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER',
'export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER',
);
// Verify role label includes app name
@ -182,7 +178,7 @@ describe('copyBaseApplicationProject', () => {
// Verify it has a universalIdentifier (UUID format)
expect(roleConfigContent).toMatch(
/universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER/,
/universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER/,
);
});
@ -283,19 +279,19 @@ describe('copyBaseApplicationProject', () => {
appDirectory: secondAppDir,
});
// Read both role configs
const firstRoleConfig = await fs.readFile(
join(firstAppDir, 'src', 'default-function.role.ts'),
join(firstAppDir, 'src', 'default.role.ts'),
'utf8',
);
const secondRoleConfig = await fs.readFile(
join(secondAppDir, 'src', 'default-function.role.ts'),
join(secondAppDir, 'src', 'default.role.ts'),
'utf8',
);
// Extract UUIDs using regex
const uuidRegex =
/DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
/DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
const firstUuid = firstRoleConfig.match(uuidRegex)?.[1];
const secondUuid = secondRoleConfig.match(uuidRegex)?.[1];

View file

@ -3,7 +3,7 @@ import { join } from 'path';
import { v4 } from 'uuid';
import { ASSETS_DIR } from 'twenty-shared/application';
const APP_FOLDER = 'src';
const SRC_FOLDER = 'src';
export const copyBaseApplicationProject = async ({
appName,
@ -26,27 +26,27 @@ export const copyBaseApplicationProject = async ({
await createYarnLock(appDirectory);
const appFolderPath = join(appDirectory, APP_FOLDER);
const sourceFolderPath = join(appDirectory, SRC_FOLDER);
await fs.ensureDir(appFolderPath);
await fs.ensureDir(sourceFolderPath);
await createDefaultServerlessFunctionRoleConfig({
await createDefaultRoleConfig({
displayName: appDisplayName,
appDirectory: appFolderPath,
appDirectory: sourceFolderPath,
});
await createDefaultFrontComponent({
appDirectory: appFolderPath,
appDirectory: sourceFolderPath,
});
await createDefaultFunction({
appDirectory: appFolderPath,
appDirectory: sourceFolderPath,
});
await createApplicationConfig({
displayName: appDisplayName,
description: appDescription,
appDirectory: appFolderPath,
appDirectory: sourceFolderPath,
});
};
@ -103,7 +103,7 @@ yarn-error.log*
await fs.writeFile(join(appDirectory, '.gitignore'), gitignoreContent);
};
const createDefaultServerlessFunctionRoleConfig = async ({
const createDefaultRoleConfig = async ({
displayName,
appDirectory,
}: {
@ -114,11 +114,11 @@ const createDefaultServerlessFunctionRoleConfig = async ({
const content = `import { defineRole } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: '${displayName} default function role',
description: '${displayName} default function role',
canReadAllObjectRecords: true,
@ -128,7 +128,7 @@ export default defineRole({
});
`;
await fs.writeFile(join(appDirectory, 'default-function.role.ts'), content);
await fs.writeFile(join(appDirectory, 'default.role.ts'), content);
};
const createDefaultFrontComponent = async ({
@ -171,23 +171,24 @@ const createDefaultFunction = async ({
const universalIdentifier = v4();
const triggerUniversalIdentifier = v4();
const content = `import { defineFunction } from 'twenty-sdk';
const content = `import { defineLogicFunction } from 'twenty-sdk';
const handler = async (): Promise<{ message: string }> => {
return { message: 'Hello, World!' };
};
export default defineFunction({
// Logic function handler - rename and implement your logic
export default defineLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'hello-world-function',
description: 'A sample serverless function',
name: 'hello-world-logic-function',
description: 'A simple logic function',
timeoutSeconds: 5,
handler,
triggers: [
{
universalIdentifier: '${triggerUniversalIdentifier}',
type: 'route',
path: '/hello-world-function',
path: '/hello-world-logic-function',
httpMethod: 'GET',
isAuthRequired: false,
},
@ -195,7 +196,10 @@ export default defineFunction({
});
`;
await fs.writeFile(join(appDirectory, 'hello-world.function.ts'), content);
await fs.writeFile(
join(appDirectory, 'hello-world.logic-function.ts'),
content,
);
};
const createApplicationConfig = async ({
@ -207,14 +211,14 @@ const createApplicationConfig = async ({
description?: string;
appDirectory: string;
}) => {
const content = `import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default-function.role';
const content = `import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '${v4()}',
displayName: '${displayName}',
description: '${description ?? ''}',
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
`;

View file

@ -14,7 +14,7 @@ export const functionRole: RoleConfig = {
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -23,8 +23,8 @@ export const functionRole: RoleConfig = {
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},

View file

@ -214,7 +214,7 @@ The SDK provides four helper functions with built-in validation for defining you
| Function | Purpose |
|----------|---------|
| `defineApp()` | Configure application metadata |
| `defineApplication()` | Configure application metadata |
| `defineObject()` | Define custom objects with fields |
| `defineFunction()` | Define logic functions with handlers |
| `defineRole()` | Configure role permissions and object access |
@ -315,14 +315,14 @@ Every app has a single `application.config.ts` file that describes:
- **How its functions run**: which role they use for permissions.
- **(Optional) variables**: keyvalue pairs exposed to your functions as environment variables.
Use `defineApp()` to define your application configuration:
Use `defineApplication()` to define your application configuration:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -335,18 +335,18 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
Notes:
- `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs.
- `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
- `functionRoleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
- `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
#### Roles and permissions
Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `functionRoleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
- The runtime API key injected as `TWENTY_API_KEY` is derived from this default function role.
- The typed client will be restricted to the permissions granted to that role.
@ -360,11 +360,11 @@ When you scaffold a new app, the CLI also creates a default role file. Use `defi
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -377,7 +377,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -386,8 +386,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -396,7 +396,7 @@ export default defineRole({
});
```
The `universalIdentifier` of this role is then referenced in `application.config.ts` as `functionRoleUniversalIdentifier`. In other words:
The `universalIdentifier` of this role is then referenced in `application.config.ts` as `roleUniversalIdentifier`. In other words:
- **\*.role.ts** defines what the default function role can do.
- **application.config.ts** points to that role so your functions inherit its permissions.
@ -592,8 +592,8 @@ When your function runs on Twenty, the platform injects credentials as environme
Notes:
- You do not need to pass URL or API key to the generated client. It reads `TWENTY_API_URL` and `TWENTY_API_KEY` from process.env at runtime.
- The API key's permissions are determined by the role referenced in your `application.config.ts` via `functionRoleUniversalIdentifier`. This is the default role used by logic functions of your application.
- Applications can define roles to follow leastprivilege. Grant only the permissions your functions need, then point `functionRoleUniversalIdentifier` to that role's universal identifier.
- The API key's permissions are determined by the role referenced in your `application.config.ts` via `roleUniversalIdentifier`. This is the default role used by logic functions of your application.
- Applications can define roles to follow leastprivilege. Grant only the permissions your functions need, then point `roleUniversalIdentifier` to that role's universal identifier.
### Hello World example

View file

@ -219,7 +219,7 @@ Once you've switched workspaces with `auth:switch`, all subsequent commands will
| دالة | الغرض |
| ------------------ | ---------------------------------------- |
| `defineApp()` | تهيئة بيانات التطبيق الوصفية |
| `defineApplication()` | تهيئة بيانات التطبيق الوصفية |
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
| `defineFunction()` | تعريف وظائف منطقية مع معالجات |
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
@ -319,14 +319,14 @@ export default defineObject({
* **كيفية تشغيل وظائفه**: الدور الذي تستخدمه للأذونات.
* **متغيرات (اختياري)**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
استخدم `defineApp()` لتعريف تهيئة تطبيقك:
استخدم `defineApplication()` لتعريف تهيئة تطبيقك:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ export default defineApp({
* حقول `universalIdentifier` هي معرّفات حتمية تخصك؛ أنشئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
* `applicationVariables` تصبح متغيرات بيئة لوظائفك (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`).
* `functionRoleUniversalIdentifier` يجب أن يطابق الدور الذي تعرّفه في ملف `*.role.ts` (انظر أدناه).
* `defaultRoleUniversalIdentifier` يجب أن يطابق الدور الذي تعرّفه في ملف `*.role.ts` (انظر أدناه).
#### الأدوار والصلاحيات
يمكن للتطبيقات تعريف أدوار تُغلّف الصلاحيات على كائنات وإجراءات مساحة العمل لديك. يعين الحقل `functionRoleUniversalIdentifier` في `application.config.ts` الدور الافتراضي الذي تستخدمه وظائف المنطق في تطبيقك.
يمكن للتطبيقات تعريف أدوار تُغلّف الصلاحيات على كائنات وإجراءات مساحة العمل لديك. يعين الحقل `defaultRoleUniversalIdentifier` في `application.config.ts` الدور الافتراضي الذي تستخدمه وظائف المنطق في تطبيقك.
* مفتاح واجهة البرمجة في وقت التشغيل المحقون باسم `TWENTY_API_KEY` مستمد من دور الوظيفة الافتراضي هذا.
* سيُقيَّد العميل مضبوط الأنواع بالأذونات الممنوحة لذلك الدور.
@ -365,11 +365,11 @@ export default defineApp({
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
يُشار بعد ذلك إلى `universalIdentifier` لهذا الدور في `application.config.ts` باسم `functionRoleUniversalIdentifier`. بعبارة أخرى:
يُشار بعد ذلك إلى `universalIdentifier` لهذا الدور في `application.config.ts` باسم `defaultRoleUniversalIdentifier`. بعبارة أخرى:
* **\\*.role.ts** يحدد ما يمكن أن يفعله الدور الافتراضي للوظيفة.
* **application.config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك صلاحياته.
@ -606,8 +606,8 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
الملاحظات:
* لا تحتاج إلى تمرير عنوان URL أو مفتاح واجهة برمجة التطبيقات إلى العميل المُولَّد. يقوم بقراءة `TWENTY_API_URL` و`TWENTY_API_KEY` من process.env وقت التشغيل.
* تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `application.config.ts` عبر `functionRoleUniversalIdentifier`. هذا هو الدور الافتراضي الذي تستخدمه الوظائف المنطقية في تطبيقك.
* يمكن للتطبيقات تعريف أدوار لاتباع مبدأ أقل الامتياز. امنح فقط الأذونات التي تحتاجها وظائفك، ثم وجّه `functionRoleUniversalIdentifier` إلى المعرّف الشامل لذلك الدور.
* تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `application.config.ts` عبر `defaultRoleUniversalIdentifier`. هذا هو الدور الافتراضي الذي تستخدمه الوظائف المنطقية في تطبيقك.
* يمكن للتطبيقات تعريف أدوار لاتباع مبدأ أقل الامتياز. امنح فقط الأذونات التي تحتاجها وظائفك، ثم وجّه `defaultRoleUniversalIdentifier` إلى المعرّف الشامل لذلك الدور.
### مثال Hello World

View file

@ -219,7 +219,7 @@ SDK poskytuje čtyři pomocné funkce s vestavěnou validací pro definování e
| Funkce | Účel |
| ------------------ | ------------------------------------------------ |
| `defineApp()` | Konfigurace metadat aplikace |
| `defineApplication()` | Konfigurace metadat aplikace |
| `defineObject()` | Definice vlastních objektů s poli |
| `defineFunction()` | Definice logických funkcí s obslužnými funkcemi |
| `defineRole()` | Konfigurace oprávnění rolí a přístupu k objektům |
@ -319,14 +319,14 @@ Každá aplikace má jeden soubor `application.config.ts`, který popisuje:
* **Jak běží její funkce**: kterou roli používají pro oprávnění.
* **(Volitelné) proměnné**: dvojice klíčhodnota zpřístupněné vašim funkcím jako proměnné prostředí.
K definování konfigurace aplikace použijte `defineApp()`:
K definování konfigurace aplikace použijte `defineApplication()`:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ Poznámky:
* Pole `universalIdentifier` jsou deterministická ID, která vlastníte; vygenerujte je jednou a udržujte je stabilní napříč synchronizacemi.
* `applicationVariables` se stanou proměnnými prostředí pro vaše funkce (například `DEFAULT_RECIPIENT_NAME` je dostupné jako `process.env.DEFAULT_RECIPIENT_NAME`).
* `functionRoleUniversalIdentifier` se musí shodovat s rolí, kterou definujete ve svém souboru `*.role.ts` (viz níže).
* `defaultRoleUniversalIdentifier` se musí shodovat s rolí, kterou definujete ve svém souboru `*.role.ts` (viz níže).
#### Role a oprávnění
Aplikace mohou definovat role, které zapouzdřují oprávnění k objektům a akcím ve vašem pracovním prostoru. Pole `functionRoleUniversalIdentifier` v `application.config.ts` určuje výchozí roli používanou logickými funkcemi vaší aplikace.
Aplikace mohou definovat role, které zapouzdřují oprávnění k objektům a akcím ve vašem pracovním prostoru. Pole `defaultRoleUniversalIdentifier` v `application.config.ts` určuje výchozí roli používanou logickými funkcemi vaší aplikace.
* Běhový klíč API vložený jako `TWENTY_API_KEY` je odvozen z této výchozí role funkcí.
* Typovaný klient bude omezen oprávněními udělenými této roli.
@ -365,11 +365,11 @@ Když vygenerujete novou aplikaci, CLI také vytvoří výchozí soubor role. K
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
Na `universalIdentifier` této role se poté odkazuje v `application.config.ts` jako na `functionRoleUniversalIdentifier`. Jinými slovy:
Na `universalIdentifier` této role se poté odkazuje v `application.config.ts` jako na `defaultRoleUniversalIdentifier`. Jinými slovy:
* **\*.role.ts** definuje, co může výchozí role funkce dělat.
* **application.config.ts** ukazuje na tuto roli, aby vaše funkce zdědily její oprávnění.
@ -606,8 +606,8 @@ Když vaše funkce běží na Twenty, platforma před spuštěním kódu vloží
Poznámky:
* Není nutné předávat URL ani klíč API vygenerovanému klientovi. Za běhu čte `TWENTY_API_URL` a `TWENTY_API_KEY` z process.env.
* Oprávnění API klíče jsou určena rolí odkazovanou v `application.config.ts` prostřednictvím `functionRoleUniversalIdentifier`. Toto je výchozí role používaná logickými funkcemi vaší aplikace.
* Aplikace mohou definovat role podle principu nejmenších oprávnění. Udělte pouze oprávnění, která vaše funkce potřebují, a poté nastavte `functionRoleUniversalIdentifier` na univerzální identifikátor této role.
* Oprávnění API klíče jsou určena rolí odkazovanou v `application.config.ts` prostřednictvím `defaultRoleUniversalIdentifier`. Toto je výchozí role používaná logickými funkcemi vaší aplikace.
* Aplikace mohou definovat role podle principu nejmenších oprávnění. Udělte pouze oprávnění, která vaše funkce potřebují, a poté nastavte `defaultRoleUniversalIdentifier` na univerzální identifikátor této role.
### Příklad Hello World

View file

@ -219,7 +219,7 @@ Das SDK stellt vier Hilfsfunktionen mit eingebauter Validierung bereit, um Ihre
| Funktion | Zweck |
| ------------------ | ---------------------------------------------------- |
| `defineApp()` | Anwendungsmetadaten konfigurieren |
| `defineApplication()` | Anwendungsmetadaten konfigurieren |
| `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren |
| `defineFunction()` | Logikfunktionen mit Handlern definieren |
| `defineRole()` | Rollenberechtigungen und Objektzugriff konfigurieren |
@ -319,14 +319,14 @@ Jede App hat eine einzelne Datei `application.config.ts`, die Folgendes beschrei
* **Wie ihre Funktionen ausgeführt werden**: welche Rolle sie für Berechtigungen verwenden.
* **(Optional) Variablen**: SchlüsselWert-Paare, die Ihren Funktionen als Umgebungsvariablen zur Verfügung gestellt werden.
Verwenden Sie `defineApp()`, um Ihre Anwendungskonfiguration zu definieren:
Verwenden Sie `defineApplication()`, um Ihre Anwendungskonfiguration zu definieren:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ Notizen:
* `universalIdentifier`-Felder sind deterministische IDs, die Sie besitzen; generieren Sie sie einmal und halten Sie sie über Synchronisierungen hinweg stabil.
* `applicationVariables` werden zu Umgebungsvariablen für Ihre Funktionen (zum Beispiel ist `DEFAULT_RECIPIENT_NAME` als `process.env.DEFAULT_RECIPIENT_NAME` verfügbar).
* `functionRoleUniversalIdentifier` muss mit der Rolle übereinstimmen, die Sie in Ihrer `*.role.ts`-Datei definieren (siehe unten).
* `defaultRoleUniversalIdentifier` muss mit der Rolle übereinstimmen, die Sie in Ihrer `*.role.ts`-Datei definieren (siehe unten).
#### Rollen und Berechtigungen
Anwendungen können Rollen definieren, die Berechtigungen für die Objekte und Aktionen Ihres Workspaces kapseln. Das Feld `functionRoleUniversalIdentifier` in `application.config.ts` legt die Standardrolle fest, die von den Logikfunktionen Ihrer App verwendet wird.
Anwendungen können Rollen definieren, die Berechtigungen für die Objekte und Aktionen Ihres Workspaces kapseln. Das Feld `defaultRoleUniversalIdentifier` in `application.config.ts` legt die Standardrolle fest, die von den Logikfunktionen Ihrer App verwendet wird.
* Der zur Laufzeit als `TWENTY_API_KEY` injizierte API-Schlüssel wird von dieser Standard-Funktionsrolle abgeleitet.
* Der typisierte Client ist auf die dieser Rolle gewährten Berechtigungen beschränkt.
@ -365,11 +365,11 @@ Wenn Sie eine neue App erzeugen, erstellt die CLI auch eine Standard-Rolldatei.
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
Der `universalIdentifier` dieser Rolle wird anschließend in `application.config.ts` als `functionRoleUniversalIdentifier` referenziert. Anders ausgedrückt:
Der `universalIdentifier` dieser Rolle wird anschließend in `application.config.ts` als `defaultRoleUniversalIdentifier` referenziert. Anders ausgedrückt:
* **\*.role.ts** definiert, was die Standard-Funktionsrolle darf.
* **application.config.ts** verweist auf diese Rolle, sodass Ihre Funktionen deren Berechtigungen erben.
@ -606,8 +606,8 @@ Wenn Ihre Funktion auf Twenty läuft, injiziert die Plattform vor der Ausführun
Notizen:
* Sie müssen dem generierten Client weder URL noch API-Schlüssel übergeben. Er liest `TWENTY_API_URL` und `TWENTY_API_KEY` zur Laufzeit aus process.env.
* Die Berechtigungen des API-Schlüssels werden durch die Rolle bestimmt, auf die in Ihrer `application.config.ts` über `functionRoleUniversalIdentifier` verwiesen wird. Dies ist die Standardrolle, die von den Logikfunktionen Ihrer Anwendung verwendet wird.
* Anwendungen können Rollen definieren, um das Least-Privilege-Prinzip einzuhalten. Gewähren Sie nur die Berechtigungen, die Ihre Funktionen benötigen, und verweisen Sie dann mit `functionRoleUniversalIdentifier` auf den universellen Bezeichner dieser Rolle.
* Die Berechtigungen des API-Schlüssels werden durch die Rolle bestimmt, auf die in Ihrer `application.config.ts` über `defaultRoleUniversalIdentifier` verwiesen wird. Dies ist die Standardrolle, die von den Logikfunktionen Ihrer Anwendung verwendet wird.
* Anwendungen können Rollen definieren, um das Least-Privilege-Prinzip einzuhalten. Gewähren Sie nur die Berechtigungen, die Ihre Funktionen benötigen, und verweisen Sie dann mit `defaultRoleUniversalIdentifier` auf den universellen Bezeichner dieser Rolle.
### Hello-World-Beispiel

View file

@ -219,7 +219,7 @@ El SDK proporciona cuatro funciones auxiliares con validación incorporada para
| Función | Propósito |
| ------------------ | ---------------------------------------------- |
| `defineApp()` | Configura los metadatos de la aplicación |
| `defineApplication()` | Configura los metadatos de la aplicación |
| `defineObject()` | Define objetos personalizados con campos |
| `defineFunction()` | Define funciones de lógica con controladores |
| `defineRole()` | Configura permisos de roles y acceso a objetos |
@ -319,14 +319,14 @@ Cada aplicación tiene un único archivo `application.config.ts` que describe:
* **Cómo se ejecutan sus funciones**: qué rol usan para permisos.
* **Variables (opcionales)**: pares clavevalor expuestos a tus funciones como variables de entorno.
Usa `defineApp()` para definir la configuración de tu aplicación:
Usa `defineApplication()` para definir la configuración de tu aplicación:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ Notas:
* Los campos `universalIdentifier` son ID deterministas bajo tu control; genéralos una vez y mantenlos estables entre sincronizaciones.
* Las `applicationVariables` se convierten en variables de entorno para tus funciones (por ejemplo, `DEFAULT_RECIPIENT_NAME` está disponible como `process.env.DEFAULT_RECIPIENT_NAME`).
* `functionRoleUniversalIdentifier` debe coincidir con el rol que defines en tu archivo `*.role.ts` (ver abajo).
* `defaultRoleUniversalIdentifier` debe coincidir con el rol que defines en tu archivo `*.role.ts` (ver abajo).
#### Roles y permisos
Las aplicaciones pueden definir roles que encapsulan permisos sobre los objetos y acciones de tu espacio de trabajo. El campo `functionRoleUniversalIdentifier` en `application.config.ts` designa el rol predeterminado que usan las funciones de lógica de tu aplicación.
Las aplicaciones pueden definir roles que encapsulan permisos sobre los objetos y acciones de tu espacio de trabajo. El campo `defaultRoleUniversalIdentifier` en `application.config.ts` designa el rol predeterminado que usan las funciones de lógica de tu aplicación.
* La clave de API en tiempo de ejecución inyectada como `TWENTY_API_KEY` se deriva de este rol de función predeterminado.
* El cliente tipado estará restringido a los permisos otorgados a ese rol.
@ -365,11 +365,11 @@ Cuando generas una nueva aplicación, la CLI también crea un archivo de rol pre
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
El `universalIdentifier` de este rol se referencia luego en `application.config.ts` como `functionRoleUniversalIdentifier`. En otras palabras:
El `universalIdentifier` de este rol se referencia luego en `application.config.ts` como `defaultRoleUniversalIdentifier`. En otras palabras:
* **\*.role.ts** define lo que puede hacer el rol de función predeterminado.
* **application.config.ts** apunta a ese rol para que tus funciones hereden sus permisos.
@ -606,8 +606,8 @@ Cuando tu función se ejecuta en Twenty, la plataforma inyecta credenciales como
Notas:
* No necesitas pasar la URL ni la clave de API al cliente generado. Lee `TWENTY_API_URL` y `TWENTY_API_KEY` de process.env en tiempo de ejecución.
* Los permisos de la clave de API están determinados por el rol referenciado en tu `application.config.ts` mediante `functionRoleUniversalIdentifier`. Este es el rol predeterminado que usan las funciones de lógica de tu aplicación.
* Las aplicaciones pueden definir roles para seguir el principio de mínimo privilegio. Concede solo los permisos que necesitan tus funciones y después apunta `functionRoleUniversalIdentifier` al identificador universal de ese rol.
* Los permisos de la clave de API están determinados por el rol referenciado en tu `application.config.ts` mediante `defaultRoleUniversalIdentifier`. Este es el rol predeterminado que usan las funciones de lógica de tu aplicación.
* Las aplicaciones pueden definir roles para seguir el principio de mínimo privilegio. Concede solo los permisos que necesitan tus funciones y después apunta `defaultRoleUniversalIdentifier` al identificador universal de ese rol.
### Ejemplo Hello World

View file

@ -219,7 +219,7 @@ Le SDK fournit quatre fonctions utilitaires avec validation intégrée pour déf
| Fonction | Objectif |
| ------------------ | ---------------------------------------------------------- |
| `defineApp()` | Configurer les métadonnées de lapplication |
| `defineApplication()` | Configurer les métadonnées de lapplication |
| `defineObject()` | Définir des objets personnalisés avec des champs |
| `defineFunction()` | Définir des fonctions logiques avec des gestionnaires |
| `defineRole()` | Configurer les autorisations de rôle et laccès aux objets |
@ -319,14 +319,14 @@ Chaque application dispose dun seul fichier `application.config.ts` qui décr
* **Exécution des fonctions** : le rôle utilisé pour les autorisations.
* **Variables (facultatif)** : paires clévaleur exposées à vos fonctions en tant que variables denvironnement.
Utilisez `defineApp()` pour définir la configuration de votre application :
Utilisez `defineApplication()` pour définir la configuration de votre application :
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ Notes :
* Les champs `universalIdentifier` sont des identifiants déterministes que vous possédez ; générez-les une fois et conservez-les stables entre les synchronisations.
* `applicationVariables` deviennent des variables denvironnement pour vos fonctions (par exemple, `DEFAULT_RECIPIENT_NAME` est disponible sous `process.env.DEFAULT_RECIPIENT_NAME`).
* `functionRoleUniversalIdentifier` doit correspondre au rôle que vous définissez dans votre fichier `*.role.ts` (voir ci-dessous).
* `defaultRoleUniversalIdentifier` doit correspondre au rôle que vous définissez dans votre fichier `*.role.ts` (voir ci-dessous).
#### Rôles et autorisations
Les applications peuvent définir des rôles qui encapsulent des autorisations sur les objets et actions de votre espace de travail. Le champ `functionRoleUniversalIdentifier` dans `application.config.ts` désigne le rôle par défaut utilisé par les fonctions logiques de votre application.
Les applications peuvent définir des rôles qui encapsulent des autorisations sur les objets et actions de votre espace de travail. Le champ `defaultRoleUniversalIdentifier` dans `application.config.ts` désigne le rôle par défaut utilisé par les fonctions logiques de votre application.
* La clé API dexécution injectée sous `TWENTY_API_KEY` est dérivée de ce rôle de fonction par défaut.
* Le client typé sera limité aux autorisations accordées à ce rôle.
@ -365,11 +365,11 @@ Lorsque vous générez une nouvelle application, la CLI crée également un fich
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
Le `universalIdentifier` de ce rôle est ensuite référencé dans `application.config.ts` en tant que `functionRoleUniversalIdentifier`. En dautres termes :
Le `universalIdentifier` de ce rôle est ensuite référencé dans `application.config.ts` en tant que `defaultRoleUniversalIdentifier`. En dautres termes :
* **\*.role.ts** définit ce que le rôle de fonction par défaut peut faire.
* **application.config.ts** pointe vers ce rôle afin que vos fonctions héritent de ses autorisations.
@ -606,8 +606,8 @@ Lorsque votre fonction sexécute sur Twenty, la plateforme injecte des identi
Notes:
* Vous navez pas besoin de passer lURL ou la clé API au client généré. Il lit `TWENTY_API_URL` et `TWENTY_API_KEY` depuis process.env à lexécution.
* Les autorisations de la clé API sont déterminées par le rôle référencé dans votre `application.config.ts` via `functionRoleUniversalIdentifier`. Il sagit du rôle par défaut utilisé par les fonctions logiques de votre application.
* Les applications peuvent définir des rôles pour appliquer le principe du moindre privilège. Naccordez que les autorisations dont vos fonctions ont besoin, puis faites pointer `functionRoleUniversalIdentifier` vers lidentifiant universel de ce rôle.
* Les autorisations de la clé API sont déterminées par le rôle référencé dans votre `application.config.ts` via `defaultRoleUniversalIdentifier`. Il sagit du rôle par défaut utilisé par les fonctions logiques de votre application.
* Les applications peuvent définir des rôles pour appliquer le principe du moindre privilège. Naccordez que les autorisations dont vos fonctions ont besoin, puis faites pointer `defaultRoleUniversalIdentifier` vers lidentifiant universel de ce rôle.
### Exemple Hello World

View file

@ -219,7 +219,7 @@ L'SDK fornisce quattro funzioni helper con convalida integrata per definire le e
| Funzione | Scopo |
| ------------------ | ------------------------------------------------------- |
| `defineApp()` | Configura i metadati dell'applicazione |
| `defineApplication()` | Configura i metadati dell'applicazione |
| `defineObject()` | Definisci oggetti personalizzati con campi |
| `defineFunction()` | Definisci funzioni logiche con handler |
| `defineRole()` | Configura i permessi dei ruoli e l'accesso agli oggetti |
@ -319,14 +319,14 @@ Ogni app ha un singolo file `application.config.ts` che descrive:
* **Come vengono eseguite le sue funzioni**: quale ruolo usano per i permessi.
* **Variabili (opzionali)**: coppie chiavevalore esposte alle funzioni come variabili d'ambiente.
Usa `defineApp()` per definire la configurazione della tua applicazione:
Usa `defineApplication()` per definire la configurazione della tua applicazione:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ Note:
* I campi `universalIdentifier` sono ID deterministici sotto il tuo controllo; generali una volta e mantienili stabili tra le sincronizzazioni.
* `applicationVariables` diventano variabili d'ambiente per le tue funzioni (ad esempio, `DEFAULT_RECIPIENT_NAME` è disponibile come `process.env.DEFAULT_RECIPIENT_NAME`).
* `functionRoleUniversalIdentifier` deve corrispondere al ruolo che definisci nel tuo file `*.role.ts` (vedi sotto).
* `defaultRoleUniversalIdentifier` deve corrispondere al ruolo che definisci nel tuo file `*.role.ts` (vedi sotto).
#### Ruoli e permessi
Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro. Il campo `functionRoleUniversalIdentifier` in `application.config.ts` designa il ruolo predefinito utilizzato dalle funzioni logiche della tua app.
Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro. Il campo `defaultRoleUniversalIdentifier` in `application.config.ts` designa il ruolo predefinito utilizzato dalle funzioni logiche della tua app.
* La chiave API di runtime iniettata come `TWENTY_API_KEY` è derivata da questo ruolo funzione predefinito.
* Il client tipizzato sarà limitato ai permessi concessi a quel ruolo.
@ -365,11 +365,11 @@ Quando generi una nuova app con lo scaffolder, la CLI crea anche un file di ruol
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
L'`universalIdentifier` di questo ruolo viene quindi referenziato in `application.config.ts` come `functionRoleUniversalIdentifier`. In altre parole:
L'`universalIdentifier` di questo ruolo viene quindi referenziato in `application.config.ts` come `defaultRoleUniversalIdentifier`. In altre parole:
* **\*.role.ts** definisce ciò che il ruolo funzione predefinito può fare.
* **application.config.ts** punta a quel ruolo in modo che le tue funzioni ne ereditino i permessi.
@ -606,8 +606,8 @@ Quando la tua funzione viene eseguita su Twenty, la piattaforma inietta le crede
Note:
* Non è necessario passare URL o chiave API al client generato. Legge `TWENTY_API_URL` e `TWENTY_API_KEY` da process.env in fase di esecuzione.
* I permessi della chiave API sono determinati dal ruolo referenziato in `application.config.ts` tramite `functionRoleUniversalIdentifier`. Questo è il ruolo predefinito utilizzato dalle funzioni logiche della tua applicazione.
* Le applicazioni possono definire ruoli per seguire il principio del privilegio minimo. Concedi solo i permessi necessari alle tue funzioni, quindi punta `functionRoleUniversalIdentifier` all'identificatore universale di quel ruolo.
* I permessi della chiave API sono determinati dal ruolo referenziato in `application.config.ts` tramite `defaultRoleUniversalIdentifier`. Questo è il ruolo predefinito utilizzato dalle funzioni logiche della tua applicazione.
* Le applicazioni possono definire ruoli per seguire il principio del privilegio minimo. Concedi solo i permessi necessari alle tue funzioni, quindi punta `defaultRoleUniversalIdentifier` all'identificatore universale di quel ruolo.
### Esempio Hello World

View file

@ -217,7 +217,7 @@ twenty-sdk は、アプリ内で使用する型付きのビルディングブロ
| 関数 | 目的 |
| ------------------ | ------------------------------------ |
| `defineApp()` | アプリケーションのメタデータを構成 |
| `defineApplication()` | アプリケーションのメタデータを構成 |
| `defineObject()` | フィールド付きのカスタムオブジェクトを定義 |
| `defineFunction()` | Define logic functions with handlers |
| `defineRole()` | ロールの権限とオブジェクトアクセスを構成 |
@ -317,14 +317,14 @@ export default defineObject({
* **関数の実行方法**: 権限に使用するロール。
* **(任意)変数**: 関数に環境変数として公開されるキーと値のペア。
アプリケーション設定を定義するには `defineApp()` を使用します:
アプリケーション設定を定義するには `defineApplication()` を使用します:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -337,7 +337,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -345,11 +345,11 @@ export default defineApp({
* `universalIdentifier` フィールドは、あなたが管理する決定的な ID です。一度生成し、同期をまたいで安定したままにしてください。
* `applicationVariables` は関数の環境変数になります(例:`DEFAULT_RECIPIENT_NAME` は `process.env.DEFAULT_RECIPIENT_NAME` として利用可能)。
* `functionRoleUniversalIdentifier` は、`*.role.ts` ファイルで定義するロールと一致している必要があります(下記参照)。
* `defaultRoleUniversalIdentifier` は、`*.role.ts` ファイルで定義するロールと一致している必要があります(下記参照)。
#### ロールと権限
アプリケーションは、ワークスペース内のオブジェクトやアクションに対する権限をカプセル化するロールを定義できます。 The field `functionRoleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
アプリケーションは、ワークスペース内のオブジェクトやアクションに対する権限をカプセル化するロールを定義できます。 The field `defaultRoleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
* `TWENTY_API_KEY` として注入される実行時の API キーは、このデフォルトの関数ロールから派生します。
* 型付きクライアントの権限は、そのロールに付与された権限に制限されます。
@ -363,11 +363,11 @@ export default defineApp({
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -380,7 +380,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -389,8 +389,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -399,7 +399,7 @@ export default defineRole({
});
```
このロールの `universalIdentifier` は、`application.config.ts` で `functionRoleUniversalIdentifier` として参照されます。 言い換えると:
このロールの `universalIdentifier` は、`application.config.ts` で `defaultRoleUniversalIdentifier` として参照されます。 言い換えると:
* **\*.role.ts** は、デフォルトの関数ロールで可能な操作を定義します。
* **application.config.ts** でそのロールを指定することで、関数はその権限を継承します。
@ -604,8 +604,8 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
ノート:
* 生成されたクライアントに URL や API キーを渡す必要はありません。 実行時に process.env から `TWENTY_API_URL` と `TWENTY_API_KEY` を読み取ります。
* API キーの権限は、`application.config.ts` で `functionRoleUniversalIdentifier` によって参照されるロールによって決まります。 This is the default role used by logic functions of your application.
* アプリケーションは、最小権限の原則に従うロールを定義できます。 関数に必要な権限のみを付与し、`functionRoleUniversalIdentifier` をそのロールのユニバーサル識別子に指定してください。
* API キーの権限は、`application.config.ts` で `defaultRoleUniversalIdentifier` によって参照されるロールによって決まります。 This is the default role used by logic functions of your application.
* アプリケーションは、最小権限の原則に従うロールを定義できます。 関数に必要な権限のみを付与し、`defaultRoleUniversalIdentifier` をそのロールのユニバーサル識別子に指定してください。
### Hello World の例

View file

@ -219,7 +219,7 @@ SDK는 앱 엔티티를 정의할 때 사용할 수 있는, 내장 검증이 포
| 함수 | 목적 |
| ------------------ | ------------------- |
| `defineApp()` | 애플리케이션 메타데이터 구성 |
| `defineApplication()` | 애플리케이션 메타데이터 구성 |
| `defineObject()` | 필드가 있는 사용자 정의 객체 정의 |
| `defineFunction()` | 핸들러가 있는 로직 함수 정의 |
| `defineRole()` | 역할 권한과 객체 접근 구성 |
@ -319,14 +319,14 @@ export default defineObject({
* **함수가 실행되는 방식**: 권한을 위해 사용하는 역할.
* **(선택 사항) 변수**: 함수에 환경 변수로 노출되는 키–값 쌍.
`defineApp()`을 사용해 애플리케이션 구성을 정의하세요:
`defineApplication()`을 사용해 애플리케이션 구성을 정의하세요:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ export default defineApp({
* `universalIdentifier` 필드는 고유하고 결정적인 ID입니다. 한 번 생성한 후 동기화 전반에 걸쳐 안정적으로 유지하세요.
* `applicationVariables`는 함수의 환경 변수가 됩니다(예: `DEFAULT_RECIPIENT_NAME`는 `process.env.DEFAULT_RECIPIENT_NAME`로 사용 가능).
* `functionRoleUniversalIdentifier`는 `*.role.ts` 파일에서 정의한 역할과 일치해야 합니다(아래 참조).
* `defaultRoleUniversalIdentifier`는 `*.role.ts` 파일에서 정의한 역할과 일치해야 합니다(아래 참조).
#### 역할 및 권한
애플리케이션은 워크스페이스의 객체와 작업에 대한 권한을 캡슐화하는 역할을 정의할 수 있습니다. `application.config.ts`의 `functionRoleUniversalIdentifier` 필드는 앱의 로직 함수가 사용하는 기본 역할을 지정합니다.
애플리케이션은 워크스페이스의 객체와 작업에 대한 권한을 캡슐화하는 역할을 정의할 수 있습니다. `application.config.ts`의 `defaultRoleUniversalIdentifier` 필드는 앱의 로직 함수가 사용하는 기본 역할을 지정합니다.
* `TWENTY_API_KEY`로 주입되는 런타임 API 키는 이 기본 함수 역할에서 파생됩니다.
* 타입드 클라이언트는 해당 역할에 부여된 권한으로 제한됩니다.
@ -365,11 +365,11 @@ export default defineApp({
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
이 역할의 `universalIdentifier`는 `application.config.ts`에서 `functionRoleUniversalIdentifier`로 참조됩니다. 다시 말해:
이 역할의 `universalIdentifier`는 `application.config.ts`에서 `defaultRoleUniversalIdentifier`로 참조됩니다. 다시 말해:
* **\*.role.ts**는 기본 함수 역할이 수행할 수 있는 작업을 정의합니다.
* **application.config.ts**는 해당 역할을 가리키므로, 함수는 그 권한을 상속받습니다.
@ -606,8 +606,8 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
노트:
* 생성된 클라이언트에 URL이나 API 키를 전달할 필요가 없습니다. 런타임에 process.env에서 `TWENTY_API_URL`과 `TWENTY_API_KEY`를 읽습니다.
* API 키의 권한은 `application.config.ts`에서 `functionRoleUniversalIdentifier`를 통해 참조된 역할에 의해 결정됩니다. 이는 애플리케이션의 로직 함수에서 사용하는 기본 역할입니다.
* 애플리케이션은 최소 권한 원칙을 따르도록 역할을 정의할 수 있습니다. 함수에 필요한 권한만 부여하고, `functionRoleUniversalIdentifier`를 해당 역할의 universal identifier로 지정하세요.
* API 키의 권한은 `application.config.ts`에서 `defaultRoleUniversalIdentifier`를 통해 참조된 역할에 의해 결정됩니다. 이는 애플리케이션의 로직 함수에서 사용하는 기본 역할입니다.
* 애플리케이션은 최소 권한 원칙을 따르도록 역할을 정의할 수 있습니다. 함수에 필요한 권한만 부여하고, `defaultRoleUniversalIdentifier`를 해당 역할의 universal identifier로 지정하세요.
### Hello World 예제

View file

@ -219,7 +219,7 @@ O SDK fornece quatro funções utilitárias com validação integrada para defin
| Função | Finalidade |
| ------------------ | ------------------------------------------------- |
| `defineApp()` | Configura os metadados do aplicativo |
| `defineApplication()` | Configura os metadados do aplicativo |
| `defineObject()` | Define objetos personalizados com campos |
| `defineFunction()` | Defina funções de lógica com handlers |
| `defineRole()` | Configura permissões de papéis e acesso a objetos |
@ -319,14 +319,14 @@ Todo aplicativo tem um único arquivo `application.config.ts` que descreve:
* **Como suas funções são executadas**: qual papel usam para permissões.
* **Variáveis (opcional)**: pares chavevalor expostos às suas funções como variáveis de ambiente.
Use `defineApp()` para definir a configuração do seu aplicativo:
Use `defineApplication()` para definir a configuração do seu aplicativo:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ Notas:
* `universalIdentifier` são IDs determinísticos que você controla; gere-os uma vez e mantenha-os estáveis entre sincronizações.
* `applicationVariables` tornam-se variáveis de ambiente para suas funções (por exemplo, `DEFAULT_RECIPIENT_NAME` fica disponível como `process.env.DEFAULT_RECIPIENT_NAME`).
* `functionRoleUniversalIdentifier` deve corresponder ao papel que você define no seu arquivo `*.role.ts` (veja abaixo).
* `defaultRoleUniversalIdentifier` deve corresponder ao papel que você define no seu arquivo `*.role.ts` (veja abaixo).
#### Papéis e permissões
Os aplicativos podem definir papéis que encapsulam permissões sobre os objetos e ações do seu espaço de trabalho. O campo `functionRoleUniversalIdentifier` em `application.config.ts` designa o papel padrão usado pelas funções de lógica do seu app.
Os aplicativos podem definir papéis que encapsulam permissões sobre os objetos e ações do seu espaço de trabalho. O campo `defaultRoleUniversalIdentifier` em `application.config.ts` designa o papel padrão usado pelas funções de lógica do seu app.
* A chave de API em tempo de execução, injetada como `TWENTY_API_KEY`, é derivada desse papel padrão de função.
* O cliente tipado ficará restrito às permissões concedidas a esse papel.
@ -365,11 +365,11 @@ Ao criar um novo aplicativo com o scaffold, a CLI também cria um arquivo de pap
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
O `universalIdentifier` desse papel é então referenciado em `application.config.ts` como `functionRoleUniversalIdentifier`. Em outras palavras:
O `universalIdentifier` desse papel é então referenciado em `application.config.ts` como `defaultRoleUniversalIdentifier`. Em outras palavras:
* **\*.role.ts** define o que o papel de função padrão pode fazer.
* **application.config.ts** aponta para esse papel para que suas funções herdem suas permissões.
@ -606,8 +606,8 @@ Quando sua função é executada no Twenty, a plataforma injeta credenciais como
Notas:
* Você não precisa passar a URL ou a chave de API para o cliente gerado. Ele lê `TWENTY_API_URL` e `TWENTY_API_KEY` de process.env em tempo de execução.
* As permissões da chave de API são determinadas pelo papel referenciado no seu `application.config.ts` via `functionRoleUniversalIdentifier`. Este é o papel padrão usado pelas funções de lógica do seu app.
* Os aplicativos podem definir papéis para seguir o princípio do menor privilégio. Conceda apenas as permissões de que suas funções precisam e, em seguida, aponte `functionRoleUniversalIdentifier` para o identificador universal desse papel.
* As permissões da chave de API são determinadas pelo papel referenciado no seu `application.config.ts` via `defaultRoleUniversalIdentifier`. Este é o papel padrão usado pelas funções de lógica do seu app.
* Os aplicativos podem definir papéis para seguir o princípio do menor privilégio. Conceda apenas as permissões de que suas funções precisam e, em seguida, aponte `defaultRoleUniversalIdentifier` para o identificador universal desse papel.
### Exemplo Hello World

View file

@ -219,7 +219,7 @@ SDK-ul oferă patru funcții ajutătoare cu validare încorporată pentru defini
| Funcție | Scop |
| ------------------ | -------------------------------------------------------- |
| `defineApp()` | Configurați metadatele aplicației |
| `defineApplication()` | Configurați metadatele aplicației |
| `defineObject()` | Definiți obiecte personalizate cu câmpuri |
| `defineFunction()` | Definiți funcții de logică cu handleri |
| `defineRole()` | Configurați permisiunile rolurilor și accesul la obiecte |
@ -319,14 +319,14 @@ Fiecare aplicație are un singur fișier `application.config.ts` care descrie:
* **Cum rulează funcțiile**: ce rol folosesc pentru permisiuni.
* **(Opțional) variabile**: perechi cheievaloare expuse funcțiilor ca variabile de mediu.
Folosiți `defineApp()` pentru a defini configurația aplicației:
Folosiți `defineApplication()` pentru a defini configurația aplicației:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ Notițe:
* Câmpurile `universalIdentifier` sunt ID-uri deterministe pe care le dețineți; generați-le o singură dată și păstrați-le stabile între sincronizări.
* `applicationVariables` devin variabile de mediu pentru funcțiile dvs. (de exemplu, `DEFAULT_RECIPIENT_NAME` este disponibil ca `process.env.DEFAULT_RECIPIENT_NAME`).
* `functionRoleUniversalIdentifier` trebuie să corespundă rolului pe care îl definiți în fișierul `*.role.ts` (vedeți mai jos).
* `defaultRoleUniversalIdentifier` trebuie să corespundă rolului pe care îl definiți în fișierul `*.role.ts` (vedeți mai jos).
#### Roluri și permisiuni
Aplicațiile pot defini roluri care încapsulează permisiuni asupra obiectelor și acțiunilor din spațiul dvs. de lucru. Câmpul `functionRoleUniversalIdentifier` din `application.config.ts` desemnează rolul implicit folosit de funcțiile de logică ale aplicației.
Aplicațiile pot defini roluri care încapsulează permisiuni asupra obiectelor și acțiunilor din spațiul dvs. de lucru. Câmpul `defaultRoleUniversalIdentifier` din `application.config.ts` desemnează rolul implicit folosit de funcțiile de logică ale aplicației.
* Cheia API de runtime injectată ca `TWENTY_API_KEY` este derivată din acest rol implicit pentru funcții.
* Clientul tipizat va fi restricționat la permisiunile acordate acelui rol.
@ -365,11 +365,11 @@ Când generați o aplicație nouă, CLI creează și un fișier de rol implicit.
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
`universalIdentifier` al acestui rol este apoi referențiat în `application.config.ts` ca `functionRoleUniversalIdentifier`. Cu alte cuvinte:
`universalIdentifier` al acestui rol este apoi referențiat în `application.config.ts` ca `defaultRoleUniversalIdentifier`. Cu alte cuvinte:
* **\*.role.ts** definește ce poate face rolul implicit pentru funcții.
* **application.config.ts** indică acel rol, astfel încât funcțiile moștenesc permisiunile lui.
@ -606,8 +606,8 @@ Când funcția rulează pe Twenty, platforma injectează acreditări ca variabil
Notițe:
* Nu trebuie să transmiteți URL-ul sau cheia API către clientul generat. Acesta citește `TWENTY_API_URL` și `TWENTY_API_KEY` din process.env la runtime.
* Permisiunile cheii API sunt determinate de rolul referențiat în `application.config.ts` prin `functionRoleUniversalIdentifier`. Acesta este rolul implicit folosit de funcțiile de logică ale aplicației.
* Aplicațiile pot defini roluri pentru a urma principiul celui mai mic privilegiu. Acordați doar permisiunile de care au nevoie funcțiile, apoi setați `functionRoleUniversalIdentifier` la identificatorul universal al acelui rol.
* Permisiunile cheii API sunt determinate de rolul referențiat în `application.config.ts` prin `defaultRoleUniversalIdentifier`. Acesta este rolul implicit folosit de funcțiile de logică ale aplicației.
* Aplicațiile pot defini roluri pentru a urma principiul celui mai mic privilegiu. Acordați doar permisiunile de care au nevoie funcțiile, apoi setați `defaultRoleUniversalIdentifier` la identificatorul universal al acelui rol.
### Exemplu Hello World

View file

@ -219,7 +219,7 @@ SDK предоставляет четыре вспомогательных фу
| Функция | Назначение |
| ------------------ | ---------------------------------------------- |
| `defineApp()` | Настраивает метаданные приложения |
| `defineApplication()` | Настраивает метаданные приложения |
| `defineObject()` | Определяет пользовательские объекты с полями |
| `defineFunction()` | Определение логических функций с обработчиками |
| `defineRole()` | Настраивает права роли и доступ к объектам |
@ -319,14 +319,14 @@ export default defineObject({
* **Как запускаются его функции**: какую роль они используют для прав доступа.
* **(Необязательно) переменные**: пары ключ-значение, предоставляемые вашим функциям как переменные окружения.
Используйте `defineApp()` для определения конфигурации вашего приложения:
Используйте `defineApplication()` для определения конфигурации вашего приложения:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ export default defineApp({
* `universalIdentifier` — это детерминированные идентификаторы, которыми вы управляете; сгенерируйте их один раз и сохраняйте стабильными между синхронизациями.
* `applicationVariables` становятся переменными окружения для ваших функций (например, `DEFAULT_RECIPIENT_NAME` доступна как `process.env.DEFAULT_RECIPIENT_NAME`).
* `functionRoleUniversalIdentifier` должен соответствовать роли, которую вы определяете в файле `*.role.ts` (см. ниже).
* `defaultRoleUniversalIdentifier` должен соответствовать роли, которую вы определяете в файле `*.role.ts` (см. ниже).
#### Роли и разрешения
Приложения могут определять роли, инкапсулирующие права на объекты и действия в вашем рабочем пространстве. Поле `functionRoleUniversalIdentifier` в `application.config.ts` обозначает роль по умолчанию, используемую логическими функциями вашего приложения.
Приложения могут определять роли, инкапсулирующие права на объекты и действия в вашем рабочем пространстве. Поле `defaultRoleUniversalIdentifier` в `application.config.ts` обозначает роль по умолчанию, используемую логическими функциями вашего приложения.
* Ключ API во время выполнения, подставляемый как `TWENTY_API_KEY`, получается из этой роли функции по умолчанию.
* Типизированный клиент будет ограничен правами, предоставленными этой ролью.
@ -365,11 +365,11 @@ export default defineApp({
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
`universalIdentifier` этой роли затем указывается в `application.config.ts` как `functionRoleUniversalIdentifier`. Иными словами:
`universalIdentifier` этой роли затем указывается в `application.config.ts` как `defaultRoleUniversalIdentifier`. Иными словами:
* **\*.role.ts** определяет, что может делать роль функции по умолчанию.
* **application.config.ts** указывает на эту роль, чтобы ваши функции наследовали её права.
@ -606,8 +606,8 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
Заметки:
* Вам не нужно передавать URL или ключ API сгенерированному клиенту. Он читает `TWENTY_API_URL` и `TWENTY_API_KEY` из process.env во время выполнения.
* Права ключа API определяются ролью, на которую ссылается ваш `application.config.ts` через `functionRoleUniversalIdentifier`. Это роль по умолчанию, используемая логическими функциями вашего приложения.
* Приложения могут определять роли, чтобы следовать принципу наименьших привилегий. Выдавайте только те права, которые нужны вашим функциям, затем укажите в `functionRoleUniversalIdentifier` универсальный идентификатор этой роли.
* Права ключа API определяются ролью, на которую ссылается ваш `application.config.ts` через `defaultRoleUniversalIdentifier`. Это роль по умолчанию, используемая логическими функциями вашего приложения.
* Приложения могут определять роли, чтобы следовать принципу наименьших привилегий. Выдавайте только те права, которые нужны вашим функциям, затем укажите в `defaultRoleUniversalIdentifier` универсальный идентификатор этой роли.
### Пример Hello World

View file

@ -219,7 +219,7 @@ SDK, uygulama varlıklarınızı tanımlamak için yerleşik doğrulamaya sahip
| Fonksiyon | Amaç |
| ------------------ | ---------------------------------------------- |
| `defineApp()` | Uygulama meta verilerini yapılandırın |
| `defineApplication()` | Uygulama meta verilerini yapılandırın |
| `defineObject()` | Alanlara sahip özel nesneler tanımlayın |
| `defineFunction()` | İşleyicilerle mantık fonksiyonları tanımlayın |
| `defineRole()` | Rol izinlerini ve nesne erişimini yapılandırın |
@ -319,14 +319,14 @@ Her uygulamanın aşağıdakileri açıklayan tek bir `application.config.ts` do
* **Fonksiyonlarının nasıl çalıştığı**: izinler için hangi rolü kullandıkları.
* **(İsteğe bağlı) değişkenler**: fonksiyonlarınıza ortam değişkenleri olarak sunulan anahtardeğer çiftleri.
Uygulama yapılandırmanızı tanımlamak için `defineApp()` kullanın:
Uygulama yapılandırmanızı tanımlamak için `defineApplication()` kullanın:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ Notlar:
* `universalIdentifier` alanları size ait belirleyici kimliklerdir; bunları bir kez oluşturun ve eşitlemeler boyunca kararlı tutun.
* `applicationVariables`, fonksiyonlarınız için ortam değişkenlerine dönüşür (örneğin, `DEFAULT_RECIPIENT_NAME` değeri `process.env.DEFAULT_RECIPIENT_NAME` olarak kullanılabilir).
* `functionRoleUniversalIdentifier`, `*.role.ts` dosyanızda tanımladığınız rolle eşleşmelidir (aşağıya bakın).
* `defaultRoleUniversalIdentifier`, `*.role.ts` dosyanızda tanımladığınız rolle eşleşmelidir (aşağıya bakın).
#### Roller ve izinler
Uygulamalar, çalışma alanınızdaki nesneler ve eylemler üzerindeki izinleri kapsülleyen roller tanımlayabilir. `application.config.ts` içindeki `functionRoleUniversalIdentifier` alanı, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan rolü belirtir.
Uygulamalar, çalışma alanınızdaki nesneler ve eylemler üzerindeki izinleri kapsülleyen roller tanımlayabilir. `application.config.ts` içindeki `defaultRoleUniversalIdentifier` alanı, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan rolü belirtir.
* `TWENTY_API_KEY` olarak enjekte edilen çalışma zamanı API anahtarı bu varsayılan fonksiyon rolünden türetilir.
* Türlendirilmiş istemci, o role tanınan izinlerle sınırlandırılır.
@ -365,11 +365,11 @@ Yeni bir uygulama oluşturduğunuzda CLI ayrıca varsayılan bir rol dosyası da
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
Bu rolün `universalIdentifier` değeri, `application.config.ts` içinde `functionRoleUniversalIdentifier` olarak referans verilir. Başka bir deyişle:
Bu rolün `universalIdentifier` değeri, `application.config.ts` içinde `defaultRoleUniversalIdentifier` olarak referans verilir. Başka bir deyişle:
* **\*.role.ts**, varsayılan fonksiyon rolünün neler yapabileceğini tanımlar.
* **application.config.ts** bu role işaret eder, böylece fonksiyonlarınız onun izinlerini devralır.
@ -606,8 +606,8 @@ Fonksiyonunuz Twenty üzerinde çalıştığında, platform kodunuz yürütülme
Notlar:
* Oluşturulan istemciye URL veya API anahtarı geçirmeniz gerekmez. Çalışma zamanında `TWENTY_API_URL` ve `TWENTY_API_KEY` değerlerini process.env üzerinden okur.
* API anahtarının izinleri, `application.config.ts` içinde `functionRoleUniversalIdentifier` aracılığıyla referans verilen role göre belirlenir. Bu, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan roldür.
* Uygulamalar, en az ayrıcalık ilkesini izlemek için roller tanımlayabilir. Yalnızca fonksiyonlarınızın ihtiyaç duyduğu izinleri verin ve ardından `functionRoleUniversalIdentifier` değerini o rolün evrensel tanımlayıcısına yönlendirin.
* API anahtarının izinleri, `application.config.ts` içinde `defaultRoleUniversalIdentifier` aracılığıyla referans verilen role göre belirlenir. Bu, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan roldür.
* Uygulamalar, en az ayrıcalık ilkesini izlemek için roller tanımlayabilir. Yalnızca fonksiyonlarınızın ihtiyaç duyduğu izinleri verin ve ardından `defaultRoleUniversalIdentifier` değerini o rolün evrensel tanımlayıcısına yönlendirin.
### Hello World örneği

View file

@ -219,7 +219,7 @@ twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以
| 函数 | 目的 |
| ------------------ | ------------ |
| `defineApp()` | 配置应用元数据 |
| `defineApplication()` | 配置应用元数据 |
| `defineObject()` | 定义带字段的自定义对象 |
| `defineFunction()` | 定义带处理程序的逻辑函数 |
| `defineRole()` | 配置角色权限和对象访问 |
@ -319,14 +319,14 @@ export default defineObject({
* **函数如何运行**:它们用于权限的角色。
* **(可选)变量**:以环境变量形式提供给函数的键值对。
使用 `defineApp()` 定义你的应用配置:
使用 `defineApplication()` 定义你的应用配置:
```typescript
// src/app/application.config.ts
import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
@ -339,7 +339,7 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
@ -347,11 +347,11 @@ export default defineApp({
* `universalIdentifier` 字段是你拥有的确定性 ID生成一次并在多次同步中保持稳定。
* `applicationVariables` 会变成函数可用的环境变量(例如,`DEFAULT_RECIPIENT_NAME` 可作为 `process.env.DEFAULT_RECIPIENT_NAME` 使用)。
* `functionRoleUniversalIdentifier` 必须与在 `*.role.ts` 文件中定义的角色一致(见下文)。
* `roleUniversalIdentifier` 必须与在 `*.role.ts` 文件中定义的角色一致(见下文)。
#### 角色和权限
应用可以定义角色,以封装对工作空间对象与操作的权限。 `application.config.ts` 中的 `functionRoleUniversalIdentifier` 字段指定你的应用逻辑函数所使用的默认角色。
应用可以定义角色,以封装对工作空间对象与操作的权限。 `application.config.ts` 中的 `roleUniversalIdentifier` 字段指定你的应用逻辑函数所使用的默认角色。
* 作为 `TWENTY_API_KEY` 注入的运行时 API 密钥源自该默认函数角色。
* 类型化客户端将受限于该角色授予的权限。
@ -365,11 +365,11 @@ export default defineApp({
// src/app/default-function.role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -382,7 +382,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -391,8 +391,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@ -401,7 +401,7 @@ export default defineRole({
});
```
随后,该角色的 `universalIdentifier` 会在 `application.config.ts` 中以 `functionRoleUniversalIdentifier` 引用。 换句话说:
随后,该角色的 `universalIdentifier` 会在 `application.config.ts` 中以 `roleUniversalIdentifier` 引用。 换句话说:
* **\*.role.ts** 定义默认函数角色可以执行的操作。
* **application.config.ts** 指向该角色,使你的函数继承其权限。
@ -606,8 +606,8 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
备注:
* 你无需向生成的客户端传递 URL 或 API 密钥。 它会在运行时从 process.env 读取 `TWENTY_API_URL` 和 `TWENTY_API_KEY`。
* API 密钥的权限由 `application.config.ts` 中通过 `functionRoleUniversalIdentifier` 引用的角色决定。 这是你的应用逻辑函数使用的默认角色。
* 应用可以定义角色以遵循最小权限原则。 仅授予函数所需的权限,然后将 `functionRoleUniversalIdentifier` 指向该角色的通用标识符。
* API 密钥的权限由 `application.config.ts` 中通过 `roleUniversalIdentifier` 引用的角色决定。 这是你的应用逻辑函数使用的默认角色。
* 应用可以定义角色以遵循最小权限原则。 仅授予函数所需的权限,然后将 `roleUniversalIdentifier` 指向该角色的通用标识符。
### Hello World 示例

View file

@ -271,7 +271,7 @@ export type Application = {
applicationVariables: Array<ApplicationVariable>;
canBeUninstalled: Scalars['Boolean'];
defaultLogicFunctionRole?: Maybe<Role>;
defaultLogicFunctionRoleId?: Maybe<Scalars['String']>;
defaultRoleId?: Maybe<Scalars['String']>;
description: Scalars['String'];
id: Scalars['UUID'];
logicFunctions: Array<LogicFunction>;

View file

@ -271,7 +271,7 @@ export type Application = {
applicationVariables: Array<ApplicationVariable>;
canBeUninstalled: Scalars['Boolean'];
defaultLogicFunctionRole?: Maybe<Role>;
defaultLogicFunctionRoleId?: Maybe<Scalars['String']>;
defaultRoleId?: Maybe<Scalars['String']>;
description: Scalars['String'];
id: Scalars['UUID'];
logicFunctions: Array<LogicFunction>;

View file

@ -33,7 +33,7 @@ const EXCLUDED_DIRECTORIES = [
'**/internal/**',
'**/cli/**',
] as const;
const ROOT_DIRECTORIES = ['application'];
const ROOT_DIRECTORIES = ['sdk'];
const prettierConfigFile = prettier.resolveConfigFile();
if (prettierConfigFile == null) {

View file

@ -1,63 +0,0 @@
import { defineApp } from '@/application';
describe('defineApp', () => {
it('should return the config when valid', () => {
const config = {
universalIdentifier: 'a9faf5f8-cf7e-4f24-9d37-fd523c30febe',
displayName: 'My App',
description: 'My app description',
icon: 'IconWorld',
functionRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2',
};
const result = defineApp(config);
expect(result).toEqual(config);
});
it('should pass through all optional fields', () => {
const config = {
universalIdentifier: 'a9faf5f8-cf7e-4f24-9d37-fd523c30febe',
displayName: 'My App',
description: 'My app description',
icon: 'IconWorld',
applicationVariables: {
API_KEY: {
universalIdentifier: '3a327392-3a0f-4605-9223-0633f063eaf6',
description: 'API Key',
isSecret: true,
},
},
functionRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2',
};
const result = defineApp(config);
expect(result).toEqual(config);
expect(result.applicationVariables).toBeDefined();
expect(result.functionRoleUniversalIdentifier).toBe(
'68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2',
);
});
it('should throw error when universalIdentifier is missing', () => {
const config = {
displayName: 'My App',
};
expect(() => defineApp(config as any)).toThrow(
'App must have a universalIdentifier',
);
});
it('should throw error when universalIdentifier is empty string', () => {
const config = {
universalIdentifier: '',
displayName: 'My App',
};
expect(() => defineApp(config as any)).toThrow(
'App must have a universalIdentifier',
);
});
});

View file

@ -1,74 +0,0 @@
import { defineFrontComponent } from '@/application';
// Mock component for testing
const MockComponent = () => null;
describe('defineFrontComponent', () => {
const validConfig = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'My Component',
component: MockComponent,
};
it('should return the config when valid', () => {
const result = defineFrontComponent(validConfig);
expect(result).toEqual(validConfig);
});
it('should pass through optional fields', () => {
const config = {
...validConfig,
description: 'A sample front component',
};
const result = defineFrontComponent(config);
expect(result.description).toBe('A sample front component');
});
it('should throw error when universalIdentifier is missing', () => {
const config = {
name: 'My Component',
component: MockComponent,
};
expect(() => defineFrontComponent(config as any)).toThrow(
'FrontComponent must have a universalIdentifier',
);
});
it('should throw error when component is missing', () => {
const config = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'My Component',
};
expect(() => defineFrontComponent(config as any)).toThrow(
'FrontComponent must have a component',
);
});
it('should throw error when component is not a function', () => {
const config = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'My Component',
component: 'not-a-function',
};
expect(() => defineFrontComponent(config as any)).toThrow(
'FrontComponent must have a component',
);
});
it('should accept config without name', () => {
const config = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
component: MockComponent,
};
const result = defineFrontComponent(config as any);
expect(result.name).toBeUndefined();
});
});

View file

@ -1,181 +0,0 @@
import { defineRole } from '@/application';
describe('defineRole', () => {
const validConfig = {
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'App User',
description: 'Standard user role',
};
it('should return the config when valid', () => {
const result = defineRole(validConfig);
expect(result).toEqual(validConfig);
});
it('should pass through all optional fields', () => {
const config = {
...validConfig,
icon: 'IconUser',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
};
const result = defineRole(config);
expect(result.icon).toBe('IconUser');
expect(result.canReadAllObjectRecords).toBe(true);
});
it('should accept objectPermissions with objectNameSingular', () => {
const config = {
...validConfig,
objectPermissions: [
{
objectNameSingular: 'postCard',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
};
const result = defineRole(config);
expect(result.objectPermissions).toHaveLength(1);
expect(result.objectPermissions![0].objectNameSingular).toBe('postCard');
});
it('should accept objectPermissions with objectUniversalIdentifier', () => {
const config = {
...validConfig,
objectPermissions: [
{
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
canReadObjectRecords: true,
},
],
};
const result = defineRole(config);
expect(result.objectPermissions![0].objectUniversalIdentifier).toBe(
'54b589ca-eeed-4950-a176-358418b85c05',
);
});
it('should accept fieldPermissions with fieldName', () => {
const config = {
...validConfig,
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
canReadFieldValue: true,
canUpdateFieldValue: false,
},
],
};
const result = defineRole(config);
expect(result.fieldPermissions).toHaveLength(1);
expect(result.fieldPermissions![0].fieldName).toBe('content');
});
it('should accept fieldPermissions with fieldUniversalIdentifier', () => {
const config = {
...validConfig,
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldUniversalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
canReadFieldValue: true,
},
],
};
const result = defineRole(config);
expect(result.fieldPermissions![0].fieldUniversalIdentifier).toBe(
'58a0a314-d7ea-4865-9850-7fb84e72f30b',
);
});
it('should accept permissionFlags', () => {
const config = {
...validConfig,
permissionFlags: ['UPLOAD_FILE', 'DOWNLOAD_FILE'],
};
const result = defineRole(config as any);
expect(result.permissionFlags).toHaveLength(2);
});
it('should throw error when universalIdentifier is missing', () => {
const config = {
label: 'App User',
};
expect(() => defineRole(config as any)).toThrow(
'Role must have a universalIdentifier',
);
});
it('should throw error when label is missing', () => {
const config = {
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
};
expect(() => defineRole(config as any)).toThrow('Role must have a label');
});
it('should throw error when objectPermission has neither objectNameSingular nor objectUniversalIdentifier', () => {
const config = {
...validConfig,
objectPermissions: [
{
canReadObjectRecords: true,
},
],
};
expect(() => defineRole(config as any)).toThrow(
'Object permission must have either objectNameSingular or objectUniversalIdentifier',
);
});
it('should throw error when fieldPermission has neither objectNameSingular nor objectUniversalIdentifier', () => {
const config = {
...validConfig,
fieldPermissions: [
{
fieldName: 'content',
canReadFieldValue: true,
},
],
};
expect(() => defineRole(config as any)).toThrow(
'Field permission must have either objectNameSingular or objectUniversalIdentifier',
);
});
it('should throw error when fieldPermission has neither fieldName nor fieldUniversalIdentifier', () => {
const config = {
...validConfig,
fieldPermissions: [
{
objectNameSingular: 'postCard',
canReadFieldValue: true,
},
],
};
expect(() => defineRole(config as any)).toThrow(
'Field permission must have either fieldName or fieldUniversalIdentifier',
);
});
});

View file

@ -1,303 +0,0 @@
import { extendObject } from '@/application/objects/extend-object';
import { FieldMetadataType } from 'twenty-shared/types';
import { type ObjectExtensionManifest } from 'twenty-shared/application';
describe('extendObject', () => {
const validConfigByName: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
type: FieldMetadataType.NUMBER,
name: 'healthScore',
label: 'Health Score',
},
],
};
const validConfigByUniversalId: ObjectExtensionManifest = {
targetObject: {
universalIdentifier: '20202020-b374-4779-a561-80086cb2e17f',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440002',
type: FieldMetadataType.TEXT,
name: 'customNote',
label: 'Custom Note',
},
],
};
describe('valid configurations', () => {
it('should return the config when targeting by nameSingular', () => {
const result = extendObject(validConfigByName);
expect(result).toEqual(validConfigByName);
});
it('should return the config when targeting by universalIdentifier', () => {
const result = extendObject(validConfigByUniversalId);
expect(result).toEqual(validConfigByUniversalId);
});
it('should pass through optional field properties', () => {
const config: ObjectExtensionManifest = {
...validConfigByName,
fields: [
{
...validConfigByName.fields[0],
description: 'A health score from 0-100',
icon: 'IconHeart',
},
],
};
const result = extendObject(config);
expect(result.fields[0].description).toBe('A health score from 0-100');
expect(result.fields[0].icon).toBe('IconHeart');
});
it('should accept SELECT field with options', () => {
const config: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440003',
type: FieldMetadataType.SELECT,
name: 'churnRisk',
label: 'Churn Risk',
options: [
{ value: 'low', label: 'Low', color: 'green', position: 0 },
{
value: 'medium',
label: 'Medium',
color: 'yellow',
position: 1,
},
{ value: 'high', label: 'High', color: 'red', position: 2 },
],
},
],
};
const result = extendObject(config);
expect(result.fields[0].label).toBe('Churn Risk');
});
it('should accept multiple fields', () => {
const config: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'person',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440004',
type: FieldMetadataType.NUMBER,
name: 'score',
label: 'Score',
},
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440005',
type: FieldMetadataType.TEXT,
name: 'notes',
label: 'Notes',
},
],
};
const result = extendObject(config);
expect(result.fields).toHaveLength(2);
});
});
describe('targetObject validation', () => {
it('should throw error when targetObject is missing', () => {
const config = {
fields: validConfigByName.fields,
};
expect(() => extendObject(config as any)).toThrow(
'Object extension must have a targetObject',
);
});
it('should throw error when targetObject has neither nameSingular nor universalIdentifier', () => {
const config = {
targetObject: {},
fields: validConfigByName.fields,
};
expect(() => extendObject(config as any)).toThrow(
'targetObject must have either nameSingular or universalIdentifier',
);
});
it('should throw error when targetObject has both nameSingular and universalIdentifier', () => {
const config = {
targetObject: {
nameSingular: 'company',
universalIdentifier: '20202020-b374-4779-a561-80086cb2e17f',
},
fields: validConfigByName.fields,
};
expect(() => extendObject(config as any)).toThrow(
'targetObject cannot have both nameSingular and universalIdentifier',
);
});
});
describe('fields validation', () => {
it('should throw error when fields is missing', () => {
const config = {
targetObject: {
nameSingular: 'company',
},
};
expect(() => extendObject(config as any)).toThrow(
'Object extension must have at least one field',
);
});
it('should throw error when fields is empty', () => {
const config = {
targetObject: {
nameSingular: 'company',
},
fields: [],
};
expect(() => extendObject(config as any)).toThrow(
'Object extension must have at least one field',
);
});
it('should throw error when field is missing label', () => {
const config = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
type: FieldMetadataType.NUMBER,
name: 'healthScore',
},
],
};
expect(() => extendObject(config as any)).toThrow(
'Field must have a label',
);
});
it('should throw error when field is missing name', () => {
const config = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
type: FieldMetadataType.NUMBER,
label: 'Health Score',
},
],
};
expect(() => extendObject(config as any)).toThrow(
'Field "Health Score" must have a name',
);
});
it('should throw error when field is missing universalIdentifier', () => {
const config = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
type: FieldMetadataType.NUMBER,
name: 'healthScore',
label: 'Health Score',
},
],
};
expect(() => extendObject(config as any)).toThrow(
'Field "Health Score" must have a universalIdentifier',
);
});
it('should throw error when SELECT field has no options', () => {
const config = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
type: FieldMetadataType.SELECT,
name: 'status',
label: 'Status',
},
],
};
expect(() => extendObject(config as any)).toThrow(
'Field "Status" is a SELECT/MULTI_SELECT type and must have options',
);
});
it('should throw error when MULTI_SELECT field has no options', () => {
const config = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
type: FieldMetadataType.MULTI_SELECT,
name: 'tags',
label: 'Tags',
},
],
};
expect(() => extendObject(config as any)).toThrow(
'Field "Tags" is a SELECT/MULTI_SELECT type and must have options',
);
});
it('should throw error when SELECT field has empty options array', () => {
const config = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
type: FieldMetadataType.SELECT,
name: 'status',
label: 'Status',
options: [],
},
],
};
expect(() => extendObject(config as any)).toThrow(
'Field "Status" is a SELECT/MULTI_SELECT type and must have options',
);
});
});
});

View file

@ -1,3 +0,0 @@
import { type Application } from 'twenty-shared/application';
export type ApplicationConfig = Application;

View file

@ -1,25 +0,0 @@
import { type Application } from 'twenty-shared/application';
/**
* Define an application configuration with validation.
*
* @example
* ```typescript
* import { defineApp } from 'twenty-sdk';
* import { APP_ID } from '../src/constants';
*
* export default defineApp({
* universalIdentifier: APP_ID,
* displayName: 'My App',
* description: 'My app description',
* icon: 'IconWorld',
* });
* ```
*/
export const defineApp = <T extends Application>(config: T): T => {
if (!config.universalIdentifier) {
throw new Error('App must have a universalIdentifier');
}
return config;
};

View file

@ -1,8 +0,0 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { type FieldManifest } from 'twenty-shared/application';
export const Field = <T extends FieldMetadataType>(
_: FieldManifest<T>,
): PropertyDecorator => {
return () => {};
};

View file

@ -1,50 +0,0 @@
import { type SyncableEntityOptions } from '@/application/syncable-entity-options.type';
import {
type RelationOnDeleteAction,
type RelationType,
} from 'twenty-shared/types';
interface WorkspaceRelationMinimumBaseOptions<TClass> {
label: string;
description?: string;
icon?: string;
inverseSideTargetUniversalIdentifier: string;
inverseSideFieldKey?: keyof TClass;
onDelete?: RelationOnDeleteAction;
}
interface WorkspaceRegularRelationBaseOptions<TClass>
extends WorkspaceRelationMinimumBaseOptions<TClass> {
isMorphRelation?: false;
}
interface WorkspaceMorphRelationBaseOptions<TClass>
extends WorkspaceRelationMinimumBaseOptions<TClass> {
isMorphRelation: true;
morphId: string;
}
type WorkspaceRelationBaseOptions<TClass> =
| WorkspaceRegularRelationBaseOptions<TClass>
| WorkspaceMorphRelationBaseOptions<TClass>;
type WorkspaceOtherRelationOptions<TClass> =
WorkspaceRelationBaseOptions<TClass> & {
type: RelationType.ONE_TO_MANY;
};
type WorkspaceManyToOneRelationOptions<TClass extends object> =
WorkspaceRelationBaseOptions<TClass> & {
type: RelationType.MANY_TO_ONE;
inverseSideFieldKey: keyof TClass;
};
type RelationOptions<T extends object> = SyncableEntityOptions &
(WorkspaceOtherRelationOptions<T> | WorkspaceManyToOneRelationOptions<T>);
export const Relation = <T extends object>(
_: RelationOptions<T>,
): PropertyDecorator => {
return () => {};
};

View file

@ -1,34 +0,0 @@
import { type FrontComponentConfig } from '@/application/front-components/front-component-config';
/**
* Define a front component configuration with validation.
*
* @example
* ```typescript
* import { defineFrontComponent } from 'twenty-sdk';
*
* const MyComponent = () => {
* return <div>Hello World</div>;
* };
*
* export default defineFrontComponent({
* universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
* name: 'My Component',
* description: 'A sample front component',
* component: MyComponent,
* });
* ```
*/
export const defineFrontComponent = <T extends FrontComponentConfig>(
config: T,
): T => {
if (!config.universalIdentifier) {
throw new Error('FrontComponent must have a universalIdentifier');
}
if (typeof config.component !== 'function') {
throw new Error('FrontComponent must have a component');
}
return config;
};

View file

@ -1,78 +0,0 @@
import { type FunctionConfig } from '@/application/functions/function-config';
/**
* Define a logic function configuration with validation.
*
* @example
* ```typescript
* import { defineFunction } from 'twenty-sdk';
* import { sendPostcard } from '../src/handlers/send-postcard';
*
* export const config = defineFunction({
* universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
* name: 'Send Postcard',
* description: 'Send a postcard to a contact',
* timeoutSeconds: 30,
* handler: sendPostcard,
* triggers: [
* {
* universalIdentifier: 'c9f84c8d-...',
* type: 'route',
* path: '/postcards/send',
* httpMethod: 'POST',
* isAuthRequired: true,
* },
* ],
* });
* ```
*/
export const defineFunction = <T extends FunctionConfig>(config: T): T => {
if (!config.universalIdentifier) {
throw new Error('Function must have a universalIdentifier');
}
if (typeof config.handler !== 'function') {
throw new Error('Function must have a handler');
}
// Validate each trigger
for (const trigger of config.triggers ?? []) {
if (!trigger.universalIdentifier) {
throw new Error('Each trigger must have a universalIdentifier');
}
if (!trigger.type) {
throw new Error('Each trigger must have a type');
}
switch (trigger.type) {
case 'route':
if (!trigger.path) {
throw new Error('Route trigger must have a path');
}
if (!trigger.httpMethod) {
throw new Error('Route trigger must have an httpMethod');
}
break;
case 'cron':
if (!trigger.pattern) {
throw new Error('Cron trigger must have a pattern');
}
break;
case 'databaseEvent':
if (!trigger.eventName) {
throw new Error('Database event trigger must have an eventName');
}
break;
default:
throw new Error(
`Unknown trigger type: ${(trigger as { type: string }).type}`,
);
}
}
return config;
};

View file

@ -1,62 +0,0 @@
import { isNonEmptyString } from '@sniptt/guards';
import { type ObjectManifest } from 'twenty-shared/application';
import { validateFieldsOrThrow } from './validate-fields';
/**
* Define an object configuration with validation.
*
* @example
* ```typescript
* import { defineObject, FieldType } from 'twenty-sdk';
* import { POST_CARD_ID, STATUS_OPTIONS } from '../../src/constants';
*
* export default defineObject({
* universalIdentifier: POST_CARD_ID,
* nameSingular: 'postCard',
* namePlural: 'postCards',
* labelSingular: 'Post Card',
* labelPlural: 'Post Cards',
* icon: 'IconMail',
* fields: [
* {
* universalIdentifier: '...',
* name: 'content',
* type: FieldType.TEXT,
* label: 'Content',
* },
* {
* universalIdentifier: '...',
* name: 'status',
* type: FieldType.SELECT,
* label: 'Status',
* options: STATUS_OPTIONS,
* },
* ],
* });
* ```
*/
export const defineObject = <T extends ObjectManifest>(config: T): T => {
if (!isNonEmptyString(config.universalIdentifier)) {
throw new Error('Object must have a universalIdentifier');
}
if (!isNonEmptyString(config.nameSingular)) {
throw new Error('Object must have a nameSingular');
}
if (!isNonEmptyString(config.namePlural)) {
throw new Error('Object must have a namePlural');
}
if (!isNonEmptyString(config.labelSingular)) {
throw new Error('Object must have a labelSingular');
}
if (!isNonEmptyString(config.labelPlural)) {
throw new Error('Object must have a labelPlural');
}
validateFieldsOrThrow(config.fields);
return config;
};

View file

@ -1,79 +0,0 @@
import { type ObjectExtensionManifest } from 'twenty-shared/application';
import { isNonEmptyString } from '@sniptt/guards';
import { validateFieldsOrThrow } from './validate-fields';
/**
* Extend an existing object with additional fields.
*
* @example
* ```typescript
* import { extendObject, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk';
*
* // Extend by object name
* export const companyExtension = extendObject({
* targetObject: {
* nameSingular: 'company',
* },
* fields: [
* {
* universalIdentifier: '...',
* name: 'healthScore',
* type: FieldType.NUMBER,
* label: 'Health Score',
* description: 'Calculated customer health metric',
* },
* {
* universalIdentifier: '...',
* name: 'churnRisk',
* type: FieldType.SELECT,
* label: 'Churn Risk',
* options: [
* { label: 'Low', value: 'low', color: 'green' },
* { label: 'Medium', value: 'medium', color: 'yellow' },
* { label: 'High', value: 'high', color: 'red' },
* ],
* },
* ],
* });
*
* // Or extend by universal identifier
* export const companyExtensionById = extendObject({
* targetObject: {
* universalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company,
* },
* fields: [...],
* });
* ```
*/
export const extendObject = <T extends ObjectExtensionManifest>(
config: T,
): T => {
if (!config.targetObject) {
throw new Error('Object extension must have a targetObject');
}
const { nameSingular, universalIdentifier } = config.targetObject;
if (
!isNonEmptyString(nameSingular) &&
!isNonEmptyString(universalIdentifier)
) {
throw new Error(
'targetObject must have either nameSingular or universalIdentifier',
);
}
if (isNonEmptyString(nameSingular) && isNonEmptyString(universalIdentifier)) {
throw new Error(
'targetObject cannot have both nameSingular and universalIdentifier - they are mutually exclusive',
);
}
if (!config.fields || config.fields.length === 0) {
throw new Error('Object extension must have at least one field');
}
validateFieldsOrThrow(config.fields);
return config;
};

View file

@ -1,7 +0,0 @@
import { type ObjectManifest } from 'twenty-shared/application';
type ObjectMetadataOptions = Omit<ObjectManifest, 'fields'>;
export const Object = (_: ObjectMetadataOptions): ClassDecorator => {
return () => {};
};

View file

@ -1,3 +0,0 @@
import type { RoleManifest } from 'twenty-shared/application';
export type RoleConfig = RoleManifest;

View file

@ -1,70 +0,0 @@
import { type RoleConfig } from '@/application/role-config';
/**
* Define a role configuration with validation.
*
* @example
* ```typescript
* import { defineRole, PermissionFlag } from 'twenty-sdk';
*
* export default defineRole({
* universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
* label: 'App User',
* description: 'Standard user role for the app',
* icon: 'IconUser',
* canReadAllObjectRecords: false,
* objectPermissions: [
* {
* objectNameSingular: 'postCard',
* canReadObjectRecords: true,
* canUpdateObjectRecords: true,
* },
* ],
* permissionFlags: [PermissionFlag.UPLOAD_FILE],
* });
* ```
*/
export const defineRole = <T extends RoleConfig>(config: T): T => {
if (!config.universalIdentifier) {
throw new Error('Role must have a universalIdentifier');
}
if (!config.label) {
throw new Error('Role must have a label');
}
// Validate object permissions if provided
if (config.objectPermissions) {
for (const permission of config.objectPermissions) {
if (
!permission.objectNameSingular &&
!permission.objectUniversalIdentifier
) {
throw new Error(
'Object permission must have either objectNameSingular or objectUniversalIdentifier',
);
}
}
}
// Validate field permissions if provided
if (config.fieldPermissions) {
for (const permission of config.fieldPermissions) {
if (
!permission.objectNameSingular &&
!permission.objectUniversalIdentifier
) {
throw new Error(
'Field permission must have either objectNameSingular or objectUniversalIdentifier',
);
}
if (!permission.fieldName && !permission.fieldUniversalIdentifier) {
throw new Error(
'Field permission must have either fieldName or fieldUniversalIdentifier',
);
}
}
}
return config;
};

View file

@ -1,9 +1,9 @@
import { defineApp } from '@/application/define-app';
import { defineApplication } from '@/sdk';
export default defineApp({
export default defineApplication({
universalIdentifier: 'invalid-app-0000-0000-0000-000000000001',
displayName: 'Invalid App',
description: 'An app with duplicate IDs for testing validation',
icon: 'IconAlertTriangle',
functionRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
});

View file

@ -1,5 +1,4 @@
import { defineObject } from '@/application/objects/define-object';
import { FieldType } from '@/application/fields/field-type';
import { defineObject, FieldType } from '@/sdk';
const DUPLICATE_ID = 'duplicate-id-0000-0000-000000000001';

View file

@ -1,5 +1,4 @@
import { defineObject } from '@/application/objects/define-object';
import { FieldType } from '@/application/fields/field-type';
import { defineObject, FieldType } from '@/sdk';
const DUPLICATE_ID = 'duplicate-id-0000-0000-000000000001';

View file

@ -1,8 +1,8 @@
import { FieldType } from '@/application';
import type { ApplicationManifest } from 'twenty-shared/application';
import { FieldType } from '@/sdk';
import type { Manifest } from 'twenty-shared/application';
import { PermissionFlagType } from 'twenty-shared/constants';
export const EXPECTED_MANIFEST: ApplicationManifest = {
export const EXPECTED_MANIFEST: Manifest = {
sources: {},
publicAssets: [
{
@ -24,7 +24,7 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
},
description: 'A simple hello world app',
displayName: 'Hello World',
functionRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
icon: 'IconWorld',
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
},
@ -66,47 +66,43 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
universalIdentifier: 'f1234567-abcd-4000-8000-000000000001',
},
],
objectExtensions: [
fields: [
{
fields: [
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
description: 'Priority level for the post card (1-10)',
label: 'Priority',
name: 'priority',
type: FieldType.NUMBER,
universalIdentifier: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d',
},
{
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
description: 'Post card category',
label: 'Category',
name: 'category',
options: [
{
description: 'Priority level for the post card (1-10)',
label: 'Priority',
name: 'priority',
type: FieldType.NUMBER,
universalIdentifier: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d',
color: 'blue',
label: 'Personal',
position: 0,
value: 'PERSONAL',
},
{
description: 'Post card category',
label: 'Category',
name: 'category',
options: [
{
color: 'blue',
label: 'Personal',
position: 0,
value: 'PERSONAL',
},
{
color: 'green',
label: 'Business',
position: 1,
value: 'BUSINESS',
},
{
color: 'orange',
label: 'Promotional',
position: 2,
value: 'PROMOTIONAL',
},
],
type: FieldType.SELECT,
universalIdentifier: '8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e',
color: 'green',
label: 'Business',
position: 1,
value: 'BUSINESS',
},
{
color: 'orange',
label: 'Promotional',
position: 2,
value: 'PROMOTIONAL',
},
],
targetObject: {
nameSingular: 'postCard',
},
type: FieldType.SELECT,
universalIdentifier: '8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e',
},
],
objects: [
@ -210,44 +206,6 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
},
],
packageJson: {
name: 'rich-app',
version: '0.1.0',
license: 'MIT',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
scripts: {
'auth:login': 'twenty auth:login',
'auth:logout': 'twenty auth:logout',
'auth:status': 'twenty auth:status',
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'function:logs': 'twenty function:logs',
'function:execute': 'twenty function:execute',
'app:uninstall': 'twenty app:uninstall',
help: 'twenty help',
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': 'latest',
},
devDependencies: {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^19.0.2',
react: '^19.0.2',
eslint: '^9.32.0',
'typescript-eslint': '^8.50.0',
},
},
roles: [
{
canBeAssignedToAgents: false,
@ -276,8 +234,8 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
{
canReadFieldValue: false,
canUpdateFieldValue: false,
fieldName: 'content',
objectNameSingular: 'postCard',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
},
],
label: 'Default function role',
@ -287,14 +245,14 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
canReadObjectRecords: true,
canSoftDeleteObjectRecords: false,
canUpdateObjectRecords: true,
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
},
],
permissionFlags: [PermissionFlagType.APPLICATIONS],
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
},
],
functions: [
logicFunctions: [
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'src/root.function.mjs',
@ -377,4 +335,42 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
},
],
packageJson: {
name: 'rich-app',
version: '0.1.0',
license: 'MIT',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
scripts: {
'auth:login': 'twenty auth:login',
'auth:logout': 'twenty auth:logout',
'auth:status': 'twenty auth:status',
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'function:logs': 'twenty function:logs',
'function:execute': 'twenty function:execute',
'app:uninstall': 'twenty app:uninstall',
help: 'twenty help',
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': 'latest',
},
devDependencies: {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^19.0.2',
react: '^19.0.2',
eslint: '^9.32.0',
'typescript-eslint': '^8.50.0',
},
},
};

View file

@ -4,8 +4,8 @@ import { OUTPUT_DIR } from 'twenty-shared/application';
export const defineEntitiesTests = (appPath: string): void => {
const outputDir = join(appPath, OUTPUT_DIR);
describe('functions', () => {
it('should have built functions preserving source path structure', async () => {
describe('logicFunctions', () => {
it('should have built logicFunctions preserving source path structure', async () => {
const files = await fs.readdir(outputDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();

View file

@ -19,13 +19,13 @@ export const defineManifestTests = (appPath: string): void => {
normalizeManifestForComparison(EXPECTED_MANIFEST),
);
for (const fn of manifest.functions) {
for (const fn of manifest.entities.logicFunctions) {
expect(fn.builtHandlerChecksum).toBeDefined();
expect(fn.builtHandlerChecksum).not.toBeNull();
expect(typeof fn.builtHandlerChecksum).toBe('string');
}
for (const component of manifest.frontComponents ?? []) {
for (const component of manifest.entities.frontComponents ?? []) {
expect(component.builtComponentChecksum).toBeDefined();
expect(component.builtComponentChecksum).not.toBeNull();
expect(typeof component.builtComponentChecksum).toBe('string');
@ -44,11 +44,11 @@ export const defineManifestTests = (appPath: string): void => {
it('should load all entity types', async () => {
const manifest = await fs.readJson(manifestOutputPath);
expect(manifest?.objects).toHaveLength(2);
expect(manifest?.functions).toHaveLength(4);
expect(manifest?.frontComponents).toHaveLength(4);
expect(manifest?.roles).toHaveLength(2);
expect(manifest?.objectExtensions).toHaveLength(1);
expect(manifest?.entities.objects).toHaveLength(2);
expect(manifest?.entities.logicFunctions).toHaveLength(4);
expect(manifest?.entities.frontComponents).toHaveLength(4);
expect(manifest?.entities.roles).toHaveLength(2);
expect(manifest?.entities.objectExtensions).toHaveLength(1);
});
});
};

View file

@ -1,7 +1,7 @@
import { defineApp } from '@/application/define-app';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './src/roles/default-function.role';
import { defineApplication } from '@/sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './src/roles/default-function.role';
export default defineApp({
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'Hello World',
description: 'A simple hello world app',
@ -14,5 +14,5 @@ export default defineApp({
isSecret: false,
},
},
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});

View file

@ -1,4 +1,4 @@
import { defineFrontComponent } from '@/application/front-components/define-front-component';
import { defineFrontComponent } from '@/sdk';
import { CardDisplay } from '../utils/card-display.component';
export default defineFrontComponent({

View file

@ -1,4 +1,4 @@
import { defineFrontComponent } from '@/application/front-components/define-front-component';
import { defineFrontComponent } from '@/sdk';
import { DEFAULT_NAME, formatGreeting } from '../utils/greeting.util';
const GreetingComponent = () => {

View file

@ -1,4 +1,4 @@
import { defineFrontComponent } from '@/application/front-components/define-front-component';
import { defineFrontComponent } from '@/sdk';
export const TestComponent = () => {
return (

View file

@ -0,0 +1,22 @@
import { defineField, FieldType } from '@/sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/postCard.object';
import { POST_CARD_EXTENSION_CATEGORY_FIELD_ID } from '@/cli/__tests__/apps/rich-app/src/fields/postCardNumber.field';
export default defineField({
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
universalIdentifier: POST_CARD_EXTENSION_CATEGORY_FIELD_ID,
type: FieldType.SELECT,
name: 'category',
label: 'Category',
description: 'Post card category',
options: [
{ value: 'PERSONAL', label: 'Personal', color: 'blue', position: 0 },
{ value: 'BUSINESS', label: 'Business', color: 'green', position: 1 },
{
value: 'PROMOTIONAL',
label: 'Promotional',
color: 'orange',
position: 2,
},
],
});

View file

@ -0,0 +1,17 @@
import { defineField, FieldType } from '@/sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/postCard.object';
export const POST_CARD_EXTENSION_PRIORITY_FIELD_ID =
'7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d';
export const POST_CARD_EXTENSION_CATEGORY_FIELD_ID =
'8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e';
export default defineField({
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
universalIdentifier: POST_CARD_EXTENSION_PRIORITY_FIELD_ID,
type: FieldType.NUMBER,
name: 'priority',
label: 'Priority',
description: 'Priority level for the post card (1-10)',
});

View file

@ -1,11 +1,11 @@
import { defineFunction } from '@/application/functions/define-function';
import { defineLogicFunction } from '@/sdk';
import { DEFAULT_NAME, formatGreeting } from '../utils/greeting.util';
const greetingHandler = () => {
return formatGreeting(DEFAULT_NAME);
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: '9d412d9e-2caf-487c-8b66-d1585883dd4e',
name: 'greeting-function',
timeoutSeconds: 5,

View file

@ -1,7 +1,7 @@
import { defineFunction } from '@/application/functions/define-function';
import { defineLogicFunction } from '@/sdk';
import { testFunction2 } from '../utils/test-function-2.util';
export const config = defineFunction({
export const config = defineLogicFunction({
universalIdentifier: 'eb3ffc98-88ec-45d4-9b4a-56833b219ccb',
name: 'test-function-2',
timeoutSeconds: 2,

View file

@ -1,11 +1,11 @@
import { defineFunction } from '@/application/functions/define-function';
import { defineLogicFunction } from '@/sdk';
import { formatFarewell } from '../utils/greeting.util';
const handler = () => {
return formatFarewell('test');
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'test-function',
timeoutSeconds: 2,

View file

@ -1,40 +0,0 @@
import { extendObject } from '@/application/objects/extend-object';
import { FieldType } from '@/application/fields/field-type';
export const POST_CARD_EXTENSION_PRIORITY_FIELD_ID =
'7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d';
export const POST_CARD_EXTENSION_CATEGORY_FIELD_ID =
'8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e';
export default extendObject({
targetObject: {
nameSingular: 'postCard',
},
fields: [
{
universalIdentifier: POST_CARD_EXTENSION_PRIORITY_FIELD_ID,
type: FieldType.NUMBER,
name: 'priority',
label: 'Priority',
description: 'Priority level for the post card (1-10)',
},
{
universalIdentifier: POST_CARD_EXTENSION_CATEGORY_FIELD_ID,
type: FieldType.SELECT,
name: 'category',
label: 'Category',
description: 'Post card category',
options: [
{ value: 'PERSONAL', label: 'Personal', color: 'blue', position: 0 },
{ value: 'BUSINESS', label: 'Business', color: 'green', position: 1 },
{
value: 'PROMOTIONAL',
label: 'Promotional',
color: 'orange',
position: 2,
},
],
},
],
});

View file

@ -1,5 +1,4 @@
import { defineObject } from '@/application/objects/define-object';
import { FieldType } from '@/application/fields/field-type';
import { defineObject, FieldType } from '@/sdk';
enum PostCardStatus {
DRAFT = 'DRAFT',
@ -8,8 +7,11 @@ enum PostCardStatus {
RETURNED = 'RETURNED',
}
export const POST_CARD_UNIVERSAL_IDENTIFIER =
'54b589ca-eeed-4950-a176-358418b85c05';
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
universalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post card',

View file

@ -1,11 +1,10 @@
import { PermissionFlag } from '@/application/permission-flag-type';
import { defineRole } from '@/application/roles/define-role';
import { PermissionFlag, defineRole } from '@/sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: false,
@ -18,7 +17,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@ -27,8 +26,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},

View file

@ -1,4 +1,4 @@
import { defineFrontComponent } from '@/application/front-components/define-front-component';
import { defineFrontComponent } from '@/sdk';
export const RootComponent = () => {
return (

View file

@ -1,10 +1,10 @@
import { defineFunction } from '@/application/functions/define-function';
import { defineLogicFunction } from '@/sdk';
const rootHandler = () => {
return 'root-function-result';
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'f0f1f2f3-f4f5-4000-8000-000000000001',
name: 'root-function',
timeoutSeconds: 5,

View file

@ -1,5 +1,4 @@
import { defineObject } from '@/application/objects/define-object';
import { FieldType } from '@/application/fields/field-type';
import { defineObject, FieldType } from '@/sdk';
export default defineObject({
universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000001',

View file

@ -1,4 +1,4 @@
import { defineRole } from '@/application/roles/define-role';
import { defineRole } from '@/sdk';
export default defineRole({
universalIdentifier: 'c0c1c2c3-c4c5-4000-8000-000000000001',

View file

@ -4,7 +4,7 @@ import { runAppDev } from '@/cli/__tests__/integration/utils/run-app-dev.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineFrontComponentsTests } from './tests/front-components.tests';
import { defineFunctionsTests } from './tests/functions.tests';
import { defineLogicFunctionsTests } from './tests/logic-functions.tests';
import { defineManifestTests } from './tests/manifest.tests';
const APP_PATH = join(__dirname, '../..');
@ -22,6 +22,6 @@ describe('root-app app:dev', () => {
defineConsoleOutputTests(() => result);
defineManifestTests(APP_PATH);
defineFunctionsTests(APP_PATH);
defineLogicFunctionsTests(APP_PATH);
defineFrontComponentsTests(APP_PATH);
});

View file

@ -1,7 +1,7 @@
import { FieldType } from '@/application';
import type { ApplicationManifest } from 'twenty-shared/application';
import { FieldType } from '@/sdk';
import type { Manifest } from 'twenty-shared/application';
export const EXPECTED_MANIFEST: ApplicationManifest = {
export const EXPECTED_MANIFEST: Manifest = {
sources: {},
yarnLock: '',
packageJson: {
@ -47,8 +47,10 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
displayName: 'Root App',
description: 'An app with all entities at root level',
icon: 'IconFolder',
functionRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
},
publicAssets: [],
fields: [],
objects: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000030',
@ -68,7 +70,7 @@ export const EXPECTED_MANIFEST: ApplicationManifest = {
],
},
],
functions: [
logicFunctions: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000010',
name: 'my-function',

View file

@ -1,9 +1,9 @@
import * as fs from 'fs-extra';
import { join } from 'path';
export const defineFunctionsTests = (appPath: string): void => {
describe('functions', () => {
it('should have built functions at root level', async () => {
export const defineLogicFunctionsTests = (appPath: string): void => {
describe('logicFunctions', () => {
it('should have built logicFunctions at root level', async () => {
const outputDir = join(appPath, '.twenty/output');
const files = await fs.readdir(outputDir, { recursive: true });
const functionFiles = files

View file

@ -1,6 +1,6 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import { type ApplicationManifest } from 'twenty-shared/application';
import { type Manifest } from 'twenty-shared/application';
import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util';
import { EXPECTED_MANIFEST } from '@/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest';
@ -16,21 +16,22 @@ export const defineManifestTests = (appPath: string): void => {
it('should have correct manifest content', async () => {
const manifestPath = join(appPath, '.twenty/output/manifest.json');
const manifest: ApplicationManifest = await fs.readJSON(manifestPath);
const manifest: Manifest = await fs.readJSON(manifestPath);
expect(manifest.application).toEqual(EXPECTED_MANIFEST.application);
expect(manifest.objects).toEqual(EXPECTED_MANIFEST.objects);
expect(
normalizeManifestForComparison({ functions: manifest.functions })
.functions,
normalizeManifestForComparison({
logicFunctions: manifest.logicFunctions,
}).logicFunctions,
).toEqual(
normalizeManifestForComparison({
functions: EXPECTED_MANIFEST.functions,
}).functions,
logicFunctions: EXPECTED_MANIFEST.logicFunctions,
}).logicFunctions,
);
for (const fn of manifest.functions) {
for (const fn of manifest.logicFunctions) {
expect(fn.builtHandlerChecksum).toBeDefined();
expect(fn.builtHandlerChecksum).not.toBeNull();
expect(typeof fn.builtHandlerChecksum).toBe('string');

View file

@ -1,9 +1,9 @@
import { defineApp } from '@/application/define-app';
import { defineApplication } from '@/sdk';
export default defineApp({
export default defineApplication({
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
displayName: 'Root App',
description: 'An app with all entities at root level',
icon: 'IconFolder',
functionRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
});

View file

@ -1,4 +1,4 @@
import { defineFrontComponent } from '@/application/front-components/define-front-component';
import { defineFrontComponent } from '@/sdk';
export const MyComponent = () => {
return (

View file

@ -1,10 +1,10 @@
import { defineFunction } from '@/application/functions/define-function';
import { defineLogicFunction } from '@/sdk';
const myHandler = () => {
return 'my-function-result';
};
export default defineFunction({
export default defineLogicFunction({
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000010',
name: 'my-function',
timeoutSeconds: 5,

View file

@ -1,5 +1,4 @@
import { FieldType } from '@/application/fields/field-type';
import { defineObject } from '@/application/objects/define-object';
import { FieldType, defineObject } from '@/sdk';
export default defineObject({
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000030',

View file

@ -1,4 +1,4 @@
import { defineRole } from '@/application/roles/define-role';
import { defineRole } from '@/sdk';
export default defineRole({
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000040',

View file

@ -2,7 +2,7 @@ export type LogPrefix =
| 'init'
| 'dev-mode'
| 'manifest-watch'
| 'functions-watch'
| 'logicFunctions-watch'
| 'front-components-watch';
export const getOutputByPrefix = (

View file

@ -1,6 +1,6 @@
// Loose type for JSON manifest imports where enum values are inferred as strings
type JsonManifestInput = {
functions?: Array<{
logicFunctions?: Array<{
builtHandlerChecksum?: string | null;
[key: string]: unknown;
}>;
@ -16,7 +16,7 @@ export const normalizeManifestForComparison = <T extends JsonManifestInput>(
manifest: T,
): T => ({
...manifest,
functions: manifest.functions?.map((fn) => ({
logicFunctions: manifest.logicFunctions?.map((fn) => ({
...fn,
builtHandlerChecksum: fn.builtHandlerChecksum ? '[checksum]' : null,
})),

View file

@ -8,14 +8,11 @@ import { AuthListCommand } from './auth/auth-list';
import { AuthLoginCommand } from './auth/auth-login';
import { AuthLogoutCommand } from './auth/auth-logout';
import { AuthStatusCommand } from './auth/auth-status';
import { FunctionExecuteCommand } from './function/function-execute';
import { FunctionLogsCommand } from './function/function-logs';
import { LogicFunctionExecuteCommand } from './logic-function/logic-function-execute';
import { LogicFunctionLogsCommand } from './logic-function/logic-function-logs';
import { AuthSwitchCommand } from './auth/auth-switch';
import {
EntityAddCommand,
isSyncableEntity,
SyncableEntity,
} from './entity/entity-add';
import { EntityAddCommand } from './entity/entity-add';
import { SyncableEntity } from 'twenty-shared/application';
export const registerCommands = (program: Command): void => {
// Auth commands
@ -67,8 +64,8 @@ export const registerCommands = (program: Command): void => {
const uninstallCommand = new AppUninstallCommand();
const addCommand = new EntityAddCommand();
const generateCommand = new AppGenerateCommand();
const logsCommand = new FunctionLogsCommand();
const executeCommand = new FunctionExecuteCommand();
const logsCommand = new LogicFunctionLogsCommand();
const executeCommand = new LogicFunctionExecuteCommand();
program
.command('app:dev [appPath]')
@ -101,14 +98,6 @@ export const registerCommands = (program: Command): void => {
`Add a new entity to your application (${Object.values(SyncableEntity).join('|')})`,
)
.action(async (entityType?: string, options?: { path?: string }) => {
if (entityType && !isSyncableEntity(entityType)) {
console.error(
chalk.red(
`Invalid entity type "${entityType}". Must be one of: ${Object.values(SyncableEntity).join('|')}`,
),
);
process.exit(1);
}
await addCommand.execute(entityType as SyncableEntity, options?.path);
});

View file

@ -1,10 +1,10 @@
import { AssetWatcher } from '@/cli/utilities/build/common/asset-watcher';
import {
createFrontComponentsWatcher,
createFunctionsWatcher,
createLogicFunctionsWatcher,
type EsbuildWatcher,
} from '@/cli/utilities/build/common/esbuild-watcher';
import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-build';
import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/update-manifest-checksums';
import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { DevModeOrchestrator } from '@/cli/utilities/dev/dev-mode-orchestrator';
@ -22,7 +22,7 @@ export class AppDevCommand {
private appPath = '';
private orchestrator: DevModeOrchestrator | null = null;
private manifestWatcher: ManifestWatcher | null = null;
private functionsWatcher: EsbuildWatcher | null = null;
private logicFunctionsWatcher: EsbuildWatcher | null = null;
private frontComponentsWatcher: EsbuildWatcher | null = null;
private assetWatcher: AssetWatcher | null = null;
private watchersStarted = false;
@ -71,16 +71,16 @@ export class AppDevCommand {
}
private async handleWatcherRestarts(result: ManifestBuildResult) {
const { functions, frontComponents } = result.filePaths;
const { logicFunctions, frontComponents } = result.filePaths;
if (!this.watchersStarted) {
this.watchersStarted = true;
await this.startFileWatchers(functions, frontComponents);
await this.startFileWatchers(logicFunctions, frontComponents);
return;
}
if (this.functionsWatcher?.shouldRestart(functions)) {
await this.functionsWatcher.restart(functions);
if (this.logicFunctionsWatcher?.shouldRestart(logicFunctions)) {
await this.logicFunctionsWatcher.restart(logicFunctions);
}
if (this.frontComponentsWatcher?.shouldRestart(frontComponents)) {
@ -89,18 +89,20 @@ export class AppDevCommand {
}
private async startFileWatchers(
functions: string[],
logicFunctions: string[],
frontComponents: string[],
): Promise<void> {
await Promise.all([
this.startFunctionsWatcher(functions),
this.startLogicFunctionsWatcher(logicFunctions),
this.startFrontComponentsWatcher(frontComponents),
this.startAssetWatcher(),
]);
}
private async startFunctionsWatcher(sourcePaths: string[]): Promise<void> {
this.functionsWatcher = createFunctionsWatcher({
private async startLogicFunctionsWatcher(
sourcePaths: string[],
): Promise<void> {
this.logicFunctionsWatcher = createLogicFunctionsWatcher({
appPath: this.appPath,
sourcePaths,
handleBuildError: this.orchestrator!.handleFileBuildError.bind(
@ -111,7 +113,7 @@ export class AppDevCommand {
),
});
await this.functionsWatcher.start();
await this.logicFunctionsWatcher.start();
}
private async startFrontComponentsWatcher(
@ -148,7 +150,7 @@ export class AppDevCommand {
await Promise.all([
this.manifestWatcher?.close(),
this.functionsWatcher?.close(),
this.logicFunctionsWatcher?.close(),
this.frontComponentsWatcher?.close(),
this.assetWatcher?.close(),
]);

View file

@ -1,9 +1,9 @@
import { ApiService } from '@/cli/utilities/api/api-service';
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import inquirer from 'inquirer';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
export class AppUninstallCommand {
private apiService = new ApiService();
@ -25,7 +25,7 @@ export class AppUninstallCommand {
process.exit(1);
}
const { manifest } = await runManifestBuild(appPath);
const { manifest } = await buildManifest(appPath);
if (!manifest) {
return { success: false, error: 'Build failed' };

View file

@ -1,132 +1,51 @@
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-component-template';
import { getFunctionBaseFile } from '@/cli/utilities/entity/entity-function-template';
import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-function-template';
import { convertToLabel } from '@/cli/utilities/entity/entity-label';
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import camelcase from 'lodash.camelcase';
import kebabcase from 'lodash.kebabcase';
import { join } from 'path';
import { join, relative } from 'path';
import { SyncableEntity } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { getFieldBaseFile } from '@/cli/utilities/entity/entity-field-template';
const APP_FOLDER = 'src';
export enum SyncableEntity {
AGENT = 'agent',
OBJECT = 'object',
FUNCTION = 'function',
FRONT_COMPONENT = 'front-component',
ROLE = 'role',
}
export const isSyncableEntity = (value: string): value is SyncableEntity => {
return Object.values(SyncableEntity).includes(value as SyncableEntity);
};
export class EntityAddCommand {
async execute(entityType?: SyncableEntity, path?: string): Promise<void> {
try {
// Default to src/ folder, allow override with path parameter
const entity = entityType ?? (await this.getEntity());
const entityName = this.getFolderName(entity);
const appPath = path
? join(CURRENT_EXECUTION_DIRECTORY, path)
: join(CURRENT_EXECUTION_DIRECTORY, APP_FOLDER);
: join(CURRENT_EXECUTION_DIRECTORY, APP_FOLDER, entityName);
await fs.ensureDir(appPath);
const entity = entityType ?? (await this.getEntity());
const { name, file } = await this.getEntityData(entity);
if (entity === SyncableEntity.OBJECT) {
const entityData = await this.getObjectData();
const filePath = join(appPath, this.getFileName(name, entity));
const name = entityData.nameSingular;
// Use *.object.ts naming convention
const objectFileName = `${camelcase(name)}.object.ts`;
const decoratedObject = getObjectBaseFile({
data: entityData,
name,
});
const filePath = join(appPath, objectFileName);
await fs.writeFile(filePath, decoratedObject);
console.log(
chalk.green(`✓ Created object:`),
chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')),
);
return;
if (await fs.pathExists(filePath)) {
const { overwrite } = await this.handleFileExist();
if (!overwrite) {
return;
}
}
if (entity === SyncableEntity.FUNCTION) {
const entityName = await this.getEntityName(entity);
await fs.writeFile(filePath, file);
// Use *.function.ts naming convention
const functionFileName = `${kebabcase(entityName)}.function.ts`;
const decoratedLogicFunction = getFunctionBaseFile({
name: entityName,
});
const filePath = join(appPath, functionFileName);
await fs.writeFile(filePath, decoratedLogicFunction);
console.log(
chalk.green(`✓ Created function:`),
chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')),
);
return;
}
if (entity === SyncableEntity.FRONT_COMPONENT) {
const entityName = await this.getEntityName(entity);
// Use *.front-component.tsx naming convention
const frontComponentFileName = `${kebabcase(entityName)}.front-component.tsx`;
const decoratedFrontComponent = getFrontComponentBaseFile({
name: entityName,
});
const filePath = join(appPath, frontComponentFileName);
await fs.writeFile(filePath, decoratedFrontComponent);
console.log(
chalk.green(`✓ Created front component:`),
chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')),
);
return;
}
if (entity === SyncableEntity.ROLE) {
const entityName = await this.getEntityName(entity);
// Use *.role.ts naming convention
const roleFileName = `${kebabcase(entityName)}.role.ts`;
const roleFileContent = getRoleBaseFile({
name: entityName,
});
const filePath = join(appPath, roleFileName);
await fs.writeFile(filePath, roleFileContent);
console.log(
chalk.green(`✓ Created role:`),
chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')),
);
return;
}
console.log(
chalk.green(`✓ Created ${entityName}:`),
chalk.cyan(relative(CURRENT_EXECUTION_DIRECTORY, filePath)),
);
} catch (error) {
console.error(
chalk.red(`Add new entity failed:`),
@ -136,6 +55,69 @@ export class EntityAddCommand {
}
}
private async getEntityData(entity: SyncableEntity) {
switch (entity) {
case SyncableEntity.Object: {
const entityData = await this.getObjectData();
const name = entityData.nameSingular;
const file = getObjectBaseFile({
data: entityData,
name,
});
return { name, file };
}
case SyncableEntity.Field: {
const entityData = await this.getFieldData();
const name = entityData.name;
const file = getFieldBaseFile({
data: entityData,
name,
});
return { name, file };
}
case SyncableEntity.LogicFunction: {
const name = await this.getEntityName(entity);
const file = getLogicFunctionBaseFile({
name,
});
return { name, file };
}
case SyncableEntity.FrontComponent: {
const name = await this.getEntityName(entity);
const file = getFrontComponentBaseFile({
name,
});
return { name, file };
}
case SyncableEntity.Role: {
const name = await this.getEntityName(entity);
const file = getRoleBaseFile({
name,
});
return { name, file };
}
default:
assertUnreachable(entity);
}
}
private async getEntity() {
const { entity } = await inquirer.prompt<{ entity: SyncableEntity }>([
{
@ -143,18 +125,24 @@ export class EntityAddCommand {
name: 'entity',
message: `What entity do you want to create?`,
default: '',
choices: [
SyncableEntity.FUNCTION,
SyncableEntity.FRONT_COMPONENT,
SyncableEntity.OBJECT,
SyncableEntity.ROLE,
],
choices: Object.values(SyncableEntity),
},
]);
return entity;
}
private async handleFileExist() {
return await inquirer.prompt<{ overwrite: boolean }>([
{
type: 'confirm',
name: 'overwrite',
message: `File already exists. Do you want to overwrite it?`,
default: false,
},
]);
}
private async getEntityName(entity: SyncableEntity) {
const { name } = await inquirer.prompt<{ name: string }>([
{
@ -167,10 +155,6 @@ export class EntityAddCommand {
return `${entity} name is required`;
}
if (!/^[a-z0-9-]+$/.test(input)) {
return 'Name must contain only lowercase letters, numbers, and hyphens';
}
return true;
},
},
@ -179,8 +163,76 @@ export class EntityAddCommand {
return name;
}
private async getFieldData() {
return inquirer.prompt<{
name: string;
label: string;
type: FieldMetadataType;
objectUniversalIdentifier: string;
description: string;
}>([
{
type: 'input',
name: 'name',
message: 'Enter a name for your field:',
default: '',
validate: (input: string) => {
if (!input || input.trim().length === 0) {
return 'Please enter a non empty string';
}
return true;
},
},
{
type: 'input',
name: 'label',
message: 'Enter a label for your field:',
default: (answers: any) => {
return convertToLabel(answers.name);
},
validate: (input: string) => {
if (!input || input.trim().length === 0) {
return 'Please enter a non empty string';
}
return true;
},
},
{
type: 'select',
name: 'type',
message: 'Select the field type:',
choices: Object.values(FieldMetadataType),
default: FieldMetadataType.TEXT,
},
{
type: 'input',
name: 'objectUniversalIdentifier',
message:
'Enter the universalIdentifier of the object this field belongs to:',
default: 'fill-later',
validate: (input: string) => {
if (!input || input.trim().length === 0) {
return 'Please enter a non empty string';
}
return true;
},
},
{
type: 'input',
name: 'description',
message: 'Enter a description for your field (optional):',
default: '',
},
]);
}
private async getObjectData() {
return inquirer.prompt([
return inquirer.prompt<{
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
}>([
{
type: 'input',
name: 'nameSingular',
@ -238,4 +290,19 @@ export class EntityAddCommand {
},
]);
}
getFolderName(entity: SyncableEntity) {
return `${kebabcase(entity)}s`;
}
getFileName(name: string, entity: SyncableEntity) {
switch (entity) {
case SyncableEntity.FrontComponent: {
return `${kebabcase(name)}.tsx`;
}
default: {
return `${kebabcase(name)}.ts`;
}
}
}
}

View file

@ -1,11 +1,11 @@
import { ApiService } from '@/cli/utilities/api/api-service';
import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import { type ApplicationManifest } from 'twenty-shared/application';
import { type Manifest } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
export class FunctionExecuteCommand {
export class LogicFunctionExecuteCommand {
private apiService = new ApiService();
async execute({
@ -30,7 +30,7 @@ export class FunctionExecuteCommand {
process.exit(1);
}
const { manifest } = await runManifestBuild(appPath);
const { manifest } = await buildManifest(appPath);
if (!manifest) {
console.error(chalk.red('Failed to build manifest.'));
@ -162,9 +162,9 @@ export class FunctionExecuteCommand {
private belongsToApplication(
fn: { universalIdentifier: string; applicationId: string | null },
manifest: ApplicationManifest,
manifest: Manifest,
): boolean {
return manifest.functions.some(
return manifest.logicFunctions.some(
(manifestFn) => manifestFn.universalIdentifier === fn.universalIdentifier,
);
}

View file

@ -1,9 +1,9 @@
import { ApiService } from '@/cli/utilities/api/api-service';
import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
export class FunctionLogsCommand {
export class LogicFunctionLogsCommand {
private apiService = new ApiService();
async execute({
@ -16,7 +16,7 @@ export class FunctionLogsCommand {
functionName?: string;
}): Promise<void> {
try {
const { manifest } = await runManifestBuild(appPath);
const { manifest } = await buildManifest(appPath);
if (!manifest) {
process.exit(1);

View file

@ -9,7 +9,7 @@ import {
printSchema,
} from 'graphql/index';
import * as path from 'path';
import { type ApplicationManifest } from 'twenty-shared/application';
import { type Manifest } from 'twenty-shared/application';
import { type FileFolder } from 'twenty-shared/types';
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import { pascalCase } from 'twenty-shared/utils';
@ -99,7 +99,7 @@ export class ApiService {
}
}
async syncApplication(manifest: ApplicationManifest): Promise<ApiResponse> {
async syncApplication(manifest: Manifest): Promise<ApiResponse> {
try {
const mutation = `
mutation SyncApplication($manifest: JSON!, $packageJson: JSON!, $yarnLock: String!) {

View file

@ -13,7 +13,7 @@ import path from 'path';
import { OUTPUT_DIR } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
export const FUNCTION_EXTERNAL_MODULES: string[] = [
export const LOGIC_FUNCTION_EXTERNAL_MODULES: string[] = [
'path',
'fs',
'crypto',
@ -204,14 +204,14 @@ const externalPatternsPlugin: esbuild.Plugin = {
},
};
export const createFunctionsWatcher = (
export const createLogicFunctionsWatcher = (
options: RestartableWatcherOptions,
): EsbuildWatcher =>
new EsbuildWatcher({
...options,
config: {
externalModules: FUNCTION_EXTERNAL_MODULES,
fileFolder: FileFolder.BuiltFunction,
externalModules: LOGIC_FUNCTION_EXTERNAL_MODULES,
fileFolder: FileFolder.BuiltLogicFunction,
platform: 'node',
extraPlugins: [externalPatternsPlugin],
},

View file

@ -0,0 +1,84 @@
import { extractDefineEntity } from '@/cli/utilities/build/manifest/manifest-extract-config';
describe('extractDefineEntity', () => {
it('should detect defineApplication in default export', () => {
const fileContent = `
import { defineApplication } from 'twenty-sdk';
export default defineApplication({ name: 'MyApp' });
`;
const result = extractDefineEntity(fileContent);
expect(result).toBe('defineApplication');
});
it('should detect defineField in default export', () => {
const fileContent = `
import { defineField } from 'twenty-sdk';
export default defineField({ name: 'myField' });
`;
const result = extractDefineEntity(fileContent);
expect(result).toBe('defineField');
});
it('should detect defineLogicFunction in default export', () => {
const fileContent = `
import { defineLogicFunction } from 'twenty-sdk';
export default defineLogicFunction({ name: 'myFunction' });
`;
const result = extractDefineEntity(fileContent);
expect(result).toBe('defineLogicFunction');
});
it('should detect defineObject in default export', () => {
const fileContent = `
import { defineObject } from 'twenty-sdk';
export default defineObject({ name: 'myObject' });
`;
const result = extractDefineEntity(fileContent);
expect(result).toBe('defineObject');
});
it('should detect defineRole in default export', () => {
const fileContent = `
import { defineRole } from 'twenty-sdk';
export default defineRole({ name: 'myRole' });
`;
const result = extractDefineEntity(fileContent);
expect(result).toBe('defineRole');
});
it('should detect defineFrontComponent in default export', () => {
const fileContent = `
import { defineFrontComponent } from 'twenty-sdk';
export default defineFrontComponent({ name: 'myComponent' });
`;
const result = extractDefineEntity(fileContent);
expect(result).toBe('defineFrontComponent');
});
it('should not detect non-target function in default export', () => {
const fileContent = `
import { someOtherFunction } from 'twenty-sdk';
export default someOtherFunction({ name: 'myFunction' });
`;
const result = extractDefineEntity(fileContent);
expect(result).toBeUndefined();
});
it('should handle files without default export', () => {
const fileContent = `
import { defineApplication } from 'twenty-sdk';
const app = defineApplication({ name: 'MyApp' });
`;
const result = extractDefineEntity(fileContent);
expect(result).toBeUndefined();
});
it('should handle files with non-call default export', () => {
const fileContent = `
import { defineApplication } from 'twenty-sdk';
export default { name: 'MyApp' };
`;
const result = extractDefineEntity(fileContent);
expect(result).toBeUndefined();
});
});

View file

@ -1,40 +1,46 @@
import { validateManifest } from '@/cli/utilities/build/manifest/manifest-validate';
import {
type Application,
type ObjectExtensionManifest,
type ApplicationManifest,
type Manifest,
type PackageJson,
type FieldManifest,
} from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { validateManifest } from '@/cli/utilities/build/manifest/validate-manifest';
const validApplication: ApplicationManifest = {
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'Test App',
defaultRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2',
};
const validField: FieldManifest = {
objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f',
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
type: FieldMetadataType.NUMBER,
name: 'healthScore',
label: 'Health Score',
};
const validManifest: Manifest = {
application: validApplication,
objects: [],
frontComponents: [],
fields: [],
logicFunctions: [],
roles: [],
publicAssets: [],
sources: {},
packageJson: {} as PackageJson,
yarnLock: '',
};
describe('validateManifest - objectExtensions', () => {
const validApplication: Application = {
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'Test App',
functionRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2',
};
const validObjectExtension: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
type: FieldMetadataType.NUMBER,
name: 'healthScore',
label: 'Health Score',
},
],
};
describe('valid object extensions', () => {
it('should pass validation with valid object extension by nameSingular', () => {
const result = validateManifest({
application: validApplication,
objects: [],
frontComponents: [],
objectExtensions: [validObjectExtension],
functions: [],
roles: [],
...validManifest,
fields: [validField],
});
expect(result.isValid).toBe(true);
@ -42,27 +48,17 @@ describe('validateManifest - objectExtensions', () => {
});
it('should pass validation with valid object extension by universalIdentifier', () => {
const extensionByUuid: ObjectExtensionManifest = {
targetObject: {
universalIdentifier: '20202020-b374-4779-a561-80086cb2e17f',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440002',
type: FieldMetadataType.TEXT,
name: 'customNote',
label: 'Custom Note',
},
],
const extensionByUuid: FieldManifest = {
objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f',
universalIdentifier: '550e8400-e29b-41d4-a716-446655440002',
type: FieldMetadataType.TEXT,
name: 'customNote',
label: 'Custom Note',
};
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [extensionByUuid],
functions: [],
frontComponents: [],
roles: [],
...validManifest,
fields: [extensionByUuid],
});
expect(result.isValid).toBe(true);
@ -70,27 +66,17 @@ describe('validateManifest - objectExtensions', () => {
});
it('should pass validation with multiple object extensions', () => {
const anotherExtension: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'person',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440003',
type: FieldMetadataType.TEXT,
name: 'nickname',
label: 'Nickname',
},
],
const anotherExtension: FieldManifest = {
objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f',
universalIdentifier: '550e8400-e29b-41d4-a716-446655440003',
type: FieldMetadataType.TEXT,
name: 'nickname',
label: 'Nickname',
};
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [validObjectExtension, anotherExtension],
functions: [],
frontComponents: [],
roles: [],
...validManifest,
fields: [validField, anotherExtension],
});
expect(result.isValid).toBe(true);
@ -98,36 +84,26 @@ describe('validateManifest - objectExtensions', () => {
});
it('should pass validation with SELECT field having options', () => {
const extensionWithSelect: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
},
fields: [
const extensionWithSelect: FieldManifest = {
objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f',
universalIdentifier: '550e8400-e29b-41d4-a716-446655440004',
type: FieldMetadataType.SELECT,
name: 'status',
label: 'Status',
options: [
{ value: 'active', label: 'Active', color: 'green', position: 0 },
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440004',
type: FieldMetadataType.SELECT,
name: 'status',
label: 'Status',
options: [
{ value: 'active', label: 'Active', color: 'green', position: 0 },
{
value: 'inactive',
label: 'Inactive',
color: 'red',
position: 1,
},
],
value: 'inactive',
label: 'Inactive',
color: 'red',
position: 1,
},
],
};
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [extensionWithSelect],
functions: [],
frontComponents: [],
roles: [],
...validManifest,
fields: [extensionWithSelect],
});
expect(result.isValid).toBe(true);
@ -135,312 +111,46 @@ describe('validateManifest - objectExtensions', () => {
});
});
describe('targetObject validation', () => {
it('should fail when targetObject is missing', () => {
const invalidExtension = {
fields: validObjectExtension.fields,
} as ObjectExtensionManifest;
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
expect(result.isValid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
message: 'Object extension must have a targetObject',
}),
);
});
it('should fail when targetObject has neither nameSingular nor universalIdentifier', () => {
const invalidExtension: ObjectExtensionManifest = {
targetObject: {} as any,
fields: validObjectExtension.fields,
};
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
expect(result.isValid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
message:
'Object extension targetObject must have either nameSingular or universalIdentifier',
}),
);
});
it('should fail when targetObject has both nameSingular and universalIdentifier', () => {
const invalidExtension: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
universalIdentifier: '20202020-b374-4779-a561-80086cb2e17f',
} as any,
fields: validObjectExtension.fields,
};
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
expect(result.isValid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
message:
'Object extension targetObject cannot have both nameSingular and universalIdentifier',
}),
);
});
});
describe('fields validation', () => {
it('should fail when fields array is empty', () => {
const invalidExtension: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
},
fields: [],
};
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
expect(result.isValid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
message: 'Object extension must have at least one field',
}),
);
});
it('should fail when field is missing universalIdentifier', () => {
const invalidExtension: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
type: FieldMetadataType.NUMBER,
name: 'healthScore',
label: 'Health Score',
} as any,
],
};
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
expect(result.isValid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
message: 'Field must have a universalIdentifier',
}),
);
});
it('should fail when field is missing type', () => {
const invalidExtension: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
name: 'healthScore',
label: 'Health Score',
} as any,
],
};
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
expect(result.isValid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
message: 'Field must have a type',
}),
);
});
it('should fail when field is missing label', () => {
const invalidExtension: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
type: FieldMetadataType.NUMBER,
name: 'healthScore',
} as any,
],
};
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
expect(result.isValid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
message: 'Field must have a label',
}),
);
});
it('should fail when SELECT field has no options', () => {
const invalidExtension: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
type: FieldMetadataType.SELECT,
name: 'status',
label: 'Status',
} as any,
],
};
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
expect(result.isValid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
message: 'SELECT/MULTI_SELECT field must have options',
}),
);
});
it('should fail when MULTI_SELECT field has empty options', () => {
const invalidExtension: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
},
fields: [
{
universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
type: FieldMetadataType.MULTI_SELECT,
name: 'tags',
label: 'Tags',
options: [],
} as any,
],
};
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [invalidExtension],
functions: [],
frontComponents: [],
roles: [],
});
expect(result.isValid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
message: 'SELECT/MULTI_SELECT field must have options',
}),
);
});
});
describe('duplicate universalIdentifier detection', () => {
it('should fail when extension field has duplicate universalIdentifier', () => {
const duplicateId = '550e8400-e29b-41d4-a716-446655440001';
const extensionWithDuplicates: ObjectExtensionManifest = {
targetObject: {
nameSingular: 'company',
const fieldsWithDuplicates: FieldManifest[] = [
{
objectUniversalIdentifier: '91c5848c-36dc-4e7e-b9ee-aa78caeff5a8',
universalIdentifier: duplicateId,
type: FieldMetadataType.NUMBER,
name: 'field1',
label: 'Field 1',
},
fields: [
{
universalIdentifier: duplicateId,
type: FieldMetadataType.NUMBER,
name: 'field1',
label: 'Field 1',
},
{
universalIdentifier: duplicateId, // Same ID!
type: FieldMetadataType.TEXT,
name: 'field2',
label: 'Field 2',
},
],
};
{
objectUniversalIdentifier: '97931020-123c-435b-ad97-9e19a5b38f1f',
universalIdentifier: duplicateId,
type: FieldMetadataType.TEXT,
name: 'field2',
label: 'Field 2',
},
];
const result = validateManifest({
application: validApplication,
objects: [],
objectExtensions: [extensionWithDuplicates],
functions: [],
frontComponents: [],
roles: [],
...validManifest,
fields: fieldsWithDuplicates,
});
expect(result.isValid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
message: expect.stringContaining('Duplicate universalIdentifier'),
}),
expect(result.errors).toContain(
'Duplicate universal identifiers: 550e8400-e29b-41d4-a716-446655440001',
);
expect(result.warnings).toContain('No object defined');
expect(result.warnings).toContain('No logic function defined');
expect(result.warnings).toContain('No front component defined');
});
it('should fail when extension field ID conflicts with object field ID', () => {
const sharedId = '550e8400-e29b-41d4-a716-446655440001';
const result = validateManifest({
application: validApplication,
...validManifest,
objects: [
{
universalIdentifier: 'obj-uuid',
@ -458,30 +168,24 @@ describe('validateManifest - objectExtensions', () => {
],
},
],
objectExtensions: [
fields: [
{
targetObject: { nameSingular: 'company' },
fields: [
{
universalIdentifier: sharedId, // Same as object field!
type: FieldMetadataType.NUMBER,
name: 'extensionField',
label: 'Extension Field',
},
],
objectUniversalIdentifier: '91c5848c-36dc-4e7e-b9ee-aa78caeff5a8',
universalIdentifier: sharedId,
type: FieldMetadataType.NUMBER,
name: 'extensionField',
label: 'Extension Field',
},
],
functions: [],
frontComponents: [],
roles: [],
});
expect(result.isValid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
message: expect.stringContaining('Duplicate universalIdentifier'),
}),
expect(result.errors).toContain(
'Duplicate universal identifiers: 550e8400-e29b-41d4-a716-446655440001',
);
expect(result.warnings).not.toContain('No object defined');
expect(result.warnings).toContain('No logic function defined');
expect(result.warnings).toContain('No front component defined');
});
});
});

View file

@ -1,94 +0,0 @@
import { glob } from 'fast-glob';
import path from 'path';
import {
type Application,
type ApplicationVariables,
} from 'twenty-shared/application';
import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from '@/cli/utilities/build/manifest/entities/entity-interface';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
const findApplicationConfigPath = async (appPath: string): Promise<string> => {
const files = await glob('**/application.config.ts', {
cwd: appPath,
ignore: ['**/node_modules/**', '**/.twenty/**', '**/dist/**'],
});
if (files.length === 0) {
throw new Error('Missing application.config.ts in your app');
}
if (files.length > 1) {
throw new Error(
`Multiple application.config.ts files found: ${files.join(', ')}. Only one is allowed.`,
);
}
return path.join(appPath, files[0]);
};
export class ApplicationEntityBuilder
implements ManifestEntityBuilder<Application>
{
async build(appPath: string): Promise<EntityBuildResult<Application>> {
const applicationConfigPath = await findApplicationConfigPath(appPath);
const { manifest: application } =
await manifestExtractFromFileServer.extractManifestFromFile<Application>(
applicationConfigPath,
);
const relativePath = path.relative(appPath, applicationConfigPath);
return { manifests: [application], filePaths: [relativePath] };
}
validate(applications: Application[], errors: ValidationError[]): void {
const application = applications[0];
if (!application) {
errors.push({
path: 'application',
message: 'Application config is required',
});
return;
}
if (!application.universalIdentifier) {
errors.push({
path: 'application',
message: 'Application must have a universalIdentifier',
});
}
}
findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] {
const seen = new Map<string, string[]>();
const application = manifest.application;
if (application?.universalIdentifier) {
seen.set(application.universalIdentifier, ['application']);
}
if (application?.applicationVariables) {
for (const [name, variable] of Object.entries(
application.applicationVariables,
) as [string, ApplicationVariables[string]][]) {
if (variable.universalIdentifier) {
const locations = seen.get(variable.universalIdentifier) ?? [];
locations.push(`application.variables.${name}`);
seen.set(variable.universalIdentifier, locations);
}
}
}
return Array.from(seen.entries())
.filter(([_, locations]) => locations.length > 1)
.map(([id, locations]) => ({ id, locations }));
}
}
export const applicationEntityBuilder = new ApplicationEntityBuilder();

View file

@ -1,25 +0,0 @@
import { glob } from 'fast-glob';
import path from 'path';
import { type AssetManifest, ASSETS_DIR } from 'twenty-shared/application';
import { type EntityBuildResult } from '@/cli/utilities/build/manifest/entities/entity-interface';
export class AssetEntityBuilder {
async build(appPath: string): Promise<EntityBuildResult<AssetManifest>> {
const assetFiles = await glob([`${ASSETS_DIR}/**/*`], {
cwd: appPath,
onlyFiles: true,
});
const manifests: AssetManifest[] = assetFiles.map((filePath) => ({
filePath,
fileName: path.basename(filePath),
fileType: path.extname(filePath).replace(/^\./, ''),
checksum: null,
}));
return { manifests, filePaths: assetFiles };
}
}
export const assetEntityBuilder = new AssetEntityBuilder();

View file

@ -1,23 +0,0 @@
import { type ApplicationManifest } from 'twenty-shared/application';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
export type EntityIdWithLocation = {
id: string;
locations: string[];
};
export type ManifestWithoutSources = Omit<
ApplicationManifest,
'sources' | 'packageJson' | 'yarnLock'
>;
export type EntityBuildResult<TManifest> = {
manifests: TManifest[];
filePaths: string[];
};
export type ManifestEntityBuilder<EntityManifest> = {
build(appPath: string): Promise<EntityBuildResult<EntityManifest>>;
validate(data: EntityManifest[], errors: ValidationError[]): void;
findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[];
};

View file

@ -1,108 +0,0 @@
import { glob } from 'fast-glob';
import { type FrontComponentManifest } from 'twenty-shared/application';
import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from '@/cli/utilities/build/manifest/entities/entity-interface';
type FrontComponentConfig = Omit<
FrontComponentManifest,
| 'sourceComponentPath'
| 'builtComponentPath'
| 'builtComponentChecksum'
| 'componentName'
> & {
component: { name: string };
};
export class FrontComponentEntityBuilder
implements ManifestEntityBuilder<FrontComponentManifest>
{
async build(
appPath: string,
): Promise<EntityBuildResult<FrontComponentManifest>> {
const componentFiles = await glob(['**/*.front-component.tsx'], {
cwd: appPath,
ignore: [
'**/node_modules/**',
'**/*.d.ts',
'**/dist/**',
'**/.twenty/**',
],
});
const manifests: FrontComponentManifest[] = [];
for (const filePath of componentFiles) {
try {
const absolutePath = `${appPath}/${filePath}`;
const { manifest: config } =
await manifestExtractFromFileServer.extractManifestFromFile<FrontComponentConfig>(
absolutePath,
);
const { component, ...rest } = config;
const builtComponentPath = this.computeBuiltComponentPath(filePath);
manifests.push({
...rest,
componentName: component.name,
sourceComponentPath: filePath,
builtComponentPath,
builtComponentChecksum: null,
});
} catch (error) {
throw new Error(
`Failed to load front component from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
return { manifests, filePaths: componentFiles };
}
private computeBuiltComponentPath(sourceComponentPath: string): string {
return sourceComponentPath.replace(/\.tsx?$/, '.mjs');
}
validate(
components: FrontComponentManifest[],
errors: ValidationError[],
): void {
for (const component of components) {
const componentPath = `front-components/${component.name ?? component.componentName ?? 'unknown'}`;
if (!component.universalIdentifier) {
errors.push({
path: componentPath,
message: 'Front component must have a universalIdentifier',
});
}
}
}
findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] {
const seen = new Map<string, string[]>();
const components = manifest.frontComponents ?? [];
for (const component of components) {
if (component.universalIdentifier) {
const location = `front-components/${component.name ?? component.componentName}`;
const locations = seen.get(component.universalIdentifier) ?? [];
locations.push(location);
seen.set(component.universalIdentifier, locations);
}
}
return Array.from(seen.entries())
.filter(([_, locations]) => locations.length > 1)
.map(([id, locations]) => ({ id, locations }));
}
}
export const frontComponentEntityBuilder = new FrontComponentEntityBuilder();

View file

@ -1,175 +0,0 @@
import { glob } from 'fast-glob';
import { type LogicFunctionManifest } from 'twenty-shared/application';
import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from '@/cli/utilities/build/manifest/entities/entity-interface';
type ExtractedFunctionManifest = Omit<
LogicFunctionManifest,
'sourceHandlerPath' | 'builtHandlerPath' | 'builtHandlerChecksum'
> & {
handler: string;
};
export class FunctionEntityBuilder
implements ManifestEntityBuilder<LogicFunctionManifest>
{
async build(
appPath: string,
): Promise<EntityBuildResult<LogicFunctionManifest>> {
const functionFiles = await glob(['**/*.function.ts'], {
cwd: appPath,
ignore: [
'**/node_modules/**',
'**/*.d.ts',
'**/dist/**',
'**/.twenty/**',
],
});
const manifests: LogicFunctionManifest[] = [];
for (const filePath of functionFiles) {
try {
const absolutePath = `${appPath}/${filePath}`;
const { manifest, exportName } =
await manifestExtractFromFileServer.extractManifestFromFile<ExtractedFunctionManifest>(
absolutePath,
);
const { handler: _, ...rest } = manifest;
// builtHandlerPath is computed from filePath (the .function.ts file)
// since that's what esbuild actually builds, not handlerPath
const builtHandlerPath = this.computeBuiltHandlerPath(filePath);
// For default exports, use 'default.handler'
// For named exports like 'export const anyName = ...', use 'anyName.handler'
const handlerName =
exportName !== null ? `${exportName}.handler` : 'default.handler';
manifests.push({
...rest,
handlerName,
sourceHandlerPath: filePath,
builtHandlerPath,
builtHandlerChecksum: null,
});
} catch (error) {
throw new Error(
`Failed to load function from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
return { manifests, filePaths: functionFiles };
}
private computeBuiltHandlerPath(sourceHandlerPath: string): string {
return sourceHandlerPath.replace(/\.tsx?$/, '.mjs');
}
validate(
functions: LogicFunctionManifest[],
errors: ValidationError[],
): void {
for (const fn of functions) {
const fnPath = `functions/${fn.name ?? fn.handlerName ?? 'unknown'}`;
if (!fn.universalIdentifier) {
errors.push({
path: fnPath,
message: 'Function must have a universalIdentifier',
});
}
for (const trigger of fn.triggers ?? []) {
const triggerPath = `${fnPath}.triggers.${trigger.type ?? 'unknown'}`;
if (!trigger.universalIdentifier) {
errors.push({
path: triggerPath,
message: 'Trigger must have a universalIdentifier',
});
}
if (!trigger.type) {
errors.push({
path: triggerPath,
message: 'Trigger must have a type',
});
continue;
}
switch (trigger.type) {
case 'route':
if (!trigger.path) {
errors.push({
path: triggerPath,
message: 'Route trigger must have a path',
});
}
if (!trigger.httpMethod) {
errors.push({
path: triggerPath,
message: 'Route trigger must have an httpMethod',
});
}
break;
case 'cron':
if (!trigger.pattern) {
errors.push({
path: triggerPath,
message: 'Cron trigger must have a pattern',
});
}
break;
case 'databaseEvent':
if (!trigger.eventName) {
errors.push({
path: triggerPath,
message: 'Database event trigger must have an eventName',
});
}
break;
}
}
}
}
findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] {
const seen = new Map<string, string[]>();
const functions = manifest.functions ?? [];
for (const fn of functions) {
if (fn.universalIdentifier) {
const location = `functions/${fn.name ?? fn.handlerName}`;
const locations = seen.get(fn.universalIdentifier) ?? [];
locations.push(location);
seen.set(fn.universalIdentifier, locations);
}
for (const trigger of fn.triggers ?? []) {
if (trigger.universalIdentifier) {
const location = `functions/${fn.name ?? fn.handlerName}.triggers.${trigger.type}`;
const locations = seen.get(trigger.universalIdentifier) ?? [];
locations.push(location);
seen.set(trigger.universalIdentifier, locations);
}
}
}
return Array.from(seen.entries())
.filter(([_, locations]) => locations.length > 1)
.map(([id, locations]) => ({ id, locations }));
}
}
export const functionEntityBuilder = new FunctionEntityBuilder();

View file

@ -1,182 +0,0 @@
import { glob } from 'fast-glob';
import { type ObjectExtensionManifest } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { isNonEmptyArray } from 'twenty-shared/utils';
import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from '@/cli/utilities/build/manifest/entities/entity-interface';
export class ObjectExtensionEntityBuilder
implements ManifestEntityBuilder<ObjectExtensionManifest>
{
async build(
appPath: string,
): Promise<EntityBuildResult<ObjectExtensionManifest>> {
const extensionFiles = await glob(['**/*.object-extension.ts'], {
cwd: appPath,
ignore: [
'**/node_modules/**',
'**/*.d.ts',
'**/dist/**',
'**/.twenty/**',
],
});
const manifests: ObjectExtensionManifest[] = [];
for (const filePath of extensionFiles) {
try {
const absolutePath = `${appPath}/${filePath}`;
const { manifest } =
await manifestExtractFromFileServer.extractManifestFromFile<ObjectExtensionManifest>(
absolutePath,
);
manifests.push(manifest);
} catch (error) {
throw new Error(
`Failed to load object extension from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
return { manifests, filePaths: extensionFiles };
}
validate(
extensions: ObjectExtensionManifest[],
errors: ValidationError[],
): void {
for (const ext of extensions) {
const targetName =
ext.targetObject?.nameSingular ??
ext.targetObject?.universalIdentifier ??
'unknown';
const extPath = `object-extensions/${targetName}`;
if (!ext.targetObject) {
errors.push({
path: extPath,
message: 'Object extension must have a targetObject',
});
continue;
}
const { nameSingular, universalIdentifier } = ext.targetObject;
if (!nameSingular && !universalIdentifier) {
errors.push({
path: extPath,
message:
'Object extension targetObject must have either nameSingular or universalIdentifier',
});
}
if (nameSingular && universalIdentifier) {
errors.push({
path: extPath,
message:
'Object extension targetObject cannot have both nameSingular and universalIdentifier',
});
}
if (!isNonEmptyArray(ext.fields)) {
errors.push({
path: extPath,
message: 'Object extension must have at least one field',
});
}
for (const field of ext.fields ?? []) {
const fieldPath = `${extPath}.fields.${field.label ?? 'unknown'}`;
if (!field.universalIdentifier) {
errors.push({
path: fieldPath,
message: 'Field must have a universalIdentifier',
});
}
if (!field.type) {
errors.push({
path: fieldPath,
message: 'Field must have a type',
});
}
if (!field.label) {
errors.push({
path: fieldPath,
message: 'Field must have a label',
});
}
if (
(field.type === FieldMetadataType.SELECT ||
field.type === FieldMetadataType.MULTI_SELECT) &&
!isNonEmptyArray(field.options)
) {
errors.push({
path: fieldPath,
message: 'SELECT/MULTI_SELECT field must have options',
});
}
}
}
}
findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] {
const extensions = manifest.objectExtensions ?? [];
const objects = manifest.objects ?? [];
const objectFieldIds = new Map<string, string>();
for (const obj of objects) {
for (const field of obj.fields ?? []) {
if (field.universalIdentifier) {
const location = `objects/${obj.nameSingular}.fields.${field.label}`;
objectFieldIds.set(field.universalIdentifier, location);
}
}
}
const extensionFieldLocations = new Map<string, string[]>();
for (const ext of extensions) {
const targetName =
ext.targetObject?.nameSingular ??
ext.targetObject?.universalIdentifier ??
'unknown';
for (const field of ext.fields ?? []) {
if (field.universalIdentifier) {
const location = `object-extensions/${targetName}.fields.${field.label}`;
const locations =
extensionFieldLocations.get(field.universalIdentifier) ?? [];
locations.push(location);
extensionFieldLocations.set(field.universalIdentifier, locations);
}
}
}
const duplicates: EntityIdWithLocation[] = [];
for (const [id, extLocations] of extensionFieldLocations.entries()) {
const objectLocation = objectFieldIds.get(id);
if (extLocations.length > 1 || objectLocation) {
const allLocations = objectLocation
? [objectLocation, ...extLocations]
: extLocations;
duplicates.push({ id, locations: allLocations });
}
}
return duplicates;
}
}
export const objectExtensionEntityBuilder = new ObjectExtensionEntityBuilder();

View file

@ -1,140 +0,0 @@
import { glob } from 'fast-glob';
import { type ObjectManifest } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { isNonEmptyArray } from 'twenty-shared/utils';
import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from '@/cli/utilities/build/manifest/entities/entity-interface';
export class ObjectEntityBuilder
implements ManifestEntityBuilder<ObjectManifest>
{
async build(appPath: string): Promise<EntityBuildResult<ObjectManifest>> {
const objectFiles = await glob(['**/*.object.ts'], {
cwd: appPath,
ignore: [
'**/node_modules/**',
'**/*.d.ts',
'**/dist/**',
'**/.twenty/**',
],
});
const manifests: ObjectManifest[] = [];
for (const filePath of objectFiles) {
try {
const absolutePath = `${appPath}/${filePath}`;
const { manifest } =
await manifestExtractFromFileServer.extractManifestFromFile<ObjectManifest>(
absolutePath,
);
manifests.push(manifest);
} catch (error) {
throw new Error(
`Failed to load object from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
return { manifests, filePaths: objectFiles };
}
validate(objects: ObjectManifest[], errors: ValidationError[]): void {
for (const obj of objects) {
const objPath = `objects/${obj.nameSingular ?? 'unknown'}`;
if (!obj.universalIdentifier) {
errors.push({
path: objPath,
message: 'Object must have a universalIdentifier',
});
}
if (!obj.nameSingular) {
errors.push({
path: objPath,
message: 'Object must have a nameSingular',
});
}
if (!obj.namePlural) {
errors.push({
path: objPath,
message: 'Object must have a namePlural',
});
}
for (const field of obj.fields ?? []) {
const fieldPath = `${objPath}.fields.${field.label ?? 'unknown'}`;
if (!field.universalIdentifier) {
errors.push({
path: fieldPath,
message: 'Field must have a universalIdentifier',
});
}
if (!field.type) {
errors.push({
path: fieldPath,
message: 'Field must have a type',
});
}
if (!field.label) {
errors.push({
path: fieldPath,
message: 'Field must have a label',
});
}
if (
(field.type === FieldMetadataType.SELECT ||
field.type === FieldMetadataType.MULTI_SELECT) &&
!isNonEmptyArray(field.options)
) {
errors.push({
path: fieldPath,
message: 'SELECT/MULTI_SELECT field must have options',
});
}
}
}
}
findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] {
const seen = new Map<string, string[]>();
const objects = manifest.objects ?? [];
for (const obj of objects) {
if (obj.universalIdentifier) {
const location = `objects/${obj.nameSingular}`;
const locations = seen.get(obj.universalIdentifier) ?? [];
locations.push(location);
seen.set(obj.universalIdentifier, locations);
}
for (const field of obj.fields ?? []) {
if (field.universalIdentifier) {
const location = `objects/${obj.nameSingular}.fields.${field.label}`;
const locations = seen.get(field.universalIdentifier) ?? [];
locations.push(location);
seen.set(field.universalIdentifier, locations);
}
}
}
return Array.from(seen.entries())
.filter(([_, locations]) => locations.length > 1)
.map(([id, locations]) => ({ id, locations }));
}
}
export const objectEntityBuilder = new ObjectEntityBuilder();

View file

@ -1,85 +0,0 @@
import { glob } from 'fast-glob';
import { type RoleManifest } from 'twenty-shared/application';
import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server';
import { type ValidationError } from '@/cli/utilities/build/manifest/manifest-types';
import {
type EntityBuildResult,
type EntityIdWithLocation,
type ManifestEntityBuilder,
type ManifestWithoutSources,
} from '@/cli/utilities/build/manifest/entities/entity-interface';
export class RoleEntityBuilder implements ManifestEntityBuilder<RoleManifest> {
async build(appPath: string): Promise<EntityBuildResult<RoleManifest>> {
const roleFiles = await glob(['**/*.role.ts'], {
cwd: appPath,
ignore: [
'**/node_modules/**',
'**/*.d.ts',
'**/dist/**',
'**/.twenty/**',
],
});
const manifests: RoleManifest[] = [];
for (const filePath of roleFiles) {
try {
const absolutePath = `${appPath}/${filePath}`;
const { manifest } =
await manifestExtractFromFileServer.extractManifestFromFile<RoleManifest>(
absolutePath,
);
manifests.push(manifest);
} catch (error) {
throw new Error(
`Failed to load role from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
return { manifests, filePaths: roleFiles };
}
validate(roles: RoleManifest[], errors: ValidationError[]): void {
for (const role of roles) {
const rolePath = `roles/${role.label ?? 'unknown'}`;
if (!role.universalIdentifier) {
errors.push({
path: rolePath,
message: 'Role must have a universalIdentifier',
});
}
if (!role.label) {
errors.push({
path: rolePath,
message: 'Role must have a label',
});
}
}
}
findDuplicates(manifest: ManifestWithoutSources): EntityIdWithLocation[] {
const seen = new Map<string, string[]>();
const roles = manifest.roles ?? [];
for (const role of roles) {
if (role.universalIdentifier) {
const location = `roles/${role.label}`;
const locations = seen.get(role.universalIdentifier) ?? [];
locations.push(location);
seen.set(role.universalIdentifier, locations);
}
}
return Array.from(seen.entries())
.filter(([_, locations]) => locations.length > 1)
.map(([id, locations]) => ({ id, locations }));
}
}
export const roleEntityBuilder = new RoleEntityBuilder();

View file

@ -1,44 +1,54 @@
import { findPathFile } from '@/cli/utilities/file/file-find';
import { parseJsoncFile } from '@/cli/utilities/file/file-jsonc';
import { glob } from 'fast-glob';
import * as fs from 'fs-extra';
import { basename, extname, relative, sep } from 'path';
import { readFile } from 'fs-extra';
import { relative, sep } from 'path';
import { glob } from 'fast-glob';
import {
type EntityFilePaths,
extractDefineEntity,
ManifestEntityKey,
TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING,
} from '@/cli/utilities/build/manifest/manifest-extract-config';
import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file';
import {
type ApplicationManifest,
OUTPUT_DIR,
type Manifest,
type AssetManifest,
ASSETS_DIR,
type FieldManifest,
type FrontComponentManifest,
type LogicFunctionManifest,
type ObjectManifest,
type RoleManifest,
} from 'twenty-shared/application';
import { FileFolder, type Sources } from 'twenty-shared/types';
import { applicationEntityBuilder } from '@/cli/utilities/build/manifest/entities/application';
import { assetEntityBuilder } from '@/cli/utilities/build/manifest/entities/asset';
import { frontComponentEntityBuilder } from '@/cli/utilities/build/manifest/entities/front-component';
import { functionEntityBuilder } from '@/cli/utilities/build/manifest/entities/function';
import { objectEntityBuilder } from '@/cli/utilities/build/manifest/entities/object';
import { objectExtensionEntityBuilder } from '@/cli/utilities/build/manifest/entities/object-extension';
import { roleEntityBuilder } from '@/cli/utilities/build/manifest/entities/role';
import { parseJsoncFile } from '@/cli/utilities/file/file-jsonc';
import { findPathFile } from '@/cli/utilities/file/file-find';
import { assertUnreachable } from 'twenty-shared/utils';
import { type FrontComponentConfig, type LogicFunctionConfig } from '@/sdk';
import type { Sources } from 'twenty-shared/types';
import * as fs from 'fs-extra';
import { manifestExtractFromFileServer } from './manifest-extract-from-file-server';
export type EntityFilePaths = {
application: string[];
objects: string[];
objectExtensions: string[];
functions: string[];
frontComponents: string[];
roles: string[];
assets: string[];
};
const loadSources = async (appPath: string): Promise<Sources> => {
const sources: Sources = {};
const tsFiles = await glob(['**/*.ts', '**/*.tsx'], {
const loadSources = async (appPath: string): Promise<string[]> => {
return await glob(['**/*.ts', '**/*.tsx'], {
cwd: appPath,
absolute: true,
ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**', '**/.twenty/**'],
onlyFiles: true,
});
};
for (const filepath of tsFiles) {
const loadAssets = async (appPath: string) => {
return await glob([`${ASSETS_DIR}/**/*`], {
cwd: appPath,
onlyFiles: true,
});
};
const computeSources = async (
appPath: string,
sourceFilePaths: string[],
): Promise<Sources> => {
const sources: Sources = {};
for (const filepath of sourceFilePaths) {
const relPath = relative(appPath, filepath);
const parts = relPath.split(sep);
const content = await fs.readFile(filepath, 'utf8');
@ -58,165 +68,190 @@ const loadSources = async (appPath: string): Promise<Sources> => {
return sources;
};
export const EMPTY_FILE_PATHS: EntityFilePaths = {
application: [],
objects: [],
objectExtensions: [],
functions: [],
frontComponents: [],
roles: [],
assets: [],
};
export type ManifestBuildResult = {
manifest: ApplicationManifest | null;
export const buildManifest = async (
appPath: string,
): Promise<{
manifest: Manifest | null;
filePaths: EntityFilePaths;
error?: string;
};
errors: string[];
}> => {
const filePaths = await loadSources(appPath);
const errors: string[] = [];
export type UpdateManifestChecksumParams = {
manifest: ApplicationManifest;
builtFileInfos: Map<
string,
{ checksum: string; builtPath: string; fileFolder: FileFolder }
>;
};
let application: ApplicationManifest | undefined;
const objects: ObjectManifest[] = [];
const fields: FieldManifest[] = [];
const roles: RoleManifest[] = [];
const logicFunctions: LogicFunctionManifest[] = [];
const frontComponents: FrontComponentManifest[] = [];
const publicAssets: AssetManifest[] = [];
export const updateManifestChecksum = ({
manifest,
builtFileInfos,
}: UpdateManifestChecksumParams): ApplicationManifest => {
let result = structuredClone(manifest);
for (const [
builtPath,
{ fileFolder, checksum },
] of builtFileInfos.entries()) {
const rootBuiltPath = relative(OUTPUT_DIR, builtPath);
if (fileFolder === FileFolder.BuiltFunction) {
const functions = result.functions ?? [];
const fnIndex = functions.findIndex(
(f) => f.builtHandlerPath === rootBuiltPath,
);
if (fnIndex === -1) {
continue;
}
result = {
...result,
functions: functions.map((fn, index) =>
index === fnIndex ? { ...fn, builtHandlerChecksum: checksum } : fn,
),
};
}
const applicationFilePaths: string[] = [];
const objectsFilePaths: string[] = [];
const fieldsFilePaths: string[] = [];
const rolesFilePaths: string[] = [];
const logicFunctionsFilePaths: string[] = [];
const frontComponentsFilePaths: string[] = [];
const publicAssetsFilePaths: string[] = [];
if (fileFolder === FileFolder.PublicAsset) {
const assets = result.publicAssets ?? [];
const assetIndex = assets.findIndex((a) => a.filePath === rootBuiltPath);
if (assetIndex === -1) {
continue;
}
result = {
...result,
publicAssets: assets.map((asset, index) =>
index === assetIndex ? { ...asset, checksum } : asset,
),
};
for (const filePath of filePaths) {
const fileContent = await readFile(filePath, 'utf-8');
const relativePath = relative(appPath, filePath);
const targetFunctionName = extractDefineEntity(fileContent);
if (!targetFunctionName) {
continue;
}
if (fileFolder === FileFolder.BuiltFrontComponent) {
const frontComponents = result.frontComponents ?? [];
const componentIndex =
frontComponents.findIndex(
(c) => c.builtComponentPath === rootBuiltPath,
) ?? -1;
if (componentIndex === -1) {
continue;
const entity = TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING[targetFunctionName];
switch (entity) {
case ManifestEntityKey.Application: {
const extract = await extractManifestFromFile<ApplicationManifest>({
appPath,
filePath,
});
application = extract.config;
errors.push(...extract.errors);
applicationFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.Objects: {
const extract = await extractManifestFromFile<ObjectManifest>({
appPath,
filePath,
});
objects.push(extract.config);
errors.push(...extract.errors);
objectsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.Fields: {
const extract = await extractManifestFromFile<FieldManifest>({
appPath,
filePath,
});
fields.push(extract.config);
errors.push(...extract.errors);
fieldsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.Roles: {
const extract = await extractManifestFromFile<RoleManifest>({
appPath,
filePath,
});
roles.push(extract.config);
errors.push(...extract.errors);
rolesFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.LogicFunctions: {
const extract = await extractManifestFromFile<LogicFunctionConfig>({
appPath,
filePath,
});
errors.push(...extract.errors);
const { handler: _, ...rest } = extract.config;
const config: LogicFunctionManifest = {
...rest,
handlerName: 'default.handler',
sourceHandlerPath: filePath,
builtHandlerPath: filePath.replace(/\.tsx?$/, '.mjs'),
builtHandlerChecksum: null,
};
logicFunctions.push(config);
logicFunctionsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.FrontComponents: {
const extract = await extractManifestFromFile<FrontComponentConfig>({
appPath,
filePath,
});
errors.push(...extract.errors);
const { component, ...rest } = extract.config;
const config: FrontComponentManifest = {
...rest,
componentName: component.name,
sourceComponentPath: filePath,
builtComponentPath: filePath.replace(/\.tsx?$/, '.mjs'),
builtComponentChecksum: null,
};
frontComponents.push(config);
frontComponentsFilePaths.push(relativePath);
break;
}
case ManifestEntityKey.PublicAssets: {
// Public assets are handled below
break;
}
default: {
assertUnreachable(entity);
}
result = {
...result,
frontComponents: frontComponents.map((component, index) =>
index === componentIndex
? { ...component, builtComponentChecksum: checksum }
: component,
),
};
}
}
return result;
};
export const runManifestBuild = async (
appPath: string,
): Promise<ManifestBuildResult> => {
try {
manifestExtractFromFileServer.init(appPath);
const assetFiles = await loadAssets(appPath);
const packageJson = await parseJsoncFile(
await findPathFile(appPath, 'package.json'),
);
const yarnLock = await readFile(
await findPathFile(appPath, 'yarn.lock'),
'utf8',
);
const [
applicationBuildResult,
objectBuildResult,
objectExtensionBuildResult,
functionBuildResult,
frontComponentBuildResult,
roleBuildResult,
assetBuildResult,
sources,
] = await Promise.all([
applicationEntityBuilder.build(appPath),
objectEntityBuilder.build(appPath),
objectExtensionEntityBuilder.build(appPath),
functionEntityBuilder.build(appPath),
frontComponentEntityBuilder.build(appPath),
roleEntityBuilder.build(appPath),
assetEntityBuilder.build(appPath),
loadSources(appPath),
]);
const application = applicationBuildResult.manifests[0];
const objectManifests = objectBuildResult.manifests;
const objectExtensionManifests = objectExtensionBuildResult.manifests;
const functionManifests = functionBuildResult.manifests;
const frontComponentManifests = frontComponentBuildResult.manifests;
const roleManifests = roleBuildResult.manifests;
const assetManifests = assetBuildResult.manifests;
const filePaths: EntityFilePaths = {
application: applicationBuildResult.filePaths,
objects: objectBuildResult.filePaths,
objectExtensions: objectExtensionBuildResult.filePaths,
functions: functionBuildResult.filePaths,
frontComponents: frontComponentBuildResult.filePaths,
roles: roleBuildResult.filePaths,
assets: assetBuildResult.filePaths,
};
const manifest: ApplicationManifest = {
application,
objects: objectManifests,
objectExtensions: objectExtensionManifests,
functions: functionManifests,
frontComponents: frontComponentManifests,
roles: roleManifests,
publicAssets: assetManifests,
sources,
packageJson,
yarnLock,
};
return { manifest, filePaths };
} catch (error) {
return {
manifest: null,
filePaths: EMPTY_FILE_PATHS,
error: error instanceof Error ? error.message : `${error}`,
};
for (const assetFile of assetFiles) {
publicAssets.push({
filePath: assetFile,
fileName: basename(assetFile),
fileType: extname(assetFile).replace(/^\./, ''),
checksum: null,
});
publicAssetsFilePaths.push(relative(appPath, assetFile));
}
if (!application) {
errors.push(
'Cannot build application, please export default defineApplication() to define an application',
);
}
const packageJson = await parseJsoncFile(
await findPathFile(appPath, 'package.json'),
);
const yarnLock = await readFile(
await findPathFile(appPath, 'yarn.lock'),
'utf8',
);
const manifest = !application
? null
: {
application,
objects,
fields,
roles,
logicFunctions,
frontComponents,
publicAssets,
sources: await computeSources(appPath, filePaths),
packageJson,
yarnLock,
};
const entityFilePaths: EntityFilePaths = {
application: applicationFilePaths,
objects: objectsFilePaths,
fields: fieldsFilePaths,
roles: rolesFilePaths,
logicFunctions: logicFunctionsFilePaths,
frontComponents: frontComponentsFilePaths,
publicAssets: publicAssetsFilePaths,
};
return { manifest, filePaths: entityFilePaths, errors };
};

View file

@ -0,0 +1,84 @@
import path from 'path';
import * as fs from 'fs-extra';
import { createRequire } from 'module';
import * as esbuild from 'esbuild';
import os from 'os';
import { isDefined, isPlainObject } from 'twenty-shared/utils';
import { type ValidationResult } from '@/sdk';
export const extractManifestFromFile = async <T>({
filePath,
appPath,
}: {
filePath: string;
appPath: string;
}): Promise<ValidationResult<T>> => {
const module = await loadModule({ filePath, appPath });
return extractDefaultConfigFromModuleOrThrow<T>(module, filePath);
};
const loadModule = async ({
filePath,
appPath,
}: {
filePath: string;
appPath: string;
}): Promise<Record<string, unknown>> => {
const tsconfigPath = path.join(appPath, 'tsconfig.json');
const hasTsconfig = await fs.pathExists(tsconfigPath);
// Resolve react from the app's node_modules for the alias
const appRequire = createRequire(path.join(appPath, 'package.json'));
let reactPath: string | undefined;
let reactDomPath: string | undefined;
try {
reactPath = path.dirname(appRequire.resolve('react/package.json'));
reactDomPath = path.dirname(appRequire.resolve('react-dom/package.json'));
} catch {
// React not installed in app, will be bundled if used
}
const result = await esbuild.build({
entryPoints: [filePath],
bundle: true,
write: false,
format: 'cjs',
platform: 'node',
target: 'node18',
jsx: 'automatic',
tsconfig: hasTsconfig ? tsconfigPath : undefined,
alias: {
...(reactPath && { react: reactPath }),
...(reactDomPath && { 'react-dom': reactDomPath }),
},
logLevel: 'silent',
});
const code = result.outputFiles[0].text;
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'twenty-manifest-'));
const tempFile = path.join(tempDir, 'module.cjs');
try {
await fs.writeFile(tempFile, code);
return require(tempFile) as Record<string, unknown>;
} finally {
await fs.remove(tempDir);
}
};
const extractDefaultConfigFromModuleOrThrow = <T>(
module: Record<string, unknown>,
filePath: string,
): ValidationResult<T> => {
if (isDefined(module.default) && isPlainObject(module.default)) {
return module.default as ValidationResult<T>;
}
throw new Error(
`Config file ${filePath} must export a config object default export`,
);
};

View file

@ -0,0 +1,82 @@
import * as ts from 'typescript';
export enum TargetFunction {
DefineApplication = 'defineApplication',
DefineField = 'defineField',
DefineLogicFunction = 'defineLogicFunction',
DefineObject = 'defineObject',
DefineRole = 'defineRole',
DefineFrontComponent = 'defineFrontComponent',
}
export enum ManifestEntityKey {
Application = 'application',
Fields = 'fields',
LogicFunctions = 'logicFunctions',
Objects = 'objects',
Roles = 'roles',
FrontComponents = 'frontComponents',
PublicAssets = 'publicAssets',
}
export type EntityFilePaths = Record<ManifestEntityKey, string[]>;
export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
TargetFunction,
ManifestEntityKey
> = {
[TargetFunction.DefineApplication]: ManifestEntityKey.Application,
[TargetFunction.DefineField]: ManifestEntityKey.Fields,
[TargetFunction.DefineLogicFunction]: ManifestEntityKey.LogicFunctions,
[TargetFunction.DefineObject]: ManifestEntityKey.Objects,
[TargetFunction.DefineRole]: ManifestEntityKey.Roles,
[TargetFunction.DefineFrontComponent]: ManifestEntityKey.FrontComponents,
};
const computeIsTargetFunctionCall = (node: ts.Node): string | undefined => {
if (!ts.isCallExpression(node)) {
return undefined;
}
const expression = node.expression;
if (ts.isIdentifier(expression)) {
if ((Object.values(TargetFunction) as string[]).includes(expression.text)) {
return expression.text;
}
}
return undefined;
};
export const extractDefineEntity = (
fileContent: string,
): TargetFunction | undefined => {
const sourceFile = ts.createSourceFile(
'temp.ts',
fileContent,
ts.ScriptTarget.Latest,
true,
);
const children: ts.Node[] = [];
ts.forEachChild(sourceFile, (node) => {
children.push(node);
});
for (const node of children) {
if (ts.isExportAssignment(node)) {
if (node.isExportEquals || !node.expression) {
return;
}
const targetFunction = computeIsTargetFunctionCall(node.expression);
if (targetFunction) {
return targetFunction as TargetFunction;
}
}
}
return;
};

View file

@ -1,115 +0,0 @@
import * as esbuild from 'esbuild';
import * as fs from 'fs-extra';
import { createRequire } from 'module';
import * as os from 'os';
import path from 'path';
import { isDefined, isPlainObject } from 'twenty-shared/utils';
export type ExtractedManifest<TManifest> = {
manifest: TManifest;
exportName: string | null;
};
export class ManifestExtractFromFileServer {
private appPath: string | null = null;
init(appPath: string): void {
this.appPath = appPath;
}
async extractManifestFromFile<TManifest>(
filepath: string,
): Promise<ExtractedManifest<TManifest>> {
if (!this.appPath) {
throw new Error(
'ManifestExtractFromFileServer not initialized. Call init(appPath) first.',
);
}
const module = await this.loadModule(filepath);
const result = this.extractConfigFromModule<TManifest>(module);
if (!result) {
throw new Error(
`Config file ${filepath} must export a config object (default export or any named object export)`,
);
}
return result;
}
private async loadModule(filepath: string): Promise<Record<string, unknown>> {
if (!this.appPath) {
throw new Error(
'ManifestExtractFromFileServer not initialized. Call init(appPath) first.',
);
}
const tsconfigPath = path.join(this.appPath, 'tsconfig.json');
const hasTsconfig = await fs.pathExists(tsconfigPath);
// Resolve react from the app's node_modules for the alias
const appRequire = createRequire(path.join(this.appPath, 'package.json'));
let reactPath: string | undefined;
let reactDomPath: string | undefined;
try {
reactPath = path.dirname(appRequire.resolve('react/package.json'));
reactDomPath = path.dirname(appRequire.resolve('react-dom/package.json'));
} catch {
// React not installed in app, will be bundled if used
}
const result = await esbuild.build({
entryPoints: [filepath],
bundle: true,
write: false,
format: 'cjs',
platform: 'node',
target: 'node18',
jsx: 'automatic',
tsconfig: hasTsconfig ? tsconfigPath : undefined,
// Use alias to resolve react from app's node_modules
alias: {
...(reactPath && { react: reactPath }),
...(reactDomPath && { 'react-dom': reactDomPath }),
},
logLevel: 'silent',
});
const code = result.outputFiles[0].text;
const tempDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'twenty-manifest-'),
);
const tempFile = path.join(tempDir, 'module.cjs');
try {
await fs.writeFile(tempFile, code);
return require(tempFile) as Record<string, unknown>;
} finally {
await fs.remove(tempDir);
}
}
private extractConfigFromModule<T>(
module: Record<string, unknown>,
): ExtractedManifest<T> | undefined {
if (isDefined(module.default) && isPlainObject(module.default)) {
return { manifest: module.default as T, exportName: null };
}
for (const [key, value] of Object.entries(module)) {
if (isPlainObject(value)) {
return { manifest: value as T, exportName: key };
}
}
return undefined;
}
}
export const manifestExtractFromFileServer =
new ManifestExtractFromFileServer();

View file

@ -2,14 +2,3 @@ export type ValidationError = {
path: string;
message: string;
};
export type ValidationWarning = {
path?: string;
message: string;
};
export type ValidationResult = {
isValid: boolean;
errors: ValidationError[];
warnings: ValidationWarning[];
};

View file

@ -1,75 +0,0 @@
import { isNonEmptyArray } from 'twenty-shared/utils';
import { applicationEntityBuilder } from './entities/application';
import {
type EntityIdWithLocation,
type ManifestWithoutSources,
} from '@/cli/utilities/build/manifest/entities/entity-interface';
import { frontComponentEntityBuilder } from './entities/front-component';
import { functionEntityBuilder } from './entities/function';
import { objectEntityBuilder } from './entities/object';
import { objectExtensionEntityBuilder } from './entities/object-extension';
import { roleEntityBuilder } from './entities/role';
import {
type ValidationError,
type ValidationResult,
type ValidationWarning,
} from '@/cli/utilities/build/manifest/manifest-types';
const collectAllDuplicates = (
manifest: ManifestWithoutSources,
): EntityIdWithLocation[] => {
return [
...applicationEntityBuilder.findDuplicates(manifest),
...objectEntityBuilder.findDuplicates(manifest),
...objectExtensionEntityBuilder.findDuplicates(manifest),
...functionEntityBuilder.findDuplicates(manifest),
...roleEntityBuilder.findDuplicates(manifest),
...frontComponentEntityBuilder.findDuplicates(manifest),
];
};
export const validateManifest = (
manifest: ManifestWithoutSources,
): ValidationResult => {
const errors: ValidationError[] = [];
const warnings: ValidationWarning[] = [];
applicationEntityBuilder.validate(
manifest.application ? [manifest.application] : [],
errors,
);
objectEntityBuilder.validate(manifest.objects ?? [], errors);
objectExtensionEntityBuilder.validate(
manifest.objectExtensions ?? [],
errors,
);
functionEntityBuilder.validate(manifest.functions ?? [], errors);
roleEntityBuilder.validate(manifest.roles ?? [], errors);
frontComponentEntityBuilder.validate(manifest.frontComponents ?? [], errors);
const duplicates = collectAllDuplicates(manifest);
for (const dup of duplicates) {
errors.push({
path: dup.locations.join(', '),
message: `Duplicate universalIdentifier: ${dup.id}`,
});
}
if (!isNonEmptyArray(manifest.objects)) {
warnings.push({
message: 'No objects defined',
});
}
if (!isNonEmptyArray(manifest.functions)) {
warnings.push({
message: 'No functions defined',
});
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
};

View file

@ -1,13 +1,10 @@
import * as fs from 'fs-extra';
import path from 'path';
import {
type ApplicationManifest,
OUTPUT_DIR,
} from 'twenty-shared/application';
import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application';
export const writeManifestToOutput = async (
appPath: string,
manifest: ApplicationManifest,
manifest: Manifest,
): Promise<string> => {
const outputDir = path.join(appPath, OUTPUT_DIR);
await fs.ensureDir(outputDir);

View file

@ -0,0 +1,82 @@
import { relative } from 'path';
import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
import type { EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config';
export type ManifestBuildResult = {
manifest: Manifest | null;
filePaths: EntityFilePaths;
error?: string;
};
export type UpdateManifestChecksumParams = {
manifest: Manifest;
builtFileInfos: Map<
string,
{ checksum: string; builtPath: string; fileFolder: FileFolder }
>;
};
export const updateManifestChecksums = ({
manifest,
builtFileInfos,
}: UpdateManifestChecksumParams): Manifest => {
let result = structuredClone(manifest);
for (const [
builtPath,
{ fileFolder, checksum },
] of builtFileInfos.entries()) {
const rootBuiltPath = relative(OUTPUT_DIR, builtPath);
if (fileFolder === FileFolder.BuiltLogicFunction) {
const logicFunctions = result.logicFunctions;
const fnIndex = logicFunctions.findIndex(
(f) => f.builtHandlerPath === rootBuiltPath,
);
if (fnIndex === -1) {
continue;
}
result = {
...result,
logicFunctions: logicFunctions.map((fn, index) =>
index === fnIndex ? { ...fn, builtHandlerChecksum: checksum } : fn,
),
};
}
if (fileFolder === FileFolder.PublicAsset) {
const assets = result.publicAssets;
const assetIndex = assets.findIndex((a) => a.filePath === rootBuiltPath);
if (assetIndex === -1) {
continue;
}
result = {
...result,
publicAssets: assets.map((asset, index) =>
index === assetIndex ? { ...asset, checksum } : asset,
),
};
continue;
}
if (fileFolder === FileFolder.BuiltFrontComponent) {
const frontComponents = result.frontComponents;
const componentIndex =
frontComponents.findIndex(
(c) => c.builtComponentPath === rootBuiltPath,
) ?? -1;
if (componentIndex === -1) {
continue;
}
result = {
...result,
frontComponents: frontComponents.map((component, index) =>
index === componentIndex
? { ...component, builtComponentChecksum: checksum }
: component,
),
};
}
}
return result;
};

View file

@ -0,0 +1,60 @@
import { type Manifest } from 'twenty-shared/application';
import { isNonEmptyArray } from 'twenty-shared/utils';
const extractDuplicates = (values: string[]): string[] => {
const seen = new Set<string>();
const duplicates = new Set<string>();
for (const value of values) {
if (seen.has(value)) {
duplicates.add(value);
} else {
seen.add(value);
}
}
return Array.from(duplicates);
};
const findUniversalIdentifiers = (obj: object): string[] => {
const universalIdentifiers: string[] = [];
if (!obj) {
return [];
}
for (const [key, val] of Object.entries(obj)) {
if (key === 'universalIdentifier' && typeof val === 'string') {
universalIdentifiers.push(val);
}
if (typeof val === 'object') {
universalIdentifiers.push(...findUniversalIdentifiers(val));
}
}
return universalIdentifiers;
};
export const validateManifest = (manifest: Manifest) => {
const errors: string[] = [];
const warnings: string[] = [];
const duplicates = extractDuplicates(findUniversalIdentifiers(manifest));
if (duplicates.length > 0) {
errors.push(`Duplicate universal identifiers: ${duplicates.join(', ')}`);
}
if (!isNonEmptyArray(manifest.objects)) {
warnings.push('No object defined');
}
if (!isNonEmptyArray(manifest.logicFunctions)) {
warnings.push('No logic function defined');
}
if (!isNonEmptyArray(manifest.frontComponents)) {
warnings.push('No front component defined');
}
return { errors, warnings, isValid: errors.length === 0 };
};

View file

@ -1,16 +1,16 @@
import {
type ManifestBuildResult,
runManifestBuild,
updateManifestChecksum,
} from '@/cli/utilities/build/manifest/manifest-build';
updateManifestChecksums,
} from '@/cli/utilities/build/manifest/update-manifest-checksums';
import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer';
import { ApiService } from '@/cli/utilities/api/api-service';
import { FileUploader } from '@/cli/utilities/file/file-uploader';
import { type FileFolder } from 'twenty-shared/types';
import { validateManifest } from '@/cli/utilities/build/manifest/manifest-validate';
import type { Location } from 'esbuild';
import { type DevUiStateManager } from '@/cli/utilities/dev/dev-ui-state-manager';
import { type EventName } from 'chokidar/handler.js';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
import { validateManifest } from '@/cli/utilities/build/manifest/validate-manifest';
export type DevModeOrchestratorOptions = {
appPath: string;
@ -232,21 +232,38 @@ export class DevModeOrchestrator {
manifestStatus: 'building',
});
const result = await runManifestBuild(this.appPath);
const result = await buildManifest(this.appPath);
if (result.error || !result.manifest) {
if (result.errors.length > 0 || !result.manifest) {
for (const error of result.errors) {
this.uiStateManager.addEvent({
message: error,
status: 'error',
});
}
this.uiStateManager.updateManifestState({
manifestStatus: 'error',
});
this.uiStateManager.addEvent({
message: result.error ?? 'Unknown error',
status: 'error',
error: result.errors[result.errors.length - 1],
});
return;
}
const validation = validateManifest(result.manifest);
if (!validation.isValid) {
for (const e of validation.errors) {
this.uiStateManager.addEvent({
message: e,
status: 'error',
});
this.uiStateManager.updateManifestState({
manifestStatus: 'error',
error: e,
});
}
return;
}
this.uiStateManager.updateManifestState({
appName: result.manifest.application.displayName,
});
@ -255,25 +272,10 @@ export class DevModeOrchestrator {
manifestFilePaths: result.filePaths,
});
if (!validation.isValid) {
for (const e of validation.errors) {
this.uiStateManager.addEvent({
message: `${e.path}: ${e.message}`,
status: 'error',
});
this.uiStateManager.updateManifestState({
manifestStatus: 'error',
});
}
return;
}
if (validation.warnings.length > 0) {
for (const warning of validation.warnings) {
const path = warning.path ? `${warning.path}: ` : '';
this.uiStateManager.addEvent({
message: `${path}${warning.message}`,
message: `${warning}`,
status: 'warning',
});
}
@ -304,7 +306,7 @@ export class DevModeOrchestrator {
await Promise.all(this.activeUploads);
}
const manifest = updateManifestChecksum({
const manifest = updateManifestChecksums({
manifest: result.manifest,
builtFileInfos: this.builtFileInfos,
});
@ -344,7 +346,7 @@ export class DevModeOrchestrator {
});
} else {
this.uiStateManager.addEvent({
message: `Sync failed: ${JSON.stringify(syncResult.error, null, 2)}`,
message: `Sync failed with error ${JSON.stringify(syncResult.error, null, 2)}`,
status: 'error',
});
this.uiStateManager.updateManifestState({
@ -353,7 +355,7 @@ export class DevModeOrchestrator {
}
} catch (error) {
this.uiStateManager.addEvent({
message: `Sync failed: ${JSON.stringify(error, null, 2)}`,
message: `Sync failed with error ${JSON.stringify(error, null, 2)}`,
status: 'error',
});
this.uiStateManager.updateManifestState({

View file

@ -6,7 +6,7 @@ import {
type UiEvent,
type DevUiState,
} from '@/cli/utilities/dev/dev-ui-state';
import { type EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-build';
import type { EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config';
const MAX_EVENT_NUMBER = 200;
@ -92,7 +92,7 @@ export class DevUiStateManager {
...this.state,
...(manifestStatus ? { manifestStatus } : {}),
...(appName ? { appName } : {}),
...(error ? { error } : {}),
...(error ? { error } : { error: undefined }),
};
this.notify();
@ -104,16 +104,14 @@ export class DevUiStateManager {
switch (entityType) {
case 'objects':
return SyncableEntity.Object;
case 'objectExtensions':
return SyncableEntity.ObjectExtension;
case 'functions':
return SyncableEntity.Function;
case 'fields':
return SyncableEntity.Field;
case 'logicFunctions':
return SyncableEntity.LogicFunction;
case 'frontComponents':
return SyncableEntity.FrontComponent;
case 'roles':
return SyncableEntity.Role;
case 'assets':
return SyncableEntity.PublicAsset;
default:
return;
}

View file

@ -26,11 +26,10 @@ const STATUS_COLORS: Record<FileStatus, string> = {
const ENTITY_LABELS: Record<SyncableEntity, string> = {
[SyncableEntity.Object]: 'Objects',
[SyncableEntity.ObjectExtension]: 'Object Extensions',
[SyncableEntity.Function]: 'Functions',
[SyncableEntity.FrontComponent]: 'Front Components',
[SyncableEntity.Field]: 'Fields',
[SyncableEntity.LogicFunction]: 'Logic functions',
[SyncableEntity.FrontComponent]: 'Front components',
[SyncableEntity.Role]: 'Roles',
[SyncableEntity.PublicAsset]: 'Public Assets',
};
const ENTITY_ORDER = Object.keys(ENTITY_LABELS) as SyncableEntity[];
@ -194,7 +193,7 @@ export const renderDevUI = async (
return (
<Text color={config.color}>
{icon ?? ''}
{icon ? `${icon} ` : ''}
{config.text}
{snapshot.error && `: ${snapshot.error}`}
</Text>

View file

@ -1,14 +1,16 @@
import { getFunctionBaseFile } from '@/cli/utilities/entity/entity-function-template';
import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-function-template';
describe('getFunctionBaseFile', () => {
it('should render proper file using defineFunction', () => {
const result = getFunctionBaseFile({
const result = getLogicFunctionBaseFile({
name: 'my-function',
universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4',
});
expect(result).toContain("import { defineFunction } from 'twenty-sdk'");
expect(result).toContain('export default defineFunction({');
expect(result).toContain(
"import { defineLogicFunction } from 'twenty-sdk'",
);
expect(result).toContain('export default defineLogicFunction({');
expect(result).toContain(
"universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4'",
@ -21,12 +23,12 @@ describe('getFunctionBaseFile', () => {
expect(result).toContain('const handler = async');
expect(result).toContain(
"description: 'Add a description for your function'",
"description: 'Add a description for your logic function'",
);
});
it('should generate unique UUID when not provided', () => {
const result = getFunctionBaseFile({
const result = getLogicFunctionBaseFile({
name: 'auto-uuid-function',
});
@ -36,7 +38,7 @@ describe('getFunctionBaseFile', () => {
});
it('should use kebab-case for function name', () => {
const result = getFunctionBaseFile({
const result = getLogicFunctionBaseFile({
name: 'my-awesome-function',
});
@ -45,7 +47,7 @@ describe('getFunctionBaseFile', () => {
});
it('should include trigger examples as comments', () => {
const result = getFunctionBaseFile({
const result = getLogicFunctionBaseFile({
name: 'example-function',
});

View file

@ -0,0 +1,31 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { v4 } from 'uuid';
export const getFieldBaseFile = ({
data,
}: {
data: {
name: string;
label: string;
type: FieldMetadataType;
objectUniversalIdentifier: string;
description?: string;
};
name: string;
}) => {
const universalIdentifier = v4();
const descriptionLine = data.description
? `\n description: '${data.description}',`
: '';
return `import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: '${universalIdentifier}',
name: '${data.name}',
label: '${data.label}',
type: FieldMetadataType.${data.type},
objectUniversalIdentifier: '${data.objectUniversalIdentifier}',${descriptionLine}
});
`;
};

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