mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Fix pre post logic function not executed (#19462)
- removes pre-install function
- execute **asyncrhonously** post-install function at application
installation
- add optional `shouldRunOnVersionUpgrade` boolean value on post-install
function definition default false
- update PostInstallPayload to
```
export type PostInstallPayload = {
previousVersion?: string;
newVersion: string;
};
```
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
parent
7a317b9182
commit
d2f51cc939
33 changed files with 805 additions and 213 deletions
|
|
@ -1,13 +1,19 @@
|
|||
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
import {
|
||||
definePostInstallLogicFunction,
|
||||
type InstallLogicFunctionPayload,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
console.log(
|
||||
'Post install logic function executed successfully!',
|
||||
payload.previousVersion,
|
||||
);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '8c726dcc-1709-4eac-aa8b-f99960a9ec1b',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ describe('App installation', () => {
|
|||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
`App uninstall failed: ${
|
||||
uninstallResult.error?.message ?? 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { definePostInstallLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const SEED_POST_CARDS = [
|
||||
{
|
||||
name: 'Greetings from Paris',
|
||||
content: 'Wish you were here! The Eiffel Tower is breathtaking.',
|
||||
},
|
||||
{
|
||||
name: 'Hello from Tokyo',
|
||||
content: 'Cherry blossoms are in full bloom. Sending love!',
|
||||
},
|
||||
];
|
||||
|
||||
const handler = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
createPostCards: {
|
||||
__args: { data: SEED_POST_CARDS as any },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
console.log(`Seeded ${SEED_POST_CARDS.length} post cards on install.`);
|
||||
return {};
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '852c6321-1563-4396-b7c5-9d370f3d30a9',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
});
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { definePreInstallLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const handler = async (params: any) => {
|
||||
console.log(
|
||||
`Pre-install logic function executed successfully with params`,
|
||||
params,
|
||||
);
|
||||
return {};
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'bf27f558-4ec6-481f-b76e-1dbcd05aef1f',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before migrations to set up the application.',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { definePostInstallLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const POST_CARDS_TO_SEED = [
|
||||
{
|
||||
name: 'Greetings from Paris',
|
||||
content:
|
||||
'Wish you were here! The Eiffel Tower looks even better in person. - Alex',
|
||||
},
|
||||
{
|
||||
name: 'Hello from Tokyo',
|
||||
content:
|
||||
'The cherry blossoms are amazing this time of year. See you soon! - Sam',
|
||||
},
|
||||
];
|
||||
|
||||
const handler = async (): Promise<{
|
||||
message: string;
|
||||
createdIds: string[];
|
||||
}> => {
|
||||
console.log('Seeding 2 post cards...');
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const createdIds: string[] = [];
|
||||
|
||||
for (const postCard of POST_CARDS_TO_SEED) {
|
||||
const { createPostCard } = await client.mutation({
|
||||
createPostCard: {
|
||||
__args: {
|
||||
data: {
|
||||
name: postCard.name,
|
||||
content: postCard.content,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createPostCard?.id) {
|
||||
throw new Error(`Failed to create post card "${postCard.name}"`);
|
||||
}
|
||||
|
||||
createdIds.push(createPostCard.id);
|
||||
}
|
||||
|
||||
console.log('Seeding complete!');
|
||||
return {
|
||||
message: `Seeded ${createdIds.length} post cards`,
|
||||
createdIds,
|
||||
};
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '9f3d8c21-b471-4a82-8e5c-6f3a7b8c9d01',
|
||||
name: 'seed-post-cards',
|
||||
description: 'Seeds the workspace with 2 sample post card records.',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
|
|
@ -644,49 +644,15 @@ export default defineLogicFunction({
|
|||
**Write a good `description`.** AI agents rely on the function's `description` field to decide when to use the tool. Be specific about what the tool does and when it should be called.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Define a pre-install logic function (one per app)">
|
||||
|
||||
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.
|
||||
|
||||
```ts 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: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
|
||||
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 exec --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.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePostInstallLogicFunction" description="Define a post-install logic function (one per app)">
|
||||
|
||||
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.
|
||||
A post-install function is a logic function that runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services.
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
|
|
@ -695,6 +661,8 @@ export default definePostInstallLogicFunction({
|
|||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
|
@ -707,10 +675,168 @@ yarn twenty exec --postInstall
|
|||
|
||||
Key points:
|
||||
- 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).
|
||||
- The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic.
|
||||
- **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook.
|
||||
- **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed.
|
||||
- `shouldRunSynchronously: false` *(default)* — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window.
|
||||
- `shouldRunSynchronously: true` — the hook is executed **inline during the install flow** (same executor as pre-install). The install request blocks until the handler finishes, and if it throws, the install caller receives a `POST_INSTALL_ERROR`. No automatic retries. **Use this for fast, must-complete-before-response work** — for example, emitting a validation error to the user, or quick setup that the client will rely on immediately after the install call returns. Keep in mind the metadata migration has already been applied by the time post-install runs, so a sync-mode failure does **not** roll back the schema changes — it only surfaces the error.
|
||||
- Make sure your handler is idempotent. In async mode the queue may retry up to three times; in either mode the hook may run again on upgrades when `shouldRunOnVersionUpgrade: true`.
|
||||
- The environment variables `APPLICATION_ID`, `APP_ACCESS_TOKEN`, and `API_URL` are available inside the handler (same as any other logic function), so you can call the Twenty API with an application access token scoped to your app.
|
||||
- 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 function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in `defineApplication()`.
|
||||
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
|
||||
- **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` to trigger it manually against a running workspace.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Define a pre-install logic function (one per app)">
|
||||
|
||||
A pre-install function is a logic function that runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
You can also manually execute the pre-install function at any time using the CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --preInstall
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Pre-install functions use `definePreInstallLogicFunction()` — same specialized config as post-install, just attached to a different lifecycle slot.
|
||||
- Both pre- and post-install handlers receive the same `InstallPayload` type: `{ previousVersion?: string; newVersion: string }`. Import it once and reuse it for both hooks.
|
||||
- **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state.
|
||||
- **Execution model**: pre-install is executed **synchronously** and **blocks the install**. If the handler throws, the install is aborted before any schema changes are applied — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade.
|
||||
- As with post-install, only one pre-install function is allowed per application. It is attached to the application manifest under `preInstallLogicFunction` automatically during the build.
|
||||
- **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty exec --preInstall` to trigger it manually.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Pre-install vs post-install: when to use which" description="Choosing the right install hook">
|
||||
|
||||
Both hooks are part of the same install flow and receive the same `InstallPayload`. The difference is **when** they run relative to the workspace metadata migration, and that changes what data they can safely touch.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ install flow │
|
||||
│ │
|
||||
│ upload package → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
│ │
|
||||
│ old schema visible new schema visible │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Pre-install is always **synchronous** (it blocks the install and can abort it). Post-install is **asynchronous by default** — enqueued on a worker with automatic retries — but can opt into synchronous execution with `shouldRunSynchronously: true`. See the `definePostInstallLogicFunction` accordion above for when to use each mode.
|
||||
|
||||
**Use `post-install` for anything that needs the new schema to exist.** This is the common case:
|
||||
|
||||
- Seeding default data (creating initial records, default views, demo content) against newly-added objects and fields.
|
||||
- Registering webhooks with third-party services now that the app has its credentials.
|
||||
- Calling your own API to finish setup that depends on the synchronized metadata.
|
||||
- Idempotent "ensure this exists" logic that should reconcile state on every upgrade — combine with `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
Example — seed a default `PostCard` record after install:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
});
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the *previous* schema and its failure rolls back the upgrade, it is the right place for anything risky:
|
||||
|
||||
- **Backing up data that is about to be dropped or restructured** — e.g. you are removing a field in v2 and need to copy its values into another field or export them to storage before the migration runs.
|
||||
- **Archiving records that a new constraint would invalidate** — e.g. a field is becoming `NOT NULL` and you need to delete or fix rows with null values first.
|
||||
- **Validating compatibility and refusing the upgrade if the current data cannot be migrated cleanly** — throw from the handler and the install aborts with no changes applied. This is safer than discovering the incompatibility mid-migration.
|
||||
- **Renaming or rekeying data** ahead of a schema change that would lose the association.
|
||||
|
||||
Example — archive records before a destructive migration:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
|
||||
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
|
||||
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Backs up legacy notes into description before the v2 migration.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Rule of thumb:**
|
||||
|
||||
| You want to… | Use |
|
||||
|---|---|
|
||||
| Seed default data, configure the workspace, register external resources | `post-install` |
|
||||
| Run long-running seeding or third-party calls that shouldn't block the install response | `post-install` (default — `shouldRunSynchronously: false`, with worker retries) |
|
||||
| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` |
|
||||
| Read or back up data that the upcoming migration would lose | `pre-install` |
|
||||
| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) |
|
||||
| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` |
|
||||
| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) |
|
||||
|
||||
<Note>
|
||||
If in doubt, default to **post-install**. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineFrontComponent" description="Define front components for custom UI" >
|
||||
|
|
@ -1818,8 +1944,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
|||
# Pass a JSON payload
|
||||
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute pre-install or post-install functions
|
||||
yarn twenty exec --preInstall
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -163,8 +163,8 @@ export const registerCommands = (program: Command): void => {
|
|||
|
||||
program
|
||||
.command('exec [appPath]')
|
||||
.option('--preInstall', 'Execute pre-install logic function if defined')
|
||||
.option('--postInstall', 'Execute post-install logic function if defined')
|
||||
.option('--preInstall', 'Execute pre-install logic function if defined')
|
||||
.option(
|
||||
'-p, --payload <payload>',
|
||||
'JSON payload to send to the function',
|
||||
|
|
@ -183,22 +183,22 @@ export const registerCommands = (program: Command): void => {
|
|||
async (
|
||||
appPath?: string,
|
||||
options?: {
|
||||
preInstall?: boolean;
|
||||
postInstall?: boolean;
|
||||
preInstall?: boolean;
|
||||
payload?: string;
|
||||
functionUniversalIdentifier?: string;
|
||||
functionName?: string;
|
||||
},
|
||||
) => {
|
||||
if (
|
||||
!options?.preInstall &&
|
||||
!options?.postInstall &&
|
||||
!options?.preInstall &&
|
||||
!options?.functionUniversalIdentifier &&
|
||||
!options?.functionName
|
||||
) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
'Error: Either --preInstall, --postInstall, --functionName (-n), or --functionUniversalIdentifier (-u) is required.',
|
||||
'Error: Either --postInstall, --preInstall, --functionName (-n), or --functionUniversalIdentifier (-u) is required.',
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ import { isDefined } from 'twenty-shared/utils';
|
|||
export class LogicFunctionExecuteCommand {
|
||||
async execute({
|
||||
appPath = CURRENT_EXECUTION_DIRECTORY,
|
||||
preInstall = false,
|
||||
postInstall = false,
|
||||
preInstall = false,
|
||||
functionUniversalIdentifier,
|
||||
functionName,
|
||||
payload = '{}',
|
||||
}: {
|
||||
appPath?: string;
|
||||
preInstall?: boolean;
|
||||
postInstall?: boolean;
|
||||
preInstall?: boolean;
|
||||
functionUniversalIdentifier?: string;
|
||||
functionName?: string;
|
||||
payload?: string;
|
||||
|
|
@ -30,19 +30,19 @@ export class LogicFunctionExecuteCommand {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
const identifier = preInstall
|
||||
? 'pre install'
|
||||
: postInstall
|
||||
? 'post install'
|
||||
const identifier = postInstall
|
||||
? 'post install'
|
||||
: preInstall
|
||||
? 'pre install'
|
||||
: (functionUniversalIdentifier ?? functionName);
|
||||
|
||||
console.log(chalk.blue(`🚀 Executing function "${identifier}"...`));
|
||||
console.log(chalk.gray(` Payload: ${JSON.stringify(parsedPayload)}\n`));
|
||||
|
||||
const executeOptions = preInstall
|
||||
? { appPath, preInstall: true as const, payload: parsedPayload }
|
||||
: postInstall
|
||||
? { appPath, postInstall: true as const, payload: parsedPayload }
|
||||
const executeOptions = postInstall
|
||||
? { appPath, postInstall: true as const, payload: parsedPayload }
|
||||
: preInstall
|
||||
? { appPath, preInstall: true as const, payload: parsedPayload }
|
||||
: functionUniversalIdentifier
|
||||
? { appPath, functionUniversalIdentifier, payload: parsedPayload }
|
||||
: { appPath, functionName: functionName!, payload: parsedPayload };
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ export type FunctionExecuteOptions = {
|
|||
remote?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
} & (
|
||||
| { preInstall: true }
|
||||
| { postInstall: true }
|
||||
| { preInstall: true }
|
||||
| { functionUniversalIdentifier: string }
|
||||
| { functionName: string }
|
||||
);
|
||||
|
|
@ -39,8 +39,8 @@ const belongsToApplication = (
|
|||
};
|
||||
|
||||
const resolveIdentifier = (options: FunctionExecuteOptions): string => {
|
||||
if ('preInstall' in options) return 'pre install';
|
||||
if ('postInstall' in options) return 'post install';
|
||||
if ('preInstall' in options) return 'pre install';
|
||||
if ('functionUniversalIdentifier' in options)
|
||||
return options.functionUniversalIdentifier;
|
||||
if ('functionName' in options) return options.functionName;
|
||||
|
|
@ -92,16 +92,16 @@ const innerFunctionExecute = async (
|
|||
);
|
||||
|
||||
const targetFunction = appFunctions.find((logicFunction) => {
|
||||
if ('preInstall' in options && options.preInstall) {
|
||||
return (
|
||||
logicFunction.universalIdentifier ===
|
||||
manifest.application.preInstallLogicFunctionUniversalIdentifier
|
||||
);
|
||||
}
|
||||
if ('postInstall' in options && options.postInstall) {
|
||||
return (
|
||||
logicFunction.universalIdentifier ===
|
||||
manifest.application.postInstallLogicFunctionUniversalIdentifier
|
||||
manifest.application.postInstallLogicFunction?.universalIdentifier
|
||||
);
|
||||
}
|
||||
if ('preInstall' in options && options.preInstall) {
|
||||
return (
|
||||
logicFunction.universalIdentifier ===
|
||||
manifest.application.preInstallLogicFunction?.universalIdentifier
|
||||
);
|
||||
}
|
||||
if ('functionUniversalIdentifier' in options) {
|
||||
|
|
|
|||
|
|
@ -34,10 +34,14 @@ import {
|
|||
type RoleManifest,
|
||||
type SkillManifest,
|
||||
type ViewManifest,
|
||||
type PostInstallLogicFunctionApplicationManifest,
|
||||
type PreInstallLogicFunctionApplicationManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { addMissingFieldOptionIds } from '@/cli/utilities/build/manifest/utils/add-missing-field-option-ids';
|
||||
import { type PostInstallLogicFunctionConfig } from '@/sdk/logic-functions/post-install-logic-function-config';
|
||||
import { type PreInstallLogicFunctionConfig } from '@/sdk/logic-functions/pre-install-logic-function-config';
|
||||
import { fromRoleConfigToRoleManifest } from '@/cli/utilities/build/manifest/utils/from-role-config-to-role-manifest';
|
||||
import { type RoleConfig } from '@/sdk/roles/role-config';
|
||||
|
||||
|
|
@ -80,9 +84,10 @@ export const buildManifest = async (
|
|||
const views: ViewManifest[] = [];
|
||||
const navigationMenuItems: NavigationMenuItemManifest[] = [];
|
||||
const pageLayouts: PageLayoutManifest[] = [];
|
||||
const preInstallLogicFunctionUniversalIdentifiers: string[] = [];
|
||||
const postInstallLogicFunctionUniversalIdentifiers: string[] = [];
|
||||
|
||||
const postInstallLogicFunctions: PostInstallLogicFunctionApplicationManifest[] =
|
||||
[];
|
||||
const preInstallLogicFunctions: PreInstallLogicFunctionApplicationManifest[] =
|
||||
[];
|
||||
const applicationFilePaths: string[] = [];
|
||||
const objectsFilePaths: string[] = [];
|
||||
const fieldsFilePaths: string[] = [];
|
||||
|
|
@ -231,19 +236,31 @@ export const buildManifest = async (
|
|||
logicFunctionsFilePaths.push(relativePath);
|
||||
|
||||
if (
|
||||
targetFunctionName === TargetFunction.DefinePreInstallLogicFunction
|
||||
targetFunctionName === TargetFunction.DefinePostInstallLogicFunction
|
||||
) {
|
||||
preInstallLogicFunctionUniversalIdentifiers.push(
|
||||
extract.config.universalIdentifier,
|
||||
);
|
||||
const postInstallHookConfig =
|
||||
extract.config as PostInstallLogicFunctionConfig;
|
||||
|
||||
postInstallLogicFunctions.push({
|
||||
universalIdentifier: extract.config.universalIdentifier,
|
||||
shouldRunOnVersionUpgrade:
|
||||
postInstallHookConfig.shouldRunOnVersionUpgrade ?? false,
|
||||
shouldRunSynchronously:
|
||||
postInstallHookConfig.shouldRunSynchronously ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
targetFunctionName === TargetFunction.DefinePostInstallLogicFunction
|
||||
targetFunctionName === TargetFunction.DefinePreInstallLogicFunction
|
||||
) {
|
||||
postInstallLogicFunctionUniversalIdentifiers.push(
|
||||
extract.config.universalIdentifier,
|
||||
);
|
||||
const preInstallHookConfig =
|
||||
extract.config as PreInstallLogicFunctionConfig;
|
||||
|
||||
preInstallLogicFunctions.push({
|
||||
universalIdentifier: extract.config.universalIdentifier,
|
||||
shouldRunOnVersionUpgrade:
|
||||
preInstallHookConfig.shouldRunOnVersionUpgrade ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -346,31 +363,29 @@ export const buildManifest = async (
|
|||
);
|
||||
}
|
||||
|
||||
if (preInstallLogicFunctionUniversalIdentifiers.length > 1) {
|
||||
errors.push(
|
||||
'Only one pre install logic function is allowed per application',
|
||||
);
|
||||
}
|
||||
|
||||
if (postInstallLogicFunctionUniversalIdentifiers.length > 1) {
|
||||
if (postInstallLogicFunctions.length > 1) {
|
||||
errors.push(
|
||||
'Only one post install logic function is allowed per application',
|
||||
);
|
||||
}
|
||||
|
||||
if (application && preInstallLogicFunctionUniversalIdentifiers.length >= 1) {
|
||||
if (preInstallLogicFunctions.length > 1) {
|
||||
errors.push(
|
||||
'Only one pre install logic function is allowed per application',
|
||||
);
|
||||
}
|
||||
|
||||
if (application && postInstallLogicFunctions.length >= 1) {
|
||||
application = {
|
||||
...application,
|
||||
preInstallLogicFunctionUniversalIdentifier:
|
||||
preInstallLogicFunctionUniversalIdentifiers[0],
|
||||
postInstallLogicFunction: postInstallLogicFunctions[0],
|
||||
};
|
||||
}
|
||||
|
||||
if (application && postInstallLogicFunctionUniversalIdentifiers.length >= 1) {
|
||||
if (application && preInstallLogicFunctions.length >= 1) {
|
||||
application = {
|
||||
...application,
|
||||
postInstallLogicFunctionUniversalIdentifier:
|
||||
postInstallLogicFunctionUniversalIdentifiers[0],
|
||||
preInstallLogicFunction: preInstallLogicFunctions[0],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ export enum TargetFunction {
|
|||
DefineApplication = 'defineApplication',
|
||||
DefineField = 'defineField',
|
||||
DefineLogicFunction = 'defineLogicFunction',
|
||||
DefinePreInstallLogicFunction = 'definePreInstallLogicFunction',
|
||||
DefinePostInstallLogicFunction = 'definePostInstallLogicFunction',
|
||||
DefinePreInstallLogicFunction = 'definePreInstallLogicFunction',
|
||||
DefineObject = 'defineObject',
|
||||
DefineRole = 'defineRole',
|
||||
DefineSkill = 'defineSkill',
|
||||
|
|
@ -40,10 +40,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.DefinePreInstallLogicFunction]:
|
||||
ManifestEntityKey.LogicFunctions,
|
||||
[TargetFunction.DefineObject]: ManifestEntityKey.Objects,
|
||||
[TargetFunction.DefineRole]: ManifestEntityKey.Roles,
|
||||
[TargetFunction.DefineSkill]: ManifestEntityKey.Skills,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ const findUniversalIdentifiers = (obj: object): string[] => {
|
|||
if (key === 'universalIdentifier' && typeof val === 'string') {
|
||||
universalIdentifiers.push(val);
|
||||
}
|
||||
|
||||
if (
|
||||
key === 'postInstallLogicFunction' ||
|
||||
key === 'preInstallLogicFunction'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof val === 'object') {
|
||||
universalIdentifiers.push(...findUniversalIdentifiers(val));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ export type ApplicationConfig = Omit<
|
|||
ApplicationManifest,
|
||||
| 'packageJsonChecksum'
|
||||
| 'yarnLockChecksum'
|
||||
| 'postInstallLogicFunctionUniversalIdentifier'
|
||||
| 'preInstallLogicFunctionUniversalIdentifier'
|
||||
| 'postInstallLogicFunction'
|
||||
| 'preInstallLogicFunction'
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-c
|
|||
import { type ObjectConfig } from '@/sdk/objects/object-config';
|
||||
import { type PageLayoutConfig } from '@/sdk/page-layouts/page-layout-config';
|
||||
import { type ViewConfig } from '@/sdk/views/view-config';
|
||||
import { type PostInstallLogicFunctionConfig } from '@/sdk/logic-functions/post-install-logic-function-config';
|
||||
import { type PreInstallLogicFunctionConfig } from '@/sdk/logic-functions/pre-install-logic-function-config';
|
||||
import { type RoleConfig } from '@/sdk/roles/role-config';
|
||||
import {
|
||||
type AgentManifest,
|
||||
type FieldManifest,
|
||||
type NavigationMenuItemManifest,
|
||||
type SkillManifest,
|
||||
|
|
@ -23,6 +26,9 @@ export type DefinableEntity =
|
|||
| FieldManifest
|
||||
| FrontComponentConfig
|
||||
| LogicFunctionConfig
|
||||
| PostInstallLogicFunctionConfig
|
||||
| PreInstallLogicFunctionConfig
|
||||
| AgentManifest
|
||||
| RoleConfig
|
||||
| SkillManifest
|
||||
| ViewConfig
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ export { defineLogicFunction } from './logic-functions/define-logic-function';
|
|||
export { definePostInstallLogicFunction } from './logic-functions/define-post-install-logic-function';
|
||||
export { definePreInstallLogicFunction } from './logic-functions/define-pre-install-logic-function';
|
||||
export type {
|
||||
InstallLogicFunctionHandler,
|
||||
InstallLogicFunctionPayload,
|
||||
} from './logic-functions/install-logic-function-payload-type';
|
||||
InstallHandler,
|
||||
InstallPayload,
|
||||
} from '@/sdk/logic-functions/install-payload-type';
|
||||
export type {
|
||||
LogicFunctionConfig,
|
||||
LogicFunctionHandler,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { definePostInstallLogicFunction } from '@/sdk/logic-functions/define-post-install-logic-function';
|
||||
import { type InstallLogicFunctionPayload } from '@/sdk/logic-functions/install-logic-function-payload-type';
|
||||
import { type InstallPayload } from '@/sdk/logic-functions/install-payload-type';
|
||||
|
||||
const mockHandler = async (payload: InstallLogicFunctionPayload) => ({
|
||||
const mockHandler = async (payload: InstallPayload) => ({
|
||||
success: true,
|
||||
previousVersion: payload.previousVersion,
|
||||
});
|
||||
|
|
@ -24,12 +24,14 @@ describe('definePostInstallLogicFunction', () => {
|
|||
...validRouteConfig,
|
||||
description: 'Send a postcard to a contact',
|
||||
timeoutSeconds: 30,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
};
|
||||
|
||||
const result = definePostInstallLogicFunction(config as any);
|
||||
|
||||
expect(result.config.description).toBe('Send a postcard to a contact');
|
||||
expect(result.config.timeoutSeconds).toBe(30);
|
||||
expect(result.config.shouldRunOnVersionUpgrade).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error when universalIdentifier is missing', () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { definePreInstallLogicFunction } from '@/sdk/logic-functions/define-pre-install-logic-function';
|
||||
import { type InstallLogicFunctionPayload } from '@/sdk/logic-functions/install-logic-function-payload-type';
|
||||
import { type InstallPayload } from '@/sdk/logic-functions/install-payload-type';
|
||||
|
||||
const mockHandler = async (payload: InstallLogicFunctionPayload) => ({
|
||||
const mockHandler = async (payload: InstallPayload) => ({
|
||||
success: true,
|
||||
previousVersion: payload.previousVersion,
|
||||
});
|
||||
|
|
@ -9,11 +9,11 @@ const mockHandler = async (payload: InstallLogicFunctionPayload) => ({
|
|||
describe('definePreInstallLogicFunction', () => {
|
||||
const validRouteConfig = {
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'Send Postcard',
|
||||
name: 'Prepare Install',
|
||||
handler: mockHandler,
|
||||
};
|
||||
|
||||
it('should return the config when valid with route trigger', () => {
|
||||
it('should return the config when valid', () => {
|
||||
const result = definePreInstallLogicFunction(validRouteConfig);
|
||||
|
||||
expect(result.config).toEqual(validRouteConfig);
|
||||
|
|
@ -22,19 +22,21 @@ describe('definePreInstallLogicFunction', () => {
|
|||
it('should pass through optional fields', () => {
|
||||
const config = {
|
||||
...validRouteConfig,
|
||||
description: 'Send a postcard to a contact',
|
||||
description: 'Prepare state before install',
|
||||
timeoutSeconds: 30,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
};
|
||||
|
||||
const result = definePreInstallLogicFunction(config as any);
|
||||
|
||||
expect(result.config.description).toBe('Send a postcard to a contact');
|
||||
expect(result.config.description).toBe('Prepare state before install');
|
||||
expect(result.config.timeoutSeconds).toBe(30);
|
||||
expect(result.config.shouldRunOnVersionUpgrade).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error when universalIdentifier is missing', () => {
|
||||
const config = {
|
||||
name: 'Send Postcard',
|
||||
name: 'Prepare Install',
|
||||
handler: mockHandler,
|
||||
};
|
||||
const result = definePreInstallLogicFunction(config as any);
|
||||
|
|
@ -48,7 +50,7 @@ describe('definePreInstallLogicFunction', () => {
|
|||
it('should return error when handler is missing', () => {
|
||||
const config = {
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'Send Postcard',
|
||||
name: 'Prepare Install',
|
||||
};
|
||||
|
||||
const result = definePreInstallLogicFunction(config as any);
|
||||
|
|
@ -62,7 +64,7 @@ describe('definePreInstallLogicFunction', () => {
|
|||
it('should return error when handler is not a function', () => {
|
||||
const config = {
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'Send Postcard',
|
||||
name: 'Prepare Install',
|
||||
handler: 'not-a-function',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,9 @@
|
|||
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';
|
||||
import { type PostInstallLogicFunctionConfig } from '@/sdk/logic-functions/post-install-logic-function-config';
|
||||
|
||||
export const definePostInstallLogicFunction: DefineEntity<
|
||||
Omit<
|
||||
LogicFunctionConfig,
|
||||
| 'cronTriggerSettings'
|
||||
| 'databaseEventTriggerSettings'
|
||||
| 'httpRouteTriggerSettings'
|
||||
| 'isTool'
|
||||
| 'handler'
|
||||
> & {
|
||||
handler: InstallLogicFunctionHandler;
|
||||
}
|
||||
PostInstallLogicFunctionConfig
|
||||
> = (config) => {
|
||||
const errors = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,9 @@
|
|||
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';
|
||||
import { type PreInstallLogicFunctionConfig } from '@/sdk/logic-functions/pre-install-logic-function-config';
|
||||
|
||||
export const definePreInstallLogicFunction: DefineEntity<
|
||||
Omit<
|
||||
LogicFunctionConfig,
|
||||
| 'cronTriggerSettings'
|
||||
| 'databaseEventTriggerSettings'
|
||||
| 'httpRouteTriggerSettings'
|
||||
| 'isTool'
|
||||
| 'handler'
|
||||
> & {
|
||||
handler: InstallLogicFunctionHandler;
|
||||
}
|
||||
PreInstallLogicFunctionConfig
|
||||
> = (config) => {
|
||||
const errors = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
export type InstallLogicFunctionPayload = {
|
||||
previousVersion: string;
|
||||
};
|
||||
|
||||
export type InstallLogicFunctionHandler = (
|
||||
payload: InstallLogicFunctionPayload,
|
||||
) => any | Promise<any>;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export type InstallPayload = {
|
||||
previousVersion?: string;
|
||||
newVersion: string;
|
||||
};
|
||||
|
||||
export type InstallHandler = (payload: InstallPayload) => any | Promise<any>;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { type PreInstallLogicFunctionConfig } from '@/sdk/logic-functions/pre-install-logic-function-config';
|
||||
|
||||
export type PostInstallLogicFunctionConfig = PreInstallLogicFunctionConfig & {
|
||||
shouldRunSynchronously?: boolean;
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import type { InstallHandler, LogicFunctionConfig } from '@/sdk';
|
||||
|
||||
export type PreInstallLogicFunctionConfig = Omit<
|
||||
LogicFunctionConfig,
|
||||
| 'cronTriggerSettings'
|
||||
| 'databaseEventTriggerSettings'
|
||||
| 'httpRouteTriggerSettings'
|
||||
| 'isTool'
|
||||
| 'handler'
|
||||
> & {
|
||||
handler: InstallHandler;
|
||||
shouldRunOnVersionUpgrade?: boolean;
|
||||
};
|
||||
|
|
@ -33,6 +33,8 @@ export class ApplicationExceptionFilter implements ExceptionFilter {
|
|||
case ApplicationExceptionCode.CANNOT_DOWNGRADE_APPLICATION:
|
||||
throw new UserInputError(exception);
|
||||
case ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED:
|
||||
case ApplicationExceptionCode.POST_INSTALL_ERROR:
|
||||
case ApplicationExceptionCode.PRE_INSTALL_ERROR:
|
||||
case ApplicationExceptionCode.TARBALL_EXTRACTION_FAILED:
|
||||
case ApplicationExceptionCode.UPGRADE_FAILED:
|
||||
throw new InternalServerError(exception);
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ import { ApplicationPackageModule } from 'src/engine/core-modules/application/ap
|
|||
import { ApplicationInstallResolver } from 'src/engine/core-modules/application/application-install/application-install.resolver';
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { LogicFunctionModule } from 'src/engine/core-modules/logic-function/logic-function.module';
|
||||
import { SdkClientModule } from 'src/engine/core-modules/sdk-client/sdk-client.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -21,9 +23,11 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
|||
ApplicationPackageModule,
|
||||
CacheLockModule,
|
||||
FeatureFlagModule,
|
||||
LogicFunctionModule,
|
||||
SdkClientModule,
|
||||
PermissionsModule,
|
||||
FileStorageModule,
|
||||
WorkspaceCacheModule,
|
||||
],
|
||||
providers: [ApplicationInstallResolver, ApplicationInstallService],
|
||||
exports: [ApplicationInstallService],
|
||||
|
|
|
|||
|
|
@ -25,7 +25,17 @@ import {
|
|||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
|
||||
import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import {
|
||||
LogicFunctionTriggerJob,
|
||||
type LogicFunctionTriggerJobData,
|
||||
} from 'src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationInstallService {
|
||||
private readonly logger = new Logger(ApplicationInstallService.name);
|
||||
|
|
@ -37,8 +47,12 @@ export class ApplicationInstallService {
|
|||
private readonly applicationPackageFetcherService: ApplicationPackageFetcherService,
|
||||
private readonly applicationSyncService: ApplicationSyncService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
|
||||
private readonly cacheLockService: CacheLockService,
|
||||
private readonly sdkClientGenerationService: SdkClientGenerationService,
|
||||
@InjectMessageQueue(MessageQueue.logicFunctionQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
async installApplication(params: {
|
||||
|
|
@ -110,7 +124,27 @@ export class ApplicationInstallService {
|
|||
|
||||
const universalIdentifier = appRegistration.universalIdentifier;
|
||||
|
||||
const existingApplication =
|
||||
await this.applicationService.findByUniversalIdentifier({
|
||||
universalIdentifier,
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
|
||||
const previousVersion = existingApplication?.version ?? undefined;
|
||||
|
||||
const newVersion = resolvedPackage.packageJson.version;
|
||||
|
||||
if (!isDefined(newVersion)) {
|
||||
throw new ApplicationException(
|
||||
`Package ${universalIdentifier} has no version`,
|
||||
ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const isVersionUpgrade = isDefined(existingApplication);
|
||||
|
||||
const { application, wasCreated } = await this.ensureApplicationExists({
|
||||
existingApplication,
|
||||
universalIdentifier,
|
||||
name: resolvedPackage.manifest.application.displayName,
|
||||
workspaceId: params.workspaceId,
|
||||
|
|
@ -156,6 +190,16 @@ export class ApplicationInstallService {
|
|||
params.workspaceId,
|
||||
);
|
||||
|
||||
await this.runPreInstallHook({
|
||||
manifest: resolvedPackage.manifest,
|
||||
workspaceId: params.workspaceId,
|
||||
applicationRegistrationId: appRegistration.id,
|
||||
previousVersion,
|
||||
newVersion,
|
||||
isVersionUpgrade,
|
||||
universalIdentifier,
|
||||
});
|
||||
|
||||
const { hasSchemaMetadataChanged } =
|
||||
await this.applicationSyncService.synchronizeFromManifest({
|
||||
workspaceId: params.workspaceId,
|
||||
|
|
@ -171,6 +215,15 @@ export class ApplicationInstallService {
|
|||
});
|
||||
}
|
||||
|
||||
await this.runPostInstallHook({
|
||||
manifest: resolvedPackage.manifest,
|
||||
workspaceId: params.workspaceId,
|
||||
previousVersion,
|
||||
newVersion,
|
||||
isVersionUpgrade,
|
||||
universalIdentifier,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Successfully installed app ${universalIdentifier} v${resolvedPackage.packageJson.version ?? 'unknown'}`,
|
||||
);
|
||||
|
|
@ -191,6 +244,185 @@ export class ApplicationInstallService {
|
|||
}
|
||||
}
|
||||
|
||||
private async runPreInstallHook(params: {
|
||||
manifest: Manifest;
|
||||
workspaceId: string;
|
||||
applicationRegistrationId?: string;
|
||||
previousVersion?: string;
|
||||
newVersion: string;
|
||||
isVersionUpgrade: boolean;
|
||||
universalIdentifier: string;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
manifest,
|
||||
workspaceId,
|
||||
applicationRegistrationId,
|
||||
previousVersion,
|
||||
newVersion,
|
||||
isVersionUpgrade,
|
||||
universalIdentifier,
|
||||
} = params;
|
||||
|
||||
if (!isDefined(manifest.application.preInstallLogicFunction)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.applicationSyncService.preInstallSynchronizeFromManifest({
|
||||
workspaceId: params.workspaceId,
|
||||
manifest,
|
||||
applicationRegistrationId,
|
||||
});
|
||||
|
||||
const {
|
||||
universalIdentifier: preInstallLogicFunctionUniversalIdentifier,
|
||||
shouldRunOnVersionUpgrade,
|
||||
} = manifest.application.preInstallLogicFunction;
|
||||
|
||||
if (isVersionUpgrade && !shouldRunOnVersionUpgrade) {
|
||||
this.logger.log(
|
||||
`Skipping pre-install hook for app ${universalIdentifier}: version upgrade and shouldRunOnVersionUpgrade is false`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { flatLogicFunctionMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatLogicFunctionMaps',
|
||||
]);
|
||||
|
||||
const flatLogicFunction =
|
||||
flatLogicFunctionMaps.byUniversalIdentifier[
|
||||
preInstallLogicFunctionUniversalIdentifier
|
||||
];
|
||||
|
||||
// preInstallSynchronizeFromManifest should have registered this function
|
||||
// moments ago — a miss here means the pared-down sync did not persist the
|
||||
// entry, which is a real failure and should abort the install.
|
||||
if (!isDefined(flatLogicFunction)) {
|
||||
throw new ApplicationException(
|
||||
`Pre-install logic function "${preInstallLogicFunctionUniversalIdentifier}" not found for application "${universalIdentifier}" after pre-install sync. The pared-down sync did not register the function as expected.`,
|
||||
ApplicationExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const payload = { previousVersion, newVersion };
|
||||
|
||||
this.logger.log(
|
||||
`Executing pre-install hook for app ${universalIdentifier} with payload:`,
|
||||
JSON.stringify(payload),
|
||||
);
|
||||
|
||||
const result = await this.logicFunctionExecutorService.execute({
|
||||
logicFunctionId: flatLogicFunction.id,
|
||||
workspaceId,
|
||||
payload,
|
||||
});
|
||||
|
||||
if (!isDefined(result)) {
|
||||
this.logger.log('Pre-install hook executed successfully');
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
throw new ApplicationException(
|
||||
result.error.errorMessage,
|
||||
ApplicationExceptionCode.PRE_INSTALL_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async runPostInstallHook(params: {
|
||||
manifest: Manifest;
|
||||
workspaceId: string;
|
||||
previousVersion?: string;
|
||||
newVersion: string;
|
||||
isVersionUpgrade: boolean;
|
||||
universalIdentifier: string;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
manifest,
|
||||
workspaceId,
|
||||
previousVersion,
|
||||
newVersion,
|
||||
isVersionUpgrade,
|
||||
universalIdentifier,
|
||||
} = params;
|
||||
|
||||
if (!isDefined(manifest.application.postInstallLogicFunction)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
universalIdentifier: postInstallLogicFunctionUniversalIdentifier,
|
||||
shouldRunOnVersionUpgrade,
|
||||
shouldRunSynchronously,
|
||||
} = manifest.application.postInstallLogicFunction;
|
||||
|
||||
if (isVersionUpgrade && !shouldRunOnVersionUpgrade) {
|
||||
this.logger.log(
|
||||
`Skipping post-install hook for app ${universalIdentifier}: version upgrade and shouldRunOnVersionUpgrade is false`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { flatLogicFunctionMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatLogicFunctionMaps',
|
||||
]);
|
||||
|
||||
const flatLogicFunction =
|
||||
flatLogicFunctionMaps.byUniversalIdentifier[
|
||||
postInstallLogicFunctionUniversalIdentifier
|
||||
];
|
||||
|
||||
if (!isDefined(flatLogicFunction)) {
|
||||
throw new ApplicationException(
|
||||
`Post-install logic function "${postInstallLogicFunctionUniversalIdentifier}" not found for application "${universalIdentifier}" after sync. Manifest may reference a stale identifier.`,
|
||||
ApplicationExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const payload = { previousVersion, newVersion };
|
||||
|
||||
this.logger.log(
|
||||
`Enqueuing post-install hook for app ${universalIdentifier} with payload:`,
|
||||
JSON.stringify(payload),
|
||||
);
|
||||
|
||||
if (!shouldRunSynchronously) {
|
||||
await this.messageQueueService.add<LogicFunctionTriggerJobData[]>(
|
||||
LogicFunctionTriggerJob.name,
|
||||
[
|
||||
{
|
||||
logicFunctionId: flatLogicFunction.id,
|
||||
workspaceId,
|
||||
payload,
|
||||
},
|
||||
],
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.logicFunctionExecutorService.execute({
|
||||
logicFunctionId: flatLogicFunction.id,
|
||||
workspaceId,
|
||||
payload,
|
||||
});
|
||||
|
||||
if (!isDefined(result)) {
|
||||
this.logger.log('Post-install hook executed successfully');
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
throw new ApplicationException(
|
||||
result.error.errorMessage,
|
||||
ApplicationExceptionCode.POST_INSTALL_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async writeFilesToStorage(
|
||||
extractedDir: string,
|
||||
manifest: Manifest,
|
||||
|
|
@ -269,19 +501,15 @@ export class ApplicationInstallService {
|
|||
}
|
||||
|
||||
private async ensureApplicationExists(params: {
|
||||
existingApplication: ApplicationEntity | null;
|
||||
universalIdentifier: string;
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
applicationRegistrationId: string;
|
||||
sourceType: ApplicationRegistrationSourceType;
|
||||
}): Promise<{ application: ApplicationEntity; wasCreated: boolean }> {
|
||||
const existing = await this.applicationService.findByUniversalIdentifier({
|
||||
universalIdentifier: params.universalIdentifier,
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
|
||||
if (isDefined(existing)) {
|
||||
return { application: existing, wasCreated: false };
|
||||
if (isDefined(params.existingApplication)) {
|
||||
return { application: params.existingApplication, wasCreated: false };
|
||||
}
|
||||
|
||||
const application = await this.applicationService.create({
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ import { type Manifest } from 'twenty-shared/application';
|
|||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util';
|
||||
import { computeApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util';
|
||||
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util';
|
||||
import { computeApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/compute-application-manifest-all-universal-flat-entity-maps.util';
|
||||
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
|
@ -33,6 +33,149 @@ export class ApplicationManifestMigrationService {
|
|||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
async syncPreInstallLogicFunctionFromManifest({
|
||||
manifest,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
}: {
|
||||
manifest: Manifest;
|
||||
workspaceId: string;
|
||||
ownerFlatApplication: FlatApplication;
|
||||
}): Promise<void> {
|
||||
const preInstallLogicFunction =
|
||||
manifest.application.preInstallLogicFunction;
|
||||
|
||||
if (!isDefined(preInstallLogicFunction)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preInstallLogicFunctionManifest = manifest.logicFunctions.find(
|
||||
(logicFunction) =>
|
||||
logicFunction.universalIdentifier ===
|
||||
preInstallLogicFunction.universalIdentifier,
|
||||
);
|
||||
|
||||
if (!isDefined(preInstallLogicFunctionManifest)) {
|
||||
throw new ApplicationException(
|
||||
`Pre-install logic function "${preInstallLogicFunction.universalIdentifier}" is declared on the application manifest but not present in manifest.logicFunctions`,
|
||||
ApplicationExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const defaultRoleManifest = manifest.roles.find(
|
||||
(role) =>
|
||||
role.universalIdentifier ===
|
||||
manifest.application.defaultRoleUniversalIdentifier,
|
||||
);
|
||||
|
||||
// Pared-down manifest: only the pre-install logic function, every other
|
||||
// entity array intentionally empty. Combined with
|
||||
// inferDeletionFromMissingEntities: false below, this produces a purely
|
||||
// additive migration that registers the pre-install logic function without
|
||||
// touching any previously-synced metadata (important on upgrades).
|
||||
const strippedDefaultRoleManifest = isDefined(defaultRoleManifest)
|
||||
? {
|
||||
...defaultRoleManifest,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const preInstallOnlyManifest: Manifest = {
|
||||
application: manifest.application,
|
||||
objects: [],
|
||||
fields: [],
|
||||
logicFunctions: [preInstallLogicFunctionManifest],
|
||||
frontComponents: [],
|
||||
roles: isDefined(strippedDefaultRoleManifest)
|
||||
? [strippedDefaultRoleManifest]
|
||||
: [],
|
||||
skills: [],
|
||||
agents: [],
|
||||
publicAssets: [],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
};
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const cacheResult = await this.workspaceCacheService.getOrRecompute(
|
||||
workspaceId,
|
||||
[
|
||||
...Object.values(ALL_METADATA_NAME).map(getMetadataFlatEntityMapsKey),
|
||||
'featureFlagsMap',
|
||||
],
|
||||
);
|
||||
|
||||
const { featureFlagsMap, ...existingAllFlatEntityMaps } = cacheResult;
|
||||
|
||||
const fromAllFlatEntityMaps = getApplicationSubAllFlatEntityMaps({
|
||||
applicationIds: [ownerFlatApplication.id],
|
||||
fromAllFlatEntityMaps: existingAllFlatEntityMaps,
|
||||
});
|
||||
|
||||
const toAllUniversalFlatEntityMaps =
|
||||
computeApplicationManifestAllUniversalFlatEntityMaps({
|
||||
manifest: preInstallOnlyManifest,
|
||||
ownerFlatApplication,
|
||||
now,
|
||||
});
|
||||
|
||||
const dependencyAllFlatEntityMaps = getApplicationSubAllFlatEntityMaps({
|
||||
applicationIds:
|
||||
ownerFlatApplication.universalIdentifier ===
|
||||
TWENTY_STANDARD_APPLICATION.universalIdentifier
|
||||
? [twentyStandardFlatApplication.id]
|
||||
: [ownerFlatApplication.id, twentyStandardFlatApplication.id],
|
||||
fromAllFlatEntityMaps: existingAllFlatEntityMaps,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromTo(
|
||||
{
|
||||
// inferDeletionFromMissingEntities is intentionally omitted (undefined)
|
||||
// so this pared-down sync is purely additive — existing metadata for
|
||||
// objects/fields/other logic functions that are absent from
|
||||
// preInstallOnlyManifest are left untouched on upgrades.
|
||||
buildOptions: {
|
||||
isSystemBuild: false,
|
||||
applicationUniversalIdentifier:
|
||||
ownerFlatApplication.universalIdentifier,
|
||||
},
|
||||
fromToAllFlatEntityMaps: buildFromToAllUniversalFlatEntityMaps({
|
||||
fromAllFlatEntityMaps,
|
||||
toAllUniversalFlatEntityMaps,
|
||||
}),
|
||||
workspaceId,
|
||||
dependencyAllFlatEntityMaps,
|
||||
additionalCacheDataMaps: { featureFlagsMap },
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Validation errors occurred while syncing pre-install logic function',
|
||||
);
|
||||
}
|
||||
|
||||
await this.syncDefaultRoleAndSettingsCustomTab({
|
||||
manifest,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Pre-install logic function synced for application ${ownerFlatApplication.universalIdentifier}`,
|
||||
);
|
||||
}
|
||||
|
||||
async syncMetadataFromManifest({
|
||||
manifest,
|
||||
workspaceId,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,42 @@ export class ApplicationSyncService {
|
|||
return syncResult;
|
||||
}
|
||||
|
||||
// Registers the application + only the pre-install logic function in
|
||||
// workspace metadata so the pre-install hook can resolve and execute it
|
||||
// before the main synchronizeFromManifest runs the full migrations.
|
||||
// No-op when the manifest does not declare a pre-install logic function.
|
||||
public async preInstallSynchronizeFromManifest({
|
||||
workspaceId,
|
||||
manifest,
|
||||
applicationRegistrationId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
manifest: Manifest;
|
||||
applicationRegistrationId?: string;
|
||||
}): Promise<void> {
|
||||
if (!isDefined(manifest.application.preInstallLogicFunction)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const application = await this.syncApplication({
|
||||
workspaceId,
|
||||
manifest,
|
||||
applicationRegistrationId,
|
||||
});
|
||||
|
||||
const ownerFlatApplication: FlatApplication = application;
|
||||
|
||||
await this.applicationManifestMigrationService.syncPreInstallLogicFunctionFromManifest(
|
||||
{
|
||||
manifest,
|
||||
workspaceId,
|
||||
ownerFlatApplication,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.log('Pre-install sync from manifest completed');
|
||||
}
|
||||
|
||||
private async syncApplication({
|
||||
workspaceId,
|
||||
manifest,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export enum ApplicationExceptionCode {
|
|||
PACKAGE_RESOLUTION_FAILED = 'PACKAGE_RESOLUTION_FAILED',
|
||||
TARBALL_EXTRACTION_FAILED = 'TARBALL_EXTRACTION_FAILED',
|
||||
UPGRADE_FAILED = 'UPGRADE_FAILED',
|
||||
PRE_INSTALL_ERROR = 'PRE_INSTALL_ERROR',
|
||||
POST_INSTALL_ERROR = 'POST_INSTALL_ERROR',
|
||||
APP_ALREADY_INSTALLED = 'APP_ALREADY_INSTALLED',
|
||||
CANNOT_DOWNGRADE_APPLICATION = 'CANNOT_DOWNGRADE_APPLICATION',
|
||||
}
|
||||
|
|
@ -52,6 +54,10 @@ const getApplicationExceptionUserFriendlyMessage = (
|
|||
return msg`Failed to extract tarball.`;
|
||||
case ApplicationExceptionCode.UPGRADE_FAILED:
|
||||
return msg`Application upgrade failed.`;
|
||||
case ApplicationExceptionCode.PRE_INSTALL_ERROR:
|
||||
return msg`Application pre-install logic function failed.`;
|
||||
case ApplicationExceptionCode.POST_INSTALL_ERROR:
|
||||
return msg`Application post-install logic function failed.`;
|
||||
case ApplicationExceptionCode.APP_ALREADY_INSTALLED:
|
||||
return msg`This version of the application is already installed in this workspace.`;
|
||||
case ApplicationExceptionCode.CANNOT_DOWNGRADE_APPLICATION:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { type ApplicationVariables } from './applicationVariablesType';
|
||||
import { type ServerVariables } from './server-variables.type';
|
||||
import { type SyncableEntityOptions } from './syncableEntityOptionsType';
|
||||
import { type PostInstallLogicFunctionApplicationManifest } from '@/application/postInstallLogicFunctionApplicationType';
|
||||
import { type PreInstallLogicFunctionApplicationManifest } from '@/application/preInstallLogicFunctionApplicationType';
|
||||
|
||||
export type ApplicationManifest = SyncableEntityOptions & {
|
||||
defaultRoleUniversalIdentifier: string;
|
||||
|
|
@ -18,8 +20,8 @@ export type ApplicationManifest = SyncableEntityOptions & {
|
|||
termsUrl?: string;
|
||||
emailSupport?: string;
|
||||
issueReportUrl?: string;
|
||||
preInstallLogicFunctionUniversalIdentifier?: string;
|
||||
postInstallLogicFunctionUniversalIdentifier?: string;
|
||||
postInstallLogicFunction?: PostInstallLogicFunctionApplicationManifest;
|
||||
preInstallLogicFunction?: PreInstallLogicFunctionApplicationManifest;
|
||||
settingsCustomTabFrontComponentUniversalIdentifier?: string;
|
||||
packageJsonChecksum: string | null;
|
||||
yarnLockChecksum: string | null;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ export type {
|
|||
PageLayoutTabManifest,
|
||||
PageLayoutManifest,
|
||||
} from './pageLayoutManifestType';
|
||||
export type { PostInstallLogicFunctionApplicationManifest } from './postInstallLogicFunctionApplicationType';
|
||||
export type { PreInstallLogicFunctionApplicationManifest } from './preInstallLogicFunctionApplicationType';
|
||||
export type {
|
||||
ObjectPermissionManifest,
|
||||
FieldPermissionManifest,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import { type PreInstallLogicFunctionApplicationManifest } from './preInstallLogicFunctionApplicationType';
|
||||
|
||||
export type PostInstallLogicFunctionApplicationManifest =
|
||||
PreInstallLogicFunctionApplicationManifest & {
|
||||
shouldRunSynchronously?: boolean;
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
|
||||
|
||||
export type PreInstallLogicFunctionApplicationManifest =
|
||||
SyncableEntityOptions & {
|
||||
universalIdentifier: string;
|
||||
shouldRunOnVersionUpgrade?: boolean;
|
||||
};
|
||||
Loading…
Reference in a new issue