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:
@@ -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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -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,
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -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);
|
||||
Reference in New Issue
Block a user