Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62f6fb0ba5 | ||
|
|
6d266e3e7b | ||
|
|
fc84ff040d | ||
|
|
f57677c078 | ||
|
|
4ff372fff3 | ||
|
|
e858b0c75b | ||
|
|
bf3f606ad1 | ||
|
|
d6ca54cec3 | ||
|
|
1c7dbf9a11 | ||
|
|
3f38c2b2a7 | ||
|
|
6d8eb8fb6a | ||
|
|
0871053565 | ||
|
|
2224075d08 | ||
|
|
b4b68f579a | ||
|
|
ed3895b456 | ||
|
|
f11980dafa | ||
|
|
769a2d27d5 | ||
|
|
79c0b37487 | ||
|
|
6c293b890e | ||
|
|
4e3e12aeb7 | ||
|
|
8d6ec2a92b | ||
|
|
d4ce4414e2 | ||
|
|
ae8a0713b0 | ||
|
|
47fb76ea38 | ||
|
|
be6d5b2184 | ||
|
|
9bd28ed28b |
@@ -66,7 +66,7 @@ export class CreateAppCommand {
|
||||
name: 'name',
|
||||
message: 'Application name:',
|
||||
when: () => !directory,
|
||||
default: 'my-awesome-app',
|
||||
default: 'twenty-app-my-awesome-app',
|
||||
validate: (input) => {
|
||||
if (input.length === 0) return 'Application name is required';
|
||||
return true;
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
expect(await fs.pathExists(packageJsonPath)).toBe(true);
|
||||
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
expect(packageJson.name).toBe('my-test-app');
|
||||
expect(packageJson.name).toBe('twenty-app-my-test-app');
|
||||
expect(packageJson.version).toBe('0.1.0');
|
||||
expect(packageJson.devDependencies['twenty-sdk']).toBe(
|
||||
createTwentyAppPackageJson.version,
|
||||
|
||||
@@ -36,6 +36,8 @@ export const copyBaseApplicationProject = async ({
|
||||
|
||||
await createYarnLock(appDirectory);
|
||||
|
||||
await createPublishWorkflow(appDirectory);
|
||||
|
||||
const sourceFolderPath = join(appDirectory, SRC_FOLDER);
|
||||
|
||||
await fs.ensureDir(sourceFolderPath);
|
||||
@@ -149,6 +151,40 @@ const createYarnLock = async (appDirectory: string) => {
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'yarn.lock'), yarnLockContent);
|
||||
};
|
||||
|
||||
const createPublishWorkflow = async (appDirectory: string) => {
|
||||
const workflowDir = join(appDirectory, '.github', 'workflows');
|
||||
|
||||
await fs.ensureDir(workflowDir);
|
||||
|
||||
const workflowContent = `name: Publish
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: https://registry.npmjs.org
|
||||
- run: yarn install --immutable
|
||||
- run: npx twenty app:build
|
||||
- run: npm publish --provenance --access public
|
||||
working-directory: .twenty/output
|
||||
env:
|
||||
NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(workflowDir, 'publish.yml'), workflowContent);
|
||||
};
|
||||
const createGitignore = async (appDirectory: string) => {
|
||||
const gitignoreContent = `# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
@@ -621,10 +657,18 @@ const createPackageJson = async ({
|
||||
devDependencies['vite-tsconfig-paths'] = '^4.2.1';
|
||||
}
|
||||
|
||||
const normalizedName = appName.startsWith('twenty-app-')
|
||||
? appName
|
||||
: `twenty-app-${appName}`;
|
||||
|
||||
const packageJson = {
|
||||
name: appName,
|
||||
name: normalizedName,
|
||||
version: '0.1.0',
|
||||
license: 'MIT',
|
||||
keywords: ['twenty-app'],
|
||||
publishConfig: {
|
||||
access: 'public' as const,
|
||||
},
|
||||
engines: {
|
||||
node: '^24.5.0',
|
||||
npm: 'please-use-yarn',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/cli/__tests__/apps/rich-app
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
@@ -169,7 +169,7 @@ export default defineObject({
|
||||
|
||||
Later commands will add more files and folders:
|
||||
|
||||
- `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
|
||||
- `yarn twenty app:dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/clients`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
|
||||
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
|
||||
|
||||
## Authentication
|
||||
@@ -431,7 +431,7 @@ Each function file uses `defineLogicFunction()` to export a configuration with a
|
||||
// src/app/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
|
||||
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
|
||||
import { CoreApiClient, type Person } from 'twenty-sdk/clients';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const client = new CoreApiClient();
|
||||
@@ -669,7 +669,7 @@ To mark a logic function as a tool, set `isTool: true` and provide a `toolInputS
|
||||
```typescript
|
||||
// src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-sdk/generated';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
const client = new CoreApiClient();
|
||||
@@ -830,13 +830,14 @@ You can create new agents in two ways:
|
||||
|
||||
### Generated typed clients
|
||||
|
||||
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
|
||||
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/clients` based on your workspace schema:
|
||||
|
||||
- **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
|
||||
- **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
|
||||
|
||||
```typescript
|
||||
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
import { MetadataApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const client = new CoreApiClient();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
@@ -845,7 +846,7 @@ const metadataClient = new MetadataApiClient();
|
||||
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
|
||||
```
|
||||
|
||||
Both clients are re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
|
||||
`CoreApiClient` is re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change. `MetadataApiClient` ships pre-built with the SDK.
|
||||
|
||||
#### Runtime credentials in logic functions
|
||||
|
||||
@@ -861,10 +862,10 @@ Notes:
|
||||
|
||||
#### Uploading files
|
||||
|
||||
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Because standard GraphQL clients do not support multipart file uploads natively, the client provides this dedicated method that implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) under the hood.
|
||||
The `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Because standard GraphQL clients do not support multipart file uploads natively, the client provides this dedicated method that implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) under the hood.
|
||||
|
||||
```typescript
|
||||
import { MetadataApiClient } from 'twenty-sdk/generated';
|
||||
import { MetadataApiClient } from 'twenty-sdk/clients';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
@@ -15,7 +15,7 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
|
||||
|
||||
* عرِّف كائنات وحقولًا مخصصة على شكل كود (نموذج بيانات مُدار)
|
||||
* أنشئ وظائف منطقية مع مشغلات مخصصة
|
||||
* حدد المهارات لوكلاء الذكاء الاصطناعي
|
||||
* تعريف المهارات والوكلاء للذكاء الاصطناعي
|
||||
* انشر التطبيق نفسه عبر مساحات عمل متعددة
|
||||
|
||||
## المتطلبات الأساسية
|
||||
@@ -39,7 +39,7 @@ yarn twenty app:dev
|
||||
يدعم المُنشئ وضعين للتحكم في ملفات الأمثلة التي سيتم تضمينها:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# الافتراضي (شامل): جميع الأمثلة (كائن، حقل، دالة منطقية، مكوّن الواجهة الأمامية، عرض، عنصر قائمة التنقل، مهارة)
|
||||
# الافتراضي (شامل): جميع الأمثلة (كائن، حقل، دالة منطقية، مكوّن الواجهة الأمامية، عرض، عنصر قائمة التنقل، مهارة، وكيل)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# الأدنى: الملفات الأساسية فقط (application-config.ts و default-role.ts)
|
||||
@@ -115,8 +115,10 @@ my-twenty-app/
|
||||
│ └── example-view.ts # تعريف عرض محفوظ — مثال
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # رابط تنقّل في الشريط الجانبي — مثال
|
||||
└── skills/
|
||||
└── example-skill.ts # تعريف مهارة لوكيل الذكاء الاصطناعي — مثال},{
|
||||
├── skills/
|
||||
│ └── example-skill.ts # تعريف مهارة لوكيل الذكاء الاصطناعي — مثال
|
||||
└── agents/
|
||||
└── example-agent.ts # تعريف وكيل الذكاء الاصطناعي — مثال
|
||||
```
|
||||
|
||||
مع `--minimal`، سيتم إنشاء الملفات الأساسية فقط (`application-config.ts`، `roles/default-role.ts`، `logic-functions/pre-install.ts`، و`logic-functions/post-install.ts`).
|
||||
@@ -148,6 +150,7 @@ my-twenty-app/
|
||||
| `defineView()` | تعريفات العروض المحفوظة |
|
||||
| `defineNavigationMenuItem()` | تعريفات عناصر قائمة التنقل |
|
||||
| `defineSkill()` | تعريفات مهارات وكيل الذكاء الاصطناعي |
|
||||
| `defineAgent()` | تعريفات وكلاء الذكاء الاصطناعي |
|
||||
|
||||
<Note>
|
||||
**تسمية الملفات مرنة.** يعتمد اكتشاف الكيانات على بنية الشجرة المجردة (AST) — إذ يقوم SDK بفحص ملفات المصدر لديك بحثًا عن النمط `export default define<Entity>({...})`. يمكنك تنظيم ملفاتك ومجلداتك كيفما تشاء. التجميع حسب نوع الكيان (مثلًا، `logic-functions/` و`roles/`) هو مجرد عرف لتنظيم الشيفرة، وليس مطلبًا إلزاميًا.
|
||||
@@ -225,6 +228,7 @@ yarn twenty auth:status
|
||||
| `defineView()` | تعريف العروض المحفوظة للكائنات |
|
||||
| `defineNavigationMenuItem()` | تعريف روابط التنقل في الشريط الجانبي |
|
||||
| `defineSkill()` | عرّف مهارات وكيل الذكاء الاصطناعي |
|
||||
| `defineAgent()` | Define AI agents with system prompts |
|
||||
|
||||
تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
|
||||
|
||||
@@ -802,6 +806,41 @@ export default defineSkill({
|
||||
* **مُنشأ بالقالب**: شغِّل `yarn twenty entity:add` واختر خيار إضافة مهارة جديدة.
|
||||
* **يدوي**: أنشئ ملفًا جديدًا واستخدم `defineSkill()` مع اتباع النمط نفسه.
|
||||
|
||||
### Agents
|
||||
|
||||
Agents define AI agents with system prompts that can operate within your workspace. Use `defineAgent()` to define agents with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt: `You are a sales assistant. Help users with:
|
||||
1. Researching prospects and companies
|
||||
2. Drafting personalized outreach messages
|
||||
3. Tracking follow-ups and next steps
|
||||
4. Analyzing deal pipeline and suggesting actions`,
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* `name` is a unique identifier string for the agent (kebab-case recommended).
|
||||
* `label` هو اسم العرض المقروء للبشر الظاهر في واجهة المستخدم.
|
||||
* `prompt` contains the system prompt — this is the instruction text that defines the agent's behavior.
|
||||
* `icon` (اختياري) يحدّد الأيقونة المعروضة في واجهة المستخدم.
|
||||
* `description` (optional) provides additional context about the agent's purpose.
|
||||
|
||||
You can create new agents in two ways:
|
||||
|
||||
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new agent.
|
||||
* **Manual**: Create a new file and use `defineAgent()`, following the same pattern.
|
||||
|
||||
### عملاء مُولَّدون مضبوطو الأنواع
|
||||
|
||||
يتم توليد عميلين مضبوطي الأنواع تلقائيًا بواسطة `yarn twenty app:dev` وتخزينهما في `node_modules/twenty-sdk/generated` استنادًا إلى مخطط مساحة العمل لديك:
|
||||
|
||||
@@ -15,7 +15,7 @@ Aplikace vám umožňují vytvářet a spravovat přizpůsobení Twenty **jako k
|
||||
|
||||
* Definujte vlastní objekty a pole jako kód (spravovaný datový model)
|
||||
* Vytvářejte logické funkce s vlastními spouštěči
|
||||
* Definujte dovednosti agentů AI
|
||||
* Definujte dovednosti a agenty AI!
|
||||
* Nasazujte stejnou aplikaci do více pracovních prostorů
|
||||
|
||||
## Předpoklady
|
||||
@@ -39,7 +39,7 @@ yarn twenty app:dev
|
||||
Nástroj pro generování kostry podporuje dva režimy pro řízení toho, které ukázkové soubory jsou zahrnuty:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Výchozí (úplný): všechny příklady (objekt, pole, logická funkce, front-endová komponenta, zobrazení, položka navigační nabídky, dovednost)
|
||||
# Výchozí (úplný): všechny příklady (objekt, pole, logická funkce, front-endová komponenta, zobrazení, položka navigační nabídky, dovednost, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimální: pouze základní soubory (application-config.ts a default-role.ts)
|
||||
@@ -115,8 +115,10 @@ my-twenty-app/
|
||||
│ └── example-view.ts # Ukázková definice uloženého zobrazení
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Ukázkový odkaz postranní navigace
|
||||
└── skills/
|
||||
└── example-skill.ts # Ukázková definice dovednosti agenta AI
|
||||
├── skills/
|
||||
│ └── example-skill.ts # Ukázková definice dovednosti agenta AI
|
||||
└── agents/
|
||||
└── example-agent.ts # Ukázková definice agenta AI
|
||||
```
|
||||
|
||||
S volbou `--minimal` se vytvoří pouze základní soubory (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` a `logic-functions/post-install.ts`).
|
||||
@@ -148,6 +150,7 @@ SDK detekuje entity analýzou vašich souborů TypeScript a hledá volání **`e
|
||||
| `defineView()` | Definice uložených zobrazení |
|
||||
| `defineNavigationMenuItem()` | Definice položek navigační nabídky |
|
||||
| `defineSkill()` | Definice dovedností agenta AI |
|
||||
| `defineAgent()` | Definice agentů AI |
|
||||
|
||||
<Note>
|
||||
**Pojmenování souborů je flexibilní.** Detekce entit je založená na AST — SDK prochází vaše zdrojové soubory a hledá vzor `export default define<Entity>({...})`. Soubory a složky můžete organizovat, jak chcete. Seskupování podle typu entity (např. `logic-functions/`, `roles/`) je pouze konvence pro organizaci kódu, nikoli požadavek.
|
||||
@@ -225,6 +228,7 @@ SDK poskytuje pomocné funkce pro definování entit vaší aplikace. Jak je pop
|
||||
| `defineView()` | Definujte uložená zobrazení pro objekty |
|
||||
| `defineNavigationMenuItem()` | Definujte odkazy postranní navigace |
|
||||
| `defineSkill()` | Definuje dovednosti agenta AI |
|
||||
| `defineAgent()` | Define AI agents with system prompts |
|
||||
|
||||
Tyto funkce validují vaši konfiguraci v době sestavení a poskytují automatické doplňování v IDE a typovou bezpečnost.
|
||||
|
||||
@@ -802,6 +806,41 @@ Nové dovednosti můžete vytvářet dvěma způsoby:
|
||||
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou dovednost.
|
||||
* **Ruční**: Vytvořte nový soubor a použijte `defineSkill()` podle stejného vzoru.
|
||||
|
||||
### Agenti
|
||||
|
||||
Agents define AI agents with system prompts that can operate within your workspace. Use `defineAgent()` to define agents with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt: `You are a sales assistant. Help users with:
|
||||
1. Researching prospects and companies
|
||||
2. Drafting personalized outreach messages
|
||||
3. Tracking follow-ups and next steps
|
||||
4. Analyzing deal pipeline and suggesting actions`,
|
||||
});
|
||||
```
|
||||
|
||||
Hlavní body:
|
||||
|
||||
* `name` is a unique identifier string for the agent (kebab-case recommended).
|
||||
* `label` je uživatelsky čitelný název zobrazovaný v UI.
|
||||
* `prompt` contains the system prompt — this is the instruction text that defines the agent's behavior.
|
||||
* `icon` (volitelné) nastavuje ikonu zobrazovanou v UI.
|
||||
* `description` (optional) provides additional context about the agent's purpose.
|
||||
|
||||
You can create new agents in two ways:
|
||||
|
||||
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new agent.
|
||||
* **Manual**: Create a new file and use `defineAgent()`, following the same pattern.
|
||||
|
||||
### Generované typované klienty
|
||||
|
||||
Dva typované klienty jsou automaticky generovány pomocí `yarn twenty app:dev` a ukládají se do `node_modules/twenty-sdk/generated` podle schématu vašeho pracovního prostoru:
|
||||
|
||||
@@ -15,7 +15,7 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
|
||||
|
||||
* Benutzerdefinierte Objekte und Felder als Code definieren (verwaltetes Datenmodell)
|
||||
* Logikfunktionen mit benutzerdefinierten Triggern erstellen
|
||||
* Fähigkeiten für KI-Agenten definieren
|
||||
* Skills und Agenten für KI definieren
|
||||
* Dieselbe App in mehreren Workspaces bereitstellen
|
||||
|
||||
## Voraussetzungen
|
||||
@@ -39,7 +39,7 @@ yarn twenty app:dev
|
||||
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Standard (umfassend): alle Beispiele (Objekt, Feld, Logikfunktion, Frontend-Komponente, View, Navigationsmenüeintrag, Skill)
|
||||
# Standard (umfassend): alle Beispiele (Objekt, Feld, Logikfunktion, Frontend-Komponente, View, Navigationsmenüeintrag, Skill, Agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: nur Kerndateien (application-config.ts und default-role.ts)
|
||||
@@ -96,27 +96,29 @@ my-twenty-app/
|
||||
.oxlintrc.json
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
public/ # Ordner für öffentliche Assets (Bilder, Schriftarten usw.)
|
||||
src/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── application-config.ts # Erforderlich - Hauptkonfiguration der Anwendung
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
│ └── default-role.ts # Standardrolle für Logikfunktionen
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
│ └── example-object.ts # Beispiel für eine benutzerdefinierte Objektdefinition
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
│ └── example-field.ts # Beispiel für eine eigenständige Felddefinition
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ ├── pre-install.ts # Pre-install logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
│ ├── hello-world.ts # Beispiel für eine Logikfunktion
|
||||
│ ├── pre-install.ts # Pre-Installations-Logikfunktion
|
||||
│ └── post-install.ts # Post-Installations-Logikfunktion
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
│ └── hello-world.tsx # Beispiel für eine Frontend-Komponente
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
│ └── example-view.ts # Beispiel für eine gespeicherte View-Definition
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
└── skills/
|
||||
└── example-skill.ts # Example AI agent skill definition
|
||||
│ └── example-navigation-menu-item.ts # Beispiel für einen Navigationslink in der Seitenleiste
|
||||
├── skills/
|
||||
│ └── example-skill.ts # Beispiel für eine Skill-Definition eines KI-Agenten
|
||||
└── agents/
|
||||
└── example-agent.ts # Beispiel für eine KI-Agenten-Definition
|
||||
```
|
||||
|
||||
Mit `--minimal` werden nur die Kerndateien erstellt (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` und `logic-functions/post-install.ts`).
|
||||
@@ -148,6 +150,7 @@ Das SDK erkennt Entitäten, indem es Ihre TypeScript-Dateien nach Aufrufen von *
|
||||
| `defineView()` | Gespeicherte View-Definitionen |
|
||||
| `defineNavigationMenuItem()` | Definitionen von Navigationsmenüeinträgen |
|
||||
| `defineSkill()` | Skill-Definitionen für KI-Agenten |
|
||||
| `defineAgent()` | KI-Agenten-Definitionen |
|
||||
|
||||
<Note>
|
||||
**Dateibenennung ist flexibel.** Die Entitätserkennung ist AST-basiert — das SDK durchsucht Ihre Quelldateien nach dem Muster `export default define<Entity>({...})`. Sie können Ihre Dateien und Ordner nach Belieben organisieren. Die Gruppierung nach Entitätstyp (z. B. `logic-functions/`, `roles/`) ist lediglich eine Konvention zur Codeorganisation, keine Voraussetzung.
|
||||
@@ -225,6 +228,7 @@ Das SDK stellt Hilfsfunktionen bereit, um die Entitäten Ihrer App zu definieren
|
||||
| `defineView()` | Gespeicherte Views für Objekte definieren |
|
||||
| `defineNavigationMenuItem()` | Seitenleisten-Navigationslinks definieren |
|
||||
| `defineSkill()` | Definieren Sie Skills für KI-Agenten |
|
||||
| `defineAgent()` | Define AI agents with system prompts |
|
||||
|
||||
Diese Funktionen validieren Ihre Konfiguration zur Build-Zeit und bieten IDE-Autovervollständigung sowie Typsicherheit.
|
||||
|
||||
@@ -802,6 +806,41 @@ Sie können neue Skills auf zwei Arten erstellen:
|
||||
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen eines neuen Skills.
|
||||
* **Manuell**: Erstellen Sie eine neue Datei und verwenden Sie `defineSkill()` nach demselben Muster.
|
||||
|
||||
### Agenten
|
||||
|
||||
Agents define AI agents with system prompts that can operate within your workspace. Use `defineAgent()` to define agents with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt: `You are a sales assistant. Help users with:
|
||||
1. Researching prospects and companies
|
||||
2. Drafting personalized outreach messages
|
||||
3. Tracking follow-ups and next steps
|
||||
4. Analyzing deal pipeline and suggesting actions`,
|
||||
});
|
||||
```
|
||||
|
||||
Hauptpunkte:
|
||||
|
||||
* `name` is a unique identifier string for the agent (kebab-case recommended).
|
||||
* `label` ist der menschenlesbare Anzeigename, der in der UI angezeigt wird.
|
||||
* `prompt` contains the system prompt — this is the instruction text that defines the agent's behavior.
|
||||
* `icon` (optional) legt das in der UI angezeigte Symbol fest.
|
||||
* `description` (optional) provides additional context about the agent's purpose.
|
||||
|
||||
You can create new agents in two ways:
|
||||
|
||||
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new agent.
|
||||
* **Manual**: Create a new file and use `defineAgent()`, following the same pattern.
|
||||
|
||||
### Generierte typisierte Clients
|
||||
|
||||
Zwei typisierte Clients werden von `yarn twenty app:dev` automatisch generiert und basierend auf Ihrem Arbeitsbereichs-Schema in `node_modules/twenty-sdk/generated` gespeichert:
|
||||
|
||||
@@ -15,7 +15,7 @@ Le app ti consentono di creare e gestire le personalizzazioni di Twenty **come c
|
||||
|
||||
* Definisci oggetti e campi personalizzati come codice (modello dati gestito)
|
||||
* Crea funzioni logiche con trigger personalizzati
|
||||
* Definire le abilità per gli agenti IA
|
||||
* Definire skill e agenti per l'IA
|
||||
* Distribuisci la stessa app su più spazi di lavoro
|
||||
|
||||
## Prerequisiti
|
||||
@@ -39,7 +39,7 @@ yarn twenty app:dev
|
||||
Lo strumento di scaffolding supporta due modalità per controllare quali file di esempio vengono inclusi:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Predefinita (esaustiva): tutti gli esempi (oggetto, campo, funzione logica, componente front-end, vista, voce del menu di navigazione, skill)
|
||||
# Predefinita (esaustiva): tutti gli esempi (oggetto, campo, funzione logica, componente front-end, vista, voce del menu di navigazione, skill, agente)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimale: solo i file principali (application-config.ts e default-role.ts)
|
||||
@@ -96,27 +96,29 @@ my-twenty-app/
|
||||
.oxlintrc.json
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
public/ # Cartella delle risorse pubbliche (immagini, font, ecc.)
|
||||
src/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── application-config.ts # Obbligatorio - configurazione principale dell'applicazione
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
│ └── default-role.ts # Ruolo predefinito per le funzioni logiche
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
│ └── example-object.ts # Definizione di oggetto personalizzato di esempio
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
│ └── example-field.ts # Definizione di campo autonomo di esempio
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ ├── pre-install.ts # Pre-install logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
│ ├── hello-world.ts # Funzione logica di esempio
|
||||
│ ├── pre-install.ts # Funzione logica di pre-installazione
|
||||
│ └── post-install.ts # Funzione logica di post-installazione
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
│ └── hello-world.tsx # Componente front-end di esempio
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
│ └── example-view.ts # Definizione di vista salvata di esempio
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
└── skills/
|
||||
└── example-skill.ts # Example AI agent skill definition
|
||||
│ └── example-navigation-menu-item.ts # Link di navigazione della barra laterale di esempio
|
||||
├── skills/
|
||||
│ └── example-skill.ts # Definizione di skill per agente IA di esempio
|
||||
└── agents/
|
||||
└── example-agent.ts # Definizione di agente IA di esempio
|
||||
```
|
||||
|
||||
Con `--minimal`, vengono creati solo i file principali (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` e `logic-functions/post-install.ts`).
|
||||
@@ -148,6 +150,7 @@ L'SDK rileva le entità analizzando i tuoi file TypeScript alla ricerca di chiam
|
||||
| `defineView()` | Definizioni di viste salvate |
|
||||
| `defineNavigationMenuItem()` | Definizioni delle voci del menu di navigazione |
|
||||
| `defineSkill()` | AI agent skill definitions |
|
||||
| `defineAgent()` | Definizioni di agenti IA |
|
||||
|
||||
<Note>
|
||||
**La denominazione dei file è flessibile.** Il rilevamento delle entità è basato sull'AST — l'SDK esegue la scansione dei file sorgente alla ricerca del pattern `export default define<Entity>({...})`. Puoi organizzare file e cartelle come preferisci. Raggruppare per tipo di entità (ad es., `logic-functions/`, `roles/`) è solo una convenzione per l'organizzazione del codice, non un requisito.
|
||||
@@ -225,6 +228,7 @@ L'SDK fornisce funzioni helper per definire le entità della tua app. Come descr
|
||||
| `defineView()` | Definisce viste salvate per gli oggetti |
|
||||
| `defineNavigationMenuItem()` | Definisce i link di navigazione della barra laterale |
|
||||
| `defineSkill()` | Define AI agent skills |
|
||||
| `defineAgent()` | Define AI agents with system prompts |
|
||||
|
||||
Queste funzioni convalidano la configurazione in fase di build e offrono il completamento automatico nell'IDE e la sicurezza dei tipi.
|
||||
|
||||
@@ -802,6 +806,41 @@ You can create new skills in two ways:
|
||||
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
|
||||
* **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
|
||||
|
||||
### Agenti
|
||||
|
||||
Agents define AI agents with system prompts that can operate within your workspace. Use `defineAgent()` to define agents with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt: `You are a sales assistant. Help users with:
|
||||
1. Researching prospects and companies
|
||||
2. Drafting personalized outreach messages
|
||||
3. Tracking follow-ups and next steps
|
||||
4. Analyzing deal pipeline and suggesting actions`,
|
||||
});
|
||||
```
|
||||
|
||||
Punti chiave:
|
||||
|
||||
* `name` is a unique identifier string for the agent (kebab-case recommended).
|
||||
* `label` is the human-readable display name shown in the UI.
|
||||
* `prompt` contains the system prompt — this is the instruction text that defines the agent's behavior.
|
||||
* `icon` (optional) sets the icon displayed in the UI.
|
||||
* `description` (optional) provides additional context about the agent's purpose.
|
||||
|
||||
You can create new agents in two ways:
|
||||
|
||||
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new agent.
|
||||
* **Manual**: Create a new file and use `defineAgent()`, following the same pattern.
|
||||
|
||||
### Client tipizzati generati
|
||||
|
||||
Due client tipizzati sono generati automaticamente da `yarn twenty app:dev` e salvati in `node_modules/twenty-sdk/generated` in base allo schema della tua area di lavoro:
|
||||
|
||||
@@ -15,7 +15,7 @@ Os aplicativos permitem criar e gerenciar personalizações do Twenty **como có
|
||||
|
||||
* Defina objetos e campos personalizados como código (modelo de dados gerenciado)
|
||||
* Crie funções de lógica com gatilhos personalizados
|
||||
* Definir habilidades para agentes de IA
|
||||
* Defina habilidades e agentes de IA
|
||||
* Implemente o mesmo aplicativo em vários espaços de trabalho
|
||||
|
||||
## Pré-requisitos
|
||||
@@ -39,7 +39,7 @@ yarn twenty app:dev
|
||||
O gerador de estrutura oferece suporte a dois modos para controlar quais arquivos de exemplo são incluídos:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Padrão (exhaustivo): todos os exemplos (objeto, campo, função de lógica, componente de front-end, visualização, item do menu de navegação, habilidade)
|
||||
# Padrão (exhaustivo): todos os exemplos (objeto, campo, função de lógica, componente de front-end, visualização, item do menu de navegação, habilidade, agente)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Mínimo: apenas arquivos principais (application-config.ts e default-role.ts)
|
||||
@@ -96,27 +96,29 @@ my-twenty-app/
|
||||
.oxlintrc.json
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Pasta de recursos públicos (imagens, fontes, etc.)
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
src/
|
||||
├── application-config.ts # Obrigatório - configuração principal da aplicação
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── roles/
|
||||
│ └── default-role.ts # Papel padrão para funções de lógica
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── objects/
|
||||
│ └── example-object.ts # Exemplo de definição de objeto personalizado
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
├── fields/
|
||||
│ └── example-field.ts # Exemplo de definição de campo independente
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Exemplo de função de lógica
|
||||
│ ├── pre-install.ts # Função de lógica de pré-instalação
|
||||
│ └── post-install.ts # Função de lógica de pós-instalação
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ ├── pre-install.ts # Pre-install logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Exemplo de componente de front-end
|
||||
│ └── hello-world.tsx # Example front component
|
||||
├── views/
|
||||
│ └── example-view.ts # Exemplo de definição de visualização salva
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Exemplo de link de navegação da barra lateral
|
||||
└── skills/
|
||||
└── example-skill.ts # Exemplo de definição de habilidade de agente de IA
|
||||
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
├── skills/
|
||||
│ └── example-skill.ts # Example AI agent skill definition
|
||||
└── agents/
|
||||
└── example-agent.ts # Example AI agent definition
|
||||
```
|
||||
|
||||
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts`, and `logic-functions/post-install.ts`).
|
||||
@@ -148,6 +150,7 @@ O SDK detecta entidades analisando seus arquivos TypeScript em busca de chamadas
|
||||
| `defineView()` | Definições de visualizações salvas |
|
||||
| `defineNavigationMenuItem()` | Definições de itens do menu de navegação |
|
||||
| `defineSkill()` | Definições de habilidades de agente de IA |
|
||||
| `defineAgent()` | Definições de agentes de IA |
|
||||
|
||||
<Note>
|
||||
**A nomeação de arquivos é flexível.** A detecção de entidades é baseada em AST — o SDK varre seus arquivos fonte em busca do padrão `export default define<Entity>({...})`. Você pode organizar seus arquivos e pastas como quiser. Agrupar por tipo de entidade (por exemplo, `logic-functions/`, `roles/`) é apenas uma convenção para organização do código, não um requisito.
|
||||
@@ -225,6 +228,7 @@ O SDK fornece funções utilitárias para definir as entidades do seu app. Confo
|
||||
| `defineView()` | Define visualizações salvas para objetos |
|
||||
| `defineNavigationMenuItem()` | Define links de navegação da barra lateral |
|
||||
| `defineSkill()` | Define habilidades de agente de IA |
|
||||
| `defineAgent()` | Define AI agents with system prompts |
|
||||
|
||||
Essas funções validam sua configuração em tempo de compilação e oferecem autocompletar na IDE e segurança de tipos.
|
||||
|
||||
@@ -803,6 +807,41 @@ Você pode criar novas habilidades de duas formas:
|
||||
* **Gerado automaticamente**: Execute `yarn twenty entity:add` e escolha a opção para adicionar uma nova habilidade.
|
||||
* **Manual**: Crie um novo arquivo e use `defineSkill()`, seguindo o mesmo padrão.
|
||||
|
||||
### Agentes
|
||||
|
||||
Agents define AI agents with system prompts that can operate within your workspace. Use `defineAgent()` to define agents with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt: `You are a sales assistant. Help users with:
|
||||
1. Researching prospects and companies
|
||||
2. Drafting personalized outreach messages
|
||||
3. Tracking follow-ups and next steps
|
||||
4. Analyzing deal pipeline and suggesting actions`,
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
|
||||
* `name` is a unique identifier string for the agent (kebab-case recommended).
|
||||
* `label` é o nome de exibição legível por humanos mostrado na UI.
|
||||
* `prompt` contains the system prompt — this is the instruction text that defines the agent's behavior.
|
||||
* `icon` (opcional) define o ícone exibido na UI.
|
||||
* `description` (optional) provides additional context about the agent's purpose.
|
||||
|
||||
You can create new agents in two ways:
|
||||
|
||||
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new agent.
|
||||
* **Manual**: Create a new file and use `defineAgent()`, following the same pattern.
|
||||
|
||||
### Clientes tipados gerados
|
||||
|
||||
Dois clientes tipados são gerados automaticamente pelo `yarn twenty app:dev` e armazenados em `node_modules/twenty-sdk/generated` com base no esquema do seu espaço de trabalho:
|
||||
|
||||
@@ -15,7 +15,7 @@ Aplicațiile vă permit să construiți și să gestionați personalizările Twe
|
||||
|
||||
* Definiți obiecte și câmpuri personalizate sub formă de cod (model de date gestionat)
|
||||
* Creați funcții de logică cu declanșatoare personalizate
|
||||
* Definiți abilități pentru agenți de IA
|
||||
* Definiți abilități și agenți pentru AI
|
||||
* Implementați aceeași aplicație în mai multe spații de lucru
|
||||
|
||||
## Cerințe
|
||||
@@ -39,7 +39,7 @@ yarn twenty app:dev
|
||||
Generatorul de schelet acceptă două moduri pentru a controla ce fișiere de exemplu sunt incluse:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Implicit (exhaustiv): toate exemplele (obiect, câmp, funcție logică, componentă de interfață, vizualizare, element de meniu de navigare, abilitate)
|
||||
# Implicit (exhaustiv): toate exemplele (obiect, câmp, funcție logică, componentă de interfață, vizualizare, element de meniu de navigare, abilitate, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: doar fișierele de bază (application-config.ts și default-role.ts)
|
||||
@@ -96,27 +96,29 @@ my-twenty-app/
|
||||
.oxlintrc.json
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
public/ # Director pentru resurse publice (imagini, fonturi etc.)
|
||||
src/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── application-config.ts # Obligatoriu - configurația principală a aplicației
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
│ └── default-role.ts # Rol implicit pentru funcțiile logice
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
│ └── example-object.ts # Exemplu de definiție a unui obiect personalizat
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
│ └── example-field.ts # Exemplu de definiție de câmp independent
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ ├── pre-install.ts # Pre-install logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
│ ├── hello-world.ts # Exemplu de funcție logică
|
||||
│ ├── pre-install.ts # Funcție logică de pre-instalare
|
||||
│ └── post-install.ts # Funcție logică post-instalare
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
│ └── hello-world.tsx # Exemplu de componentă de interfață
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
│ └── example-view.ts # Exemplu de definiție a unei vizualizări salvate
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
└── skills/
|
||||
└── example-skill.ts # Example AI agent skill definition
|
||||
│ └── example-navigation-menu-item.ts # Exemplu de link de navigare în bara laterală
|
||||
├── skills/
|
||||
│ └── example-skill.ts # Exemplu de definiție a unei abilități a agentului AI
|
||||
└── agents/
|
||||
└── example-agent.ts # Exemplu de definiție a unui agent AI
|
||||
```
|
||||
|
||||
Cu `--minimal`, sunt create doar fișierele de bază (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` și `logic-functions/post-install.ts`).
|
||||
@@ -148,6 +150,7 @@ SDK-ul detectează entitățile analizând fișierele TypeScript pentru apeluri
|
||||
| `defineView()` | Definiții pentru vizualizări salvate |
|
||||
| `defineNavigationMenuItem()` | Definiții pentru elemente de meniu de navigare |
|
||||
| `defineSkill()` | Definiții ale abilităților agentului AI |
|
||||
| `defineAgent()` | Definiții ale agenților AI |
|
||||
|
||||
<Note>
|
||||
**Denumirea fișierelor este flexibilă.** Detectarea entităților se bazează pe AST — SDK-ul scanează fișierele sursă pentru tiparul `export default define<Entity>({...})`. Puteți organiza fișierele și folderele cum doriți. Gruparea după tipul de entitate (de exemplu, `logic-functions/`, `roles/`) este doar o convenție pentru organizarea codului, nu o cerință.
|
||||
@@ -225,6 +228,7 @@ SDK-ul oferă funcții ajutătoare pentru definirea entităților aplicației. D
|
||||
| `defineView()` | Definește vizualizări salvate pentru obiecte |
|
||||
| `defineNavigationMenuItem()` | Definește linkuri de navigare în bara laterală |
|
||||
| `defineSkill()` | Definiți abilități pentru agentul AI |
|
||||
| `defineAgent()` | Define AI agents with system prompts |
|
||||
|
||||
Aceste funcții validează configurația în timpul build-ului și oferă completare automată în IDE și siguranța tipurilor.
|
||||
|
||||
@@ -802,6 +806,41 @@ Puteți crea abilități noi în două moduri:
|
||||
* **Generat**: Rulați `yarn twenty entity:add` și alegeți opțiunea de a adăuga o abilitate nouă.
|
||||
* **Manual**: Creați un fișier nou și folosiți `defineSkill()`, urmând același model.
|
||||
|
||||
### Agenți
|
||||
|
||||
Agents define AI agents with system prompts that can operate within your workspace. Use `defineAgent()` to define agents with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt: `You are a sales assistant. Help users with:
|
||||
1. Researching prospects and companies
|
||||
2. Drafting personalized outreach messages
|
||||
3. Tracking follow-ups and next steps
|
||||
4. Analyzing deal pipeline and suggesting actions`,
|
||||
});
|
||||
```
|
||||
|
||||
Puncte cheie:
|
||||
|
||||
* `name` is a unique identifier string for the agent (kebab-case recommended).
|
||||
* `label` este numele lizibil afișat în interfața cu utilizatorul (UI).
|
||||
* `prompt` contains the system prompt — this is the instruction text that defines the agent's behavior.
|
||||
* `icon` (opțional) setează pictograma afișată în UI.
|
||||
* `description` (optional) provides additional context about the agent's purpose.
|
||||
|
||||
You can create new agents in two ways:
|
||||
|
||||
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new agent.
|
||||
* **Manual**: Create a new file and use `defineAgent()`, following the same pattern.
|
||||
|
||||
### Generated typed clients
|
||||
|
||||
Two typed clients are auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
|
||||
|
||||
@@ -15,7 +15,7 @@ description: Создавайте и управляйте настройками
|
||||
|
||||
* Определяйте пользовательские объекты и поля в виде кода (управляемая модель данных)
|
||||
* Создавайте логические функции с пользовательскими триггерами
|
||||
* Определите навыки для ИИ-агентов
|
||||
* Определите навыки и агентов для ИИ
|
||||
* Развёртывайте одно и то же приложение в нескольких рабочих пространствах
|
||||
|
||||
## Требования
|
||||
@@ -39,7 +39,7 @@ yarn twenty app:dev
|
||||
Генератор каркаса поддерживает два режима для управления тем, какие файлы-примеры включаются:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# По умолчанию (полный набор): все примеры (объект, поле, логическая функция, фронтенд-компонент, представление, пункт меню навигации, навык)
|
||||
# По умолчанию (полный набор): все примеры (объект, поле, логическая функция, фронтенд-компонент, представление, пункт меню навигации, навык, агент)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Минимальный: только основные файлы (application-config.ts и default-role.ts)
|
||||
@@ -96,27 +96,29 @@ my-twenty-app/
|
||||
.oxlintrc.json
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
public/ # Папка публичных ресурсов (изображения, шрифты и т. д.)
|
||||
src/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── application-config.ts # Обязательный — основная конфигурация приложения
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
│ └── default-role.ts # Роль по умолчанию для логических функций
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
│ └── example-object.ts # Пример определения пользовательского объекта
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
│ └── example-field.ts # Пример определения отдельного поля
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ ├── pre-install.ts # Pre-install logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
│ ├── hello-world.ts # Пример логической функции
|
||||
│ ├── pre-install.ts # Предустановочная логическая функция
|
||||
│ └── post-install.ts # Послеустановочная логическая функция
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
│ └── hello-world.tsx # Пример фронтенд-компонента
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
│ └── example-view.ts # Пример определения сохранённого представления
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
└── skills/
|
||||
└── example-skill.ts # Example AI agent skill definition
|
||||
│ └── example-navigation-menu-item.ts # Пример ссылки боковой панели навигации
|
||||
├── skills/
|
||||
│ └── example-skill.ts # Пример определения навыка агента ИИ
|
||||
└── agents/
|
||||
└── example-agent.ts # Пример определения агента ИИ
|
||||
```
|
||||
|
||||
С `--minimal` создаются только основные файлы (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` и `logic-functions/post-install.ts`).
|
||||
@@ -148,6 +150,7 @@ SDK обнаруживает сущности, разбирая ваши фай
|
||||
| `defineView()` | Определения сохранённых представлений |
|
||||
| `defineNavigationMenuItem()` | Определения пунктов меню навигации |
|
||||
| `defineSkill()` | Определения навыков агента ИИ |
|
||||
| `defineAgent()` | Определения агентов ИИ |
|
||||
|
||||
<Note>
|
||||
**Имена файлов заданы гибко.** Обнаружение сущностей основано на AST — SDK сканирует ваши исходные файлы в поисках шаблона `export default define<Entity>({...})`. Вы можете организовывать файлы и папки как угодно. Группировка по типу сущности (например, `logic-functions/`, `roles/`) — это лишь соглашение для организации кода, а не требование.
|
||||
@@ -225,6 +228,7 @@ SDK предоставляет вспомогательные функции д
|
||||
| `defineView()` | Определяйте сохранённые представления для объектов |
|
||||
| `defineNavigationMenuItem()` | Определяйте ссылки боковой панели навигации |
|
||||
| `defineSkill()` | Определение навыков агента ИИ |
|
||||
| `defineAgent()` | Define AI agents with system prompts |
|
||||
|
||||
Эти функции проверяют вашу конфигурацию на этапе сборки и обеспечивают автодополнение в IDE и безопасность типов.
|
||||
|
||||
@@ -802,6 +806,41 @@ export default defineSkill({
|
||||
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового навыка.
|
||||
* **Вручную**: Создайте новый файл и используйте `defineSkill()`, следуя тому же шаблону.
|
||||
|
||||
### Агенты
|
||||
|
||||
Agents define AI agents with system prompts that can operate within your workspace. Use `defineAgent()` to define agents with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt: `You are a sales assistant. Help users with:
|
||||
1. Researching prospects and companies
|
||||
2. Drafting personalized outreach messages
|
||||
3. Tracking follow-ups and next steps
|
||||
4. Analyzing deal pipeline and suggesting actions`,
|
||||
});
|
||||
```
|
||||
|
||||
Основные моменты:
|
||||
|
||||
* `name` is a unique identifier string for the agent (kebab-case recommended).
|
||||
* `label` — читаемое человеком отображаемое имя, показываемое в UI.
|
||||
* `prompt` contains the system prompt — this is the instruction text that defines the agent's behavior.
|
||||
* `icon` (необязательно) задаёт значок, отображаемый в UI.
|
||||
* `description` (optional) provides additional context about the agent's purpose.
|
||||
|
||||
You can create new agents in two ways:
|
||||
|
||||
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new agent.
|
||||
* **Manual**: Create a new file and use `defineAgent()`, following the same pattern.
|
||||
|
||||
### Сгенерированные типизированные клиенты
|
||||
|
||||
Два типизированных клиента автоматически генерируются с помощью `yarn twenty app:dev` и сохраняются в `node_modules/twenty-sdk/generated` на основе схемы вашего рабочего пространства:
|
||||
|
||||
@@ -15,7 +15,7 @@ Uygulamalar, Twenty özelleştirmelerini **kod olarak** oluşturup yönetmenizi
|
||||
|
||||
* Özel nesneleri ve alanları kod olarak tanımlayın (yönetilen veri modeli)
|
||||
* Özel tetikleyicilerle mantık fonksiyonları oluşturun
|
||||
* Yapay zekâ ajanları için becerileri tanımlayın
|
||||
* Define skills and agents for AI
|
||||
* Aynı uygulamayı birden çok çalışma alanına dağıtın
|
||||
|
||||
## Ön Gereksinimler
|
||||
@@ -39,10 +39,10 @@ yarn twenty app:dev
|
||||
İskelet oluşturucu, hangi örnek dosyaların dahil edileceğini kontrol etmek için iki modu destekler:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Varsayılan (kapsamlı): tüm örnekler (nesne, alan, mantık fonksiyonu, ön bileşen, görünüm, gezinme menüsü öğesi, yetenek)
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: yalnızca çekirdek dosyalar (application-config.ts ve default-role.ts)
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
|
||||
@@ -96,27 +96,29 @@ my-twenty-app/
|
||||
.oxlintrc.json
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Genel varlıklar klasörü (görseller, yazı tipleri vb.)
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
src/
|
||||
├── application-config.ts # Gerekli - ana uygulama yapılandırması
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── roles/
|
||||
│ └── default-role.ts # Mantık fonksiyonları için varsayılan rol
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── objects/
|
||||
│ └── example-object.ts # Örnek özel nesne tanımı
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
├── fields/
|
||||
│ └── example-field.ts # Örnek bağımsız alan tanımı
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Örnek mantık fonksiyonu
|
||||
│ ├── pre-install.ts # Kurulum öncesi mantık fonksiyonu
|
||||
│ └── post-install.ts # Kurulum sonrası mantık fonksiyonu
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ ├── pre-install.ts # Pre-install logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Örnek ön bileşen
|
||||
│ └── hello-world.tsx # Example front component
|
||||
├── views/
|
||||
│ └── example-view.ts # Örnek kaydedilmiş görünüm tanımı
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Örnek kenar çubuğu gezinme bağlantısı
|
||||
└── skills/
|
||||
└── example-skill.ts # Örnek yapay zekâ ajanı yetenek tanımı
|
||||
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
├── skills/
|
||||
│ └── example-skill.ts # Example AI agent skill definition
|
||||
└── agents/
|
||||
└── example-agent.ts # Example AI agent definition
|
||||
```
|
||||
|
||||
`--minimal` ile yalnızca çekirdek dosyalar oluşturulur (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` ve `logic-functions/post-install.ts`).
|
||||
@@ -148,6 +150,7 @@ SDK, TypeScript dosyalarınızı **`export default define<Entity>({...})`** ça
|
||||
| `defineView()` | Kaydedilmiş görünüm tanımları |
|
||||
| `defineNavigationMenuItem()` | Gezinme menüsü öğesi tanımları |
|
||||
| `defineSkill()` | Yapay zekâ ajanı yetenek tanımları |
|
||||
| `defineAgent()` | AI agent definitions |
|
||||
|
||||
<Note>
|
||||
**Dosya adlandırma esnektir.** Varlık algılama AST tabanlıdır — SDK, kaynak dosyalarınızı `export default define<Entity>({...})` desenini bulmak için tarar. Dosyalarınızı ve klasörlerinizi dilediğiniz gibi düzenleyebilirsiniz. Varlık türüne göre gruplama (örn. `logic-functions/`, `roles/`) bir gereklilik değil, yalnızca kod organizasyonu için bir gelenektir.
|
||||
@@ -225,6 +228,7 @@ SDK, uygulama varlıklarınızı tanımlamak için yardımcı fonksiyonlar sağl
|
||||
| `defineView()` | Nesneler için kaydedilmiş görünümler tanımlayın |
|
||||
| `defineNavigationMenuItem()` | Kenar çubuğu gezinme bağlantılarını tanımlayın |
|
||||
| `defineSkill()` | Yapay zekâ ajanı yeteneklerini tanımlayın |
|
||||
| `defineAgent()` | Define AI agents with system prompts |
|
||||
|
||||
Bu fonksiyonlar, derleme zamanında yapılandırmanızı doğrular ve IDE otomatik tamamlama ile tür güvenliği sağlar.
|
||||
|
||||
@@ -802,6 +806,41 @@ Yeni yetenekleri iki şekilde oluşturabilirsiniz:
|
||||
* **Şablondan**: `yarn twenty entity:add` komutunu çalıştırın ve yeni bir yetenek ekleme seçeneğini seçin.
|
||||
* **Manuel**: Yeni bir dosya oluşturun ve aynı deseni izleyerek `defineSkill()` kullanın.
|
||||
|
||||
### Temsilciler
|
||||
|
||||
Agents define AI agents with system prompts that can operate within your workspace. Use `defineAgent()` to define agents with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt: `You are a sales assistant. Help users with:
|
||||
1. Researching prospects and companies
|
||||
2. Drafting personalized outreach messages
|
||||
3. Tracking follow-ups and next steps
|
||||
4. Analyzing deal pipeline and suggesting actions`,
|
||||
});
|
||||
```
|
||||
|
||||
Önemli noktalar:
|
||||
|
||||
* `name` is a unique identifier string for the agent (kebab-case recommended).
|
||||
* `label`, UI'de gösterilen, insan tarafından okunabilir addır.
|
||||
* `prompt` contains the system prompt — this is the instruction text that defines the agent's behavior.
|
||||
* `icon` (isteğe bağlı), UI'de gösterilen simgeyi ayarlar.
|
||||
* `description` (optional) provides additional context about the agent's purpose.
|
||||
|
||||
You can create new agents in two ways:
|
||||
|
||||
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new agent.
|
||||
* **Manual**: Create a new file and use `defineAgent()`, following the same pattern.
|
||||
|
||||
### Oluşturulan tiplendirilmiş istemciler
|
||||
|
||||
Çalışma alanı şemanıza göre `yarn twenty app:dev` tarafından iki tiplendirilmiş istemci otomatik olarak oluşturulur ve `node_modules/twenty-sdk/generated` içine kaydedilir:
|
||||
|
||||
@@ -15,7 +15,7 @@ description: 以代码的形式构建并管理 Twenty 自定义项。
|
||||
|
||||
* 以代码定义自定义对象和字段(受管理的数据模型)
|
||||
* 构建带有自定义触发器的逻辑函数
|
||||
* 为 AI 智能体定义技能
|
||||
* Define skills and agents for AI
|
||||
* 将同一个应用部署到多个工作空间
|
||||
|
||||
## 先决条件
|
||||
@@ -39,10 +39,10 @@ yarn twenty app:dev
|
||||
脚手架工具支持两种模式,用于控制包含哪些示例文件:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# 默认(完整):所有示例(对象、字段、逻辑函数、前端组件、视图、导航菜单项、技能)
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# 最小化:仅核心文件(application-config.ts 和 default-role.ts)
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
```
|
||||
|
||||
@@ -115,8 +115,10 @@ my-twenty-app/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
└── skills/
|
||||
└── example-skill.ts # Example AI agent skill definition
|
||||
├── skills/
|
||||
│ └── example-skill.ts # Example AI agent skill definition
|
||||
└── agents/
|
||||
└── example-agent.ts # Example AI agent definition
|
||||
```
|
||||
|
||||
使用 `--minimal` 时,只会创建核心文件(`application-config.ts`、`roles/default-role.ts`、`logic-functions/pre-install.ts` 和 `logic-functions/post-install.ts`)。
|
||||
@@ -148,6 +150,7 @@ my-twenty-app/
|
||||
| `defineView()` | Saved view definitions |
|
||||
| `defineNavigationMenuItem()` | Navigation menu item definitions |
|
||||
| `defineSkill()` | AI agent skill definitions |
|
||||
| `defineAgent()` | AI agent definitions |
|
||||
|
||||
<Note>
|
||||
**文件命名是灵活的。** 实体检测基于 AST — SDK 会扫描你的源文件以查找 `export default define<Entity>({...})` 模式。 你可以按照自己的喜好组织文件和文件夹。 按实体类型分组(例如 `logic-functions/`、`roles/`)只是代码组织的一种约定,并非必需。
|
||||
@@ -212,19 +215,20 @@ twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以
|
||||
|
||||
该 SDK 提供辅助函数用于定义你的应用实体。 如 [实体检测](#entity-detection) 中所述,你必须使用 `export default define<Entity>({...})` 才能让你的实体被检测到:
|
||||
|
||||
| 函数 | 目的 |
|
||||
| ---------------------------------- | ------------------------------- |
|
||||
| `defineApplication()` | 配置应用元数据(必需,每个应用一个) |
|
||||
| `defineObject()` | 定义带字段的自定义对象 |
|
||||
| `defineLogicFunction()` | 定义带处理程序的逻辑函数 |
|
||||
| `definePreInstallLogicFunction()` | 定义一个安装前逻辑函数(每个应用一个) |
|
||||
| `definePostInstallLogicFunction()` | 定义一个安装后逻辑函数(每个应用一个) |
|
||||
| `defineFrontComponent()` | 为自定义 UI 定义前端组件 |
|
||||
| `defineRole()` | 配置角色权限和对象访问 |
|
||||
| `defineField()` | 为现有对象扩展额外字段 |
|
||||
| `defineView()` | Define saved views for objects |
|
||||
| `defineNavigationMenuItem()` | Define sidebar navigation links |
|
||||
| `defineSkill()` | Define AI agent skills |
|
||||
| 函数 | 目的 |
|
||||
| ---------------------------------- | ------------------------------------ |
|
||||
| `defineApplication()` | 配置应用元数据(必需,每个应用一个) |
|
||||
| `defineObject()` | 定义带字段的自定义对象 |
|
||||
| `defineLogicFunction()` | 定义带处理程序的逻辑函数 |
|
||||
| `definePreInstallLogicFunction()` | 定义一个安装前逻辑函数(每个应用一个) |
|
||||
| `definePostInstallLogicFunction()` | 定义一个安装后逻辑函数(每个应用一个) |
|
||||
| `defineFrontComponent()` | 为自定义 UI 定义前端组件 |
|
||||
| `defineRole()` | 配置角色权限和对象访问 |
|
||||
| `defineField()` | 为现有对象扩展额外字段 |
|
||||
| `defineView()` | Define saved views for objects |
|
||||
| `defineNavigationMenuItem()` | Define sidebar navigation links |
|
||||
| `defineSkill()` | Define AI agent skills |
|
||||
| `defineAgent()` | Define AI agents with system prompts |
|
||||
|
||||
这些函数会在构建时校验你的配置,并提供 IDE 自动补全和类型安全。
|
||||
|
||||
@@ -802,6 +806,41 @@ You can create new skills in two ways:
|
||||
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
|
||||
* **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
|
||||
|
||||
### 代理
|
||||
|
||||
Agents define AI agents with system prompts that can operate within your workspace. Use `defineAgent()` to define agents with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/agents/example-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-assistant',
|
||||
label: 'Sales Assistant',
|
||||
description: 'An AI agent that helps with sales tasks',
|
||||
icon: 'IconRobot',
|
||||
prompt: `You are a sales assistant. Help users with:
|
||||
1. Researching prospects and companies
|
||||
2. Drafting personalized outreach messages
|
||||
3. Tracking follow-ups and next steps
|
||||
4. Analyzing deal pipeline and suggesting actions`,
|
||||
});
|
||||
```
|
||||
|
||||
关键点:
|
||||
|
||||
* `name` is a unique identifier string for the agent (kebab-case recommended).
|
||||
* `label` is the human-readable display name shown in the UI.
|
||||
* `prompt` contains the system prompt — this is the instruction text that defines the agent's behavior.
|
||||
* `icon` (optional) sets the icon displayed in the UI.
|
||||
* `description` (optional) provides additional context about the agent's purpose.
|
||||
|
||||
You can create new agents in two ways:
|
||||
|
||||
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new agent.
|
||||
* **Manual**: Create a new file and use `defineAgent()`, following the same pattern.
|
||||
|
||||
### 生成的类型化客户端
|
||||
|
||||
两个类型化客户端由 `yarn twenty app:dev` 自动生成(基于你的工作区架构),并存放在 `node_modules/twenty-sdk/generated`:
|
||||
|
||||
File diff suppressed because one or more lines are too long
+67
@@ -0,0 +1,67 @@
|
||||
import { readdir, rm } from 'node:fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
import { appBuild } from '@/cli/public-operations/app-build';
|
||||
import { pathExists } from '@/cli/utilities/file/fs-utils';
|
||||
import { GENERATED_DIR, OUTPUT_DIR } from 'twenty-shared/application';
|
||||
|
||||
const APP_PATH = join(__dirname, '../..');
|
||||
|
||||
describe('rich-app app:build', () => {
|
||||
let buildResult: Awaited<ReturnType<typeof appBuild>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const generatedPath = join(
|
||||
APP_PATH,
|
||||
'node_modules',
|
||||
'twenty-sdk',
|
||||
GENERATED_DIR,
|
||||
);
|
||||
|
||||
await rm(generatedPath, { recursive: true, force: true });
|
||||
|
||||
buildResult = await appBuild({ appPath: APP_PATH });
|
||||
}, 60_000);
|
||||
|
||||
it('should complete build successfully', () => {
|
||||
expect(buildResult.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should produce manifest in output directory', async () => {
|
||||
const manifestPath = join(APP_PATH, OUTPUT_DIR, 'manifest.json');
|
||||
|
||||
expect(await pathExists(manifestPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should produce built logic function files', async () => {
|
||||
const outputDir = join(APP_PATH, OUTPUT_DIR);
|
||||
const files = await readdir(outputDir, { recursive: true });
|
||||
const functionFiles = files
|
||||
.map((file) => file.toString())
|
||||
.filter((file) => file.endsWith('.function.mjs'))
|
||||
.sort();
|
||||
|
||||
expect(functionFiles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// app:build only creates client stubs (empty CoreApiClient / MetadataApiClient)
|
||||
// because it doesn't sync with the server or generate the real API client.
|
||||
// Logic functions that use CoreApiClient will get an empty class at runtime.
|
||||
// When this is fixed, update this test to assert the client IS fully generated.
|
||||
// See: https://github.com/twentyhq/twenty/pull/18460
|
||||
it('should only produce client stubs (not a fully generated CoreApiClient)', async () => {
|
||||
const clientIndexPath = join(
|
||||
APP_PATH,
|
||||
'node_modules',
|
||||
'twenty-sdk',
|
||||
GENERATED_DIR,
|
||||
'core',
|
||||
'index.ts',
|
||||
);
|
||||
|
||||
const { readFile } = await import('node:fs/promises');
|
||||
const content = await readFile(clientIndexPath, 'utf-8');
|
||||
|
||||
expect(content).toBe('export class CoreApiClient {}\n');
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 570 B After Width: | Height: | Size: 570 B |
+3
-3
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
POST_CARD_ON_POST_CARD_RECIPIENT_ID,
|
||||
POST_CARD_RECIPIENTS_ON_POST_CARD_ID,
|
||||
} from '@/cli/__tests__/apps/rich-app/src/fields/post-card-recipients-on-post-card.field';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card-recipient.object';
|
||||
} from '@/app-seeds/rich-app/src/fields/post-card-recipients-on-post-card.field';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/app-seeds/rich-app/src/objects/post-card-recipient.object';
|
||||
import { defineField, FieldType, OnDeleteAction, RelationType } from '@/sdk';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/app-seeds/rich-app/src/objects/post-card.object';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: POST_CARD_ON_POST_CARD_RECIPIENT_ID,
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card-recipient.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/app-seeds/rich-app/src/objects/post-card-recipient.object';
|
||||
import { defineField, FieldType, RelationType } from '@/sdk';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/app-seeds/rich-app/src/objects/post-card.object';
|
||||
|
||||
export const POST_CARD_RECIPIENTS_ON_POST_CARD_ID =
|
||||
'a1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d';
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { defineField, FieldType, RelationType } from '@/sdk';
|
||||
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/recipient.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card-recipient.object';
|
||||
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/app-seeds/rich-app/src/objects/recipient.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/app-seeds/rich-app/src/objects/post-card-recipient.object';
|
||||
|
||||
export const POST_CARD_RECIPIENTS_ON_RECIPIENT_ID =
|
||||
'a1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d';
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { defineField, FieldType } from '@/sdk';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/app-seeds/rich-app/src/objects/post-card.object';
|
||||
|
||||
export default defineField({
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { defineField, FieldType } from '@/sdk';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '@/app-seeds/rich-app/src/objects/post-card.object';
|
||||
|
||||
export default defineField({
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
import { defineField, FieldType, RelationType, OnDeleteAction } from '@/sdk';
|
||||
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/recipient.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/apps/rich-app/src/objects/post-card-recipient.object';
|
||||
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/app-seeds/rich-app/src/objects/recipient.object';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '@/app-seeds/rich-app/src/objects/post-card-recipient.object';
|
||||
import {
|
||||
POST_CARD_RECIPIENTS_ON_RECIPIENT_ID,
|
||||
RECIPIENT_ON_POST_CARD_RECIPIENT_ID,
|
||||
} from '@/cli/__tests__/apps/rich-app/src/fields/post-card-recipients-on-recipient.field';
|
||||
} from '@/app-seeds/rich-app/src/fields/post-card-recipients-on-recipient.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: RECIPIENT_ON_POST_CARD_RECIPIENT_ID,
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { defineNavigationMenuItem } from '@/sdk';
|
||||
import { ALL_POST_CARD_RECIPIENTS_VIEW_ID } from '@/cli/__tests__/apps/rich-app/src/views/all-post-card-recipients.view';
|
||||
import { ALL_POST_CARD_RECIPIENTS_VIEW_ID } from '@/app-seeds/rich-app/src/views/all-post-card-recipients.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { defineNavigationMenuItem } from '@/sdk';
|
||||
import { ALL_POST_CARDS_VIEW_ID } from '@/cli/__tests__/apps/rich-app/src/views/all-post-cards.view';
|
||||
import { ALL_POST_CARDS_VIEW_ID } from '@/app-seeds/rich-app/src/views/all-post-cards.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { defineNavigationMenuItem } from '@/sdk';
|
||||
import { ALL_RECIPIENTS_VIEW_ID } from '@/cli/__tests__/apps/rich-app/src/views/all-recipients.view';
|
||||
import { ALL_RECIPIENTS_VIEW_ID } from '@/app-seeds/rich-app/src/views/all-recipients.view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { PermissionFlag, defineRole } from '@/sdk';
|
||||
import {
|
||||
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
} from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
|
||||
} from '@/app-seeds/rich-app/src/objects/post-card.object';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
SENT_AT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '@/cli/__tests__/apps/rich-app/src/objects/post-card-recipient.object';
|
||||
} from '@/app-seeds/rich-app/src/objects/post-card-recipient.object';
|
||||
import { defineView } from '@/sdk';
|
||||
import { ViewType } from 'twenty-shared/types';
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { defineView } from '@/sdk';
|
||||
import {
|
||||
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
} from '@/cli/__tests__/apps/rich-app/src/objects/post-card.object';
|
||||
} from '@/app-seeds/rich-app/src/objects/post-card.object';
|
||||
import { ViewType } from 'twenty-shared/types';
|
||||
|
||||
export const ALL_POST_CARDS_VIEW_ID = 'b1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d';
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
RECIPIENT_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
} from '@/cli/__tests__/apps/rich-app/src/objects/recipient.object';
|
||||
} from '@/app-seeds/rich-app/src/objects/recipient.object';
|
||||
import { defineView } from '@/sdk';
|
||||
import { ViewType } from 'twenty-shared/types';
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../../../../../src/*"]
|
||||
"@/*": ["../../../src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*"],
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { join } from 'path';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
import { EXPECTED_MANIFEST } from '@/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest';
|
||||
import { EXPECTED_MANIFEST } from '@/app-seeds/root-app/__integration__/app-dev/expected-manifest';
|
||||
import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util';
|
||||
import { pathExists, readJson } from '@/cli/utilities/file/fs-utils';
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../../../../../src/*"]
|
||||
"@/*": ["../../../src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*"]
|
||||
+20
@@ -76,4 +76,24 @@ describe('functionExecute E2E', () => {
|
||||
error: { code: 'FUNCTION_NOT_FOUND' },
|
||||
});
|
||||
});
|
||||
|
||||
// Verifies that CoreApiClient is fully generated (not just stubs) after
|
||||
// appGenerateClient. If app:build is used without client generation, the
|
||||
// bundled CoreApiClient would be an empty stub and this test would fail.
|
||||
// See: https://github.com/twentyhq/twenty/pull/18460
|
||||
it('should execute a function that uses CoreApiClient with real query/mutation methods', async () => {
|
||||
const result = await functionExecute({
|
||||
appPath: APP_PATH,
|
||||
functionName: 'check-core-client',
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
success: true,
|
||||
data: {
|
||||
functionName: 'check-core-client',
|
||||
status: 'SUCCESS',
|
||||
data: { hasQuery: true, hasMutation: true },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { CoreApiClient } from 'twenty-sdk/generated';
|
||||
import { defineLogicFunction } from '@/sdk';
|
||||
|
||||
export const CHECK_CORE_CLIENT_UNIVERSAL_IDENTIFIER =
|
||||
'b7c8d9e0-1a2b-4c3d-8e5f-6a7b8c9d0e1f';
|
||||
|
||||
const checkCoreClientHandler = () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
return {
|
||||
hasQuery: typeof client.query === 'function',
|
||||
hasMutation: typeof client.mutation === 'function',
|
||||
};
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: CHECK_CORE_CLIENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'check-core-client',
|
||||
timeoutSeconds: 5,
|
||||
handler: checkCoreClientHandler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/check-core-client',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
});
|
||||
@@ -36,6 +36,8 @@
|
||||
"src/front-component-renderer/host/generated/host-component-registry.ts",
|
||||
"src/front-component-renderer/remote/generated/remote-components.ts",
|
||||
"src/front-component-renderer/remote/generated/remote-elements.ts",
|
||||
"src/front-component-renderer/__stories__/**/*"
|
||||
"src/front-component-renderer/__stories__/**/*",
|
||||
"src/app-seeds/*/src/**/*",
|
||||
"src/cli/__tests__/apps/*/src/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
{
|
||||
"include": "engine/workspace-manager/dev-seeder/data/sample-files/**",
|
||||
"outDir": "dist/assets"
|
||||
},
|
||||
{
|
||||
"include": "engine/workspace-manager/dev-seeder/core/constants/*.json",
|
||||
"outDir": "dist"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+1
-11
@@ -10,23 +10,13 @@ export class AddWorkspaceIdToApplicationRegistration1772267875869
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD "workspaceId" uuid`,
|
||||
);
|
||||
|
||||
// Delete any orphaned registrations that can't be assigned a workspace
|
||||
await queryRunner.query(`
|
||||
DELETE FROM "core"."applicationRegistration"
|
||||
WHERE "workspaceId" IS NULL
|
||||
`);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ALTER COLUMN "workspaceId" SET NOT NULL`,
|
||||
);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX "IDX_APPLICATION_REGISTRATION_WORKSPACE_ID"
|
||||
ON "core"."applicationRegistration" ("workspaceId")
|
||||
`);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_94ab20372e448d45088357f884e" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_94ab20372e448d45088357f884e" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -36,9 +36,28 @@ export class AddAppRegistrationSourceFields1772267875870
|
||||
ADD CONSTRAINT "CHK_NPM_HAS_SOURCE_PACKAGE"
|
||||
CHECK ("sourceType" <> 'npm' OR "sourcePackage" IS NOT NULL)`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD COLUMN "provenanceRepositoryUrl" text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD COLUMN "isProvenanceVerified" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD COLUMN "provenanceVerifiedAt" timestamptz`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" DROP COLUMN "provenanceVerifiedAt"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" DROP COLUMN "isProvenanceVerified"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" DROP COLUMN "provenanceRepositoryUrl"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT IF EXISTS "CHK_NPM_HAS_SOURCE_PACKAGE"`,
|
||||
);
|
||||
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddIsListedToAppRegistration1772732588833
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddIsListedToAppRegistration1772732588833';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD "isListed" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ALTER COLUMN "workspaceId" DROP NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT IF EXISTS "FK_applicationRegistration_workspaceId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_applicationRegistration_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT "FK_applicationRegistration_workspaceId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT "FK_94ab20372e448d45088357f884e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ALTER COLUMN "workspaceId" DROP NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_94ab20372e448d45088357f884e" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_applicationRegistration_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT "FK_94ab20372e448d45088357f884e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ALTER COLUMN "workspaceId" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_94ab20372e448d45088357f884e" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT IF EXISTS "FK_applicationRegistration_workspaceId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_applicationRegistration_workspaceId" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" ALTER COLUMN "workspaceId" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."applicationRegistration" DROP COLUMN "isListed"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -9,6 +9,7 @@ import { ApplicationManifestModule } from 'src/engine/core-modules/application/a
|
||||
import { ApplicationPackageModule } from 'src/engine/core-modules/application/application-package/application-package.module';
|
||||
import { ApplicationInstallResolver } from 'src/engine/core-modules/application/application-install/application-install.resolver';
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
|
||||
import { ApplicationQueryResolver } from 'src/engine/core-modules/application/application-install/application-query.resolver';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@@ -23,7 +24,11 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
PermissionsModule,
|
||||
FileStorageModule,
|
||||
],
|
||||
providers: [ApplicationInstallResolver, ApplicationInstallService],
|
||||
providers: [
|
||||
ApplicationInstallResolver,
|
||||
ApplicationQueryResolver,
|
||||
ApplicationInstallService,
|
||||
],
|
||||
exports: [ApplicationInstallService],
|
||||
})
|
||||
export class ApplicationInstallModule {}
|
||||
|
||||
+1
-31
@@ -1,15 +1,12 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
import { Args, Mutation } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
|
||||
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -27,36 +24,9 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
export class ApplicationInstallResolver {
|
||||
constructor(
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly applicationInstallService: ApplicationInstallService,
|
||||
) {}
|
||||
|
||||
@Query(() => [ApplicationDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
|
||||
async findManyApplications(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
return this.applicationService.findManyApplications(workspaceId);
|
||||
}
|
||||
|
||||
@Query(() => ApplicationDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
|
||||
async findOneApplication(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
@Args('id', { type: () => UUIDScalarType, nullable: true }) id?: string,
|
||||
@Args('universalIdentifier', {
|
||||
type: () => UUIDScalarType,
|
||||
nullable: true,
|
||||
})
|
||||
universalIdentifier?: string,
|
||||
) {
|
||||
return await this.applicationService.findOneApplicationOrThrow({
|
||||
id,
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
|
||||
|
||||
+62
-20
@@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join, relative } from 'path';
|
||||
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
type ResolvedPackage,
|
||||
} from 'src/engine/core-modules/application/application-package/application-package-fetcher.service';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
|
||||
import { readJsonFileOrThrow } from 'src/engine/core-modules/application/application-package/utils/read-json-file.util';
|
||||
import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
|
||||
@@ -94,6 +96,32 @@ export class ApplicationInstallService {
|
||||
);
|
||||
}
|
||||
|
||||
async installFromLocalDirectory(params: {
|
||||
appRegistrationId: string;
|
||||
extractedDir: string;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
const appRegistration = await this.appRegistrationRepository.findOneOrFail({
|
||||
where: { id: params.appRegistrationId },
|
||||
});
|
||||
|
||||
const manifest = await readJsonFileOrThrow<Manifest>(
|
||||
params.extractedDir,
|
||||
'manifest.json',
|
||||
);
|
||||
|
||||
await this.syncFromExtractedDirectory({
|
||||
appRegistration,
|
||||
extractedDir: params.extractedDir,
|
||||
manifest,
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Successfully installed app ${appRegistration.universalIdentifier} from local directory`,
|
||||
);
|
||||
}
|
||||
|
||||
private async doInstallApplication(
|
||||
appRegistration: ApplicationRegistrationEntity,
|
||||
params: { version?: string; workspaceId: string },
|
||||
@@ -111,30 +139,15 @@ export class ApplicationInstallService {
|
||||
return true;
|
||||
}
|
||||
|
||||
const universalIdentifier = appRegistration.universalIdentifier;
|
||||
|
||||
await this.ensureApplicationExists({
|
||||
universalIdentifier,
|
||||
name: resolvedPackage.manifest.application.displayName,
|
||||
workspaceId: params.workspaceId,
|
||||
applicationRegistrationId: appRegistration.id,
|
||||
sourceType: appRegistration.sourceType,
|
||||
});
|
||||
|
||||
await this.writeFilesToStorage(
|
||||
resolvedPackage.extractedDir,
|
||||
universalIdentifier,
|
||||
params.workspaceId,
|
||||
);
|
||||
|
||||
await this.applicationSyncService.synchronizeFromManifest({
|
||||
workspaceId: params.workspaceId,
|
||||
await this.syncFromExtractedDirectory({
|
||||
appRegistration,
|
||||
extractedDir: resolvedPackage.extractedDir,
|
||||
manifest: resolvedPackage.manifest,
|
||||
applicationRegistrationId: appRegistration.id,
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Successfully installed app ${universalIdentifier} v${resolvedPackage.packageJson.version ?? 'unknown'}`,
|
||||
`Successfully installed app ${appRegistration.universalIdentifier} v${resolvedPackage.packageJson.version ?? 'unknown'}`,
|
||||
);
|
||||
|
||||
return true;
|
||||
@@ -153,6 +166,35 @@ export class ApplicationInstallService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncFromExtractedDirectory(params: {
|
||||
appRegistration: ApplicationRegistrationEntity;
|
||||
extractedDir: string;
|
||||
manifest: Manifest;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
const { appRegistration, extractedDir, manifest, workspaceId } = params;
|
||||
|
||||
await this.ensureApplicationExists({
|
||||
universalIdentifier: appRegistration.universalIdentifier,
|
||||
name: manifest.application.displayName,
|
||||
workspaceId,
|
||||
applicationRegistrationId: appRegistration.id,
|
||||
sourceType: appRegistration.sourceType,
|
||||
});
|
||||
|
||||
await this.writeFilesToStorage(
|
||||
extractedDir,
|
||||
appRegistration.universalIdentifier,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.applicationSyncService.synchronizeFromManifest({
|
||||
workspaceId,
|
||||
manifest,
|
||||
applicationRegistrationId: appRegistration.id,
|
||||
});
|
||||
}
|
||||
|
||||
private async writeFilesToStorage(
|
||||
extractedDir: string,
|
||||
applicationUniversalIdentifier: string,
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { FeatureFlagGuard } from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@MetadataResolver()
|
||||
@UseFilters(ApplicationExceptionFilter, AuthGraphqlApiExceptionFilter)
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
export class ApplicationQueryResolver {
|
||||
constructor(private readonly applicationService: ApplicationService) {}
|
||||
|
||||
@Query(() => [ApplicationDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
|
||||
async findManyApplications(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
return this.applicationService.findManyApplications(workspaceId);
|
||||
}
|
||||
|
||||
@Query(() => ApplicationDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
|
||||
async findOneApplication(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
@Args('id', { type: () => UUIDScalarType, nullable: true }) id?: string,
|
||||
@Args('universalIdentifier', {
|
||||
type: () => UUIDScalarType,
|
||||
nullable: true,
|
||||
})
|
||||
universalIdentifier?: string,
|
||||
) {
|
||||
return await this.applicationService.findOneApplicationOrThrow({
|
||||
id,
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
-22
@@ -11,10 +11,6 @@ import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
|
||||
import { RunWorkspaceMigrationInput } from 'src/engine/core-modules/application/application-manifest/dtos/run-workspace-migration.input';
|
||||
@@ -29,7 +25,6 @@ import {
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
|
||||
|
||||
@@ -43,7 +38,6 @@ export class ApplicationManifestResolver {
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly applicationSyncService: ApplicationSyncService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@@ -53,22 +47,6 @@ export class ApplicationManifestResolver {
|
||||
@Args() { workspaceMigration: { actions } }: RunWorkspaceMigrationInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
const { featureFlagsMap } = await this.workspaceCacheService.getOrRecompute(
|
||||
workspaceId,
|
||||
['featureFlagsMap'],
|
||||
);
|
||||
|
||||
if (
|
||||
featureFlagsMap[
|
||||
FeatureFlagKey.IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED
|
||||
] !== true
|
||||
) {
|
||||
throw new ApplicationException(
|
||||
'Application installation from tarball is not enabled',
|
||||
ApplicationExceptionCode.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
|
||||
+5
@@ -246,6 +246,11 @@ export class MarketplaceAppDTO {
|
||||
@Field()
|
||||
aboutDescription: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
readme?: string;
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [String])
|
||||
providers: string[];
|
||||
|
||||
+71
-4
@@ -1,8 +1,13 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationNpmRegistrationService } from 'src/engine/core-modules/application/application-registration/application-npm-registration.service';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
import { MARKETPLACE_CATALOG_INDEX } from 'src/engine/core-modules/application/application-marketplace/constants/marketplace-catalog-index.constant';
|
||||
import { type MarketplaceDisplayData } from 'src/engine/core-modules/application/application-marketplace/types/marketplace-display-data.type';
|
||||
import { type NpmPackument } from 'src/engine/core-modules/application/application-marketplace/types/npm-packument.type';
|
||||
import { MarketplaceService } from 'src/engine/core-modules/application/application-marketplace/marketplace.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -12,6 +17,7 @@ export class MarketplaceCatalogSyncService {
|
||||
constructor(
|
||||
private readonly applicationRegistrationService: ApplicationRegistrationService,
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly applicationNpmRegistrationService: ApplicationNpmRegistrationService,
|
||||
) {}
|
||||
|
||||
async syncCatalog(): Promise<void> {
|
||||
@@ -36,7 +42,6 @@ export class MarketplaceCatalogSyncService {
|
||||
websiteUrl: entry.websiteUrl ?? null,
|
||||
termsUrl: entry.termsUrl ?? null,
|
||||
latestAvailableVersion: entry.richDisplayData.version ?? null,
|
||||
isListed: true,
|
||||
isFeatured: entry.isFeatured,
|
||||
marketplaceDisplayData: entry.richDisplayData,
|
||||
ownerWorkspaceId: null,
|
||||
@@ -62,21 +67,50 @@ export class MarketplaceCatalogSyncService {
|
||||
}
|
||||
|
||||
try {
|
||||
const packageName = app.sourcePackage ?? app.name;
|
||||
|
||||
let isProvenanceVerified = false;
|
||||
let provenanceRepositoryUrl: string | null = null;
|
||||
let provenanceVerifiedAt: Date | null = null;
|
||||
|
||||
if (app.version) {
|
||||
const provenance =
|
||||
await this.applicationNpmRegistrationService.fetchProvenanceMetadata(
|
||||
packageName,
|
||||
app.version,
|
||||
);
|
||||
|
||||
if (provenance?.hasProvenance) {
|
||||
isProvenanceVerified = true;
|
||||
provenanceRepositoryUrl = provenance.repositoryUrl;
|
||||
provenanceVerifiedAt = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
const packument =
|
||||
await this.marketplaceService.fetchPackument(packageName);
|
||||
|
||||
const displayData = isDefined(packument)
|
||||
? this.buildDisplayDataFromPackument(packument)
|
||||
: null;
|
||||
|
||||
await this.applicationRegistrationService.upsertFromCatalog({
|
||||
universalIdentifier: app.id,
|
||||
name: app.name,
|
||||
description: app.description,
|
||||
author: app.author,
|
||||
sourceType: ApplicationRegistrationSourceType.NPM,
|
||||
sourcePackage: app.sourcePackage ?? app.name,
|
||||
sourcePackage: packageName,
|
||||
logoUrl: null,
|
||||
websiteUrl: app.websiteUrl ?? null,
|
||||
termsUrl: null,
|
||||
latestAvailableVersion: app.version ?? null,
|
||||
isListed: true,
|
||||
isFeatured: false,
|
||||
marketplaceDisplayData: null,
|
||||
marketplaceDisplayData: displayData,
|
||||
ownerWorkspaceId: null,
|
||||
isProvenanceVerified,
|
||||
provenanceRepositoryUrl,
|
||||
provenanceVerifiedAt,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
@@ -85,4 +119,37 @@ export class MarketplaceCatalogSyncService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private buildDisplayDataFromPackument(
|
||||
packument: NpmPackument,
|
||||
): MarketplaceDisplayData {
|
||||
const repositoryUrl = this.extractRepositoryUrl(packument.repository);
|
||||
|
||||
return {
|
||||
readme:
|
||||
isDefined(packument.readme) &&
|
||||
packument.readme !== 'ERROR: No README data found!'
|
||||
? packument.readme
|
||||
: undefined,
|
||||
aboutDescription: packument.description,
|
||||
providers: isDefined(repositoryUrl) ? [repositoryUrl] : [],
|
||||
};
|
||||
}
|
||||
|
||||
private extractRepositoryUrl(
|
||||
repository?: { type?: string; url?: string } | string,
|
||||
): string | null {
|
||||
if (!isDefined(repository)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url =
|
||||
typeof repository === 'string' ? repository : (repository.url ?? null);
|
||||
|
||||
if (!isDefined(url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return url.replace(/^git\+/, '').replace(/\.git$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -35,7 +35,7 @@ export class MarketplaceQueryService {
|
||||
}
|
||||
|
||||
const registrations =
|
||||
await this.applicationRegistrationService.findManyListed();
|
||||
await this.applicationRegistrationService.findManyNpm();
|
||||
|
||||
if (registrations.length === 0) {
|
||||
if (!this.hasSyncBeenEnqueued) {
|
||||
@@ -105,6 +105,7 @@ export class MarketplaceQueryService {
|
||||
screenshots: displayData?.screenshots ?? [],
|
||||
aboutDescription:
|
||||
displayData?.aboutDescription ?? registration.description ?? '',
|
||||
readme: displayData?.readme,
|
||||
providers: displayData?.providers ?? [],
|
||||
websiteUrl: registration.websiteUrl ?? undefined,
|
||||
termsUrl: registration.termsUrl ?? undefined,
|
||||
|
||||
+38
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app.dto';
|
||||
import { type NpmPackument } from 'src/engine/core-modules/application/application-marketplace/types/npm-packument.type';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
const npmSearchResultSchema = z.object({
|
||||
@@ -28,6 +29,31 @@ export class MarketplaceService {
|
||||
|
||||
constructor(private readonly twentyConfigService: TwentyConfigService) {}
|
||||
|
||||
async fetchPackument(packageName: string): Promise<NpmPackument | null> {
|
||||
const registryUrl = this.twentyConfigService.get('APP_REGISTRY_URL');
|
||||
|
||||
try {
|
||||
const { data } = await axios.get<NpmPackument>(
|
||||
`${registryUrl}/${encodeURIComponent(packageName)}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'Twenty-Marketplace',
|
||||
},
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to fetch packument for ${packageName}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAppsFromNpmRegistry(): Promise<MarketplaceAppDTO[]> {
|
||||
const registryUrl = this.twentyConfigService.get('APP_REGISTRY_URL');
|
||||
|
||||
@@ -53,6 +79,11 @@ export class MarketplaceService {
|
||||
return parsed.data.objects
|
||||
.map((result) => {
|
||||
const { name, version, description, author, links } = result.package;
|
||||
|
||||
if (!this.hasValidAppPrefix(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const twentyKeyword = (result.package.keywords ?? []).find(
|
||||
(keyword) => keyword.startsWith('twenty-uid:'),
|
||||
);
|
||||
@@ -92,4 +123,11 @@ export class MarketplaceService {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private hasValidAppPrefix(packageName: string): boolean {
|
||||
return (
|
||||
packageName.startsWith('twenty-app-') ||
|
||||
/^@[^/]+\/twenty-app-/.test(packageName)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ export type MarketplaceDisplayData = {
|
||||
logo?: string;
|
||||
screenshots?: string[];
|
||||
aboutDescription?: string;
|
||||
readme?: string;
|
||||
providers?: string[];
|
||||
objects?: MarketplaceDisplayObject[];
|
||||
fields?: MarketplaceDisplayField[];
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// Subset of the npm registry packument response (GET /{packageName}).
|
||||
// Only the fields we actually use are typed; the full response is much larger.
|
||||
export type NpmPackument = {
|
||||
name: string;
|
||||
description?: string;
|
||||
readme?: string;
|
||||
license?: string;
|
||||
repository?: { type?: string; url?: string } | string;
|
||||
homepage?: string;
|
||||
};
|
||||
-7
@@ -65,13 +65,6 @@ export class ApplicationPackageFetcherService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
async resolveNpmPackage(
|
||||
packageName: string,
|
||||
targetVersion?: string,
|
||||
): Promise<ResolvedPackage> {
|
||||
return this.resolveFromNpm(packageName, targetVersion);
|
||||
}
|
||||
|
||||
async resolvePackage(
|
||||
appRegistration: ApplicationRegistrationEntity,
|
||||
options?: { targetVersion?: string },
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { ApplicationException } from 'src/engine/core-modules/application/application.exception';
|
||||
import { assertValidNpmPackageName } from 'src/engine/core-modules/application/application-package/utils/assert-valid-npm-package-name.util';
|
||||
|
||||
describe('assertValidNpmPackageName', () => {
|
||||
it('should accept valid twenty-app- prefixed names', () => {
|
||||
expect(() =>
|
||||
assertValidNpmPackageName('twenty-app-my-cool-app'),
|
||||
).not.toThrow();
|
||||
expect(() => assertValidNpmPackageName('twenty-app-hello')).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept scoped packages with twenty-app- prefix', () => {
|
||||
expect(() =>
|
||||
assertValidNpmPackageName('@myorg/twenty-app-my-app'),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should reject names without twenty-app- prefix', () => {
|
||||
expect(() => assertValidNpmPackageName('my-cool-app')).toThrow(
|
||||
ApplicationException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject scoped packages without twenty-app- prefix', () => {
|
||||
expect(() => assertValidNpmPackageName('@myorg/my-cool-app')).toThrow(
|
||||
ApplicationException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject invalid npm package names', () => {
|
||||
expect(() => assertValidNpmPackageName('Twenty-App-Test')).toThrow(
|
||||
ApplicationException,
|
||||
);
|
||||
expect(() => assertValidNpmPackageName('twenty-app-../evil')).toThrow(
|
||||
ApplicationException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject empty or malformed names', () => {
|
||||
expect(() => assertValidNpmPackageName('')).toThrow(ApplicationException);
|
||||
});
|
||||
});
|
||||
+13
@@ -8,6 +8,9 @@ import {
|
||||
const NPM_PACKAGE_NAME_REGEX =
|
||||
/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
||||
|
||||
const TWENTY_APP_PREFIX = 'twenty-app-';
|
||||
const TWENTY_APP_SCOPED_PREFIX = /^@[^/]+\/twenty-app-/;
|
||||
|
||||
export const assertValidNpmPackageName = (name: string): void => {
|
||||
if (!NPM_PACKAGE_NAME_REGEX.test(name) || name.includes('..')) {
|
||||
throw new ApplicationException(
|
||||
@@ -15,4 +18,14 @@ export const assertValidNpmPackageName = (name: string): void => {
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!name.startsWith(TWENTY_APP_PREFIX) &&
|
||||
!TWENTY_APP_SCOPED_PREFIX.test(name)
|
||||
) {
|
||||
throw new ApplicationException(
|
||||
'npm package name must start with "twenty-app-" (or "@scope/twenty-app-")',
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
export type ProvenanceMetadata = {
|
||||
repositoryUrl: string | null;
|
||||
hasProvenance: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationNpmRegistrationService {
|
||||
private readonly logger = new Logger(ApplicationNpmRegistrationService.name);
|
||||
|
||||
constructor(private readonly twentyConfigService: TwentyConfigService) {}
|
||||
|
||||
async fetchProvenanceMetadata(
|
||||
packageName: string,
|
||||
version: string,
|
||||
): Promise<ProvenanceMetadata | null> {
|
||||
const registryUrl = this.twentyConfigService.get('APP_REGISTRY_URL');
|
||||
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
`${registryUrl}/-/npm/v1/attestations/${encodeURIComponent(packageName)}@${version}`,
|
||||
{
|
||||
headers: { 'User-Agent': 'Twenty-Provenance' },
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
const repositoryUrl = this.extractRepositoryUrl(data);
|
||||
|
||||
return { repositoryUrl, hasProvenance: true };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private extractRepositoryUrl(attestationData: unknown): string | null {
|
||||
try {
|
||||
const data = attestationData as {
|
||||
attestations?: Array<{
|
||||
predicateType?: string;
|
||||
bundle?: {
|
||||
dsseEnvelope?: {
|
||||
payload?: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
if (!data?.attestations?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const attestation of data.attestations) {
|
||||
if (attestation.predicateType !== 'https://slsa.dev/provenance/v1') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const payload = attestation.bundle?.dsseEnvelope?.payload;
|
||||
|
||||
if (!payload) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const decoded = JSON.parse(
|
||||
Buffer.from(payload, 'base64').toString('utf-8'),
|
||||
);
|
||||
|
||||
const resolvedDependencies =
|
||||
decoded?.predicate?.buildDefinition?.resolvedDependencies;
|
||||
|
||||
if (Array.isArray(resolvedDependencies)) {
|
||||
for (const dep of resolvedDependencies) {
|
||||
if (
|
||||
typeof dep.uri === 'string' &&
|
||||
dep.uri.startsWith('git+https://')
|
||||
) {
|
||||
return dep.uri.replace(/^git\+/, '').replace(/@[^@]+$/, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to extract repository URL from attestation: ${error}`,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-4
@@ -136,10 +136,6 @@ export class ApplicationRegistrationEntity {
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
termsUrl: string | null;
|
||||
|
||||
@Field(() => Boolean)
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isListed: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
@Column({ name: 'isFeatured', type: 'boolean', default: false })
|
||||
isFeatured: boolean;
|
||||
@@ -147,6 +143,17 @@ export class ApplicationRegistrationEntity {
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
marketplaceDisplayData: MarketplaceDisplayData | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
provenanceRepositoryUrl: string | null;
|
||||
|
||||
@Field(() => Boolean)
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isProvenanceVerified: boolean;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
provenanceVerifiedAt: Date | null;
|
||||
|
||||
@OneToMany(
|
||||
() => ApplicationRegistrationVariableEntity,
|
||||
(variable) => variable.applicationRegistration,
|
||||
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationNpmRegistrationService } from 'src/engine/core-modules/application/application-registration/application-npm-registration.service';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationResolver } from 'src/engine/core-modules/application/application-registration/application-registration.resolver';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
@@ -34,9 +35,11 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
ApplicationRegistrationService,
|
||||
ApplicationRegistrationResolver,
|
||||
ApplicationTarballService,
|
||||
ApplicationNpmRegistrationService,
|
||||
],
|
||||
exports: [
|
||||
ApplicationRegistrationService,
|
||||
ApplicationNpmRegistrationService,
|
||||
ApplicationRegistrationVariableModule,
|
||||
],
|
||||
})
|
||||
|
||||
+19
-30
@@ -71,21 +71,6 @@ export class ApplicationRegistrationService {
|
||||
return registration;
|
||||
}
|
||||
|
||||
async findOneByIdGlobal(id: string): Promise<ApplicationRegistrationEntity> {
|
||||
const registration = await this.applicationRegistrationRepository.findOne({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!registration) {
|
||||
throw new ApplicationRegistrationException(
|
||||
`Application registration with id ${id} not found`,
|
||||
ApplicationRegistrationExceptionCode.APPLICATION_REGISTRATION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return registration;
|
||||
}
|
||||
|
||||
// Global lookup — used by OAuth flow (no workspace scoping)
|
||||
async findOneByClientId(
|
||||
clientId: string,
|
||||
@@ -220,8 +205,6 @@ export class ApplicationRegistrationService {
|
||||
updateData.oAuthScopes = update.oAuthScopes;
|
||||
if (isDefined(update.websiteUrl)) updateData.websiteUrl = update.websiteUrl;
|
||||
if (isDefined(update.termsUrl)) updateData.termsUrl = update.termsUrl;
|
||||
if (isDefined(update.isListed)) updateData.isListed = update.isListed;
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await this.applicationRegistrationRepository.update(id, updateData);
|
||||
}
|
||||
@@ -276,11 +259,18 @@ export class ApplicationRegistrationService {
|
||||
| 'websiteUrl'
|
||||
| 'termsUrl'
|
||||
| 'latestAvailableVersion'
|
||||
| 'isListed'
|
||||
| 'isFeatured'
|
||||
| 'marketplaceDisplayData'
|
||||
| 'ownerWorkspaceId'
|
||||
>,
|
||||
> &
|
||||
Partial<
|
||||
Pick<
|
||||
ApplicationRegistrationEntity,
|
||||
| 'isProvenanceVerified'
|
||||
| 'provenanceRepositoryUrl'
|
||||
| 'provenanceVerifiedAt'
|
||||
>
|
||||
>,
|
||||
): Promise<void> {
|
||||
const existing = await this.findOneByUniversalIdentifier(
|
||||
params.universalIdentifier,
|
||||
@@ -299,6 +289,11 @@ export class ApplicationRegistrationService {
|
||||
termsUrl: params.termsUrl,
|
||||
latestAvailableVersion: params.latestAvailableVersion,
|
||||
marketplaceDisplayData: params.marketplaceDisplayData,
|
||||
...(isDefined(params.isProvenanceVerified) && {
|
||||
isProvenanceVerified: params.isProvenanceVerified,
|
||||
provenanceRepositoryUrl: params.provenanceRepositoryUrl ?? null,
|
||||
provenanceVerifiedAt: params.provenanceVerifiedAt ?? null,
|
||||
}),
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -315,29 +310,23 @@ export class ApplicationRegistrationService {
|
||||
websiteUrl: params.websiteUrl,
|
||||
termsUrl: params.termsUrl,
|
||||
latestAvailableVersion: params.latestAvailableVersion,
|
||||
isListed: params.isListed,
|
||||
isFeatured: params.isFeatured,
|
||||
marketplaceDisplayData: params.marketplaceDisplayData,
|
||||
oAuthClientId: v4(),
|
||||
oAuthRedirectUris: [],
|
||||
oAuthScopes: [],
|
||||
ownerWorkspaceId: params.ownerWorkspaceId,
|
||||
isProvenanceVerified: params.isProvenanceVerified ?? false,
|
||||
provenanceRepositoryUrl: params.provenanceRepositoryUrl ?? null,
|
||||
provenanceVerifiedAt: params.provenanceVerifiedAt ?? null,
|
||||
});
|
||||
|
||||
await this.applicationRegistrationRepository.save(registration);
|
||||
}
|
||||
|
||||
async findManyBySourceType(
|
||||
sourceType: ApplicationRegistrationSourceType,
|
||||
): Promise<ApplicationRegistrationEntity[]> {
|
||||
async findManyNpm(): Promise<ApplicationRegistrationEntity[]> {
|
||||
return this.applicationRegistrationRepository.find({
|
||||
where: { sourceType },
|
||||
});
|
||||
}
|
||||
|
||||
async findManyListed(): Promise<ApplicationRegistrationEntity[]> {
|
||||
return this.applicationRegistrationRepository.find({
|
||||
where: { isListed: true },
|
||||
where: { sourceType: ApplicationRegistrationSourceType.NPM },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+7
@@ -63,6 +63,10 @@ export class ApplicationTarballService {
|
||||
};
|
||||
}>(contentDir, 'manifest.json');
|
||||
|
||||
const packageJson = await readJsonFile<{
|
||||
version?: string;
|
||||
}>(contentDir, 'package.json');
|
||||
|
||||
if (manifest === null) {
|
||||
throw new ApplicationRegistrationException(
|
||||
'manifest.json not found or invalid in tarball',
|
||||
@@ -137,6 +141,9 @@ export class ApplicationTarballService {
|
||||
await this.appRegistrationRepository.update(appRegistration.id, {
|
||||
sourceType: ApplicationRegistrationSourceType.TARBALL,
|
||||
tarballFileId: savedFile.id,
|
||||
...(isDefined(packageJson?.version) && {
|
||||
latestAvailableVersion: packageJson.version,
|
||||
}),
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user