Add serverless function in twenty-cli (#14819)

- add serverlessFunction schema in twenty-cli
- add trigger schema in twenty-cli
- update serverless function code save and get
- sync serverless function
This commit is contained in:
martmull
2025-10-01 22:11:03 +02:00
committed by GitHub
parent 64dc726f50
commit 6fdd7894a1
47 changed files with 853 additions and 159 deletions
@@ -22,7 +22,7 @@ describe('app-template', () => {
expect(basePackageJson).toEqual({
$schema: APP_MANIFEST_SCHEMA_URL,
standardId: 'mocked-uuid-12345',
universalIdentifier: 'mocked-uuid-12345',
label: 'My Test App',
description: 'A Twenty application for my-test-app',
version: '0.0.1',
@@ -31,6 +31,7 @@ describe('app-template', () => {
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
license: 'MIT',
});
});
@@ -40,7 +41,7 @@ describe('app-template', () => {
const basePackageJson = createBasePackageJson(appName, '');
expect(basePackageJson.label).toBe('Calculator');
expect(basePackageJson.standardId).toBe('mocked-uuid-12345');
expect(basePackageJson.universalIdentifier).toBe('mocked-uuid-12345');
});
it('should handle kebab-case app names correctly', () => {
@@ -48,14 +49,14 @@ describe('app-template', () => {
const basePackageJson = createBasePackageJson(appName, '');
expect(basePackageJson.label).toBe('User Management System');
expect(basePackageJson.standardId).toBe('mocked-uuid-12345');
expect(basePackageJson.universalIdentifier).toBe('mocked-uuid-12345');
});
it('should generate unique standardIds', () => {
it('should generate unique universalIdentifiers', () => {
const basePackageJson = createBasePackageJson('test-app', '');
expect(basePackageJson.standardId).toBeDefined();
expect(typeof basePackageJson.standardId).toBe('string');
expect(basePackageJson.universalIdentifier).toBeDefined();
expect(typeof basePackageJson.universalIdentifier).toBe('string');
});
});
@@ -1,3 +1,4 @@
import assert from 'assert';
import * as fs from 'fs-extra';
import * as path from 'path';
import {
@@ -8,6 +9,8 @@ import {
import { parseJsoncFile } from './jsonc-parser';
import { validateSchema } from '../utils/schema-validator';
type Sources = { [key: string]: string | Sources };
const findPathFile = async (
appPath: string,
fileName: string,
@@ -28,16 +31,49 @@ const loadCoreEntity = async (
const coreEntities: CoreEntityManifest[] = [];
if (await fs.pathExists(coreEntityPath)) {
const files = await fs.readdir(coreEntityPath);
const coreEntityFileNames = files.filter(
(file) => file.endsWith('.jsonc') || file.endsWith('.json'),
);
const entities = await fs.readdir(coreEntityPath);
for (const fileName of coreEntityFileNames) {
const coreEntityManifest = await parseJsoncFile(
path.join(coreEntityPath, fileName),
for (const entity of entities) {
const entityPath = path.join(coreEntityPath, entity);
const entityResources = await fs.readdir(entityPath);
const entityManifests = entityResources.filter(
(file) =>
file.endsWith('.manifest.jsonc') || file.endsWith('.manifest.json'),
);
assert(
entityManifests.length === 1,
'Entity should have strictly one manifest file',
);
const entityManifest = entityManifests[0];
const coreEntityManifest = await parseJsoncFile(
path.join(coreEntityPath, entity, entityManifest),
);
const entitySources = entityResources.filter(
(folder) => folder === 'src',
);
assert(
entitySources.length <= 1,
'Entity should have less than one src folder or file',
);
if (entitySources.length === 1) {
const entitySourcePath = path.join(
coreEntityPath,
entity,
entitySources[0],
);
const sources = await loadFolderContentIntoJson(entitySourcePath);
coreEntityManifest['code'] = { src: sources };
}
await validator(coreEntityManifest, coreEntityPath);
coreEntities.push(coreEntityManifest);
@@ -47,6 +83,26 @@ const loadCoreEntity = async (
return coreEntities;
};
const loadFolderContentIntoJson = async (
sourcePath: string,
): Promise<Sources> => {
const sources: Sources = {};
const resources = await fs.readdir(sourcePath);
for (const resource of resources) {
const resourcePath = path.join(sourcePath, resource);
const stats = await fs.stat(resourcePath);
if (stats.isFile()) {
sources[resource] = await fs.readFile(resourcePath, 'utf8');
} else {
sources[resource] = await loadFolderContentIntoJson(resourcePath);
}
}
return sources;
};
export const loadManifest = async (
appPath: string,
): Promise<{
@@ -60,7 +116,7 @@ export const loadManifest = async (
const yarnLockPath = await findPathFile(appPath, 'yarn.lock');
const rawYarnLock = await fs.readFile(yarnLockPath, 'utf8');
await validateSchema('app-manifest', rawPackageJson, packageJsonPath);
await validateSchema('appManifest', rawPackageJson, packageJsonPath);
const agents = await loadCoreEntity(
path.join(appPath, 'agents'),
@@ -72,6 +128,11 @@ export const loadManifest = async (
(manifest, path) => validateSchema('object', manifest, path),
);
const serverlessFunctions = await loadCoreEntity(
path.join(appPath, 'serverlessFunctions'),
(manifest, path) => validateSchema('serverlessFunction', manifest, path),
);
return {
packageJson: rawPackageJson,
yarnLock: rawYarnLock,
@@ -79,6 +140,7 @@ export const loadManifest = async (
...rawPackageJson,
agents,
objects,
serverlessFunctions,
},
};
};
@@ -10,7 +10,7 @@ export const createBasePackageJson = (
return {
$schema: schemas.appManifest,
standardId: randomUUID(),
universalIdentifier: randomUUID(),
label: appName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
@@ -20,6 +20,7 @@ export const createBasePackageJson = (
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
description,
license: 'MIT',
version: '0.0.1',
@@ -5,6 +5,8 @@ import {
AGENT_SCHEMA_URL,
APP_MANIFEST_SCHEMA_URL,
OBJECT_SCHEMA_URL,
SERVERLESS_FUNCTION_SCHEMA_URL,
TRIGGER_SCHEMA_URL,
} from '../constants/schemas';
export class SchemaValidationError extends Error {
@@ -31,7 +33,7 @@ const formatErrors = (errors: any[]): string => {
};
export const validateSchema = async (
schemaName: 'app-manifest' | 'agent' | 'object',
schemaName: 'appManifest' | 'agent' | 'object' | 'serverlessFunction',
manifest: any,
filePath?: string,
): Promise<void> => {
@@ -39,18 +41,19 @@ export const validateSchema = async (
allErrors: true,
verbose: true,
strict: false,
$data: true,
});
const schemaUrls = getSchemaUrls();
let schema;
for (const name of Object.keys(schemaUrls) as (keyof typeof schemaUrls)[]) {
const formattedName = name === 'appManifest' ? 'app-manifest' : name;
const schemasDir = path.join(__dirname, '../../schemas');
const schemaPath = path.join(schemasDir, `${formattedName}.schema.json`);
const schemaPath = path.join(schemasDir, `${name}.schema.json`);
ajv.addSchema(await fs.readJson(schemaPath));
if (formattedName === schemaName) {
if (name === schemaName) {
schema = ajv.getSchema(schemaUrls[name])?.schema;
}
}
@@ -71,8 +74,10 @@ export const validateSchema = async (
export const getSchemaUrls = () => {
return {
trigger: TRIGGER_SCHEMA_URL,
agent: AGENT_SCHEMA_URL,
object: OBJECT_SCHEMA_URL,
serverlessFunction: SERVERLESS_FUNCTION_SCHEMA_URL,
appManifest: APP_MANIFEST_SCHEMA_URL,
};
};