Update manifest structure (#17547)

Move all sync entities in an `entities` key. Rename functions to
logicFunctions

```json
{
  application: {
    ...
  },
  entities: {
    objects: [],
    logicFunctions: [],
    ...
  }
}
```
This commit is contained in:
martmull
2026-01-30 16:26:45 +01:00
committed by GitHub
parent bc022f82cb
commit f46da3eefd
162 changed files with 2555 additions and 3778 deletions
@@ -8,14 +8,11 @@ import { AuthListCommand } from './auth/auth-list';
import { AuthLoginCommand } from './auth/auth-login';
import { AuthLogoutCommand } from './auth/auth-logout';
import { AuthStatusCommand } from './auth/auth-status';
import { FunctionExecuteCommand } from './function/function-execute';
import { FunctionLogsCommand } from './function/function-logs';
import { LogicFunctionExecuteCommand } from './logic-function/logic-function-execute';
import { LogicFunctionLogsCommand } from './logic-function/logic-function-logs';
import { AuthSwitchCommand } from './auth/auth-switch';
import {
EntityAddCommand,
isSyncableEntity,
SyncableEntity,
} from './entity/entity-add';
import { EntityAddCommand } from './entity/entity-add';
import { SyncableEntity } from 'twenty-shared/application';
export const registerCommands = (program: Command): void => {
// Auth commands
@@ -67,8 +64,8 @@ export const registerCommands = (program: Command): void => {
const uninstallCommand = new AppUninstallCommand();
const addCommand = new EntityAddCommand();
const generateCommand = new AppGenerateCommand();
const logsCommand = new FunctionLogsCommand();
const executeCommand = new FunctionExecuteCommand();
const logsCommand = new LogicFunctionLogsCommand();
const executeCommand = new LogicFunctionExecuteCommand();
program
.command('app:dev [appPath]')
@@ -101,14 +98,6 @@ export const registerCommands = (program: Command): void => {
`Add a new entity to your application (${Object.values(SyncableEntity).join('|')})`,
)
.action(async (entityType?: string, options?: { path?: string }) => {
if (entityType && !isSyncableEntity(entityType)) {
console.error(
chalk.red(
`Invalid entity type "${entityType}". Must be one of: ${Object.values(SyncableEntity).join('|')}`,
),
);
process.exit(1);
}
await addCommand.execute(entityType as SyncableEntity, options?.path);
});
@@ -1,10 +1,10 @@
import { AssetWatcher } from '@/cli/utilities/build/common/asset-watcher';
import {
createFrontComponentsWatcher,
createFunctionsWatcher,
createLogicFunctionsWatcher,
type EsbuildWatcher,
} from '@/cli/utilities/build/common/esbuild-watcher';
import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-build';
import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/update-manifest-checksums';
import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { DevModeOrchestrator } from '@/cli/utilities/dev/dev-mode-orchestrator';
@@ -22,7 +22,7 @@ export class AppDevCommand {
private appPath = '';
private orchestrator: DevModeOrchestrator | null = null;
private manifestWatcher: ManifestWatcher | null = null;
private functionsWatcher: EsbuildWatcher | null = null;
private logicFunctionsWatcher: EsbuildWatcher | null = null;
private frontComponentsWatcher: EsbuildWatcher | null = null;
private assetWatcher: AssetWatcher | null = null;
private watchersStarted = false;
@@ -71,16 +71,16 @@ export class AppDevCommand {
}
private async handleWatcherRestarts(result: ManifestBuildResult) {
const { functions, frontComponents } = result.filePaths;
const { logicFunctions, frontComponents } = result.filePaths;
if (!this.watchersStarted) {
this.watchersStarted = true;
await this.startFileWatchers(functions, frontComponents);
await this.startFileWatchers(logicFunctions, frontComponents);
return;
}
if (this.functionsWatcher?.shouldRestart(functions)) {
await this.functionsWatcher.restart(functions);
if (this.logicFunctionsWatcher?.shouldRestart(logicFunctions)) {
await this.logicFunctionsWatcher.restart(logicFunctions);
}
if (this.frontComponentsWatcher?.shouldRestart(frontComponents)) {
@@ -89,18 +89,20 @@ export class AppDevCommand {
}
private async startFileWatchers(
functions: string[],
logicFunctions: string[],
frontComponents: string[],
): Promise<void> {
await Promise.all([
this.startFunctionsWatcher(functions),
this.startLogicFunctionsWatcher(logicFunctions),
this.startFrontComponentsWatcher(frontComponents),
this.startAssetWatcher(),
]);
}
private async startFunctionsWatcher(sourcePaths: string[]): Promise<void> {
this.functionsWatcher = createFunctionsWatcher({
private async startLogicFunctionsWatcher(
sourcePaths: string[],
): Promise<void> {
this.logicFunctionsWatcher = createLogicFunctionsWatcher({
appPath: this.appPath,
sourcePaths,
handleBuildError: this.orchestrator!.handleFileBuildError.bind(
@@ -111,7 +113,7 @@ export class AppDevCommand {
),
});
await this.functionsWatcher.start();
await this.logicFunctionsWatcher.start();
}
private async startFrontComponentsWatcher(
@@ -148,7 +150,7 @@ export class AppDevCommand {
await Promise.all([
this.manifestWatcher?.close(),
this.functionsWatcher?.close(),
this.logicFunctionsWatcher?.close(),
this.frontComponentsWatcher?.close(),
this.assetWatcher?.close(),
]);
@@ -1,9 +1,9 @@
import { ApiService } from '@/cli/utilities/api/api-service';
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import inquirer from 'inquirer';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
export class AppUninstallCommand {
private apiService = new ApiService();
@@ -25,7 +25,7 @@ export class AppUninstallCommand {
process.exit(1);
}
const { manifest } = await runManifestBuild(appPath);
const { manifest } = await buildManifest(appPath);
if (!manifest) {
return { success: false, error: 'Build failed' };
@@ -1,132 +1,51 @@
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-component-template';
import { getFunctionBaseFile } from '@/cli/utilities/entity/entity-function-template';
import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-function-template';
import { convertToLabel } from '@/cli/utilities/entity/entity-label';
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import camelcase from 'lodash.camelcase';
import kebabcase from 'lodash.kebabcase';
import { join } from 'path';
import { join, relative } from 'path';
import { SyncableEntity } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { getFieldBaseFile } from '@/cli/utilities/entity/entity-field-template';
const APP_FOLDER = 'src';
export enum SyncableEntity {
AGENT = 'agent',
OBJECT = 'object',
FUNCTION = 'function',
FRONT_COMPONENT = 'front-component',
ROLE = 'role',
}
export const isSyncableEntity = (value: string): value is SyncableEntity => {
return Object.values(SyncableEntity).includes(value as SyncableEntity);
};
export class EntityAddCommand {
async execute(entityType?: SyncableEntity, path?: string): Promise<void> {
try {
// Default to src/ folder, allow override with path parameter
const entity = entityType ?? (await this.getEntity());
const entityName = this.getFolderName(entity);
const appPath = path
? join(CURRENT_EXECUTION_DIRECTORY, path)
: join(CURRENT_EXECUTION_DIRECTORY, APP_FOLDER);
: join(CURRENT_EXECUTION_DIRECTORY, APP_FOLDER, entityName);
await fs.ensureDir(appPath);
const entity = entityType ?? (await this.getEntity());
const { name, file } = await this.getEntityData(entity);
if (entity === SyncableEntity.OBJECT) {
const entityData = await this.getObjectData();
const filePath = join(appPath, this.getFileName(name, entity));
const name = entityData.nameSingular;
// Use *.object.ts naming convention
const objectFileName = `${camelcase(name)}.object.ts`;
const decoratedObject = getObjectBaseFile({
data: entityData,
name,
});
const filePath = join(appPath, objectFileName);
await fs.writeFile(filePath, decoratedObject);
console.log(
chalk.green(`✓ Created object:`),
chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')),
);
return;
if (await fs.pathExists(filePath)) {
const { overwrite } = await this.handleFileExist();
if (!overwrite) {
return;
}
}
if (entity === SyncableEntity.FUNCTION) {
const entityName = await this.getEntityName(entity);
await fs.writeFile(filePath, file);
// Use *.function.ts naming convention
const functionFileName = `${kebabcase(entityName)}.function.ts`;
const decoratedLogicFunction = getFunctionBaseFile({
name: entityName,
});
const filePath = join(appPath, functionFileName);
await fs.writeFile(filePath, decoratedLogicFunction);
console.log(
chalk.green(`✓ Created function:`),
chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')),
);
return;
}
if (entity === SyncableEntity.FRONT_COMPONENT) {
const entityName = await this.getEntityName(entity);
// Use *.front-component.tsx naming convention
const frontComponentFileName = `${kebabcase(entityName)}.front-component.tsx`;
const decoratedFrontComponent = getFrontComponentBaseFile({
name: entityName,
});
const filePath = join(appPath, frontComponentFileName);
await fs.writeFile(filePath, decoratedFrontComponent);
console.log(
chalk.green(`✓ Created front component:`),
chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')),
);
return;
}
if (entity === SyncableEntity.ROLE) {
const entityName = await this.getEntityName(entity);
// Use *.role.ts naming convention
const roleFileName = `${kebabcase(entityName)}.role.ts`;
const roleFileContent = getRoleBaseFile({
name: entityName,
});
const filePath = join(appPath, roleFileName);
await fs.writeFile(filePath, roleFileContent);
console.log(
chalk.green(`✓ Created role:`),
chalk.cyan(filePath.replace(CURRENT_EXECUTION_DIRECTORY + '/', '')),
);
return;
}
console.log(
chalk.green(`✓ Created ${entityName}:`),
chalk.cyan(relative(CURRENT_EXECUTION_DIRECTORY, filePath)),
);
} catch (error) {
console.error(
chalk.red(`Add new entity failed:`),
@@ -136,6 +55,69 @@ export class EntityAddCommand {
}
}
private async getEntityData(entity: SyncableEntity) {
switch (entity) {
case SyncableEntity.Object: {
const entityData = await this.getObjectData();
const name = entityData.nameSingular;
const file = getObjectBaseFile({
data: entityData,
name,
});
return { name, file };
}
case SyncableEntity.Field: {
const entityData = await this.getFieldData();
const name = entityData.name;
const file = getFieldBaseFile({
data: entityData,
name,
});
return { name, file };
}
case SyncableEntity.LogicFunction: {
const name = await this.getEntityName(entity);
const file = getLogicFunctionBaseFile({
name,
});
return { name, file };
}
case SyncableEntity.FrontComponent: {
const name = await this.getEntityName(entity);
const file = getFrontComponentBaseFile({
name,
});
return { name, file };
}
case SyncableEntity.Role: {
const name = await this.getEntityName(entity);
const file = getRoleBaseFile({
name,
});
return { name, file };
}
default:
assertUnreachable(entity);
}
}
private async getEntity() {
const { entity } = await inquirer.prompt<{ entity: SyncableEntity }>([
{
@@ -143,18 +125,24 @@ export class EntityAddCommand {
name: 'entity',
message: `What entity do you want to create?`,
default: '',
choices: [
SyncableEntity.FUNCTION,
SyncableEntity.FRONT_COMPONENT,
SyncableEntity.OBJECT,
SyncableEntity.ROLE,
],
choices: Object.values(SyncableEntity),
},
]);
return entity;
}
private async handleFileExist() {
return await inquirer.prompt<{ overwrite: boolean }>([
{
type: 'confirm',
name: 'overwrite',
message: `File already exists. Do you want to overwrite it?`,
default: false,
},
]);
}
private async getEntityName(entity: SyncableEntity) {
const { name } = await inquirer.prompt<{ name: string }>([
{
@@ -167,10 +155,6 @@ export class EntityAddCommand {
return `${entity} name is required`;
}
if (!/^[a-z0-9-]+$/.test(input)) {
return 'Name must contain only lowercase letters, numbers, and hyphens';
}
return true;
},
},
@@ -179,8 +163,76 @@ export class EntityAddCommand {
return name;
}
private async getFieldData() {
return inquirer.prompt<{
name: string;
label: string;
type: FieldMetadataType;
objectUniversalIdentifier: string;
description: string;
}>([
{
type: 'input',
name: 'name',
message: 'Enter a name for your field:',
default: '',
validate: (input: string) => {
if (!input || input.trim().length === 0) {
return 'Please enter a non empty string';
}
return true;
},
},
{
type: 'input',
name: 'label',
message: 'Enter a label for your field:',
default: (answers: any) => {
return convertToLabel(answers.name);
},
validate: (input: string) => {
if (!input || input.trim().length === 0) {
return 'Please enter a non empty string';
}
return true;
},
},
{
type: 'select',
name: 'type',
message: 'Select the field type:',
choices: Object.values(FieldMetadataType),
default: FieldMetadataType.TEXT,
},
{
type: 'input',
name: 'objectUniversalIdentifier',
message:
'Enter the universalIdentifier of the object this field belongs to:',
default: 'fill-later',
validate: (input: string) => {
if (!input || input.trim().length === 0) {
return 'Please enter a non empty string';
}
return true;
},
},
{
type: 'input',
name: 'description',
message: 'Enter a description for your field (optional):',
default: '',
},
]);
}
private async getObjectData() {
return inquirer.prompt([
return inquirer.prompt<{
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
}>([
{
type: 'input',
name: 'nameSingular',
@@ -238,4 +290,19 @@ export class EntityAddCommand {
},
]);
}
getFolderName(entity: SyncableEntity) {
return `${kebabcase(entity)}s`;
}
getFileName(name: string, entity: SyncableEntity) {
switch (entity) {
case SyncableEntity.FrontComponent: {
return `${kebabcase(name)}.tsx`;
}
default: {
return `${kebabcase(name)}.ts`;
}
}
}
}
@@ -1,11 +1,11 @@
import { ApiService } from '@/cli/utilities/api/api-service';
import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import { type ApplicationManifest } from 'twenty-shared/application';
import { type Manifest } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
export class FunctionExecuteCommand {
export class LogicFunctionExecuteCommand {
private apiService = new ApiService();
async execute({
@@ -30,7 +30,7 @@ export class FunctionExecuteCommand {
process.exit(1);
}
const { manifest } = await runManifestBuild(appPath);
const { manifest } = await buildManifest(appPath);
if (!manifest) {
console.error(chalk.red('Failed to build manifest.'));
@@ -162,9 +162,9 @@ export class FunctionExecuteCommand {
private belongsToApplication(
fn: { universalIdentifier: string; applicationId: string | null },
manifest: ApplicationManifest,
manifest: Manifest,
): boolean {
return manifest.functions.some(
return manifest.logicFunctions.some(
(manifestFn) => manifestFn.universalIdentifier === fn.universalIdentifier,
);
}
@@ -1,9 +1,9 @@
import { ApiService } from '@/cli/utilities/api/api-service';
import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import chalk from 'chalk';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
export class FunctionLogsCommand {
export class LogicFunctionLogsCommand {
private apiService = new ApiService();
async execute({
@@ -16,7 +16,7 @@ export class FunctionLogsCommand {
functionName?: string;
}): Promise<void> {
try {
const { manifest } = await runManifestBuild(appPath);
const { manifest } = await buildManifest(appPath);
if (!manifest) {
process.exit(1);