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
This commit is contained in:
martmull
2026-01-09 14:06:30 +01:00
committed by GitHub
parent defc988d7f
commit 40eef5c464
49 changed files with 3106 additions and 1288 deletions
@@ -2,7 +2,7 @@ import * as fs from 'fs-extra';
import { join } from 'path';
import { v4 } from 'uuid';
const SOURCE_FOLDER = 'src';
const APP_FOLDER = 'src/app';
export const copyBaseApplicationProject = async ({
appName,
@@ -23,23 +23,19 @@ export const copyBaseApplicationProject = async ({
await createYarnLock(appDirectory);
const sourceFolderPath = join(appDirectory, SOURCE_FOLDER);
const appFolderPath = join(appDirectory, APP_FOLDER);
await fs.ensureDir(sourceFolderPath);
const defaultServerlessFunctionRoleUniversalIdentifier = v4();
await fs.ensureDir(appFolderPath);
await createDefaultServerlessFunctionRoleConfig({
displayName: appDisplayName,
appDirectory: sourceFolderPath,
defaultServerlessFunctionRoleUniversalIdentifier,
appDirectory: appFolderPath,
});
await createApplicationConfig({
displayName: appDisplayName,
description: appDescription,
appDirectory: sourceFolderPath,
defaultServerlessFunctionRoleUniversalIdentifier,
appDirectory: appFolderPath,
});
};
@@ -94,49 +90,49 @@ yarn-error.log*
const createDefaultServerlessFunctionRoleConfig = async ({
displayName,
appDirectory,
defaultServerlessFunctionRoleUniversalIdentifier,
}: {
displayName: string;
appDirectory: string;
defaultServerlessFunctionRoleUniversalIdentifier: string;
}) => {
const content = `import { type RoleConfig } from 'twenty-sdk';
const universalIdentifier = v4();
export const functionRole: RoleConfig = {
universalIdentifier: '${defaultServerlessFunctionRoleUniversalIdentifier}',
const content = `import { defineRole } from 'twenty-sdk';
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineRole({
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
label: '${displayName} default function role',
description: '${displayName} default function role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
};
});
`;
await fs.writeFile(join(appDirectory, 'role.config.ts'), content);
await fs.writeFile(join(appDirectory, 'default-function.role.ts'), content);
};
const createApplicationConfig = async ({
displayName,
description,
appDirectory,
defaultServerlessFunctionRoleUniversalIdentifier,
}: {
displayName: string;
description?: string;
appDirectory: string;
defaultServerlessFunctionRoleUniversalIdentifier: string;
}) => {
const content = `import { type ApplicationConfig } from 'twenty-sdk';
const content = `import { defineApp } from 'twenty-sdk';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
const config: ApplicationConfig = {
export default defineApp({
universalIdentifier: '${v4()}',
displayName: '${displayName}',
description: '${description ?? ''}',
functionRoleUniversalIdentifier: '${defaultServerlessFunctionRoleUniversalIdentifier}',
};
export default config;
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
});
`;
await fs.writeFile(join(appDirectory, 'application.config.ts'), content);
@@ -172,7 +168,7 @@ const createPackageJson = async ({
'lint-fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': '0.2.4',
'twenty-sdk': '0.3.0-alpha',
},
devDependencies: {
typescript: '^5.9.3',