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
@@ -1,610 +1,184 @@
import { ensureDirSync, writeFileSync, removeSync } from 'fs-extra';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { loadManifest } from '@/cli/utils/load-manifest';
import { type ApplicationConfig } from '@/application';
import { join } from 'path';
import {
loadManifest,
type LoadManifestResult,
} from '@/cli/utils/load-manifest';
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/test-app/src/app/default-function.role';
const write = (root: string, file: string, content: string) => {
const abs = join(root, file);
ensureDirSync(resolve(abs, '..'));
writeFileSync(abs, content, 'utf8');
};
const TEST_APP_PATH = join(__dirname, '../../__tests__/test-app');
const tsLibMock = `declare module 'tslib' {
export const __decorate: any;
export const __metadata: any;
export const __param: any;
export const __awaiter: any;
export const __read: any;
export const __spread: any;
export const __spreadArray: any;
export const __assign: any;
}`;
describe('loadManifest with test-app', () => {
let manifest: LoadManifestResult['manifest'];
let packageJson: LoadManifestResult['packageJson'];
let yarnLock: LoadManifestResult['yarnLock'];
let warnings: LoadManifestResult['warnings'];
let shouldGenerate: LoadManifestResult['shouldGenerate'];
const twentySdkTypesMock = `
declare module 'twenty-sdk' {
export type SyncableEntityOptions = { universalIdentifier: string };
beforeAll(async () => {
const result = await loadManifest(TEST_APP_PATH);
type ApplicationVariable = SyncableEntityOptions & {
value?: string;
description?: string;
isSecret?: boolean;
};
export type ApplicationConfig = SyncableEntityOptions & {
displayName?: string;
description?: string;
icon?: string;
applicationVariables?: Record<string, ApplicationVariable>;
functionRoleUniversalIdentifier?: string;
};
export type RoleConfig = SyncableEntityOptions & {
label: string;
description?: string;
icon?: string;
canReadAllObjectRecords?: boolean;
canUpdateAllObjectRecords?: boolean;
canSoftDeleteAllObjectRecords?: boolean;
canDestroyAllObjectRecords?: boolean;
objectPermissions?: any[];
fieldPermissions?: any[];
permissionFlags?: any[];
};
export enum PermissionFlag {
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
WORKSPACE = 'WORKSPACE',
WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS',
ROLES = 'ROLES',
DATA_MODEL = 'DATA_MODEL',
SECURITY = 'SECURITY',
WORKFLOWS = 'WORKFLOWS',
IMPERSONATE = 'IMPERSONATE',
SSO_BYPASS = 'SSO_BYPASS',
APPLICATIONS = 'APPLICATIONS',
LAYOUTS = 'LAYOUTS',
BILLING = 'BILLING',
AI_SETTINGS = 'AI_SETTINGS',
// Tool permissions
AI = 'AI',
VIEWS = 'VIEWS',
UPLOAD_FILE = 'UPLOAD_FILE',
DOWNLOAD_FILE = 'DOWNLOAD_FILE',
SEND_EMAIL_TOOL = 'SEND_EMAIL_TOOL',
HTTP_REQUEST_TOOL = 'HTTP_REQUEST_TOOL',
IMPORT_CSV = 'IMPORT_CSV',
EXPORT_CSV = 'EXPORT_CSV',
CONNECTED_ACCOUNTS = 'CONNECTED_ACCOUNTS',
PROFILE_INFORMATION = 'PROFILE_INFORMATION',
}
type RouteTrigger = {
type: 'route';
path: string;
httpMethod: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
isAuthRequired: boolean;
};
type CronTrigger = {
type: 'cron';
pattern: string;
};
type DatabaseEventTrigger = {
type: 'databaseEvent';
eventName: string;
};
type ServerlessFunctionTrigger = SyncableEntityOptions &
(RouteTrigger | CronTrigger | DatabaseEventTrigger);
export type FunctionConfig = SyncableEntityOptions & {
name?: string;
description?: string;
timeoutSeconds?: number;
triggers?: ServerlessFunctionTrigger[];
};
type ObjectMetadataOptions = SyncableEntityOptions & {
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
description?: string;
icon?: string;
};
export const Object = (_: ObjectMetadataOptions): ClassDecorator => {
return () => {};
};
export class BaseObjectMetadata {}
export enum FieldType {
TEXT = 'TEXT',
FULL_NAME = 'FULL_NAME',
ADDRESS = 'ADDRESS',
SELECT = 'SELECT',
DATE_TIME = 'DATE_TIME',
}
export const Field: (_: any) => PropertyDecorator;
}
`;
const defaultRoleMock = `
import { PermissionFlag, type RoleConfig } from 'twenty-sdk';
export const functionRole: RoleConfig = {
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
label: 'hello-world-role',
description: 'A role to define app permissions',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
objectPermissions: [
{
objectNameSingular: 'postCard',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectNameSingular: 'postCard',
fieldName: 'content',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
}
`;
const serverlessFunctionMock = `
import { type FunctionConfig } from 'twenty-sdk';
export const main = async (params: any): Promise<any> => {
return {};
}
export const config: FunctionConfig = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'hello',
timeoutSeconds: 2,
triggers: [
{
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
type: 'route',
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false
},
{
universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
type: 'cron',
pattern: '0 0 1 1 *', // Every year 1st of January
},
{
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
type: 'databaseEvent',
eventName: 'person.created'
}
]
};`;
const objectMock = `import {
Object,
Field,
FieldType
} from 'twenty-sdk';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
@Object({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post card',
labelPlural: 'Post cards',
description: ' A post card object',
icon: 'IconMail',
})
export class PostCard {
@Field({
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
})
content: string;
@Field({
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
type: FieldType.FULL_NAME,
label: 'Recipient name',
})
recipientName: string;
@Field({
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
type: FieldType.ADDRESS,
label: 'Recipient address',
})
recipientAddress: string;
@Field({
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
type: FieldType.SELECT,
label: 'Status',
defaultValue: \`'\${PostCardStatus.DRAFT}'\`,
options: [
{
value: PostCardStatus.DRAFT,
label: 'Draft',
position: 0,
color: 'gray',
},
{
value: PostCardStatus.SENT,
label: 'Sent',
position: 1,
color: 'orange',
},
{
value: PostCardStatus.DELIVERED,
label: 'Delivered',
position: 2,
color: 'green',
},
{
value: PostCardStatus.RETURNED,
label: 'Returned',
position: 3,
color: 'orange',
},
],
})
status: 'draft' | 'sent' | 'delivered' | 'returned';
@Field({
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
type: FieldType.DATE_TIME,
label: 'Delivered at',
isNullable: true,
defaultValue: null,
})
deliveredAt?: Date;
}
`;
const packageJsonMock = {
name: 'my-app',
version: '0.0.1',
license: 'MIT',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
scripts: {
'create-entity': 'twenty app add',
dev: 'twenty app dev',
generate: 'twenty app generate',
sync: 'twenty app sync',
uninstall: 'twenty app uninstall',
auth: 'twenty auth login',
},
dependencies: {
'twenty-sdk': '0.1.0',
},
devDependencies: {
'@types/node': '^24.7.2',
typescript: '^5.9.3',
},
};
const tsConfigJsonMock = {
compileOnSave: false,
compilerOptions: {
sourceMap: true,
declaration: true,
outDir: './dist',
rootDir: '.',
moduleResolution: 'node',
allowSyntheticDefaultImports: true,
emitDecoratorMetadata: true,
experimentalDecorators: true,
importHelpers: true,
allowUnreachableCode: false,
strictNullChecks: true,
alwaysStrict: true,
noImplicitAny: true,
strictBindCallApply: false,
target: 'es2018',
module: 'esnext',
lib: ['es2020', 'dom'],
skipLibCheck: true,
skipDefaultLibCheck: true,
resolveJsonModule: true,
},
exclude: ['node_modules', 'dist', '**/*.test.ts', '**/*.spec.ts'],
};
const yarnLockMock = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
`;
const applicationMockConfig: ApplicationConfig = {
universalIdentifier: 'a9faf5f8-cf7e-4f24-9d37-fd523c30febe',
displayName: 'My App',
description: 'My app description',
icon: 'IconWorld',
applicationVariables: {
TWENTY_API_KEY: {
universalIdentifier: '3a327392-3a0f-4605-9223-0633f063eaf6',
description: 'Twenty API Key',
isSecret: true,
},
TWENTY_API_URL: {
universalIdentifier: 'aa7210a6-75b0-46ca-bcbe-09a5b42a76ec',
description: 'Twenty API Url',
isSecret: false,
},
},
functionRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2',
};
const applicationConfigMock = `import { type ApplicationConfig } from 'twenty-sdk';
const config: ApplicationConfig = ${JSON.stringify(applicationMockConfig)};
export default config;
`;
describe('loadManifest (integration)', () => {
const appDirectory = join(tmpdir(), 'test-app');
beforeEach(async () => {
await ensureDirSync(appDirectory);
write(appDirectory, 'yarn.lock', yarnLockMock);
write(appDirectory, 'application.config.ts', applicationConfigMock);
write(
appDirectory,
'tsconfig.json',
JSON.stringify(tsConfigJsonMock, null, 2),
);
write(
appDirectory,
'package.json',
JSON.stringify(packageJsonMock, null, 2),
);
write(appDirectory, 'src/Account.ts', objectMock);
write(appDirectory, 'src/hello.ts', serverlessFunctionMock);
write(appDirectory, 'src/defaultRole.ts', defaultRoleMock);
write(
appDirectory,
'src/types/twenty-sdk-application.d.ts',
twentySdkTypesMock,
);
write(
appDirectory,
'src/types/tslib.d.ts',
// minimal + future-proof
tsLibMock,
);
manifest = result.manifest;
packageJson = result.packageJson;
yarnLock = result.yarnLock;
warnings = result.warnings;
shouldGenerate = result.shouldGenerate;
});
afterEach(() => {
removeSync(appDirectory);
});
it('builds a full manifest for a valid workspace', async () => {
const { packageJson, yarnLock, manifest } =
await loadManifest(appDirectory);
expect(packageJson.name).toBe('my-app');
it('should load manifest from test-app directory', async () => {
// Check package.json loaded correctly
expect(packageJson.name).toBe('test-app');
expect(packageJson.version).toBe('0.0.1');
expect(packageJson.license).toBe('MIT');
expect(yarnLock).toContain(
'# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.',
// Check yarn.lock exists (can be empty)
expect(yarnLock).toBeDefined();
// Check no warnings
expect(warnings).toEqual([]);
// Check application config
expect(manifest.application).toBeDefined();
expect(manifest.application.universalIdentifier).toBe(
'4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
);
expect(manifest.application.displayName).toBe('Hello World');
expect(manifest.application.description).toBe('A simple hello world app');
expect(manifest.application.icon).toBe('IconWorld');
// application
expect(manifest.application).toEqual(applicationMockConfig);
expect(manifest.objects).toHaveLength(1);
expect(manifest.objects.length).toBe(1);
const postCard = manifest.objects[0];
expect(postCard.universalIdentifier).toBe(
'54b589ca-eeed-4950-a176-358418b85c05',
);
expect(postCard.nameSingular).toBe('postCard');
expect(postCard.namePlural).toBe('postCards');
expect(postCard.labelSingular).toBe('Post card');
expect(postCard.labelPlural).toBe('Post cards');
expect(postCard.icon).toBe('IconMail');
for (const object of manifest.objects) {
const { universalIdentifier: _, fields, ...otherInfo } = object;
expect(otherInfo).toEqual({
description: ' A post card object',
icon: 'IconMail',
labelPlural: 'Post cards',
labelSingular: 'Post card',
namePlural: 'postCards',
nameSingular: 'postCard',
});
// Check fields
expect(postCard.fields).toHaveLength(5);
expect(Array.isArray(fields)).toBe(true);
const contentField = postCard.fields?.find((f) => f.name === 'content');
expect(contentField).toBeDefined();
expect(contentField?.universalIdentifier).toBe(
'58a0a314-d7ea-4865-9850-7fb84e72f30b',
);
expect(contentField?.type).toBe('TEXT');
expect(contentField?.label).toBe('Content');
expect(fields).toEqual([
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
type: 'TEXT',
label: 'Content',
description: "Postcard's content",
name: 'content',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
type: 'FULL_NAME',
label: 'Recipient name',
name: 'recipientName',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
type: 'ADDRESS',
label: 'Recipient address',
name: 'recipientAddress',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
type: 'SELECT',
label: 'Status',
defaultValue: "'DRAFT'",
options: [
{ value: 'DRAFT', label: 'Draft', position: 0, color: 'gray' },
{ value: 'SENT', label: 'Sent', position: 1, color: 'orange' },
{
value: 'DELIVERED',
label: 'Delivered',
position: 2,
color: 'green',
},
{
value: 'RETURNED',
label: 'Returned',
position: 3,
color: 'orange',
},
],
name: 'status',
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
type: 'DATE_TIME',
label: 'Delivered at',
isNullable: true,
defaultValue: null,
name: 'deliveredAt',
},
]);
}
const statusField = postCard.fields?.find((f) => f.name === 'status');
expect(statusField).toBeDefined();
expect(statusField?.type).toBe('SELECT');
expect(statusField?.options).toHaveLength(4);
// serverless functions
for (const serverlessFunction of manifest.serverlessFunctions) {
const {
universalIdentifier: _,
handlerPath: __,
triggers,
...otherInfo
} = serverlessFunction;
expect(manifest.serverlessFunctions).toHaveLength(2);
expect(otherInfo).toEqual({
handlerName: 'main',
name: 'hello',
timeoutSeconds: 2,
});
const testFunction = manifest.serverlessFunctions[1];
expect(testFunction.universalIdentifier).toBe(
'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
);
expect(testFunction.name).toBe('test-function');
expect(testFunction.timeoutSeconds).toBe(2);
expect(testFunction.handlerName).toBe('handler');
expect(testFunction.handlerPath).toBe('src/app/test-function.function.ts');
for (const trigger of triggers) {
const { universalIdentifier: _, ...otherInfo } = trigger;
switch (trigger.type) {
case 'route':
expect(otherInfo).toEqual({
isAuthRequired: false,
httpMethod: 'GET',
path: '/post-card/create',
type: 'route',
});
break;
case 'cron':
expect(otherInfo).toEqual({
pattern: '0 0 1 1 *',
type: 'cron',
});
break;
case 'databaseEvent':
expect(otherInfo).toEqual({
eventName: 'person.created',
type: 'databaseEvent',
});
break;
}
}
}
// Check triggers
expect(testFunction.triggers).toHaveLength(3);
const routeTrigger = testFunction.triggers.find((t) => t.type === 'route');
expect(routeTrigger).toBeDefined();
expect(routeTrigger?.universalIdentifier).toBe(
'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
);
expect(routeTrigger?.path).toBe('/post-card/create');
expect(routeTrigger?.httpMethod).toBe('GET');
const cronTrigger = testFunction.triggers.find((t) => t.type === 'cron');
expect(cronTrigger).toBeDefined();
expect(cronTrigger?.pattern).toBe('0 0 1 1 *');
const dbEventTrigger = testFunction.triggers.find(
(t) => t.type === 'databaseEvent',
);
expect(dbEventTrigger).toBeDefined();
expect(dbEventTrigger?.eventName).toBe('person.created');
// Second function
const testFunction2 = manifest.serverlessFunctions[0];
expect(testFunction2.universalIdentifier).toBe(
'eb3ffc98-88ec-45d4-9b4a-56833b219ccb',
);
expect(testFunction2.name).toBe('test-function-2');
expect(testFunction2.timeoutSeconds).toBe(2);
expect(testFunction2.handlerName).toBe('testFunction2');
expect(testFunction2.handlerPath).toBe('src/utils/test-function-2.util.ts');
//Role
expect(manifest.roles).toHaveLength(1);
for (const role of manifest.roles ?? []) {
const {
universalIdentifier: _,
objectPermissions,
fieldPermissions,
permissionFlags,
...otherInfo
} = role;
expect(otherInfo).toEqual({
label: 'hello-world-role',
description: 'A role to define app permissions',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
});
const role = manifest.roles![0];
expect(role.universalIdentifier).toBe(
'b648f87b-1d26-4961-b974-0908fd991061',
);
expect(role.label).toBe('Default function role');
expect(role.description).toBe('Default role for function Twenty client');
expect(role.canReadAllObjectRecords).toBe(false);
expect(role.canUpdateAllObjectRecords).toBe(false);
expect(Array.isArray(objectPermissions)).toBe(true);
expect(Array.isArray(fieldPermissions)).toBe(true);
expect(Array.isArray(permissionFlags)).toBe(true);
}
});
// Check object permissions
expect(role.objectPermissions).toHaveLength(1);
expect(role.objectPermissions![0].objectNameSingular).toBe('postCard');
expect(role.objectPermissions![0].canReadObjectRecords).toBe(true);
expect(role.objectPermissions![0].canUpdateObjectRecords).toBe(true);
it('should not define serverless for util file', async () => {
write(
appDirectory,
'src/utils/format.ts',
`
export const format = async (params: any): Promise<any> => {
return {};
}
`,
// Check field permissions
expect(role.fieldPermissions).toHaveLength(1);
expect(role.fieldPermissions![0].objectNameSingular).toBe('postCard');
expect(role.fieldPermissions![0].fieldName).toBe('content');
expect(role.fieldPermissions![0].canReadFieldValue).toBe(false);
expect(manifest.sources).toBeDefined();
expect(manifest.sources['src']).toBeDefined();
// Check that the source files are loaded
const srcSources = manifest.sources['src'] as Record<string, unknown>;
const appSources = srcSources['app'] as Record<string, string>;
expect(appSources['application.config.ts']).toBeDefined();
expect(appSources['postCard.object.ts']).toBeDefined();
expect(appSources['test-function.function.ts']).toBeDefined();
expect(appSources['default-function.role.ts']).toBeDefined();
// Verify source content contains expected code
expect(appSources['application.config.ts']).toContain('defineApp');
expect(appSources['postCard.object.ts']).toContain('defineObject');
expect(appSources['test-function.function.ts']).toContain('defineFunction');
expect(appSources['default-function.role.ts']).toContain('defineRole');
expect(shouldGenerate).toBe(false);
const expectedRoleId = DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER;
expect(manifest.application.functionRoleUniversalIdentifier).toBe(
expectedRoleId,
);
const { manifest } = await loadManifest(appDirectory);
expect(manifest.serverlessFunctions.length).toBe(1);
});
const linkedRole = manifest.roles?.find(
(r) => r.universalIdentifier === expectedRoleId,
);
expect(linkedRole).toBeDefined();
it('manifest should contains typescript sources', async () => {
const { manifest } = await loadManifest(appDirectory);
// the method is already exercised in loadManifest; just assert again:
expect(Object.keys(manifest.sources)).toEqual([
'application.config.ts',
'src',
]);
expect(Object.keys(manifest.sources['src'])).toEqual([
'Account.ts',
'defaultRole.ts',
'hello.ts',
]);
});
expect(manifest.application.applicationVariables).toBeDefined();
it('manifest should contains typescript sources', async () => {
const { shouldGenerate } = await loadManifest(appDirectory);
expect(shouldGenerate).toBe(false);
const defaultRecipient =
manifest.application.applicationVariables?.DEFAULT_RECIPIENT_NAME;
expect(defaultRecipient).toBeDefined();
expect(defaultRecipient?.universalIdentifier).toBe(
'19e94e59-d4fe-4251-8981-b96d0a9f74de',
);
expect(defaultRecipient?.description).toBe(
'Default recipient name for postcards',
);
expect(defaultRecipient?.value).toBe('Alex Karp');
expect(defaultRecipient?.isSecret).toBe(false);
});
});