Inject none secret env variables into front components (#20511)
## Summary - Inject non-secret application variables (`isSecret: false`) into front component `process.env` via the existing Web Worker `setWorkerEnv` mechanism - Filter secret variables server-side in the resolver so they never reach the browser - Set application variables before system variables (`TWENTY_API_URL`, `TWENTY_APP_ACCESS_TOKEN`) to prevent override - Wire up environment variable keys in the logic function code editor for TypeScript autocomplete ## Test plan - [x] Unit tests for `buildNonSecretEnvVar` (6 passing) - [x] Typecheck passes for `twenty-front` and `twenty-server` - [x] Install an app with both `isSecret: false` and `isSecret: true` variables, open a front component, verify only non-secret vars appear in `process.env` - [x] Open a logic function editor, verify autocomplete suggests declared variable keys
This commit is contained in:
@@ -323,6 +323,7 @@ type FrontComponent {
|
||||
isHeadless: Boolean!
|
||||
usesSdkClient: Boolean!
|
||||
applicationTokenPair: ApplicationTokenPair
|
||||
applicationVariables: JSON
|
||||
}
|
||||
|
||||
type CommandMenuItem {
|
||||
|
||||
@@ -276,6 +276,7 @@ export interface FrontComponent {
|
||||
isHeadless: Scalars['Boolean']
|
||||
usesSdkClient: Scalars['Boolean']
|
||||
applicationTokenPair?: ApplicationTokenPair
|
||||
applicationVariables?: Scalars['JSON']
|
||||
__typename: 'FrontComponent'
|
||||
}
|
||||
|
||||
@@ -3173,6 +3174,7 @@ export interface FrontComponentGenqlSelection{
|
||||
isHeadless?: boolean | number
|
||||
usesSdkClient?: boolean | number
|
||||
applicationTokenPair?: ApplicationTokenPairGenqlSelection
|
||||
applicationVariables?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
@@ -707,6 +707,9 @@ export default {
|
||||
"applicationTokenPair": [
|
||||
33
|
||||
],
|
||||
"applicationVariables": [
|
||||
15
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
|
||||
@@ -31,7 +31,7 @@ export default defineApplication({
|
||||
|
||||
Notes:
|
||||
- `universalIdentifier` fields are deterministic IDs you own. Generate them once and keep them stable across syncs.
|
||||
- `applicationVariables` become environment variables for your functions and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
- `applicationVariables` become environment variables for your functions and front components. In logic functions (server-side), they are available as `process.env.VARIABLE_NAME`. In front components, use `getApplicationVariable('VARIABLE_NAME')` from `twenty-sdk/front-component`. Variables marked with `isSecret: true` are only injected into logic functions. Front components receive only non-secret variables.
|
||||
- The default role is detected automatically from the role file marked with [`defineApplicationRole()`](/developers/extend/apps/config/roles) — you do not need to reference it from `defineApplication()`.
|
||||
- Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`.
|
||||
- Passing `defaultRoleUniversalIdentifier` explicitly is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`.
|
||||
|
||||
@@ -239,6 +239,38 @@ Available hooks:
|
||||
| `useFrontComponentId()` | `string` | This component instance's ID |
|
||||
| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function |
|
||||
|
||||
## Application variables
|
||||
|
||||
Application variables defined in [`defineApplication()`](/developers/extend/apps/config/application) with `isSecret: false` are available inside front components via the `getApplicationVariable` utility:
|
||||
|
||||
```tsx src/front-components/greeting.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||||
|
||||
const Greeting = () => {
|
||||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||||
|
||||
return <p>Hello, {recipientName}!</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'greeting',
|
||||
component: Greeting,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Secret variables (`isSecret: true`) are **not** exposed to front components. They are only available in [logic functions](/developers/extend/apps/logic/logic-functions), which run server-side. This prevents sensitive values like API keys from being sent to the browser.
|
||||
</Warning>
|
||||
|
||||
The following system variables are always available via `process.env`:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `TWENTY_API_URL` | Base URL of the Twenty API |
|
||||
| `TWENTY_APP_ACCESS_TOKEN` | Short-lived token scoped to your app's role |
|
||||
|
||||
## Host communication API
|
||||
|
||||
Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`:
|
||||
|
||||
+4
@@ -23,6 +23,7 @@ type FrontComponentContentProps = {
|
||||
applicationAccessToken?: string;
|
||||
apiUrl?: string;
|
||||
sdkClientUrls?: SdkClientUrls;
|
||||
applicationVariables?: Record<string, string>;
|
||||
executionContext: FrontComponentExecutionContext;
|
||||
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
|
||||
onError: (error?: Error) => void;
|
||||
@@ -34,6 +35,7 @@ export const FrontComponentRenderer = ({
|
||||
applicationAccessToken,
|
||||
apiUrl,
|
||||
sdkClientUrls,
|
||||
applicationVariables,
|
||||
executionContext,
|
||||
frontComponentHostCommunicationApi,
|
||||
onError,
|
||||
@@ -55,6 +57,7 @@ export const FrontComponentRenderer = ({
|
||||
applicationAccessToken={applicationAccessToken}
|
||||
apiUrl={apiUrl}
|
||||
sdkClientUrls={sdkClientUrls}
|
||||
applicationVariables={applicationVariables}
|
||||
frontComponentId={executionContext.frontComponentId}
|
||||
setReceiver={setReceiver}
|
||||
setThread={setThread}
|
||||
@@ -69,6 +72,7 @@ export const FrontComponentRenderer = ({
|
||||
applicationAccessToken,
|
||||
apiUrl,
|
||||
sdkClientUrls,
|
||||
applicationVariables,
|
||||
executionContext.frontComponentId,
|
||||
]);
|
||||
|
||||
|
||||
+4
@@ -36,6 +36,7 @@ type FrontComponentWorkerEffectProps = {
|
||||
applicationAccessToken?: string;
|
||||
apiUrl?: string;
|
||||
sdkClientUrls?: SdkClientUrls;
|
||||
applicationVariables?: Record<string, string>;
|
||||
frontComponentId: string;
|
||||
setReceiver: React.Dispatch<React.SetStateAction<RemoteReceiver | null>>;
|
||||
setThread: React.Dispatch<
|
||||
@@ -52,6 +53,7 @@ export const FrontComponentWorkerEffect = ({
|
||||
applicationAccessToken,
|
||||
apiUrl,
|
||||
sdkClientUrls,
|
||||
applicationVariables,
|
||||
frontComponentId,
|
||||
setReceiver,
|
||||
setThread,
|
||||
@@ -121,6 +123,7 @@ export const FrontComponentWorkerEffect = ({
|
||||
applicationAccessToken,
|
||||
apiUrl,
|
||||
sdkClientUrls,
|
||||
applicationVariables,
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
setError(error);
|
||||
@@ -143,6 +146,7 @@ export const FrontComponentWorkerEffect = ({
|
||||
applicationAccessToken,
|
||||
apiUrl,
|
||||
sdkClientUrls,
|
||||
applicationVariables,
|
||||
frontComponentId,
|
||||
setError,
|
||||
setReceiver,
|
||||
|
||||
@@ -91,6 +91,13 @@ const render: WorkerExports['render'] = async (
|
||||
document.body.append(root);
|
||||
installStyleBridge(root);
|
||||
|
||||
if (isDefined(renderContext.applicationVariables)) {
|
||||
setWorkerEnv({
|
||||
applicationVariables: JSON.stringify(renderContext.applicationVariables),
|
||||
});
|
||||
}
|
||||
|
||||
// System variables are set after application variables so they cannot be overridden
|
||||
if (isDefined(renderContext.apiUrl)) {
|
||||
setWorkerEnv({
|
||||
TWENTY_API_URL: renderContext.apiUrl,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const setWorkerEnv = (environmentVariables: Record<string, string>) => {
|
||||
export const setWorkerEnv = (variables: Record<string, string>) => {
|
||||
const globalObject = globalThis as Record<string, unknown>;
|
||||
const processObject =
|
||||
(globalObject['process'] as Record<string, unknown> | undefined) ?? {};
|
||||
@@ -7,7 +7,7 @@ export const setWorkerEnv = (environmentVariables: Record<string, string>) => {
|
||||
|
||||
processObject['env'] = {
|
||||
...processEnvironment,
|
||||
...environmentVariables,
|
||||
...variables,
|
||||
};
|
||||
|
||||
globalObject['process'] = processObject;
|
||||
|
||||
@@ -8,4 +8,5 @@ export type HostToWorkerRenderContext = {
|
||||
applicationAccessToken?: string;
|
||||
apiUrl?: string;
|
||||
sdkClientUrls?: SdkClientUrls;
|
||||
applicationVariables?: Record<string, string>;
|
||||
};
|
||||
|
||||
@@ -1841,6 +1841,7 @@ export type FrontComponent = {
|
||||
__typename?: 'FrontComponent';
|
||||
applicationId: Scalars['UUID'];
|
||||
applicationTokenPair?: Maybe<ApplicationTokenPair>;
|
||||
applicationVariables?: Maybe<Scalars['JSON']>;
|
||||
builtComponentChecksum: Scalars['String'];
|
||||
builtComponentPath: Scalars['String'];
|
||||
componentName: Scalars['String'];
|
||||
@@ -6587,7 +6588,7 @@ export type FindOneFrontComponentQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type FindOneFrontComponentQuery = { __typename?: 'Query', frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, applicationId: string, builtComponentChecksum: string, isHeadless: boolean, usesSdkClient: boolean, applicationTokenPair?: { __typename?: 'ApplicationTokenPair', applicationAccessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, applicationRefreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } | null } | null };
|
||||
export type FindOneFrontComponentQuery = { __typename?: 'Query', frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, applicationId: string, builtComponentChecksum: string, isHeadless: boolean, usesSdkClient: boolean, applicationVariables?: any | null, applicationTokenPair?: { __typename?: 'ApplicationTokenPair', applicationAccessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, applicationRefreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } | null } | null };
|
||||
|
||||
export type LogicFunctionFieldsFragment = { __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, cronTriggerSettings?: any | null, databaseEventTriggerSettings?: any | null, httpRouteTriggerSettings?: any | null, toolTriggerSettings?: any | null, workflowActionTriggerSettings?: any | null, applicationId?: string | null, universalIdentifier?: string | null, createdAt: string, updatedAt: string };
|
||||
|
||||
@@ -8045,7 +8046,7 @@ export const UploadFilesFieldFileDocument = {"kind":"Document","definitions":[{"
|
||||
export const UploadWorkflowFileDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UploadWorkflowFile"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"file"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Upload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uploadWorkflowFile"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"file"},"value":{"kind":"Variable","name":{"kind":"Name","value":"file"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode<UploadWorkflowFileMutation, UploadWorkflowFileMutationVariables>;
|
||||
export const RenewApplicationTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RenewApplicationToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRefreshToken"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"renewApplicationToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRefreshToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRefreshToken"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationAccessToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"applicationRefreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]} as unknown as DocumentNode<RenewApplicationTokenMutation, RenewApplicationTokenMutationVariables>;
|
||||
export const FindManyFrontComponentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyFrontComponents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"frontComponents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentChecksum"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentPath"}},{"kind":"Field","name":{"kind":"Name","value":"componentName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}},{"kind":"Field","name":{"kind":"Name","value":"sourceComponentPath"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"usesSdkClient"}}]}}]}}]} as unknown as DocumentNode<FindManyFrontComponentsQuery, FindManyFrontComponentsQueryVariables>;
|
||||
export const FindOneFrontComponentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneFrontComponent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"frontComponent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentChecksum"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}},{"kind":"Field","name":{"kind":"Name","value":"usesSdkClient"}},{"kind":"Field","name":{"kind":"Name","value":"applicationTokenPair"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationAccessToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"applicationRefreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]}}]} as unknown as DocumentNode<FindOneFrontComponentQuery, FindOneFrontComponentQueryVariables>;
|
||||
export const FindOneFrontComponentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneFrontComponent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"frontComponent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentChecksum"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}},{"kind":"Field","name":{"kind":"Name","value":"usesSdkClient"}},{"kind":"Field","name":{"kind":"Name","value":"applicationVariables"}},{"kind":"Field","name":{"kind":"Name","value":"applicationTokenPair"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationAccessToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"applicationRefreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]}}]} as unknown as DocumentNode<FindOneFrontComponentQuery, FindOneFrontComponentQueryVariables>;
|
||||
export const CreateOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateLogicFunctionFromSourceInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"toolTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"workflowActionTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<CreateOneLogicFunctionMutation, CreateOneLogicFunctionMutationVariables>;
|
||||
export const DeleteOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"toolTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"workflowActionTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<DeleteOneLogicFunctionMutation, DeleteOneLogicFunctionMutationVariables>;
|
||||
export const ExecuteOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ExecuteOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ExecuteOneLogicFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"executeOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"logs"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<ExecuteOneLogicFunctionMutation, ExecuteOneLogicFunctionMutationVariables>;
|
||||
|
||||
+5
@@ -100,6 +100,9 @@ export const FrontComponentRenderer = ({
|
||||
|
||||
const accessToken = applicationTokenPair.applicationAccessToken.token;
|
||||
|
||||
const applicationVariables =
|
||||
data.frontComponent.applicationVariables ?? undefined;
|
||||
|
||||
if (usesSdkClient) {
|
||||
return (
|
||||
<FrontComponentRendererProvider frontComponentId={frontComponentId}>
|
||||
@@ -112,6 +115,7 @@ export const FrontComponentRenderer = ({
|
||||
frontComponentHostCommunicationApi={
|
||||
frontComponentHostCommunicationApi
|
||||
}
|
||||
applicationVariables={applicationVariables}
|
||||
onError={handleError}
|
||||
/>
|
||||
</FrontComponentRendererProvider>
|
||||
@@ -127,6 +131,7 @@ export const FrontComponentRenderer = ({
|
||||
apiUrl={REACT_APP_SERVER_BASE_URL}
|
||||
executionContext={executionContext}
|
||||
frontComponentHostCommunicationApi={frontComponentHostCommunicationApi}
|
||||
applicationVariables={applicationVariables}
|
||||
onError={handleError}
|
||||
/>
|
||||
</FrontComponentRendererProvider>
|
||||
|
||||
+3
@@ -16,6 +16,7 @@ type FrontComponentRendererWithSdkClientProps = {
|
||||
applicationId: string;
|
||||
executionContext: FrontComponentExecutionContext;
|
||||
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
|
||||
applicationVariables?: Record<string, string>;
|
||||
onError: (error?: Error) => void;
|
||||
};
|
||||
|
||||
@@ -26,6 +27,7 @@ export const FrontComponentRendererWithSdkClient = ({
|
||||
applicationId,
|
||||
executionContext,
|
||||
frontComponentHostCommunicationApi,
|
||||
applicationVariables,
|
||||
onError,
|
||||
}: FrontComponentRendererWithSdkClientProps) => {
|
||||
const sdkClientState = useAtomValue(
|
||||
@@ -50,6 +52,7 @@ export const FrontComponentRendererWithSdkClient = ({
|
||||
frontComponentHostCommunicationApi={
|
||||
frontComponentHostCommunicationApi
|
||||
}
|
||||
applicationVariables={applicationVariables}
|
||||
onError={onError}
|
||||
/>
|
||||
)}
|
||||
|
||||
+1
@@ -9,6 +9,7 @@ export const FIND_ONE_FRONT_COMPONENT = gql`
|
||||
builtComponentChecksum
|
||||
isHeadless
|
||||
usesSdkClient
|
||||
applicationVariables
|
||||
applicationTokenPair {
|
||||
applicationAccessToken {
|
||||
token
|
||||
|
||||
+9
-6
@@ -16,6 +16,7 @@ type SettingsLogicFunctionCodeEditorProps = Omit<EditorProps, 'onChange'> & {
|
||||
currentFilePath: string;
|
||||
files: File[];
|
||||
onChange: (value: string) => void;
|
||||
applicationVariableKeys?: string[];
|
||||
};
|
||||
|
||||
export const SettingsLogicFunctionCodeEditor = ({
|
||||
@@ -24,6 +25,7 @@ export const SettingsLogicFunctionCodeEditor = ({
|
||||
onChange,
|
||||
height = 450,
|
||||
options = undefined,
|
||||
applicationVariableKeys,
|
||||
}: SettingsLogicFunctionCodeEditorProps) => {
|
||||
const { logicFunctionId = '' } = useParams();
|
||||
const { availablePackages } = useGetAvailablePackages({
|
||||
@@ -63,15 +65,16 @@ export const SettingsLogicFunctionCodeEditor = ({
|
||||
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
||||
});
|
||||
|
||||
// TODO load that with proper env variables
|
||||
const environmentVariables = {};
|
||||
const applicationVariables = Object.fromEntries(
|
||||
(applicationVariableKeys ?? []).map((key) => [key, '']),
|
||||
);
|
||||
|
||||
if (isDefined(environmentVariables)) {
|
||||
const envTypeDefinitions = Object.keys(environmentVariables)
|
||||
if (isDefined(applicationVariables)) {
|
||||
const envTypeDefinitions = Object.keys(applicationVariables)
|
||||
// oxlint-disable-next-line lingui/no-unlocalized-strings
|
||||
.map((key) => `${key}: string;`)
|
||||
.join('\n');
|
||||
const environmentDefinition = `
|
||||
const applicationVariableDefinition = `
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
${envTypeDefinitions}
|
||||
@@ -85,7 +88,7 @@ export const SettingsLogicFunctionCodeEditor = ({
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setExtraLibs([
|
||||
{
|
||||
content: environmentDefinition,
|
||||
content: applicationVariableDefinition,
|
||||
filePath: 'ts:process-env.d.ts',
|
||||
},
|
||||
]);
|
||||
|
||||
+3
@@ -23,11 +23,13 @@ export const SettingsLogicFunctionCodeEditorTab = ({
|
||||
handleExecute,
|
||||
onChange,
|
||||
isTesting = false,
|
||||
applicationVariableKeys,
|
||||
}: {
|
||||
files: File[];
|
||||
handleExecute: () => void;
|
||||
onChange: (value: string) => void;
|
||||
isTesting?: boolean;
|
||||
applicationVariableKeys?: string[];
|
||||
}) => {
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
activeTabIdComponentState,
|
||||
@@ -68,6 +70,7 @@ export const SettingsLogicFunctionCodeEditorTab = ({
|
||||
files={files}
|
||||
currentFilePath={activeTabId}
|
||||
onChange={(newCodeValue: string) => onChange(newCodeValue)}
|
||||
applicationVariableKeys={applicationVariableKeys}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
+6
@@ -44,6 +44,11 @@ export const SettingsLogicFunctionDetail = () => {
|
||||
|
||||
const applicationName = data?.findOneApplication?.name;
|
||||
|
||||
const applicationVariableKeys =
|
||||
data?.findOneApplication?.applicationVariables?.map(
|
||||
(variable) => variable.key,
|
||||
) ?? [];
|
||||
|
||||
const workspaceCustomApplicationId =
|
||||
currentWorkspace?.workspaceCustomApplication?.id;
|
||||
|
||||
@@ -152,6 +157,7 @@ export const SettingsLogicFunctionDetail = () => {
|
||||
handleExecute={handleTestFunction}
|
||||
onChange={onChange('sourceHandlerCode')}
|
||||
isTesting={isExecuting}
|
||||
applicationVariableKeys={applicationVariableKeys}
|
||||
/>
|
||||
)}
|
||||
{isTriggersTab && (
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export const getApplicationVariable = (key: string): string | undefined => {
|
||||
const raw = process.env.applicationVariables;
|
||||
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const variables = JSON.parse(raw) as Record<string, string>;
|
||||
|
||||
return variables[key];
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export {
|
||||
objectMetadataItem,
|
||||
} from './conditional-availability/conditional-availability-variables';
|
||||
export { closeSidePanel } from './functions/closeSidePanel';
|
||||
export { getApplicationVariable } from './functions/getApplicationVariable';
|
||||
export { enqueueSnackbar } from './functions/enqueueSnackbar';
|
||||
export { navigate } from './functions/navigate';
|
||||
export { openCommandConfirmationModal } from './functions/openCommandConfirmationModal';
|
||||
|
||||
+4
@@ -8,6 +8,7 @@ import {
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApplicationTokenPairDTO } from 'src/engine/core-modules/application/application-oauth/dtos/application-token-pair.dto';
|
||||
@@ -74,4 +75,7 @@ export class FrontComponentDTO {
|
||||
|
||||
@Field(() => ApplicationTokenPairDTO, { nullable: true })
|
||||
applicationTokenPair?: ApplicationTokenPairDTO;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
applicationVariables?: Record<string, string>;
|
||||
}
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ import { FrontComponentService } from 'src/engine/metadata-modules/front-compone
|
||||
import { FrontComponentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@@ -23,6 +24,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
PermissionsModule,
|
||||
FlatFrontComponentModule,
|
||||
SubscriptionsModule,
|
||||
WorkspaceCacheModule,
|
||||
],
|
||||
controllers: [FrontComponentController],
|
||||
providers: [
|
||||
|
||||
+26
@@ -2,6 +2,7 @@ import { Inject, UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
@@ -21,6 +22,8 @@ import { FrontComponentDTO } from 'src/engine/metadata-modules/front-component/d
|
||||
import { UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input';
|
||||
import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service';
|
||||
import { FrontComponentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor';
|
||||
import { stripSecretFromApplicationVariables } from 'src/engine/metadata-modules/front-component/utils/strip-secret-from-application-variables';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@@ -35,6 +38,7 @@ export class FrontComponentResolver {
|
||||
private readonly frontComponentService: FrontComponentService,
|
||||
@Inject(ApplicationTokenService)
|
||||
private readonly applicationTokenService: ApplicationTokenService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
@Query(() => [FrontComponentDTO])
|
||||
@@ -67,9 +71,31 @@ export class FrontComponentResolver {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const { applicationVariableMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspace.id, [
|
||||
'applicationVariableMaps',
|
||||
]);
|
||||
|
||||
const variableUniversalIdentifiers =
|
||||
applicationVariableMaps.universalIdentifiersByApplicationId[
|
||||
dto.applicationId
|
||||
] ?? [];
|
||||
|
||||
const flatApplicationVariables = variableUniversalIdentifiers
|
||||
.map(
|
||||
(universalIdentifier) =>
|
||||
applicationVariableMaps.byUniversalIdentifier[universalIdentifier],
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
const applicationVariables = stripSecretFromApplicationVariables(
|
||||
flatApplicationVariables,
|
||||
);
|
||||
|
||||
return {
|
||||
...dto,
|
||||
applicationTokenPair: tokenPair,
|
||||
applicationVariables,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type';
|
||||
import { stripSecretFromApplicationVariables } from 'src/engine/metadata-modules/front-component/utils/strip-secret-from-application-variables';
|
||||
|
||||
const makeFlatVariable = (
|
||||
overrides: Partial<FlatApplicationVariable>,
|
||||
): FlatApplicationVariable => ({
|
||||
id: '1',
|
||||
key: 'KEY',
|
||||
value: 'value',
|
||||
description: '',
|
||||
isSecret: false,
|
||||
applicationId: 'app-1',
|
||||
workspaceId: '00000000-0000-0000-0000-000000000000',
|
||||
universalIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('stripSecretFromApplicationVariables', () => {
|
||||
it('should return empty object for empty array', () => {
|
||||
expect(stripSecretFromApplicationVariables([])).toEqual({});
|
||||
});
|
||||
|
||||
it('should include non-secret variables', () => {
|
||||
const variables = [
|
||||
makeFlatVariable({ key: 'PUBLIC_URL', value: 'https://example.com' }),
|
||||
makeFlatVariable({ id: '2', key: 'DEBUG', value: 'true' }),
|
||||
];
|
||||
|
||||
expect(stripSecretFromApplicationVariables(variables)).toEqual({
|
||||
PUBLIC_URL: 'https://example.com',
|
||||
DEBUG: 'true',
|
||||
});
|
||||
});
|
||||
|
||||
it('should exclude secret variables', () => {
|
||||
const variables = [
|
||||
makeFlatVariable({ key: 'PUBLIC_URL', value: 'https://example.com' }),
|
||||
makeFlatVariable({
|
||||
id: '2',
|
||||
key: 'API_SECRET',
|
||||
value: 'encrypted_secret',
|
||||
isSecret: true,
|
||||
}),
|
||||
makeFlatVariable({ id: '3', key: 'DEBUG', value: 'true' }),
|
||||
];
|
||||
|
||||
const result = stripSecretFromApplicationVariables(variables);
|
||||
|
||||
expect(result).toEqual({
|
||||
PUBLIC_URL: 'https://example.com',
|
||||
DEBUG: 'true',
|
||||
});
|
||||
expect(result).not.toHaveProperty('API_SECRET');
|
||||
});
|
||||
|
||||
it('should handle null and undefined values', () => {
|
||||
const variables = [
|
||||
makeFlatVariable({
|
||||
key: 'NULL_VALUE',
|
||||
value: null as unknown as string,
|
||||
}),
|
||||
makeFlatVariable({
|
||||
id: '2',
|
||||
key: 'UNDEFINED_VALUE',
|
||||
value: undefined as unknown as string,
|
||||
}),
|
||||
];
|
||||
|
||||
expect(stripSecretFromApplicationVariables(variables)).toEqual({
|
||||
NULL_VALUE: '',
|
||||
UNDEFINED_VALUE: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert non-string values to strings', () => {
|
||||
const variables = [
|
||||
makeFlatVariable({
|
||||
key: 'NUMBER_VALUE',
|
||||
value: 123 as unknown as string,
|
||||
}),
|
||||
];
|
||||
|
||||
expect(stripSecretFromApplicationVariables(variables)).toEqual({
|
||||
NUMBER_VALUE: '123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty object when all variables are secret', () => {
|
||||
const variables = [
|
||||
makeFlatVariable({ key: 'SECRET_1', value: 'val1', isSecret: true }),
|
||||
makeFlatVariable({
|
||||
id: '2',
|
||||
key: 'SECRET_2',
|
||||
value: 'val2',
|
||||
isSecret: true,
|
||||
}),
|
||||
];
|
||||
|
||||
expect(stripSecretFromApplicationVariables(variables)).toEqual({});
|
||||
});
|
||||
});
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type';
|
||||
|
||||
export const stripSecretFromApplicationVariables = (
|
||||
flatApplicationVariables: FlatApplicationVariable[],
|
||||
): Record<string, string> => {
|
||||
return flatApplicationVariables.reduce<Record<string, string>>(
|
||||
(acc, flatApplicationVariable) => {
|
||||
if (flatApplicationVariable.isSecret) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[flatApplicationVariable.key] = String(
|
||||
flatApplicationVariable.value ?? '',
|
||||
);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
};
|
||||
+8
@@ -110,6 +110,14 @@ export class WorkspaceMigrationRunnerService {
|
||||
);
|
||||
}
|
||||
|
||||
if (flatMapsKeysSet.has('flatApplicationVariableMaps')) {
|
||||
asyncOperations.push(
|
||||
this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'applicationVariableMaps',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return asyncOperations;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user