Add define post isntall logic function (#18248)

As title
This commit is contained in:
martmull
2026-02-26 11:09:21 +01:00
committed by GitHub
parent fde8168a85
commit 120096346a
29 changed files with 585 additions and 71 deletions
+4
View File
@@ -50,6 +50,9 @@ yarn twenty function:logs
# Execute a function with a JSON payload
yarn twenty function:execute -n my-function -p '{"key": "value"}'
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
@@ -92,6 +95,7 @@ In interactive mode, you can pick from:
**Core files (always created):**
- `application-config.ts` — Application metadata configuration
- `roles/default-role.ts` — Default role for logic functions
- `logic-functions/pre-install.ts` — Pre-install logic function (runs before app installation)
- `logic-functions/post-install.ts` — Post-install logic function (runs after app installation)
- TypeScript configuration, ESLint, package.json, .gitignore
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.6.1",
"version": "0.6.2",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -3,6 +3,9 @@
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/cli/__tests__/apps/rich-app
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
@@ -375,6 +375,18 @@ describe('copyBaseApplicationProject', () => {
),
),
).toBe(true);
// Install functions should always exist
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'pre-install.ts'),
),
).toBe(true);
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'post-install.ts'),
),
).toBe(true);
});
});
@@ -398,6 +410,18 @@ describe('copyBaseApplicationProject', () => {
await fs.pathExists(join(srcPath, 'roles', DEFAULT_ROLE_FILE_NAME)),
).toBe(true);
// Install functions should always exist (not gated by exampleOptions)
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'pre-install.ts'),
),
).toBe(true);
expect(
await fs.pathExists(
join(srcPath, 'logic-functions', 'post-install.ts'),
),
).toBe(true);
// Example files should not exist
expect(
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
@@ -676,4 +700,124 @@ describe('copyBaseApplicationProject', () => {
expect(content).toContain('position: 0');
});
});
describe('pre-install logic function', () => {
it('should create pre-install.ts with definePreInstallLogicFunction and typed payload', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const preInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'pre-install.ts',
);
expect(await fs.pathExists(preInstallPath)).toBe(true);
const content = await fs.readFile(preInstallPath, 'utf8');
expect(content).toContain(
"import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk'",
);
expect(content).toContain(
'export default definePreInstallLogicFunction({',
);
expect(content).toContain("name: 'pre-install'");
expect(content).toContain('timeoutSeconds: 300');
expect(content).toContain(
'const handler = async (payload: InstallLogicFunctionPayload): Promise<void>',
);
expect(content).toContain('payload.previousVersion');
// Verify it has a universalIdentifier (UUID format)
expect(content).toMatch(
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
);
});
it('should always create pre-install.ts regardless of example options', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const preInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'pre-install.ts',
);
expect(await fs.pathExists(preInstallPath)).toBe(true);
});
});
describe('post-install logic function', () => {
it('should create post-install.ts with definePostInstallLogicFunction and typed payload', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const postInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'post-install.ts',
);
expect(await fs.pathExists(postInstallPath)).toBe(true);
const content = await fs.readFile(postInstallPath, 'utf8');
expect(content).toContain(
"import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk'",
);
expect(content).toContain(
'export default definePostInstallLogicFunction({',
);
expect(content).toContain("name: 'post-install'");
expect(content).toContain('timeoutSeconds: 300');
expect(content).toContain(
'const handler = async (payload: InstallLogicFunctionPayload): Promise<void>',
);
expect(content).toContain('payload.previousVersion');
// Verify it has a universalIdentifier (UUID format)
expect(content).toMatch(
/universalIdentifier: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'/,
);
});
it('should always create post-install.ts regardless of example options', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
exampleOptions: NO_EXAMPLES,
});
const postInstallPath = join(
testAppDirectory,
'src',
'logic-functions',
'post-install.ts',
);
expect(await fs.pathExists(postInstallPath)).toBe(true);
});
});
});
@@ -97,6 +97,12 @@ export const copyBaseApplicationProject = async ({
});
}
await createDefaultPreInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
fileName: 'pre-install.ts',
});
await createDefaultPostInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
@@ -268,6 +274,36 @@ export default defineLogicFunction({
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultPreInstallFunction = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createDefaultPostInstallFunction = async ({
appDirectory,
fileFolder,
@@ -279,16 +315,14 @@ const createDefaultPostInstallFunction = async ({
}) => {
const universalIdentifier = v4();
const content = `import { defineLogicFunction } from 'twenty-sdk';
const content = `import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '${universalIdentifier}';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '${universalIdentifier}',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -477,14 +511,12 @@ const createApplicationConfig = async ({
}) => {
const content = `import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '${v4()}',
displayName: '${displayName}',
description: '${description ?? ''}',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
`;
@@ -60,6 +60,9 @@ yarn twenty function:logs
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
@@ -79,7 +82,7 @@ When you run `npx create-twenty-app@latest my-twenty-app`, the scaffolder:
- Copies a minimal base application into `my-twenty-app/`
- Adds a local `twenty-sdk` dependency and Yarn 4 configuration
- Creates config files and scripts wired to the `twenty` CLI
- Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
- Generates core files (application config, default function role, pre-install and post-install functions) plus example files based on the scaffolding mode
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
@@ -106,6 +109,7 @@ my-twenty-app/
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Example logic function
│ ├── pre-install.ts # Pre-install logic function
│ └── post-install.ts # Post-install logic function
├── front-components/
│ └── hello-world.tsx # Example front component
@@ -117,7 +121,7 @@ my-twenty-app/
└── example-skill.ts # Example AI agent skill definition
```
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
At a high level:
@@ -138,6 +142,8 @@ The SDK detects entities by parsing your TypeScript files for **`export default
|-----------------|-------------|
| `defineObject()` | Custom object definitions |
| `defineLogicFunction()` | Logic function definitions |
| `definePreInstallLogicFunction()` | Pre-install logic function (runs before installation) |
| `definePostInstallLogicFunction()` | Post-install logic function (runs after installation) |
| `defineFrontComponent()` | Front component definitions |
| `defineRole()` | Role definitions |
| `defineField()` | Field extensions for existing objects |
@@ -212,6 +218,8 @@ The SDK provides helper functions for defining your app entities. As described i
| `defineApplication()` | Configure application metadata (required, one per app) |
| `defineObject()` | Define custom objects with fields |
| `defineLogicFunction()` | Define logic functions with handlers |
| `definePreInstallLogicFunction()` | Define a pre-install logic function (one per app) |
| `definePostInstallLogicFunction()` | Define a post-install logic function (one per app) |
| `defineFrontComponent()` | Define front components for custom UI |
| `defineRole()` | Configure role permissions and object access |
| `defineField()` | Extend existing objects with additional fields |
@@ -318,6 +326,7 @@ Every app has a single `application-config.ts` file that describes:
- **Who the app is**: identifiers, display name, and description.
- **How its functions run**: which role they use for permissions.
- **(Optional) variables**: keyvalue pairs exposed to your functions as environment variables.
- **(Optional) pre-install function**: a logic function that runs before the app is installed.
- **(Optional) post-install function**: a logic function that runs after the app is installed.
Use `defineApplication()` to define your application configuration:
@@ -326,7 +335,6 @@ Use `defineApplication()` to define your application configuration:
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
@@ -342,7 +350,6 @@ export default defineApplication({
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
@@ -350,7 +357,7 @@ Notes:
- `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs.
- `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
- `defaultRoleUniversalIdentifier` must match the role file (see below).
- `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
- Pre-install and post-install functions are automatically detected during the manifest build. See [Pre-install functions](#pre-install-functions) and [Post-install functions](#post-install-functions).
#### Roles and permissions
@@ -483,6 +490,43 @@ Notes:
- The `triggers` array is optional. Functions without triggers can be used as utility functions called by other functions.
- You can mix multiple trigger types in a single function.
### Pre-install functions
A pre-install function is a logic function that runs automatically before your app is installed on a workspace. This is useful for validation tasks, prerequisite checks, or preparing workspace state before the main installation proceeds.
When you scaffold a new app with `create-twenty-app`, a pre-install function is generated for you at `src/logic-functions/pre-install.ts`:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
You can also manually execute the pre-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
```
Key points:
- Pre-install functions use `definePreInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
- The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
- Only one pre-install function is allowed per application. The manifest build will error if more than one is detected.
- The function's `universalIdentifier` is automatically set as `preInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer preparation tasks.
- Pre-install functions do not need triggers — they are invoked by the platform before installation or manually via `function:execute --preInstall`.
### Post-install functions
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
@@ -491,16 +535,14 @@ When you scaffold a new app with `create-twenty-app`, a post-install function is
```typescript
// src/logic-functions/post-install.ts
import { defineLogicFunction } from 'twenty-sdk';
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
const handler = async (): Promise<void> => {
console.log('Post install logic function executed successfully!');
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default defineLogicFunction({
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
@@ -508,17 +550,6 @@ export default defineLogicFunction({
});
```
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
```typescript
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
export default defineApplication({
// ...
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
@@ -526,8 +557,10 @@ yarn twenty function:execute --postInstall
```
Key points:
- Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
- The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
- The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
- Only one post-install function is allowed per application. The manifest build will error if more than one is detected.
- The function's `universalIdentifier` is automatically set as `postInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
- Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
+5 -1
View File
@@ -155,7 +155,8 @@ Application development commands.
- `twenty function:execute [appPath]` — Execute a logic function with a JSON payload.
- Options:
- `--postInstall`: Execute the post-install logic function defined in the application config (required if `-n` and `-u` not provided).
- `--preInstall`: Execute the pre-install logic function defined in the application manifest (required if `--postInstall`, `-n`, and `-u` not provided).
- `--postInstall`: Execute the post-install logic function defined in the application manifest (required if `--preInstall`, `-n`, and `-u` not provided).
- `-n, --functionName <name>`: Name of the function to execute (required if `--postInstall` and `-u` not provided).
- `-u, --functionUniversalIdentifier <id>`: Universal ID of the function to execute (required if `--postInstall` and `-n` not provided).
- `-p, --payload <payload>`: JSON payload to send to the function (default: `{}`).
@@ -208,6 +209,9 @@ twenty function:execute -n my-function -p '{"name": "test"}'
# Execute a function by universal identifier
twenty function:execute -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf -p '{"key": "value"}'
# Execute the pre-install function
twenty function:execute --preInstall
# Execute the post-install function
twenty function:execute --postInstall
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-sdk",
"version": "0.6.1",
"version": "0.6.2",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/sdk/index.d.ts",
@@ -3,6 +3,7 @@ import {
extractDefineEntity,
ManifestEntityKey,
TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING,
TargetFunction,
} from '@/cli/utilities/build/manifest/manifest-extract-config';
import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file';
import {
@@ -72,6 +73,8 @@ export const buildManifest = async (
const views: ViewManifest[] = [];
const navigationMenuItems: NavigationMenuItemManifest[] = [];
const pageLayouts: PageLayoutManifest[] = [];
const preInstallLogicFunctionUniversalIdentifiers: string[] = [];
const postInstallLogicFunctionUniversalIdentifiers: string[] = [];
const applicationFilePaths: string[] = [];
const objectsFilePaths: string[] = [];
@@ -207,6 +210,23 @@ export const buildManifest = async (
logicFunctions.push(config);
logicFunctionsFilePaths.push(relativePath);
if (
targetFunctionName === TargetFunction.DefinePreInstallLogicFunction
) {
preInstallLogicFunctionUniversalIdentifiers.push(
extract.config.universalIdentifier,
);
}
if (
targetFunctionName === TargetFunction.DefinePostInstallLogicFunction
) {
postInstallLogicFunctionUniversalIdentifiers.push(
extract.config.universalIdentifier,
);
}
break;
}
case ManifestEntityKey.FrontComponents: {
@@ -304,6 +324,34 @@ export const buildManifest = async (
);
}
if (preInstallLogicFunctionUniversalIdentifiers.length > 1) {
errors.push(
'Only one pre install logic function is allowed per application',
);
}
if (postInstallLogicFunctionUniversalIdentifiers.length > 1) {
errors.push(
'Only one post install logic function is allowed per application',
);
}
if (application && preInstallLogicFunctionUniversalIdentifiers.length >= 1) {
application = {
...application,
preInstallLogicFunctionUniversalIdentifier:
preInstallLogicFunctionUniversalIdentifiers[0],
};
}
if (application && postInstallLogicFunctionUniversalIdentifiers.length >= 1) {
application = {
...application,
postInstallLogicFunctionUniversalIdentifier:
postInstallLogicFunctionUniversalIdentifiers[0],
};
}
const manifest = !application
? null
: {
@@ -4,6 +4,8 @@ export enum TargetFunction {
DefineApplication = 'defineApplication',
DefineField = 'defineField',
DefineLogicFunction = 'defineLogicFunction',
DefinePreInstallLogicFunction = 'definePreInstallLogicFunction',
DefinePostInstallLogicFunction = 'definePostInstallLogicFunction',
DefineObject = 'defineObject',
DefineRole = 'defineRole',
DefineSkill = 'defineSkill',
@@ -36,6 +38,10 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
[TargetFunction.DefineApplication]: ManifestEntityKey.Application,
[TargetFunction.DefineField]: ManifestEntityKey.Fields,
[TargetFunction.DefineLogicFunction]: ManifestEntityKey.LogicFunctions,
[TargetFunction.DefinePreInstallLogicFunction]:
ManifestEntityKey.LogicFunctions,
[TargetFunction.DefinePostInstallLogicFunction]:
ManifestEntityKey.LogicFunctions,
[TargetFunction.DefineObject]: ManifestEntityKey.Objects,
[TargetFunction.DefineRole]: ManifestEntityKey.Roles,
[TargetFunction.DefineSkill]: ManifestEntityKey.Skills,
@@ -2,5 +2,8 @@ import { type ApplicationManifest } from 'twenty-shared/application';
export type ApplicationConfig = Omit<
ApplicationManifest,
'packageJsonChecksum' | 'yarnLockChecksum' | 'apiClientChecksum'
| 'packageJsonChecksum'
| 'yarnLockChecksum'
| 'apiClientChecksum'
| 'postInstallLogicFunctionUniversalIdentifier'
>;
+6
View File
@@ -29,6 +29,12 @@ export type {
FrontComponentType,
} from './front-component-config';
export { defineLogicFunction } from './logic-functions/define-logic-function';
export type {
InstallLogicFunctionPayload,
InstallLogicFunctionHandler,
} from './logic-functions/install-logic-function-payload-type';
export { definePreInstallLogicFunction } from './logic-functions/define-pre-install-logic-function';
export { definePostInstallLogicFunction } from './logic-functions/define-post-install-logic-function';
export type {
LogicFunctionConfig,
LogicFunctionHandler,
@@ -0,0 +1,76 @@
import { definePostInstallLogicFunction } from '@/sdk/logic-functions/define-post-install-logic-function';
import { type InstallLogicFunctionPayload } from '@/sdk/logic-functions/install-logic-function-payload-type';
const mockHandler = async (payload: InstallLogicFunctionPayload) => ({
success: true,
previousVersion: payload.previousVersion,
});
describe('definePostInstallLogicFunction', () => {
const validRouteConfig = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'Send Postcard',
handler: mockHandler,
};
it('should return the config when valid with route trigger', () => {
const result = definePostInstallLogicFunction(validRouteConfig);
expect(result.config).toEqual(validRouteConfig);
});
it('should pass through optional fields', () => {
const config = {
...validRouteConfig,
description: 'Send a postcard to a contact',
timeoutSeconds: 30,
};
const result = definePostInstallLogicFunction(config as any);
expect(result.config.description).toBe('Send a postcard to a contact');
expect(result.config.timeoutSeconds).toBe(30);
});
it('should return error when universalIdentifier is missing', () => {
const config = {
name: 'Send Postcard',
handler: mockHandler,
};
const result = definePostInstallLogicFunction(config as any);
expect(result.success).toBe(false);
expect(result.errors).toContain(
'Post install logic function must have a universalIdentifier',
);
});
it('should return error when handler is missing', () => {
const config = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'Send Postcard',
};
const result = definePostInstallLogicFunction(config as any);
expect(result.success).toBe(false);
expect(result.errors).toContain(
'Post install logic function must have a handler',
);
});
it('should return error when handler is not a function', () => {
const config = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'Send Postcard',
handler: 'not-a-function',
};
const result = definePostInstallLogicFunction(config as any);
expect(result.success).toBe(false);
expect(result.errors).toContain(
'Post install logic function handler must be a function',
);
});
});
@@ -0,0 +1,76 @@
import { definePreInstallLogicFunction } from '@/sdk/logic-functions/define-pre-install-logic-function';
import { type InstallLogicFunctionPayload } from '@/sdk/logic-functions/install-logic-function-payload-type';
const mockHandler = async (payload: InstallLogicFunctionPayload) => ({
success: true,
previousVersion: payload.previousVersion,
});
describe('definePreInstallLogicFunction', () => {
const validRouteConfig = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'Send Postcard',
handler: mockHandler,
};
it('should return the config when valid with route trigger', () => {
const result = definePreInstallLogicFunction(validRouteConfig);
expect(result.config).toEqual(validRouteConfig);
});
it('should pass through optional fields', () => {
const config = {
...validRouteConfig,
description: 'Send a postcard to a contact',
timeoutSeconds: 30,
};
const result = definePreInstallLogicFunction(config as any);
expect(result.config.description).toBe('Send a postcard to a contact');
expect(result.config.timeoutSeconds).toBe(30);
});
it('should return error when universalIdentifier is missing', () => {
const config = {
name: 'Send Postcard',
handler: mockHandler,
};
const result = definePreInstallLogicFunction(config as any);
expect(result.success).toBe(false);
expect(result.errors).toContain(
'Pre install logic function must have a universalIdentifier',
);
});
it('should return error when handler is missing', () => {
const config = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'Send Postcard',
};
const result = definePreInstallLogicFunction(config as any);
expect(result.success).toBe(false);
expect(result.errors).toContain(
'Pre install logic function must have a handler',
);
});
it('should return error when handler is not a function', () => {
const config = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'Send Postcard',
handler: 'not-a-function',
};
const result = definePreInstallLogicFunction(config as any);
expect(result.success).toBe(false);
expect(result.errors).toContain(
'Pre install logic function handler must be a function',
);
});
});
@@ -0,0 +1,36 @@
import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config';
import { type InstallLogicFunctionHandler } from '@/sdk/logic-functions/install-logic-function-payload-type';
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
import type { DefineEntity } from '@/sdk/common/types/define-entity.type';
export const definePostInstallLogicFunction: DefineEntity<
Omit<
LogicFunctionConfig,
| 'cronTriggerSettings'
| 'databaseEventTriggerSettings'
| 'httpRouteTriggerSettings'
| 'isTool'
| 'handler'
> & {
handler: InstallLogicFunctionHandler;
}
> = (config) => {
const errors = [];
if (!config.universalIdentifier) {
errors.push('Post install logic function must have a universalIdentifier');
}
if (!config.handler) {
errors.push('Post install logic function must have a handler');
}
if (typeof config.handler !== 'function') {
errors.push('Post install logic function handler must be a function');
}
return createValidationResult({
config,
errors,
});
};
@@ -0,0 +1,36 @@
import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config';
import { type InstallLogicFunctionHandler } from '@/sdk/logic-functions/install-logic-function-payload-type';
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
import type { DefineEntity } from '@/sdk/common/types/define-entity.type';
export const definePreInstallLogicFunction: DefineEntity<
Omit<
LogicFunctionConfig,
| 'cronTriggerSettings'
| 'databaseEventTriggerSettings'
| 'httpRouteTriggerSettings'
| 'isTool'
| 'handler'
> & {
handler: InstallLogicFunctionHandler;
}
> = (config) => {
const errors = [];
if (!config.universalIdentifier) {
errors.push('Pre install logic function must have a universalIdentifier');
}
if (!config.handler) {
errors.push('Pre install logic function must have a handler');
}
if (typeof config.handler !== 'function') {
errors.push('Pre install logic function handler must be a function');
}
return createValidationResult({
config,
errors,
});
};
@@ -0,0 +1,7 @@
export type InstallLogicFunctionPayload = {
previousVersion: string;
};
export type InstallLogicFunctionHandler = (
payload: InstallLogicFunctionPayload,
) => any | Promise<any>;
@@ -130,8 +130,8 @@ describe('areFlatObjectMetadataNamesSyncedWithLabels', () => {
it('should return true with complex labels', () => {
const result = areFlatObjectMetadataNamesSyncedWithLabels({
flatObjectMetadata: {
nameSingular: 'wrongCreatedAtObject',
namePlural: 'wrongCreatedAtObjects',
nameSingular: 'wrongCreatedatObject',
namePlural: 'wrongCreatedatObjects',
labelSingular: 'Wrong CreatedAt Object',
labelPlural: 'Wrong CreatedAt Objects',
},
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Create input validation - UUID Gql create input - failure UUID - should fail with : {"uuidField":"non-uuid"} 1`] = `"Invalid UUID"`;
exports[`Create input validation - UUID Gql create input - failure UUID - should fail with : {"uuidField":"non-uuid"} 1`] = `"Invalid UUID: 'non-uuid'"`;
exports[`Create input validation - UUID Rest create input - failure UUID - should fail with : {"uuidField":"non-uuid"} 1`] = `"["Invalid UUID value 'non-uuid' for field \\"uuidField\\""]"`;
@@ -57,7 +57,7 @@ exports[`CommandMenuItem creation should fail when creating with empty workflowV
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: ''",
"name": "ValidationError",
}
`;
@@ -12,20 +12,6 @@ exports[`CommandMenuItem deletion should fail when deleting a non-existent comma
}
`;
exports[`CommandMenuItem deletion should fail when deleting with missing id 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
},
"message": "Variable "$id" of required type "UUID!" was not provided.",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem deletion should fail when deleting with empty id 1`] = `
{
"extensions": {
@@ -36,7 +22,7 @@ exports[`CommandMenuItem deletion should fail when deleting with empty id 1`] =
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: ''",
"name": "ValidationError",
}
`;
@@ -55,3 +41,17 @@ exports[`CommandMenuItem deletion should fail when deleting with invalid id (not
"name": "ValidationError",
}
`;
exports[`CommandMenuItem deletion should fail when deleting with missing id 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
},
"message": "Variable "$id" of required type "UUID!" was not provided.",
"name": "GraphQLError",
}
`;
@@ -22,7 +22,7 @@ exports[`CommandMenuItem update should fail when updating with empty id 1`] = `
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: ''",
"name": "ValidationError",
}
`;
@@ -10,7 +10,7 @@ exports[`NavigationMenuItem creation should fail when creating with empty target
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: ''",
"name": "ValidationError",
}
`;
@@ -25,7 +25,7 @@ exports[`NavigationMenuItem creation should fail when creating with empty target
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: ''",
"name": "ValidationError",
}
`;
@@ -141,4 +141,3 @@ exports[`NavigationMenuItem creation should fail when creating with missing targ
"name": "NotFoundError",
}
`;
@@ -22,7 +22,7 @@ exports[`NavigationMenuItem deletion should fail when deleting with empty id 1`]
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: ''",
"name": "ValidationError",
}
`;
@@ -22,7 +22,7 @@ exports[`NavigationMenuItem update should fail when updating with empty id 1`] =
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: ''",
"name": "ValidationError",
}
`;
@@ -67,7 +67,7 @@ exports[`NavigationMenuItem update should fail when updating with missing id 1`]
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: ''",
"name": "ValidationError",
}
`;
@@ -65,7 +65,7 @@ exports[`Object metadata update should fail when labelIdentifier is not a uuid 1
"userFriendlyMessage": "An error occurred.",
"value": "not-a-uuid",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: 'not-a-uuid'",
"name": "ValidationError",
},
]
@@ -22,7 +22,7 @@ exports[`Row Level Permission Predicate upsert should fail when fieldMetadataId
"userFriendlyMessage": "An error occurred.",
"value": "invalid-uuid",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: 'invalid-uuid'",
"name": "ValidationError",
}
`;
@@ -49,7 +49,7 @@ exports[`Row Level Permission Predicate upsert should fail when objectMetadataId
"userFriendlyMessage": "An error occurred.",
"value": "invalid-uuid",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: 'invalid-uuid'",
"name": "ValidationError",
}
`;
@@ -64,7 +64,7 @@ exports[`Row Level Permission Predicate upsert should fail when objectMetadataId
"userFriendlyMessage": "An error occurred.",
"value": "invalid-uuid",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: 'invalid-uuid'",
"name": "ValidationError",
}
`;
@@ -91,7 +91,7 @@ exports[`Row Level Permission Predicate upsert should fail when roleId is not a
"userFriendlyMessage": "An error occurred.",
"value": "invalid-uuid",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: 'invalid-uuid'",
"name": "ValidationError",
}
`;
@@ -46,7 +46,7 @@ exports[`View Filter Group creation should fail when viewId is not a valid UUID
"userFriendlyMessage": "An error occurred.",
"value": "invalid-uuid",
},
"message": "Invalid UUID: 'not-a-valid-uuid'",
"message": "Invalid UUID: 'invalid-uuid'",
"name": "ValidationError",
}
`;
@@ -14,12 +14,13 @@ export type ApplicationMarketplaceData = {
export type ApplicationManifest = SyncableEntityOptions & {
defaultRoleUniversalIdentifier: string;
postInstallLogicFunctionUniversalIdentifier?: string;
displayName: string;
description: string;
icon?: string;
applicationVariables?: ApplicationVariables;
marketplaceData?: ApplicationMarketplaceData;
preInstallLogicFunctionUniversalIdentifier?: string;
postInstallLogicFunctionUniversalIdentifier?: string;
packageJsonChecksum: string | null;
yarnLockChecksum: string | null;
apiClientChecksum: string | null;