Commit Graph

15 Commits

Author SHA1 Message Date
Marie 8379e19587 [Apps] Small improvements (#17129)
- Fix undefined in breadcrumbs
<img width="783" height="220" alt="breadcrumbs_bad"
src="https://github.com/user-attachments/assets/557648da-f825-4bf6-b66f-2ac3992eeab1"
/>
- Make role required in app definition
2026-01-14 17:28:51 +00:00
martmull d55b83375d Allow app to extend object (#17116)
- exports `extendObject` utils from `twenty-sdk`
- define `*.object-extension.ts` filename pattern to extend and object
- handle object-extension configs in manifest
2026-01-13 15:07:14 +00:00
martmull 3509838a3a Improve application ast 2 (#17045)
fix some issues with cli tools
2026-01-09 14:54:13 +00:00
martmull 40eef5c464 Improve application ast (#17016)
# Summary

- Introduces a new, flexible folder structure for Twenty SDK
applications using file suffix-based entity detection
- Adds defineApp, defineFunction, defineObject, and defineRole helper
functions with built-in validation
- Refactors manifest loading to use jiti runtime evaluation for
TypeScript config files
- Separates validation logic into dedicated module with comprehensive
error reporting

  # New Application Folder Structure

Applications now use a convention-over-configuration approach where
entities are detected by their file suffix, allowing flexible
organization within the src/app/ folder.

  # Required Structure

    my-app/
    ├── package.json
    ├── yarn.lock
    └── src/
        ├── app/
│ └── application.config.ts # Required - main application configuration
└── utils/ # Optional - handler implementations & utilities

  # Entity Detection by File Suffix

    - *.object.ts - Custom object definitions
    - *.function.ts - Serverless function definitions
    - *.role.ts - Role definitions

  # Supported Folder Organizations

  ## Traditional (by type):
    src/app/
    ├── application.config.ts
    ├── objects/
    │   └── postCard.object.ts
    ├── functions/
    │   └── createPostCard.function.ts
    └── roles/
        └── admin.role.ts

  ## Feature-based:
    src/app/
    ├── application.config.ts
    └── post-card/
        ├── postCard.object.ts
        ├── createPostCard.function.ts
        └── postCardAdmin.role.ts

  ## Flat:
    src/app/
    ├── application.config.ts
    ├── postCard.object.ts
    ├── createPostCard.function.ts
    └── admin.role.ts

  # New Helper Functions

  ## defineApp(config)

    import { defineApp } from 'twenty-sdk';

    export default defineApp({
      universalIdentifier: '4ec0391d-...',
      displayName: 'My App',
      description: 'App description',
      icon: 'IconWorld',
    });

  ## defineObject(config)

    import { defineObject, FieldType } from 'twenty-sdk';

    export default defineObject({
      universalIdentifier: '54b589ca-...',
      nameSingular: 'postCard',
      namePlural: 'postCards',
      labelSingular: 'Post Card',
      labelPlural: 'Post Cards',
      icon: 'IconMail',
      fields: [
        {
          universalIdentifier: '58a0a314-...',
          type: FieldType.TEXT,
          name: 'content',
          label: 'Content',
        },
      ],
    });

  ## defineFunction(config)

    import { defineFunction } from 'twenty-sdk';
    import { myHandler } from '../utils/my-handler';

    export default defineFunction({
      universalIdentifier: 'e56d363b-...',
      name: 'My Function',
      handler: myHandler,
      triggers: [
        {
          universalIdentifier: 'c9f84c8d-...',
          type: 'route',
          path: '/my-route',
          httpMethod: 'POST',
        },
      ],
    });

  ## defineRole(config)

    import { defineRole, PermissionFlag } from 'twenty-sdk';

    export default defineRole({
      universalIdentifier: 'b648f87b-...',
      label: 'App User',
      objectPermissions: [
        {
          objectNameSingular: 'postCard',
          canReadObjectRecords: true,
        },
      ],
      permissionFlags: [PermissionFlag.UPLOAD_FILE],
    });

  # Test plan

    - Verify npx twenty app sync works with new folder structure
    - Verify npx twenty app dev works with new folder structure
    - Verify validation errors display correctly for invalid configs
- Verify all three folder organization styles work (traditional,
feature-based, flat)
    - Run existing E2E tests to ensure backward compatibility
2026-01-09 13:06:30 +00:00
martmull 1cbbd04761 Function trigger updates 2 (#16608)
- Improves route trigger job performances
- expose function params types

## Before

<img width="938" height="271" alt="image"
src="https://github.com/user-attachments/assets/5752ba64-f31d-44ed-974d-536e63458f2c"
/>


## After

<img width="1000" height="559" alt="image"
src="https://github.com/user-attachments/assets/b1f4927a-5f43-49f0-a606-244c72356772"
/>
2025-12-16 18:20:26 +01:00
martmull e289f3056e 1895 extensibility v1 application tokens 3 (#16504)
- moves applicationRoleId to application entity
- add new `APPLICATION` FieldActorSource and `APPLICATION`
JwtTokenTypeEnum value
- create a new token with applicationId when executing a function
- when applicationId is in token, check for application.defaultRole
permissions
-use twenty-shared types in `twenty-sdk/application`
- create a new import from generate called "Twenty" that you can use
directly without having to set TWENTY_API_KEY AND TWENTY_API_URL (keep
metadata or core parameter only)
- provide to serverless unique one time BEARER TOKEN to run it

Result
<img width="977" height="566" alt="image"
src="https://github.com/user-attachments/assets/e78428a0-5b13-4975-aa13-58ee3b32450c"
/>

<img width="910" height="596" alt="image"
src="https://github.com/user-attachments/assets/6ec72bf5-7655-4093-a45e-ad269595a324"
/>

<img width="741" height="568" alt="image"
src="https://github.com/user-attachments/assets/7683944c-fd79-4417-8fb2-8e4815cc112f"
/>
2025-12-15 17:44:23 +01:00
martmull 7f1e69740a 1895 extensibility v1 application tokens (#16365)
First PR to implement application tokens
- add new application role in twenty-server
- move duplicated constants and types to twenty-shared
- will  add role configuration utils into twenty-sdk in another PR
2025-12-08 10:42:46 +01:00
martmull e498367e2f Merge twenty-cli into twenty-sdk (#16150)
- Moves twenty-cli content into twenty-sdk
- add a new twenty-sdk:0.1.0 version
- this new twenty-sdk exports a cli command called 'twenty' (like
twenty-cli before)
- deprecates twenty-cli
- simplify app init command base-project
- use `twenty-sdk:0.1.0` in base project
- move the "twenty-sdk/application" barrel to "twenty-sdk"
- add `create-twenty-app` package

<img width="1512" height="919" alt="image"
src="https://github.com/user-attachments/assets/007bef45-4e71-419a-9213-cebed376adbf"
/>

<img width="1506" height="929" alt="image"
src="https://github.com/user-attachments/assets/3de2fec6-1624-4923-ae13-f4e1cf165eb5"
/>
2025-12-01 11:44:35 +01:00
martmull 3190ca5b9e 1858 extensibility create relation metadata decorator in thwenty sdkapplications (#15907)
as title

First PR 
I will update the twenty-cli in another PR
2025-11-20 10:22:42 +01:00
Paul Rastoin 2a44bde848 Dynamic grql api wrapper on application sync (#15791)
# Introduction

Important note: for the moment testing this locally will require some
hack due to latest twenty-sdk not being published.
You will need to build twenty-cli and `cd packages/twenty-cli && yarn
link`
To finally sync the app in your app folder as `cd app-folder && twenty
app sync`

close https://github.com/twentyhq/core-team-issues/issues/1863

In this PR is introduced the generate sdk programmatic call to
[genql](https://genql.dev/) exposed in a `client` barrel of `twenty-sdk`
located in this package as there's high chances that will add a codegen
layer above it at some point ?

The cli calls this method after a sync application and writes a client
in a generated folder. It will make a graql introspection query on the
whole workspace. We should later improve that and only filter by current
applicationId and its dependencies ( when twenty-standard application is
introduced )

Fully typesafe ( input, output, filters etc ) auto-completed client

## Hello-world app serverless refactor

<img width="2480" height="1326" alt="image"
src="https://github.com/user-attachments/assets/b18ea372-b21d-4560-8fbc-1dc348427a95"
/>

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2025-11-17 14:46:59 +01:00
martmull f4663038b6 Rename sdk utils (#15769)
Follow ups
2025-11-12 11:20:59 +01:00
martmull 534a2bf647 Rename sdk utils (#15766)
As title
2025-11-12 10:42:55 +01:00
martmull 9880f192a5 Move composite types to twenty-shared (#15741) 2025-11-10 22:11:52 +01:00
martmull abde3c04ac 1630 extensibility twenty cli ability to create edit and delete fields (#15501)
As title

- adds decorators in twenty-sdk
- update twenty-cli load-manifest to it gets @FieldMetadata infos +
testing
- update twenty-server so it CRUD fields properly, using
universalIdentifier
- Fix UI so we can update managed objects records
- move FieldMetadata items from twenty-server to twenty-shared
2025-11-05 17:50:06 +01:00
martmull a6cc80eedd 1751 extensibility twenty sdk v2 use twenty sdk to define a serverless function trigger (#15347)
This PR adds 2 columns handlerPath and handlerName in serverlessFunction
to locate the entrypoint of a serverless in a codebase

It adds the following decorators in twenty-sdk:
- ServerlessFunction
- DatabaseEventTrigger
- RouteTrigger
- CronTrigger
- ApplicationVariable

It still supports deprecated entity.manifest.jsonc 

Overall code needs to be cleaned a little bit, but it should work
properly so you can try to test if the DEVX fits your needs

See updates in hello-world application

```typescript
import axios from 'axios';
import {
  DatabaseEventTrigger,
  ServerlessFunction,
  RouteTrigger,
  CronTrigger,
  ApplicationVariable,
} from 'twenty-sdk';

@ApplicationVariable({
  universalIdentifier: 'dedc53eb-9c12-4fe2-ba86-4a2add19d305',
  key: 'TWENTY_API_KEY',
  description: 'Twenty API Key',
  isSecret: true,
})
@DatabaseEventTrigger({
  universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
  eventName: 'person.created',
})
@RouteTrigger({
  universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
  path: '/post-card/create',
  httpMethod: 'GET',
  isAuthRequired: false,
})
@CronTrigger({
  universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
  pattern: '0 0 1 1 *', // Every year 1st of January
})
@ServerlessFunction({
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
})
class CreateNewPostCard {
  main = async (params: { recipient: string }): Promise<string> => {
    const { recipient } = params;

    const options = {
      method: 'POST',
      url: 'http://localhost:3000/rest/postCards',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
      },
      data: { name: recipient ?? 'Unknown' },
    };

    try {
      const { data } = await axios.request(options);

      console.log(`New post card to "${recipient}" created`);

      return data;
    } catch (error) {
      console.error(error);
      throw error;
    }
  };
}

export const createNewPostCardHandler = new CreateNewPostCard().main;

```


### [edit] V2 

After the v1 proposal, I see that using a class method to define the
serverless function handler is pretty confusing. Lets leave
serverlessFunction configuration decorators on the class, but move the
handler like before. Here is the v2 hello-world serverless function:

```typescript
import axios from 'axios';
import {
  DatabaseEventTrigger,
  ServerlessFunction,
  RouteTrigger,
  CronTrigger,
  ApplicationVariable,
} from 'twenty-sdk';

@ApplicationVariable({
  universalIdentifier: 'dedc53eb-9c12-4fe2-ba86-4a2add19d305',
  key: 'TWENTY_API_KEY',
  description: 'Twenty API Key',
  isSecret: true,
})
@DatabaseEventTrigger({
  universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
  eventName: 'person.created',
})
@RouteTrigger({
  universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
  path: '/post-card/create',
  httpMethod: 'GET',
  isAuthRequired: false,
})
@CronTrigger({
  universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
  pattern: '0 0 1 1 *', // Every year 1st of January
})
@ServerlessFunction({
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
})
export class ServerlessFunctionDefinition {}

export const main = async (params: { recipient: string }): Promise<string> => {
  const { recipient } = params;

  const options = {
    method: 'POST',
    url: 'http://localhost:3000/rest/postCards',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
    },
    data: { name: recipient ?? 'Unknown' },
  };

  try {
    const { data } = await axios.request(options);

    console.log(`New post card to "${recipient}" created`);

    return data;
  } catch (error) {
    console.error(error);
    throw error;
  }
};

```


### [edit] V3

After the v2 proposal, we don't really like decorators on empty classes.
We decided to go with a Vercel approach with a config constant

```typescript
import axios from 'axios';
import { ServerlessFunctionConfig } from 'twenty-sdk';

export const main = async (params: { recipient: string }): Promise<string> => {
  const { recipient } = params;

  const options = {
    method: 'POST',
    url: 'http://localhost:3000/rest/postCards',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
    },
    data: { name: recipient ?? 'Unknown' },
  };

  try {
    const { data } = await axios.request(options);

    console.log(`New post card to "${recipient}" created`);

    return data;
  } catch (error) {
    console.error(error);
    throw error;
  }
};

export const config: ServerlessFunctionConfig = {
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
  routeTriggers: [
  {
    universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
    path: '/post-card/create',
    httpMethod: 'GET',
    isAuthRequired: false,
  }
  ],
  cronTriggers: [
    {
      universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
      pattern: '0 0 1 1 *', // Every year 1st of January
    }
  ],
  databaseEventTriggers: [
  {
    universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
    eventName: 'person.created',
   }
  ]
}

```
2025-10-29 16:51:43 +00:00