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>
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import chalk from 'chalk';
|
||||
import * as chokidar from 'chokidar';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
import { loadManifest } from '../utils/load-manifest';
|
||||
import { AppSyncCommand } from './app-sync.command';
|
||||
|
||||
export class AppDevCommand {
|
||||
private apiService = new ApiService();
|
||||
private syncCommand = new AppSyncCommand();
|
||||
|
||||
async execute(options: {
|
||||
appPath?: string;
|
||||
@@ -18,13 +17,7 @@ export class AppDevCommand {
|
||||
|
||||
this.logStartupInfo(appPath, debounceMs);
|
||||
|
||||
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
|
||||
|
||||
await this.apiService.syncApplication({
|
||||
manifest,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
});
|
||||
await this.syncCommand.execute(appPath);
|
||||
|
||||
const watcher = this.setupFileWatcher(appPath, debounceMs);
|
||||
|
||||
@@ -64,13 +57,7 @@ export class AppDevCommand {
|
||||
timeout = setTimeout(async () => {
|
||||
console.log(chalk.blue('🔄 Changes detected, syncing...'));
|
||||
|
||||
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
|
||||
|
||||
await this.apiService.syncApplication({
|
||||
manifest,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
});
|
||||
await this.syncCommand.execute(appPath);
|
||||
|
||||
console.log(
|
||||
chalk.gray('👀 Watching for changes... (Press Ctrl+C to stop)'),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import chalk from 'chalk';
|
||||
import { GenerateService } from '../services/generate.service';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
|
||||
export class AppGenerateCommand {
|
||||
private generateService = new GenerateService();
|
||||
|
||||
async execute(appPath: string = CURRENT_EXECUTION_DIRECTORY) {
|
||||
try {
|
||||
await this.generateService.generateClient(appPath);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Generate Twenty client failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,12 @@ import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-dire
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { ApiResponse } from '../types/config.types';
|
||||
import { loadManifest } from '../utils/load-manifest';
|
||||
import { GenerateService } from '../services/generate.service';
|
||||
|
||||
export class AppSyncCommand {
|
||||
private apiService = new ApiService();
|
||||
private generateService = new GenerateService();
|
||||
|
||||
// TODO improve typing
|
||||
async execute(
|
||||
appPath: string = CURRENT_EXECUTION_DIRECTORY,
|
||||
): Promise<ApiResponse<any>> {
|
||||
@@ -16,21 +17,7 @@ export class AppSyncCommand {
|
||||
console.log(chalk.gray(`📁 App Path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
|
||||
|
||||
const result = await this.apiService.syncApplication({
|
||||
manifest,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red('❌ Sync failed:'), result.error);
|
||||
} else {
|
||||
console.log(chalk.green('✅ Application synced successfully'));
|
||||
}
|
||||
|
||||
return result;
|
||||
return await this.synchronize({ appPath });
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Sync failed:'),
|
||||
@@ -39,4 +26,38 @@ export class AppSyncCommand {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async synchronize({ appPath }: { appPath: string }) {
|
||||
const { manifest, packageJson, yarnLock, isTwentyClientUsed } =
|
||||
await loadManifest(appPath);
|
||||
|
||||
let serverlessSyncResult = await this.apiService.syncApplication({
|
||||
manifest,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
});
|
||||
|
||||
if (isTwentyClientUsed) {
|
||||
await this.generateService.generateClient(appPath);
|
||||
|
||||
const { manifest: manifestWithClient } = await loadManifest(appPath);
|
||||
|
||||
serverlessSyncResult = await this.apiService.syncApplication({
|
||||
manifest: manifestWithClient,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
});
|
||||
}
|
||||
|
||||
if (!serverlessSyncResult.success) {
|
||||
console.error(
|
||||
chalk.red('❌ Serverless functions Sync failed:'),
|
||||
serverlessSyncResult.error,
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green('✅ Serverless functions synced successfully'));
|
||||
}
|
||||
|
||||
return serverlessSyncResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AppDevCommand } from './app-dev.command';
|
||||
import { AppInitCommand } from './app-init.command';
|
||||
import { AppSyncCommand } from './app-sync.command';
|
||||
import { formatPath } from '../utils/format-path';
|
||||
import { AppGenerateCommand } from './app-generate.command';
|
||||
|
||||
export class AppCommand {
|
||||
private devCommand = new AppDevCommand();
|
||||
@@ -17,6 +18,7 @@ export class AppCommand {
|
||||
private deleteCommand = new AppDeleteCommand();
|
||||
private initCommand = new AppInitCommand();
|
||||
private addCommand = new AppAddCommand();
|
||||
private generateCommand = new AppGenerateCommand();
|
||||
|
||||
getCommand(): Command {
|
||||
const appCommand = new Command('app');
|
||||
@@ -96,6 +98,13 @@ export class AppCommand {
|
||||
await this.addCommand.execute(entityType as SyncableEntity);
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('generate [outputPath]')
|
||||
.description('Generate Twenty client')
|
||||
.action(async (appPath?: string) => {
|
||||
await this.generateCommand.execute(formatPath(appPath));
|
||||
});
|
||||
|
||||
return appCommand;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user