Compare commits

...
Author SHA1 Message Date
Weiko c27815ca34 fix 2026-02-24 15:34:28 +01:00
Weiko 19aeec66d2 fix merge 2026-02-24 10:59:46 +01:00
Weiko 16e2204d4d Merge branch 'main' into c--introduce-metadata-client-in-twenty-sdk 2026-02-24 09:26:29 +01:00
Weiko 611c2706c8 remove output 2026-02-20 17:17:52 +01:00
Weiko df5af17760 Introduce metadataClient to twenty-sdk
use better import

update hello-world
2026-02-20 17:15:36 +01:00
23 changed files with 279 additions and 159 deletions
@@ -5,7 +5,7 @@ import {
type ObjectRecordCreateEvent,
type ObjectRecordUpdateEvent,
} from 'twenty-sdk';
import Twenty, { type Person } from '../generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const MAILCHIMP_API_URL: string =
process.env.MAILCHIMP_SERVER_PREFIX !== '' &&
@@ -88,7 +88,7 @@ type DatabaseEvent =
| DatabaseEventPayload<ObjectRecordUpdateEvent<Person>>;
const fetchCompanyData = async (companyId: string): Promise<twentyCompany> => {
const { company } = await new Twenty().query({
const { company } = await new CoreApiClient().query({
company: {
__args: { filter: { id: { eq: companyId } } },
name: true,
@@ -1,5 +1,5 @@
import { defineFunction } from 'twenty-sdk';
import { createClient } from '../../generated';
import { createClient } from 'twenty-sdk/core-api';
// TODO: import from twenty-sdk when 0.4.0 is deployed
type ServerlessFunctionEvent<TBody = object> = {
@@ -430,10 +430,10 @@ 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 Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -642,10 +642,10 @@ 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 Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -773,9 +773,9 @@ You can create new skills in two ways:
The typed client is auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema. Use it in your functions:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -795,13 +795,13 @@ Notes:
#### Uploading files
The generated `Twenty` client 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 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.
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/metadata-api';
import * as fs from 'fs';
const client = new Twenty();
const client = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
@@ -433,10 +433,10 @@ export default defineRole({
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -653,10 +653,10 @@ const handler = async (event: RoutePayload) => {
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -786,9 +786,9 @@ export default defineSkill({
يُولَّد العميل مضبوط الأنواع تلقائيًا بواسطة `yarn twenty app:dev` ويُخزَّن في `node_modules/twenty-sdk/generated` استنادًا إلى مخطط مساحة العمل لديك. استخدمه في وظائفك:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -809,13 +809,13 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
#### رفع الملفات
يتضمن العميل `Twenty` المُولَّد طريقة `uploadFile` لإرفاق الملفات بالحقول من نوع ملف ضمن كائنات مساحة العمل الخاصة بك. نظرًا لأن عملاء GraphQL القياسيون لا يدعمون تحميل الملفات متعددة الأجزاء افتراضيًا، يوفر العميل هذه الطريقة المخصصة التي تطبق [مواصفة طلب GraphQL متعدد الأجزاء](https://github.com/jaydenseric/graphql-multipart-request-spec) في الخلفية.
يتضمن العميل `MetadataApiClient` المُولَّد طريقة `uploadFile` لإرفاق الملفات بالحقول من نوع ملف ضمن كائنات مساحة العمل الخاصة بك. نظرًا لأن عملاء GraphQL القياسيون لا يدعمون تحميل الملفات متعددة الأجزاء افتراضيًا، يوفر العميل هذه الطريقة المخصصة التي تطبق [مواصفة طلب GraphQL متعدد الأجزاء](https://github.com/jaydenseric/graphql-multipart-request-spec) في الخلفية.
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/metadata-api';
import * as fs from 'fs';
const client = new Twenty();
const client = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
@@ -433,10 +433,10 @@ Každý soubor funkce používá `defineLogicFunction()` k exportu konfigurace s
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -653,10 +653,10 @@ Chcete-li označit logickou funkci jako nástroj, nastavte `isTool: true` a posk
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -786,9 +786,9 @@ Nové dovednosti můžete vytvářet dvěma způsoby:
Typovaný klient je automaticky generován pomocí `yarn twenty app:dev` a ukládá se do `node_modules/twenty-sdk/generated` podle schématu vašeho pracovního prostoru. Použijte jej ve svých funkcích:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -809,13 +809,13 @@ Poznámky:
#### Nahrávání souborů
Vygenerovaný klient `Twenty` obsahuje metodu `uploadFile` pro připojování souborů k polím typu souboru u objektů ve vašem pracovním prostoru. Protože standardní klienti GraphQL nativně nepodporují nahrávání souborů pomocí multipart, klient poskytuje tuto speciální metodu, která interně implementuje [specifikaci multipart požadavků GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
Vygenerovaný klient `MetadataApiClient` obsahuje metodu `uploadFile` pro připojování souborů k polím typu souboru u objektů ve vašem pracovním prostoru. Protože standardní klienti GraphQL nativně nepodporují nahrávání souborů pomocí multipart, klient poskytuje tuto speciální metodu, která interně implementuje [specifikaci multipart požadavků GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/metadata-api';
import * as fs from 'fs';
const client = new Twenty();
const client = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
@@ -433,10 +433,10 @@ Jede Funktionsdatei verwendet `defineLogicFunction()`, um eine Konfiguration mit
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -653,10 +653,10 @@ Um eine Logikfunktion als Tool zu markieren, setzen Sie `isTool: true` und geben
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -786,9 +786,9 @@ Sie können neue Skills auf zwei Arten erstellen:
Der typisierte Client wird von `yarn twenty app:dev` automatisch generiert und basierend auf Ihrem Arbeitsbereichs-Schema in `node_modules/twenty-sdk/generated` gespeichert. Verwenden Sie ihn in Ihren Funktionen:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -809,13 +809,13 @@ Notizen:
#### Dateien hochladen
Der generierte `Twenty`-Client enthält eine Methode `uploadFile`, um Dateien an Felder des Typs Datei in Ihren Arbeitsbereichsobjekten anzuhängen. Da Standard-GraphQL-Clients Multipart-Datei-Uploads nicht nativ unterstützen, stellt der Client diese dedizierte Methode bereit, die unter der Haube die [GraphQL-Multipart-Anfragespezifikation](https://github.com/jaydenseric/graphql-multipart-request-spec) implementiert.
Der generierte `MetadataApiClient`-Client enthält eine Methode `uploadFile`, um Dateien an Felder des Typs Datei in Ihren Arbeitsbereichsobjekten anzuhängen. Da Standard-GraphQL-Clients Multipart-Datei-Uploads nicht nativ unterstützen, stellt der Client diese dedizierte Methode bereit, die unter der Haube die [GraphQL-Multipart-Anfragespezifikation](https://github.com/jaydenseric/graphql-multipart-request-spec) implementiert.
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/metadata-api';
import * as fs from 'fs';
const client = new Twenty();
const client = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
@@ -418,10 +418,10 @@ Cada archivo de función usa `defineFunction()` para exportar una configuración
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -585,9 +585,9 @@ Puedes crear funciones nuevas de dos maneras:
`yarn app:dev` genera automáticamente el cliente Twenty tipado en `node_modules/twenty-sdk/generated`. Úsalo en tus funciones:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -418,10 +418,10 @@ Chaque fichier de fonction utilise `defineFunction()` pour exporter une configur
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -585,9 +585,9 @@ Vous pouvez créer de nouvelles fonctions de deux façons :
`yarn app:dev` génère automatiquement le client Twenty typé dans `node_modules/twenty-sdk/generated`. Utilisez-le dans vos fonctions :
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -433,10 +433,10 @@ Ogni file di funzione usa `defineLogicFunction()` per esportare una configurazio
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -653,10 +653,10 @@ Per contrassegnare una funzione logica come strumento, imposta `isTool: true` e
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -786,9 +786,9 @@ You can create new skills in two ways:
Il client tipizzato è generato automaticamente da `yarn twenty app:dev` e salvato in `node_modules/twenty-sdk/generated` in base allo schema della tua area di lavoro. Usalo nelle tue funzioni:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -809,13 +809,13 @@ Note:
#### Caricamento dei file
Il client `Twenty` generato include un metodo `uploadFile` per allegare file ai campi di tipo file sugli oggetti del tuo spazio di lavoro. Poiché i client GraphQL standard non supportano nativamente il caricamento di file multipart, il client fornisce questo metodo dedicato che implementa la [specifica della richiesta GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec) dietro le quinte.
Il client `MetadataApiClient` generato include un metodo `uploadFile` per allegare file ai campi di tipo file sugli oggetti del tuo spazio di lavoro. Poiché i client GraphQL standard non supportano nativamente il caricamento di file multipart, il client fornisce questo metodo dedicato che implementa la [specifica della richiesta GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec) dietro le quinte.
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/metadata-api';
import * as fs from 'fs';
const client = new Twenty();
const client = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
@@ -416,10 +416,10 @@ export default defineRole({
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -583,9 +583,9 @@ const handler = async (event: RoutePayload) => {
`yarn app:dev` は `node_modules/twenty-sdk/generated` に型付き Twenty クライアントを自動生成します。 関数内で使用します:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -418,10 +418,10 @@ The `universalIdentifier` of this role is then referenced in `application.config
// src/app/createPostCard.function.ts
import { defineFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -585,9 +585,9 @@ const handler = async (event: RoutePayload) => {
`yarn app:dev`는 `node_modules/twenty-sdk/generated`에 타입드 Twenty 클라이언트를 자동으로 생성합니다. 함수에서 사용하세요:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -433,10 +433,10 @@ Cada arquivo de função usa `defineLogicFunction()` para exportar uma configura
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -653,10 +653,10 @@ Para marcar uma função lógica como ferramenta, defina `isTool: true` e forne
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -786,9 +786,9 @@ Você pode criar novas habilidades de duas formas:
O cliente tipado é gerado automaticamente pelo `yarn twenty app:dev` e armazenado em `node_modules/twenty-sdk/generated` com base no esquema do seu espaço de trabalho. Use-o em suas funções:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -809,13 +809,13 @@ Notas:
#### Carregamento de ficheiros
O cliente `Twenty` gerado inclui um método `uploadFile` para anexar ficheiros a campos do tipo ficheiro nos objetos do seu espaço de trabalho. Como os clientes GraphQL padrão não suportam nativamente o carregamento de ficheiros multipart, o cliente fornece este método dedicado que implementa, nos bastidores, a [especificação de pedidos multipart do GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
O cliente `MetadataApiClient` gerado inclui um método `uploadFile` para anexar ficheiros a campos do tipo ficheiro nos objetos do seu espaço de trabalho. Como os clientes GraphQL padrão não suportam nativamente o carregamento de ficheiros multipart, o cliente fornece este método dedicado que implementa, nos bastidores, a [especificação de pedidos multipart do GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/metadata-api';
import * as fs from 'fs';
const client = new Twenty();
const client = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
@@ -433,10 +433,10 @@ Fiecare fișier de funcție folosește `defineLogicFunction()` pentru a exporta
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -653,10 +653,10 @@ Pentru a marca o funcție logică drept instrument, setați `isTool: true` și f
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -786,9 +786,9 @@ Puteți crea abilități noi în două moduri:
Clientul tipizat este generat automat de `yarn twenty app:dev` și stocat în `node_modules/twenty-sdk/generated`, pe baza schemei spațiului tău de lucru. Folosiți-l în funcțiile dvs.:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -809,13 +809,13 @@ Notițe:
#### Încărcarea fișierelor
Clientul `Twenty` generat include o metodă `uploadFile` pentru atașarea fișierelor la câmpuri de tip fișier ale obiectelor din spațiul de lucru. Deoarece clienții GraphQL standard nu acceptă în mod nativ încărcarea fișierelor multipart, clientul oferă această metodă dedicată care implementează, sub capotă, [specificația cererilor GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec).
Clientul `MetadataApiClient` generat include o metodă `uploadFile` pentru atașarea fișierelor la câmpuri de tip fișier ale obiectelor din spațiul de lucru. Deoarece clienții GraphQL standard nu acceptă în mod nativ încărcarea fișierelor multipart, clientul oferă această metodă dedicată care implementează, sub capotă, [specificația cererilor GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec).
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/metadata-api';
import * as fs from 'fs';
const client = new Twenty();
const client = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
@@ -433,10 +433,10 @@ export default defineRole({
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -653,10 +653,10 @@ const handler = async (event: RoutePayload) => {
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -786,9 +786,9 @@ export default defineSkill({
Типизированный клиент автоматически генерируется с помощью `yarn twenty app:dev` и сохраняется в `node_modules/twenty-sdk/generated` на основе схемы вашего рабочего пространства. Используйте его в своих функциях:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -809,13 +809,13 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
#### Загрузка файлов
Сгенерированный клиент `Twenty` включает метод `uploadFile` для прикрепления файлов к полям типа «файл» в объектах вашего рабочего пространства. Поскольку стандартные клиенты GraphQL изначально не поддерживают многочастовую загрузку файлов, клиент предоставляет специальный метод, который под капотом реализует [спецификацию многочастных запросов GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
Сгенерированный клиент `MetadataApiClient` включает метод `uploadFile` для прикрепления файлов к полям типа «файл» в объектах вашего рабочего пространства. Поскольку стандартные клиенты GraphQL изначально не поддерживают многочастовую загрузку файлов, клиент предоставляет специальный метод, который под капотом реализует [спецификацию многочастных запросов GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/metadata-api';
import * as fs from 'fs';
const client = new Twenty();
const client = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
@@ -433,10 +433,10 @@ Her fonksiyon dosyası, bir işleyici ve isteğe bağlı tetikleyiciler içeren
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -653,10 +653,10 @@ Bir mantık işlevini bir araç olarak işaretlemek için `isTool: true` olarak
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -786,9 +786,9 @@ Yeni yetenekleri iki şekilde oluşturabilirsiniz:
Tipli istemci, `yarn twenty app:dev` tarafından otomatik olarak oluşturulur ve çalışma alanı şemanıza göre `node_modules/twenty-sdk/generated` içine kaydedilir. Fonksiyonlarınızda kullanın:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -809,13 +809,13 @@ Notlar:
#### Dosya yükleme
Oluşturulan `Twenty` istemcisi, çalışma alanı nesnelerinizdeki dosya türündeki alanlara dosya eklemek için bir `uploadFile` yöntemi içerir. Standart GraphQL istemcileri çok parçalı dosya yüklemelerini yerel olarak desteklemediğinden, istemci arka planda [GraphQL çok parçalı istek belirtimi](https://github.com/jaydenseric/graphql-multipart-request-spec) uygulayan bu özel yöntemi sağlar.
Oluşturulan `MetadataApiClient` istemcisi, çalışma alanı nesnelerinizdeki dosya türündeki alanlara dosya eklemek için bir `uploadFile` yöntemi içerir. Standart GraphQL istemcileri çok parçalı dosya yüklemelerini yerel olarak desteklemediğinden, istemci arka planda [GraphQL çok parçalı istek belirtimi](https://github.com/jaydenseric/graphql-multipart-request-spec) uygulayan bu özel yöntemi sağlar.
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/metadata-api';
import * as fs from 'fs';
const client = new Twenty();
const client = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
@@ -433,10 +433,10 @@ export default defineRole({
// src/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import Twenty, { type Person } from '~/generated';
import { CoreApiClient, type Person } from 'twenty-sdk/core-api';
const handler = async (params: RoutePayload) => {
const client = new Twenty(); // generated typed client
const client = new CoreApiClient(); // generated typed client
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
@@ -653,10 +653,10 @@ const handler = async (event: RoutePayload) => {
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new Twenty();
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
@@ -786,9 +786,9 @@ You can create new skills in two ways:
类型化客户端由 `yarn twenty app:dev` 自动生成,并基于你的工作区架构存放在 `node_modules/twenty-sdk/generated`。 在你的函数中使用它:
```typescript
import Twenty from '~/generated';
import { CoreApiClient } from 'twenty-sdk/core-api';
const client = new Twenty();
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
```
@@ -809,13 +809,13 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
#### 上传文件
生成的 `Twenty` 客户端包含一个 `uploadFile` 方法,用于将文件附加到工作区对象的文件类型字段。 由于标准 GraphQL 客户端不原生支持多部分文件上传,该客户端提供了一个专用方法,在底层实现了 [GraphQL 多部分请求规范](https://github.com/jaydenseric/graphql-multipart-request-spec)。
生成的 `MetadataApiClient` 包含一个 `uploadFile` 方法,用于将文件附加到工作区对象的文件类型字段。 由于标准 GraphQL 客户端不原生支持多部分文件上传,该客户端提供了一个专用方法,在底层实现了 [GraphQL 多部分请求规范](https://github.com/jaydenseric/graphql-multipart-request-spec)。
```typescript
import Twenty from '~/generated';
import { MetadataApiClient } from 'twenty-sdk/metadata-api';
import * as fs from 'fs';
const client = new Twenty();
const client = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
+7 -1
View File
@@ -1,6 +1,12 @@
node_modules
.twenty
generated
generated/*
!generated/core
generated/core/*
!generated/core/index.ts
!generated/metadata
generated/metadata/*
!generated/metadata/index.ts
storybook-static
src/front-component-renderer/__stories__/example-sources-built
src/front-component-renderer/__stories__/example-sources-built-preact
+14 -6
View File
@@ -40,10 +40,15 @@
"import": "./dist/front-component-renderer/index.mjs",
"require": "./dist/front-component-renderer/index.cjs"
},
"./generated": {
"types": "./generated/index.ts",
"import": "./generated/index.ts",
"require": "./generated/index.ts"
"./core-api": {
"types": "./generated/core/index.ts",
"import": "./generated/core/index.ts",
"require": "./generated/core/index.ts"
},
"./metadata-api": {
"types": "./generated/metadata/index.ts",
"import": "./generated/metadata/index.ts",
"require": "./generated/metadata/index.ts"
}
},
"license": "AGPL-3.0",
@@ -116,8 +121,11 @@
"front-component-renderer": [
"dist/front-component-renderer/index.d.ts"
],
"generated": [
"generated/index.ts"
"core-api": [
"generated/core/index.ts"
],
"metadata-api": [
"generated/metadata/index.ts"
]
}
}
@@ -433,8 +433,10 @@ export class ApiService {
async getSchema(options?: {
authToken?: string;
endpoint?: '/graphql' | '/metadata';
}): Promise<ApiResponse<string>> {
try {
const endpoint = options?.endpoint ?? '/graphql';
const introspectionQuery = getIntrospectionQuery();
const headers: Record<string, string> = {
@@ -447,7 +449,7 @@ export class ApiService {
}
const response = await this.client.post(
'/graphql',
endpoint,
{
query: introspectionQuery,
},
@@ -10,11 +10,12 @@ import {
} from '@/cli/utilities/build/common/restartable-watcher-interface';
import { createTypecheckPlugin } from '@/cli/utilities/build/common/typecheck-plugin';
import * as esbuild from 'esbuild';
import fs from 'fs';
import path from 'path';
import {
OUTPUT_DIR,
NODE_ESM_CJS_BANNER,
GENERATED_DIR,
NODE_ESM_CJS_BANNER,
OUTPUT_DIR,
} from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
@@ -37,8 +38,6 @@ export const LOGIC_FUNCTION_EXTERNAL_MODULES: string[] = [
'tls',
'child_process',
'worker_threads',
'twenty-sdk',
'twenty-sdk/*',
'twenty-shared',
'twenty-shared/*',
];
@@ -193,20 +192,48 @@ export class EsbuildWatcher implements RestartableWatcher {
}
}
// Resolves twenty-sdk/generated to the actual file path so esbuild
// bundles it instead of treating it as external (via twenty-sdk/*)
// Resolves all twenty-sdk imports to bypass esbuild's package.json exports check.
// - twenty-sdk/core-api → generated/core/index.ts (bundled)
// - twenty-sdk/metadata-api → generated/metadata/index.ts (bundled)
// - twenty-sdk → package entry point (bundled)
// - twenty-sdk/* (anything else) → external
const SUBPATH_TO_GENERATED_DIR: Record<string, string> = {
'twenty-sdk/core-api': 'core',
'twenty-sdk/metadata-api': 'metadata',
};
const createSdkGeneratedResolverPlugin = (appPath: string): esbuild.Plugin => ({
name: 'sdk-generated-resolver',
setup: (build) => {
build.onResolve({ filter: /^twenty-sdk\/generated/ }, () => ({
path: path.join(
appPath,
'node_modules',
'twenty-sdk',
GENERATED_DIR,
'index.ts',
),
}));
build.onResolve({ filter: /^twenty-sdk(\/|$)/ }, (args) => {
const sdkDir = path.join(appPath, 'node_modules', 'twenty-sdk');
// twenty-sdk/core-api or twenty-sdk/metadata-api → generated subdirectory
const generatedSubDir = SUBPATH_TO_GENERATED_DIR[args.path];
if (generatedSubDir) {
const resolvedPath = path.join(
sdkDir,
GENERATED_DIR,
generatedSubDir,
'index.ts',
);
if (!fs.existsSync(resolvedPath)) {
return undefined;
}
return { path: resolvedPath };
}
// twenty-sdk (main package) → resolve to dist entry point for bundling
if (args.path === 'twenty-sdk') {
return { path: path.join(sdkDir, 'dist', 'index.mjs') };
}
// any other twenty-sdk/* subpath → keep as external
return { path: args.path, external: true };
});
},
});
@@ -6,7 +6,11 @@ import os from 'os';
import path from 'path';
import { isDefined, isPlainObject } from 'twenty-shared/utils';
const MANIFEST_MOCK_MODULES = ['twenty-sdk/ui'];
const MANIFEST_MOCK_MODULES = [
'twenty-sdk/ui',
'twenty-sdk/core-api',
'twenty-sdk/metadata-api',
];
const manifestMockPlugin: esbuild.Plugin = {
name: 'manifest-mock',
@@ -25,22 +25,66 @@ export class ClientService {
const outputPath = this.resolveGeneratedPath(appPath);
const tempPath = `${outputPath}.tmp`;
const getSchemaResponse = await this.apiService.getSchema({ authToken });
await fs.ensureDir(tempPath);
await fs.emptyDir(tempPath);
await this.generateSubClient({
authToken,
endpoint: '/graphql',
subDir: 'core',
className: 'CoreApiClient',
urlSuffix: '/graphql',
tempPath,
});
await this.generateSubClient({
authToken,
endpoint: '/metadata',
subDir: 'metadata',
className: 'MetadataApiClient',
urlSuffix: '/metadata',
tempPath,
});
await fs.remove(outputPath);
await fs.move(tempPath, outputPath);
}
private async generateSubClient({
authToken,
endpoint,
subDir,
className,
urlSuffix,
tempPath,
}: {
authToken?: string;
endpoint: '/graphql' | '/metadata';
subDir: string;
className: string;
urlSuffix: string;
tempPath: string;
}): Promise<void> {
const getSchemaResponse = await this.apiService.getSchema({
authToken,
endpoint,
});
if (!getSchemaResponse.success) {
throw new Error(
`Failed to introspect schema: ${JSON.stringify(getSchemaResponse.error)}`,
`Failed to introspect schema (${endpoint}): ${JSON.stringify(getSchemaResponse.error)}`,
);
}
const { data: schema } = getSchemaResponse;
await fs.ensureDir(tempPath);
await fs.emptyDir(tempPath);
const subPath = join(tempPath, subDir);
await fs.ensureDir(subPath);
await generate({
schema,
output: tempPath,
output: subPath,
scalarTypes: {
DateTime: 'string',
JSON: 'Record<string, unknown>',
@@ -48,40 +92,61 @@ export class ClientService {
},
});
await this.injectTwentyClient(tempPath);
await fs.remove(outputPath);
await fs.move(tempPath, outputPath);
await this.injectApiClient(subPath, className, urlSuffix);
}
private resolveGeneratedPath(appPath: string): string {
return join(appPath, 'node_modules', 'twenty-sdk', GENERATED_DIR);
}
private async injectTwentyClient(output: string) {
const twentyClientContent = `
private async injectApiClient(
output: string,
className: string,
urlSuffix: string,
) {
const clientContent = this.buildApiClientCode({
className,
urlSuffix,
includeUploadFile: urlSuffix === '/metadata',
});
await fs.appendFile(join(output, 'index.ts'), clientContent);
}
private buildApiClientCode({
className,
urlSuffix,
includeUploadFile,
}: {
className: string;
urlSuffix: string;
includeUploadFile: boolean;
}): string {
const uploadFileMethod = includeUploadFile
? this.buildUploadFileMethod()
: '';
return `
// ----------------------------------------------------
// Custom Twenty client (auto-injected)
// ${className} (auto-injected)
// ----------------------------------------------------
const defaultOptions: ClientOptions = {
url: \`\${process.env.${DEFAULT_API_URL_NAME}}/graphql\`,
metadataUrl: \`\${process.env.${DEFAULT_API_URL_NAME}}/metadata\`,
url: \`\${process.env.${DEFAULT_API_URL_NAME}}${urlSuffix}\`,
headers: {
'Content-Type': 'application/json',
Authorization: \`Bearer \${process.env.${DEFAULT_API_KEY_NAME}}\`,
},
}
export default class Twenty {
export class ${className} {
private client: Client;
private url: string;
private metadataUrl: string;
private authorizationToken: string;
constructor(options?: ClientOptions) {
const merged: ClientOptions = {
const merged: ClientOptions = {
...defaultOptions,
...options,
headers: {
@@ -91,7 +156,6 @@ export default class Twenty {
};
this.client = createClient(merged);
this.url = merged.url;
this.metadataUrl = merged.metadataUrl;
this.authorizationToken = merged.headers.Authorization;
}
@@ -102,7 +166,14 @@ export default class Twenty {
mutation<R extends MutationGenqlSelection>(request: R & { __name?: string }) {
return this.client.mutation(request);
}
${uploadFileMethod}
}
`;
}
private buildUploadFileMethod(): string {
return `
async uploadFile(
fileBuffer: Buffer,
filename: string,
@@ -129,7 +200,7 @@ export default class Twenty {
form.append('map', JSON.stringify({ '0': ['variables.file'] }));
form.append('0', new Blob([fileBuffer], { type: contentType }), filename);
const response = await fetch(this.metadataUrl, {
const response = await fetch(this.url, {
method: 'POST',
headers: {
Authorization: this.authorizationToken,
@@ -144,11 +215,6 @@ export default class Twenty {
}
return result.data.uploadFilesFieldFileByUniversalIdentifier;
}
}
`;
await fs.appendFile(join(output, 'index.ts'), twentyClientContent);
}`;
}
}
@@ -13,7 +13,12 @@ import {
} from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
const API_CLIENT_FILES = ['types.ts', 'schema.ts'];
const API_CLIENT_FILES = [
'core/types.ts',
'core/schema.ts',
'metadata/types.ts',
'metadata/schema.ts',
];
export type UploadFilesOrchestratorStepOutput = {
fileUploader: FileUploader | null;
@@ -128,6 +133,8 @@ export class UploadFilesOrchestratorStep {
const outputDir = join(appPath, OUTPUT_DIR, API_CLIENT_DIR);
await fs.ensureDir(outputDir);
await fs.ensureDir(join(outputDir, 'core'));
await fs.ensureDir(join(outputDir, 'metadata'));
for (const fileName of API_CLIENT_FILES) {
const absoluteSourcePath = join(generatedDir, fileName);