Fix twenty cli (#15997)

As title

fixes "app add" and "app init" commands
adds tests
This commit is contained in:
martmull
2025-11-21 19:21:30 +01:00
committed by GitHub
parent 3b5949ec3c
commit aa5d30a911
22 changed files with 201 additions and 696 deletions
@@ -0,0 +1,10 @@
import { convertToLabel } from '../convert-to-label';
describe('convertToLabel', () => {
it('should convert to label', () => {
expect(convertToLabel('toto')).toBe('Toto');
expect(convertToLabel('totoTata')).toBe('Toto tata');
expect(convertToLabel('totoTataTiti')).toBe('Toto tata titi');
expect(convertToLabel('toto-tata-titi')).toBe('Toto tata titi');
});
});
@@ -1,21 +0,0 @@
import { getObjectMetadataDecoratedClass } from '../../utils/get-object-metadata-decorated-class';
describe('getDecoratedClass', () => {
it('should return properly formatted class', () => {
const result = getObjectMetadataDecoratedClass({
data: { nameSingular: 'Name', namePlural: 'Names' },
name: 'MyNewObject',
});
const expectedResult = `import { ObjectMetadata } from 'twenty-sdk/application';
@ObjectMetadata({
nameSingular: 'Name',
namePlural: 'Names',
})
export class MyNewObject {}
`;
expect(result).toEqual(expectedResult);
});
});
@@ -0,0 +1,30 @@
import { getObjectDecoratedClass } from '../get-object-decorated-class';
describe('getObjectDecoratedClass', () => {
it('should return proper object file', () => {
expect(
getObjectDecoratedClass({
data: {
universalIdentifier: '4122a047-260f-4cf1-bf4f-a268579d7ddf',
nameSingular: 'name',
namePlural: 'names',
labelSingular: 'Name',
labelPlural: 'Names',
},
name: 'MyNewObject',
}),
).toBe(
`import { Object } from 'twenty-sdk/application';
@Object({
universalIdentifier: '4122a047-260f-4cf1-bf4f-a268579d7ddf',
nameSingular: 'name',
namePlural: 'names',
labelSingular: 'Name',
labelPlural: 'Names',
})
export class MyNewObject {}
`,
);
});
});
@@ -0,0 +1,34 @@
import { getServerlessFunctionBaseFile } from '../get-serverless-function-base-file';
describe('getServerlessFunctionBaseFile', () => {
it('should render proper file', () => {
expect(
getServerlessFunctionBaseFile({
name: 'serverless-function-name',
universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4',
}),
)
.toBe(`import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
export const main = async (params: {
a: string;
b: number;
}): Promise<{ message: string }> => {
const { a, b } = params;
// Rename the parameters and code below with your own logic
// This is just an example
const message = \`Hello, input: \${a} and \${b}\`;
return { message };
};
export const config: ServerlessFunctionConfig = {
universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4',
name: 'serverless-function-name',
timeoutSeconds: 5,
};
`);
});
});
@@ -1,5 +1,3 @@
import { randomUUID } from 'crypto';
import { getSchemaUrls } from './schema-validator';
import * as fs from 'fs-extra';
import { BASE_APPLICATION_PROJECT_PATH } from '../constants/constants-path';
import { writeJsoncFile } from '../utils/jsonc-parser';
@@ -77,10 +75,7 @@ const createBasePackageJson = async ({
}) => {
const base = JSON.parse(await readBaseApplicationProjectFile('package.json'));
const schemas = getSchemaUrls();
base['$schema'] = schemas.appManifest;
base['universalIdentifier'] = randomUUID();
base['universalIdentifier'] = v4();
base['name'] = appName;
await writeJsoncFile(join(appDirectory, 'package.json'), base);
@@ -0,0 +1,6 @@
import { startCase } from 'lodash';
export const convertToLabel = (str: string) => {
const s = startCase(str).toLowerCase();
return s.charAt(0).toUpperCase() + s.slice(1);
};
@@ -1,6 +1,6 @@
import camelcase from 'lodash.camelcase';
export const getObjectMetadataDecoratedClass = ({
export const getObjectDecoratedClass = ({
data,
name,
}: {
@@ -15,9 +15,9 @@ export const getObjectMetadataDecoratedClass = ({
const className = camelCaseName[0].toUpperCase() + camelCaseName.slice(1);
return `import { ObjectMetadata } from 'twenty-sdk/application';
return `import { Object } from 'twenty-sdk/application';
@ObjectMetadata({
@Object({
${decoratorOptions}
})
export class ${className} {}
@@ -1,7 +1,13 @@
import kebabCase from 'lodash.kebabcase';
import { v4 } from 'uuid';
export const getServerlessFunctionBaseFile = ({ name }: { name: string }) => {
export const getServerlessFunctionBaseFile = ({
name,
universalIdentifier = v4(),
}: {
name: string;
universalIdentifier?: string;
}) => {
const kebabCaseName = kebabCase(name);
return `import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
@@ -20,7 +26,7 @@ export const main = async (params: {
};
export const config: ServerlessFunctionConfig = {
universalIdentifier: '${v4()}',
universalIdentifier: '${universalIdentifier}',
name: '${kebabCaseName}',
timeoutSeconds: 5,
};
@@ -1,83 +0,0 @@
import Ajv from 'ajv';
import * as fs from 'fs-extra';
import * as path from 'path';
import {
AGENT_SCHEMA_URL,
APP_MANIFEST_SCHEMA_URL,
OBJECT_SCHEMA_URL,
SERVERLESS_FUNCTION_SCHEMA_URL,
TRIGGER_SCHEMA_URL,
} from '../constants/schemas';
import { BASE_SCHEMAS_PATH } from '../constants/constants-path';
export class SchemaValidationError extends Error {
constructor(
message: string,
public readonly errors: any[],
public readonly filePath?: string,
) {
super(message);
this.name = 'SchemaValidationError';
}
}
const formatErrors = (errors: any[]): string => {
return errors
.map((error) => {
const path = error.instancePath || 'root';
const message = error.message;
const value =
error.data !== undefined ? ` (got: ${JSON.stringify(error.data)})` : '';
return `${path}: ${message}${value}`;
})
.join('\n');
};
export const validateSchema = async (
schemaName: 'appManifest' | 'agent' | 'object' | 'serverlessFunction',
manifest: any,
filePath?: string,
): Promise<void> => {
const ajv = new Ajv({
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 schemaPath = path.join(BASE_SCHEMAS_PATH, `${name}.schema.json`);
ajv.addSchema(await fs.readJson(schemaPath));
if (name === schemaName) {
schema = ajv.getSchema(schemaUrls[name])?.schema;
}
}
if (!schema) throw new Error(`Schema ${schemaName} not found.`);
const valid = ajv.validate(schema, manifest);
if (!valid) {
const errorMessages = formatErrors(ajv.errors || []);
throw new SchemaValidationError(
`${schemaName} validation failed:\n${errorMessages}`,
ajv.errors || [],
filePath,
);
}
};
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,
};
};