Add Client Api generation (#17961)

## Add API client generation to SDK dev mode and refactor orchestrator
into step-based pipeline

### Why

The SDK dev mode lacked typed API client generation, forcing developers
to work without auto-generated GraphQL types when building applications.
Additionally, the orchestrator was a monolithic class that mixed watcher
management, token handling, and sync logic — making it difficult to
extend with new steps like client generation.

### How

- **Refactored the orchestrator** into a step-based pipeline with
dedicated classes: `CheckServer`, `EnsureValidTokens`,
`ResolveApplication`, `BuildManifest`, `UploadFiles`,
`GenerateApiClient`, `SyncApplication`, and `StartWatchers`. Each step
has typed input/output/status, managed by a new `OrchestratorState`
class.
- **Added `GenerateApiClientOrchestratorStep`** that detects
object/field schema changes and regenerates a typed GraphQL client (via
`@genql/cli`) into `node_modules/twenty-sdk/generated` for seamless
imports.
- **Replaced `checkApplicationExist`** with `findOneApplication` on both
server resolver and SDK API service, returning the entity data instead
of a boolean.
- **Added application token pair mutations**
(`generateApplicationToken`, `renewApplicationToken`) to the API
service, with the server now returning `ApplicationTokenPairDTO`
containing both access and refresh tokens.
- **Restructured the dev UI** into `dev/ui/components/` with dedicated
panel, section, and event log components.
- **Simplified `AppDevCommand`** from ~180 lines of watcher management
down to ~40 lines that delegate entirely to the orchestrator.
This commit is contained in:
Charles Bochet
2026-02-17 18:45:52 +01:00
committed by GitHub
parent 0891886aa0
commit c0cc0689d6
72 changed files with 2419 additions and 1422 deletions
@@ -16,7 +16,6 @@
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"entity:add": "twenty entity:add",
"app:generate": "twenty app:generate",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"app:uninstall": "twenty app:uninstall",
@@ -27,7 +27,7 @@ export const EXPECTED_MANIFEST: Manifest = {
icon: 'IconWorld',
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
packageJsonChecksum: '42c3913415952d91ff1cf67ef6452872',
packageJsonChecksum: '2851d0e2c3621a57e1fd103a245b6fde',
},
frontComponents: [
{
@@ -16,7 +16,6 @@
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"entity:add": "twenty entity:add",
"app:generate": "twenty app:generate",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"app:uninstall": "twenty app:uninstall",
@@ -9,7 +9,7 @@ export const EXPECTED_MANIFEST: Manifest = {
description: 'An app with all entities at root level',
icon: 'IconFolder',
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
packageJsonChecksum: '351460efb13a6c1bee63e27cf87f6ece',
packageJsonChecksum: '93ae1e2eb3db18351d06f43550700dcc',
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
},
publicAssets: [],
@@ -16,7 +16,6 @@
"auth:list": "twenty auth:list",
"app:dev": "twenty app:dev",
"entity:add": "twenty entity:add",
"app:generate": "twenty app:generate",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"app:uninstall": "twenty app:uninstall",
@@ -2,12 +2,30 @@ import { vi } from 'vitest';
const mockApiService = {
validateAuth: vi.fn().mockResolvedValue({ authValid: true, serverUp: true }),
checkApplicationExist: vi
.fn()
.mockResolvedValue({ success: true, data: false }),
findOneApplication: vi.fn().mockResolvedValue({ success: true, data: null }),
createApplication: vi
.fn()
.mockResolvedValue({ success: true, data: { id: 'mock-id' } }),
generateApplicationToken: vi.fn().mockResolvedValue({
success: true,
data: {
applicationAccessToken: { token: 'mock-access-token', expiresAt: '' },
applicationRefreshToken: { token: 'mock-refresh-token', expiresAt: '' },
},
}),
renewApplicationToken: vi.fn().mockResolvedValue({
success: true,
data: {
applicationAccessToken: {
token: 'mock-renewed-access-token',
expiresAt: '',
},
applicationRefreshToken: {
token: 'mock-renewed-refresh-token',
expiresAt: '',
},
},
}),
syncApplication: vi.fn().mockResolvedValue({ success: true, data: true }),
uploadFile: vi.fn().mockResolvedValue({ success: true, data: true }),
};
@@ -15,8 +33,10 @@ const mockApiService = {
vi.mock('@/cli/utilities/api/api-service', () => ({
ApiService: class {
validateAuth = mockApiService.validateAuth;
checkApplicationExist = mockApiService.checkApplicationExist;
findOneApplication = mockApiService.findOneApplication;
createApplication = mockApiService.createApplication;
generateApplicationToken = mockApiService.generateApplicationToken;
renewApplicationToken = mockApiService.renewApplicationToken;
syncApplication = mockApiService.syncApplication;
uploadFile = mockApiService.uploadFile;
},
@@ -28,6 +48,6 @@ vi.mock('@/cli/utilities/file/file-uploader', () => ({
},
}));
vi.mock('@/cli/utilities/dev/dev-ui', () => ({
vi.mock('@/cli/utilities/dev/ui/components/dev-ui', () => ({
renderDevUI: vi.fn().mockResolvedValue({ unmount: vi.fn() }),
}));