617f571400
## Summary - Converts applicationVariable from a bespoke sync path to a proper SyncableEntity, unifying it with the workspace migration pipeline used by all other manifest-managed entities (agent, skill, frontComponent, webhook, etc.) - Removes the upsertManyApplicationVariableEntities method and its direct-DB-mutation approach in favor of the standard validate → build → run action handler pipeline - Adds universalIdentifier, deletedAt columns and makes applicationId NOT NULL via an instance command migration ## Motivation Before this change, applicationVariable was the only manifest-managed entity that bypassed ApplicationManifestMigrationService.syncMetadataFromManifest(). It used a bespoke service method called directly from syncApplication(), creating two mental models, two validation styles, and two cache invalidation patterns. Now there's one unified pipeline for all manifest entities. ## What changed ### Entity refactor: - ApplicationVariableEntity now extends SyncableEntity (gains universalIdentifier, non-nullable applicationId with CASCADE, soft-delete via deletedAt) ### New flat entity layer (flat-application-variable/): - Type, maps type, editable properties constant, entity-to-flat converter, cache service, module ### New migration pipeline wiring: - Manifest converter (fromApplicationVariableManifestToUniversalFlatApplicationVariable) - Validator service (FlatApplicationVariableValidatorService) - Builder service (WorkspaceMigrationApplicationVariableActionsBuilderService) - Create/Update/Delete action handlers with secret encryption hooks - Registered in orchestrator, builder module, runner module, and all type registries ### Removed bespoke path: - Deleted upsertManyApplicationVariableEntities from ApplicationVariableEntityService - Removed its call from ApplicationSyncService.syncApplication() - Kept update() (operator-set value at runtime) and getDisplayValue() (runtime display) ### Database migration: - Instance command to add columns, backfill universalIdentifier, enforce NOT NULL constraints, and update indexes ## Test plan - npx nx typecheck twenty-server passes (0 errors) - Unit tests pass (application-variable.service.spec.ts, build-env-var.spec.ts) - Install an app with applicationVariables in its manifest → variables appear with correct universalIdentifier - Update app manifest (add/remove/modify a variable) → migration pipeline handles diff correctly - Operator-set value via update endpoint persists correctly with encryption - Uninstall app → variables cascade-deleted - app dev --once on example app syncs without errors
125 lines
3.5 KiB
TypeScript
125 lines
3.5 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { defineFrontComponent } from 'twenty-sdk/define';
|
|
import {
|
|
enqueueSnackbar,
|
|
unmountFrontComponent,
|
|
updateProgress,
|
|
useRecordId,
|
|
} from 'twenty-sdk/front-component';
|
|
import { CoreApiClient } from 'twenty-client-sdk/core';
|
|
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
|
|
|
const SYSTEM_PROMPT =
|
|
'You are a postcard writing assistant. Write a short, warm postcard message ' +
|
|
'under 150 words. Use a personal tone. Output ONLY the postcard message ' +
|
|
'text, nothing else — no greeting label, no sign-off label, just the message.';
|
|
|
|
const GeneratePostCardEffect = () => {
|
|
const recordId = useRecordId();
|
|
|
|
useEffect(() => {
|
|
if (recordId === null) {
|
|
enqueueSnackbar({
|
|
message: 'Please select exactly one record',
|
|
variant: 'error',
|
|
});
|
|
unmountFrontComponent();
|
|
return;
|
|
}
|
|
|
|
const generate = async () => {
|
|
try {
|
|
const client = new CoreApiClient();
|
|
|
|
const userPrompt = 'Write a postcard message for a friend.';
|
|
|
|
const apiBaseUrl = process.env.TWENTY_API_URL;
|
|
const token =
|
|
process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
|
|
|
|
if (!apiBaseUrl || !token) {
|
|
throw new Error('API configuration missing');
|
|
}
|
|
|
|
const response = await fetch(`${apiBaseUrl}/rest/ai/generate-text`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
systemPrompt: SYSTEM_PROMPT,
|
|
userPrompt,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorBody = await response.text();
|
|
|
|
if (errorBody.includes('No AI models are available')) {
|
|
throw new Error(
|
|
'No AI models configured. Go to Settings > Admin Panel > AI to add a provider API key.',
|
|
);
|
|
}
|
|
|
|
throw new Error(`AI request failed with status ${response.status}`);
|
|
}
|
|
|
|
const data = (await response.json()) as { text?: string };
|
|
const generatedContent = data.text ?? '';
|
|
|
|
await updateProgress(0.7);
|
|
|
|
await client.mutation({
|
|
updatePostCard: {
|
|
__args: {
|
|
id: recordId,
|
|
data: { content: generatedContent },
|
|
},
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
await updateProgress(1);
|
|
|
|
await enqueueSnackbar({
|
|
message: 'Post card content generated!',
|
|
variant: 'success',
|
|
});
|
|
|
|
await unmountFrontComponent();
|
|
} catch (error) {
|
|
await enqueueSnackbar({
|
|
message:
|
|
error instanceof Error
|
|
? error.message
|
|
: 'Failed to generate content',
|
|
variant: 'error',
|
|
});
|
|
await unmountFrontComponent();
|
|
}
|
|
};
|
|
|
|
generate();
|
|
}, [recordId]);
|
|
|
|
return null;
|
|
};
|
|
|
|
export default defineFrontComponent({
|
|
universalIdentifier: 'bd7649c0-7540-4267-ac4b-f062fbd635a3',
|
|
name: 'Generate Post Card',
|
|
description: 'Generates postcard content using AI',
|
|
isHeadless: true,
|
|
component: GeneratePostCardEffect,
|
|
command: {
|
|
universalIdentifier: '0f795c1c-8e25-44da-8962-80bef9602ee2',
|
|
label: 'Generate post card content',
|
|
shortLabel: 'Generate content',
|
|
icon: 'IconSparkles',
|
|
isPinned: true,
|
|
availabilityType: 'RECORD_SELECTION',
|
|
availabilityObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
|
},
|
|
});
|