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