mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
This PR adds 2 columns handlerPath and handlerName in serverlessFunction
to locate the entrypoint of a serverless in a codebase
It adds the following decorators in twenty-sdk:
- ServerlessFunction
- DatabaseEventTrigger
- RouteTrigger
- CronTrigger
- ApplicationVariable
It still supports deprecated entity.manifest.jsonc
Overall code needs to be cleaned a little bit, but it should work
properly so you can try to test if the DEVX fits your needs
See updates in hello-world application
```typescript
import axios from 'axios';
import {
DatabaseEventTrigger,
ServerlessFunction,
RouteTrigger,
CronTrigger,
ApplicationVariable,
} from 'twenty-sdk';
@ApplicationVariable({
universalIdentifier: 'dedc53eb-9c12-4fe2-ba86-4a2add19d305',
key: 'TWENTY_API_KEY',
description: 'Twenty API Key',
isSecret: true,
})
@DatabaseEventTrigger({
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
eventName: 'person.created',
})
@RouteTrigger({
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
})
@CronTrigger({
universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
pattern: '0 0 1 1 *', // Every year 1st of January
})
@ServerlessFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
})
class CreateNewPostCard {
main = async (params: { recipient: string }): Promise<string> => {
const { recipient } = params;
const options = {
method: 'POST',
url: 'http://localhost:3000/rest/postCards',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
},
data: { name: recipient ?? 'Unknown' },
};
try {
const { data } = await axios.request(options);
console.log(`New post card to "${recipient}" created`);
return data;
} catch (error) {
console.error(error);
throw error;
}
};
}
export const createNewPostCardHandler = new CreateNewPostCard().main;
```
### [edit] V2
After the v1 proposal, I see that using a class method to define the
serverless function handler is pretty confusing. Lets leave
serverlessFunction configuration decorators on the class, but move the
handler like before. Here is the v2 hello-world serverless function:
```typescript
import axios from 'axios';
import {
DatabaseEventTrigger,
ServerlessFunction,
RouteTrigger,
CronTrigger,
ApplicationVariable,
} from 'twenty-sdk';
@ApplicationVariable({
universalIdentifier: 'dedc53eb-9c12-4fe2-ba86-4a2add19d305',
key: 'TWENTY_API_KEY',
description: 'Twenty API Key',
isSecret: true,
})
@DatabaseEventTrigger({
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
eventName: 'person.created',
})
@RouteTrigger({
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
})
@CronTrigger({
universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
pattern: '0 0 1 1 *', // Every year 1st of January
})
@ServerlessFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
})
export class ServerlessFunctionDefinition {}
export const main = async (params: { recipient: string }): Promise<string> => {
const { recipient } = params;
const options = {
method: 'POST',
url: 'http://localhost:3000/rest/postCards',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
},
data: { name: recipient ?? 'Unknown' },
};
try {
const { data } = await axios.request(options);
console.log(`New post card to "${recipient}" created`);
return data;
} catch (error) {
console.error(error);
throw error;
}
};
```
### [edit] V3
After the v2 proposal, we don't really like decorators on empty classes.
We decided to go with a Vercel approach with a config constant
```typescript
import axios from 'axios';
import { ServerlessFunctionConfig } from 'twenty-sdk';
export const main = async (params: { recipient: string }): Promise<string> => {
const { recipient } = params;
const options = {
method: 'POST',
url: 'http://localhost:3000/rest/postCards',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
},
data: { name: recipient ?? 'Unknown' },
};
try {
const { data } = await axios.request(options);
console.log(`New post card to "${recipient}" created`);
return data;
} catch (error) {
console.error(error);
throw error;
}
};
export const config: ServerlessFunctionConfig = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
routeTriggers: [
{
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
}
],
cronTriggers: [
{
universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
pattern: '0 0 1 1 *', // Every year 1st of January
}
],
databaseEventTriggers: [
{
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
eventName: 'person.created',
}
]
}
```
194 lines
5 KiB
JavaScript
194 lines
5 KiB
JavaScript
import js from '@eslint/js';
|
|
import nxPlugin from '@nx/eslint-plugin';
|
|
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
|
import typescriptParser from '@typescript-eslint/parser';
|
|
import importPlugin from 'eslint-plugin-import';
|
|
import linguiPlugin from 'eslint-plugin-lingui';
|
|
import preferArrowPlugin from 'eslint-plugin-prefer-arrow';
|
|
import prettierPlugin from 'eslint-plugin-prettier';
|
|
import unicornPlugin from 'eslint-plugin-unicorn';
|
|
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
|
|
import jsoncParser from 'jsonc-eslint-parser';
|
|
|
|
export default [
|
|
// Base JavaScript configuration
|
|
js.configs.recommended,
|
|
|
|
// Lingui recommended rules
|
|
linguiPlugin.configs['flat/recommended'],
|
|
|
|
// Global ignores
|
|
{
|
|
ignores: ['**/node_modules/**'],
|
|
},
|
|
|
|
// Base configuration for all files
|
|
{
|
|
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
plugins: {
|
|
prettier: prettierPlugin,
|
|
lingui: linguiPlugin,
|
|
'@nx': nxPlugin,
|
|
'prefer-arrow': preferArrowPlugin,
|
|
import: importPlugin,
|
|
'unused-imports': unusedImportsPlugin,
|
|
unicorn: unicornPlugin,
|
|
},
|
|
rules: {
|
|
// General rules
|
|
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
|
'no-console': [
|
|
'warn',
|
|
{ allow: ['group', 'groupCollapsed', 'groupEnd'] },
|
|
],
|
|
'no-control-regex': 0,
|
|
'no-debugger': 'error',
|
|
'no-duplicate-imports': 'error',
|
|
'no-undef': 'off',
|
|
'no-unused-vars': 'off',
|
|
|
|
// Nx rules
|
|
'@nx/enforce-module-boundaries': [
|
|
'error',
|
|
{
|
|
enforceBuildableLibDependency: true,
|
|
allow: [],
|
|
depConstraints: [
|
|
{
|
|
sourceTag: 'scope:apps',
|
|
onlyDependOnLibsWithTags: ['scope:apps', 'scope:sdk'],
|
|
},
|
|
{
|
|
sourceTag: 'scope:sdk',
|
|
onlyDependOnLibsWithTags: ['scope:sdk'],
|
|
},
|
|
{
|
|
sourceTag: 'scope:shared',
|
|
onlyDependOnLibsWithTags: ['scope:shared'],
|
|
},
|
|
{
|
|
sourceTag: 'scope:backend',
|
|
onlyDependOnLibsWithTags: ['scope:shared', 'scope:backend'],
|
|
},
|
|
{
|
|
sourceTag: 'scope:frontend',
|
|
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
|
|
},
|
|
{
|
|
sourceTag: 'scope:zapier',
|
|
onlyDependOnLibsWithTags: ['scope:shared'],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
|
|
// Import rules
|
|
'import/no-relative-packages': 'error',
|
|
'import/no-useless-path-segments': 'error',
|
|
'import/no-duplicates': ['error', { considerQueryString: true }],
|
|
|
|
// Prefer arrow functions
|
|
'prefer-arrow/prefer-arrow-functions': [
|
|
'error',
|
|
{
|
|
disallowPrototype: true,
|
|
singleReturnOnly: false,
|
|
classPropertiesAllowed: false,
|
|
},
|
|
],
|
|
|
|
// Unused imports
|
|
'unused-imports/no-unused-imports': 'warn',
|
|
'unused-imports/no-unused-vars': [
|
|
'warn',
|
|
{
|
|
vars: 'all',
|
|
varsIgnorePattern: '^_',
|
|
args: 'after-used',
|
|
argsIgnorePattern: '^_',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// TypeScript specific configuration
|
|
{
|
|
files: ['**/*.{ts,tsx}'],
|
|
languageOptions: {
|
|
parser: typescriptParser,
|
|
parserOptions: {
|
|
ecmaFeatures: {
|
|
jsx: true,
|
|
},
|
|
},
|
|
},
|
|
plugins: {
|
|
'@typescript-eslint': typescriptEslint,
|
|
},
|
|
rules: {
|
|
// TypeScript rules
|
|
'no-redeclare': 'off', // Turn off base rule for TypeScript
|
|
'@typescript-eslint/no-redeclare': 'error', // Use TypeScript-aware version
|
|
'@typescript-eslint/ban-ts-comment': 'error',
|
|
'@typescript-eslint/consistent-type-imports': [
|
|
'error',
|
|
{
|
|
prefer: 'type-imports',
|
|
fixStyle: 'inline-type-imports',
|
|
},
|
|
],
|
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
'@typescript-eslint/interface-name-prefix': 'off',
|
|
'@typescript-eslint/no-empty-interface': [
|
|
'error',
|
|
{
|
|
allowSingleExtends: true,
|
|
},
|
|
],
|
|
'@typescript-eslint/no-explicit-any': 'off',
|
|
'@typescript-eslint/no-empty-function': 'off',
|
|
'@typescript-eslint/no-unused-vars': 'off',
|
|
},
|
|
},
|
|
|
|
// JavaScript specific configuration
|
|
{
|
|
files: ['*.{js,jsx}'],
|
|
rules: {
|
|
// JavaScript-specific rules if needed
|
|
},
|
|
},
|
|
|
|
// Test files
|
|
{
|
|
files: [
|
|
'*.spec.@(ts|tsx|js|jsx)',
|
|
'*.integration-spec.@(ts|tsx|js|jsx)',
|
|
'*.test.@(ts|tsx|js|jsx)',
|
|
],
|
|
languageOptions: {
|
|
globals: {
|
|
jest: true,
|
|
describe: true,
|
|
it: true,
|
|
expect: true,
|
|
beforeEach: true,
|
|
afterEach: true,
|
|
beforeAll: true,
|
|
afterAll: true,
|
|
},
|
|
},
|
|
rules: {
|
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
},
|
|
},
|
|
|
|
// JSON files
|
|
{
|
|
files: ['**/*.json'],
|
|
languageOptions: {
|
|
parser: jsoncParser,
|
|
},
|
|
},
|
|
];
|