Upload files instead ofsources (#17608)

This commit is contained in:
martmull
2026-02-03 12:11:10 +01:00
committed by GitHub
parent cdaa8f1d9d
commit 65678ed99f
64 changed files with 1148 additions and 473 deletions
@@ -266,8 +266,8 @@ const createPackageJson = async ({
devDependencies: {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^19.0.2',
react: '^19.0.2',
'@types/react': '^19.0.0',
react: '^19.0.0',
eslint: '^9.32.0',
'typescript-eslint': '^8.50.0',
},
@@ -916,6 +916,14 @@ export type CreateAppTokenInput = {
expiresAt: Scalars['DateTime'];
};
export type CreateApplicationInput = {
description?: InputMaybe<Scalars['String']>;
name: Scalars['String'];
sourcePath: Scalars['String'];
universalIdentifier: Scalars['String'];
version: Scalars['String'];
};
export type CreateApprovedAccessDomainInput = {
domain: Scalars['String'];
email: Scalars['String'];
@@ -2047,6 +2055,7 @@ export type Mutation = {
createObjectEvent: Analytics;
createOneAgent: Agent;
createOneAppToken: AppToken;
createOneApplication: Application;
createOneField: Field;
createOneLogicFunction: LogicFunction;
createOneLogicFunctionLayer: LogicFunctionLayer;
@@ -2370,6 +2379,11 @@ export type MutationCreateOneAppTokenArgs = {
};
export type MutationCreateOneApplicationArgs = {
input: CreateApplicationInput;
};
export type MutationCreateOneFieldArgs = {
input: CreateOneFieldMetadataInput;
};
@@ -2381,8 +2395,9 @@ export type MutationCreateOneLogicFunctionArgs = {
export type MutationCreateOneLogicFunctionLayerArgs = {
packageJson: Scalars['JSON'];
yarnLock: Scalars['String'];
applicationUniversalIdentifier: Scalars['String'];
packageJsonChecksum: Scalars['JSON'];
yarnLockChecksum: Scalars['String'];
};
@@ -2843,8 +2858,6 @@ export type MutationSubmitFormStepArgs = {
export type MutationSyncApplicationArgs = {
manifest: Scalars['JSON'];
packageJson: Scalars['JSON'];
yarnLock: Scalars['String'];
};
@@ -3546,6 +3559,7 @@ export type Query = {
chatMessages: Array<AgentMessage>;
chatThread: AgentChatThread;
chatThreads: Array<AgentChatThread>;
checkApplicationExist: Scalars['Boolean'];
checkUserExists: CheckUserExistOutput;
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValidOutput;
commandMenuItem?: Maybe<CommandMenuItem>;
@@ -3658,6 +3672,12 @@ export type QueryChatThreadArgs = {
};
export type QueryCheckApplicationExistArgs = {
id?: InputMaybe<Scalars['UUID']>;
universalIdentifier?: InputMaybe<Scalars['UUID']>;
};
export type QueryCheckUserExistsArgs = {
captchaToken?: InputMaybe<Scalars['String']>;
email: Scalars['String'];
@@ -3691,7 +3711,8 @@ export type QueryFindOneAgentArgs = {
export type QueryFindOneApplicationArgs = {
id: Scalars['UUID'];
id?: InputMaybe<Scalars['UUID']>;
universalIdentifier?: InputMaybe<Scalars['UUID']>;
};
+26 -5
View File
@@ -912,6 +912,14 @@ export type CreateApiKeyInput = {
roleId: Scalars['UUID'];
};
export type CreateApplicationInput = {
description?: InputMaybe<Scalars['String']>;
name: Scalars['String'];
sourcePath: Scalars['String'];
universalIdentifier: Scalars['String'];
version: Scalars['String'];
};
export type CreateApprovedAccessDomainInput = {
domain: Scalars['String'];
email: Scalars['String'];
@@ -2016,6 +2024,7 @@ export type Mutation = {
createObjectEvent: Analytics;
createOneAgent: Agent;
createOneAppToken: AppToken;
createOneApplication: Application;
createOneField: Field;
createOneLogicFunction: LogicFunction;
createOneLogicFunctionLayer: LogicFunctionLayer;
@@ -2316,6 +2325,11 @@ export type MutationCreateOneAgentArgs = {
};
export type MutationCreateOneApplicationArgs = {
input: CreateApplicationInput;
};
export type MutationCreateOneFieldArgs = {
input: CreateOneFieldMetadataInput;
};
@@ -2327,8 +2341,9 @@ export type MutationCreateOneLogicFunctionArgs = {
export type MutationCreateOneLogicFunctionLayerArgs = {
packageJson: Scalars['JSON'];
yarnLock: Scalars['String'];
applicationUniversalIdentifier: Scalars['String'];
packageJsonChecksum: Scalars['JSON'];
yarnLockChecksum: Scalars['String'];
};
@@ -2758,8 +2773,6 @@ export type MutationSubmitFormStepArgs = {
export type MutationSyncApplicationArgs = {
manifest: Scalars['JSON'];
packageJson: Scalars['JSON'];
yarnLock: Scalars['String'];
};
@@ -3447,6 +3460,7 @@ export type Query = {
apiKeys: Array<ApiKey>;
barChartData: BarChartDataOutput;
billingPortalSession: BillingSessionOutput;
checkApplicationExist: Scalars['Boolean'];
checkUserExists: CheckUserExistOutput;
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValidOutput;
commandMenuItem?: Maybe<CommandMenuItem>;
@@ -3540,6 +3554,12 @@ export type QueryBillingPortalSessionArgs = {
};
export type QueryCheckApplicationExistArgs = {
id?: InputMaybe<Scalars['UUID']>;
universalIdentifier?: InputMaybe<Scalars['UUID']>;
};
export type QueryCheckUserExistsArgs = {
captchaToken?: InputMaybe<Scalars['String']>;
email: Scalars['String'];
@@ -3562,7 +3582,8 @@ export type QueryFindOneAgentArgs = {
export type QueryFindOneApplicationArgs = {
id: Scalars['UUID'];
id?: InputMaybe<Scalars['UUID']>;
universalIdentifier?: InputMaybe<Scalars['UUID']>;
};
@@ -11,7 +11,7 @@ describe('invalid-app manifest', () => {
const result = await runAppDev({ appPath: APP_PATH, timeout: 10000 });
expect(result.success).toBe(false);
expect(result.output).toContain('Duplicate universalIdentifier');
expect(result.output).toContain('Duplicate universal identifiers');
const manifestExists = await fs.pathExists(MANIFEST_OUTPUT_PATH);
@@ -12,7 +12,6 @@ export const EXPECTED_MANIFEST: Manifest = {
fileType: 'png',
},
],
yarnLock: '',
application: {
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
@@ -27,6 +26,8 @@ export const EXPECTED_MANIFEST: Manifest = {
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
icon: 'IconWorld',
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
packageJsonChecksum: '42c3913415952d91ff1cf67ef6452872',
},
frontComponents: [
{
@@ -68,14 +69,6 @@ export const EXPECTED_MANIFEST: Manifest = {
],
fields: [
{
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
description: 'Priority level for the post card (1-10)',
label: 'Priority',
name: 'priority',
type: FieldType.NUMBER,
universalIdentifier: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d',
},
{
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
description: 'Post card category',
@@ -104,6 +97,14 @@ export const EXPECTED_MANIFEST: Manifest = {
type: FieldType.SELECT,
universalIdentifier: '8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e',
},
{
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
description: 'Priority level for the post card (1-10)',
label: 'Priority',
name: 'priority',
type: FieldType.NUMBER,
universalIdentifier: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d',
},
],
objects: [
{
@@ -234,8 +235,8 @@ export const EXPECTED_MANIFEST: Manifest = {
{
canReadFieldValue: false,
canUpdateFieldValue: false,
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
},
],
label: 'Default function role',
@@ -245,7 +246,7 @@ export const EXPECTED_MANIFEST: Manifest = {
canReadObjectRecords: true,
canSoftDeleteObjectRecords: false,
canUpdateObjectRecords: true,
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
},
],
permissionFlags: [PermissionFlagType.APPLICATIONS],
@@ -273,10 +274,10 @@ export const EXPECTED_MANIFEST: Manifest = {
},
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'src/functions/greeting.function.mjs',
builtHandlerPath: 'src/logic-functions/greeting.function.mjs',
handlerName: 'default.handler',
name: 'greeting-function',
sourceHandlerPath: 'src/functions/greeting.function.ts',
sourceHandlerPath: 'src/logic-functions/greeting.function.ts',
timeoutSeconds: 5,
triggers: [
{
@@ -291,10 +292,10 @@ export const EXPECTED_MANIFEST: Manifest = {
},
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'src/functions/test-function-2.function.mjs',
handlerName: 'config.handler',
builtHandlerPath: 'src/logic-functions/test-function-2.function.mjs',
handlerName: 'default.handler',
name: 'test-function-2',
sourceHandlerPath: 'src/functions/test-function-2.function.ts',
sourceHandlerPath: 'src/logic-functions/test-function-2.function.ts',
timeoutSeconds: 2,
triggers: [
{
@@ -307,10 +308,10 @@ export const EXPECTED_MANIFEST: Manifest = {
},
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'src/functions/test-function.function.mjs',
builtHandlerPath: 'src/logic-functions/test-function.function.mjs',
handlerName: 'default.handler',
name: 'test-function',
sourceHandlerPath: 'src/functions/test-function.function.ts',
sourceHandlerPath: 'src/logic-functions/test-function.function.ts',
timeoutSeconds: 2,
triggers: [
{
@@ -335,42 +336,4 @@ export const EXPECTED_MANIFEST: Manifest = {
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
},
],
packageJson: {
name: 'rich-app',
version: '0.1.0',
license: 'MIT',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
scripts: {
'auth:login': 'twenty auth:login',
'auth:logout': 'twenty auth:logout',
'auth:status': 'twenty auth:status',
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'function:logs': 'twenty function:logs',
'function:execute': 'twenty function:execute',
'app:uninstall': 'twenty app:uninstall',
help: 'twenty help',
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': 'latest',
},
devDependencies: {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^19.0.2',
react: '^19.0.2',
eslint: '^9.32.0',
'typescript-eslint': '^8.50.0',
},
},
};
@@ -11,6 +11,7 @@ export const defineEntitiesTests = (appPath: string): void => {
expect(sortedFiles).toEqual([
'manifest.json',
'package.json',
'public',
'public/favicon.png',
'src',
@@ -21,17 +22,18 @@ export const defineEntitiesTests = (appPath: string): void => {
'src/components/greeting.front-component.mjs.map',
'src/components/test.front-component.mjs',
'src/components/test.front-component.mjs.map',
'src/functions',
'src/functions/greeting.function.mjs',
'src/functions/greeting.function.mjs.map',
'src/functions/test-function-2.function.mjs',
'src/functions/test-function-2.function.mjs.map',
'src/functions/test-function.function.mjs',
'src/functions/test-function.function.mjs.map',
'src/logic-functions',
'src/logic-functions/greeting.function.mjs',
'src/logic-functions/greeting.function.mjs.map',
'src/logic-functions/test-function-2.function.mjs',
'src/logic-functions/test-function-2.function.mjs.map',
'src/logic-functions/test-function.function.mjs',
'src/logic-functions/test-function.function.mjs.map',
'src/root.front-component.mjs',
'src/root.front-component.mjs.map',
'src/root.function.mjs',
'src/root.function.mjs.map',
'yarn.lock',
]);
});
@@ -19,13 +19,13 @@ export const defineManifestTests = (appPath: string): void => {
normalizeManifestForComparison(EXPECTED_MANIFEST),
);
for (const fn of manifest.entities.logicFunctions) {
for (const fn of manifest.logicFunctions) {
expect(fn.builtHandlerChecksum).toBeDefined();
expect(fn.builtHandlerChecksum).not.toBeNull();
expect(typeof fn.builtHandlerChecksum).toBe('string');
}
for (const component of manifest.entities.frontComponents ?? []) {
for (const component of manifest.frontComponents ?? []) {
expect(component.builtComponentChecksum).toBeDefined();
expect(component.builtComponentChecksum).not.toBeNull();
expect(typeof component.builtComponentChecksum).toBe('string');
@@ -44,11 +44,11 @@ export const defineManifestTests = (appPath: string): void => {
it('should load all entity types', async () => {
const manifest = await fs.readJson(manifestOutputPath);
expect(manifest?.entities.objects).toHaveLength(2);
expect(manifest?.entities.logicFunctions).toHaveLength(4);
expect(manifest?.entities.frontComponents).toHaveLength(4);
expect(manifest?.entities.roles).toHaveLength(2);
expect(manifest?.entities.objectExtensions).toHaveLength(1);
expect(manifest.objects).toHaveLength(2);
expect(manifest.logicFunctions).toHaveLength(4);
expect(manifest.frontComponents).toHaveLength(4);
expect(manifest.roles).toHaveLength(2);
expect(manifest.fields).toHaveLength(2);
});
});
};
@@ -1,7 +1,7 @@
import { defineLogicFunction } from '@/sdk';
import { testFunction2 } from '../utils/test-function-2.util';
export const config = defineLogicFunction({
export default defineLogicFunction({
universalIdentifier: 'eb3ffc98-88ec-45d4-9b4a-56833b219ccb',
name: 'test-function-2',
timeoutSeconds: 2,
@@ -10,6 +10,8 @@ enum PostCardStatus {
export const POST_CARD_UNIVERSAL_IDENTIFIER =
'54b589ca-eeed-4950-a176-358418b85c05';
export const CONTENT_FIELD_UNIVERSAL_IDENTIFIER =
'58a0a314-d7ea-4865-9850-7fb84e72f30b';
export default defineObject({
universalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
nameSingular: 'postCard',
@@ -20,7 +22,7 @@ export default defineObject({
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
universalIdentifier: CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
@@ -1,4 +1,8 @@
import { PermissionFlag, defineRole } from '@/sdk';
import {
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
POST_CARD_UNIVERSAL_IDENTIFIER,
} from '@/cli/__tests__/apps/rich-app/src/objects/postCard.object';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
@@ -17,7 +21,7 @@ export default defineRole({
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
@@ -26,8 +30,8 @@ export default defineRole({
],
fieldPermissions: [
{
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fieldUniversalIdentifier: CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@@ -3,51 +3,14 @@ import type { Manifest } from 'twenty-shared/application';
export const EXPECTED_MANIFEST: Manifest = {
sources: {},
yarnLock: '',
packageJson: {
name: 'root-app',
version: '0.1.0',
license: 'MIT',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
scripts: {
'auth:login': 'twenty auth:login',
'auth:logout': 'twenty auth:logout',
'auth:status': 'twenty auth:status',
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'function:logs': 'twenty function:logs',
'function:execute': 'twenty function:execute',
'app:uninstall': 'twenty app:uninstall',
help: 'twenty help',
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': 'latest',
},
devDependencies: {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^19.0.2',
react: '^19.0.2',
eslint: '^9.32.0',
'typescript-eslint': '^8.50.0',
},
},
application: {
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
displayName: 'Root App',
description: 'An app with all entities at root level',
icon: 'IconFolder',
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
packageJsonChecksum: '351460efb13a6c1bee63e27cf87f6ece',
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
},
publicAssets: [],
fields: [],
@@ -1,10 +1,9 @@
import { AssetWatcher } from '@/cli/utilities/build/common/asset-watcher';
import {
createFrontComponentsWatcher,
createLogicFunctionsWatcher,
type EsbuildWatcher,
} from '@/cli/utilities/build/common/esbuild-watcher';
import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/update-manifest-checksums';
import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-update-checksums';
import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { DevModeOrchestrator } from '@/cli/utilities/dev/dev-mode-orchestrator';
@@ -12,7 +11,9 @@ import path from 'path';
import * as fs from 'fs-extra';
import { DevUiStateManager } from '@/cli/utilities/dev/dev-ui-state-manager';
import { renderDevUI } from '@/cli/utilities/dev/dev-ui';
import { OUTPUT_DIR } from 'twenty-shared/application';
import { ASSETS_DIR, OUTPUT_DIR } from 'twenty-shared/application';
import { FileUploadWatcher } from '@/cli/utilities/build/common/file-upload-watcher';
import { FileFolder } from 'twenty-shared/types';
export type AppDevOptions = {
appPath?: string;
@@ -24,7 +25,8 @@ export class AppDevCommand {
private manifestWatcher: ManifestWatcher | null = null;
private logicFunctionsWatcher: EsbuildWatcher | null = null;
private frontComponentsWatcher: EsbuildWatcher | null = null;
private assetWatcher: AssetWatcher | null = null;
private assetWatcher: FileUploadWatcher | null = null;
private dependencyWatcher: FileUploadWatcher | null = null;
private watchersStarted = false;
private uiStateManager: DevUiStateManager | null = null;
private unmountUI: (() => void) | null = null;
@@ -96,6 +98,7 @@ export class AppDevCommand {
this.startLogicFunctionsWatcher(logicFunctions),
this.startFrontComponentsWatcher(frontComponents),
this.startAssetWatcher(),
this.startDependencyWatcher(),
]);
}
@@ -134,8 +137,10 @@ export class AppDevCommand {
}
private async startAssetWatcher(): Promise<void> {
this.assetWatcher = new AssetWatcher({
this.assetWatcher = new FileUploadWatcher({
appPath: this.appPath,
fileFolder: FileFolder.PublicAsset,
watchPaths: [ASSETS_DIR],
handleFileBuilt: this.orchestrator!.handleFileBuilt.bind(
this.orchestrator,
),
@@ -144,6 +149,19 @@ export class AppDevCommand {
await this.assetWatcher.start();
}
private async startDependencyWatcher(): Promise<void> {
this.dependencyWatcher = new FileUploadWatcher({
appPath: this.appPath,
fileFolder: FileFolder.Source,
watchPaths: ['package.json', 'yarn.lock'],
handleFileBuilt: this.orchestrator!.handleFileBuilt.bind(
this.orchestrator,
),
});
this.dependencyWatcher.start();
}
private setupGracefulShutdown(): void {
const shutdown = async () => {
this.unmountUI?.();
@@ -153,6 +171,7 @@ export class AppDevCommand {
this.logicFunctionsWatcher?.close(),
this.frontComponentsWatcher?.close(),
this.assetWatcher?.close(),
this.dependencyWatcher?.close(),
]);
process.exit(0);
@@ -95,7 +95,6 @@ export class LogicFunctionExecuteCommand {
const result = await this.apiService.executeLogicFunction({
functionId: targetFunction.id,
payload: parsedPayload,
version: 'draft',
});
if (!result.success) {
@@ -9,7 +9,10 @@ import {
printSchema,
} from 'graphql/index';
import * as path from 'path';
import { type Manifest } from 'twenty-shared/application';
import {
type ApplicationManifest,
type Manifest,
} from 'twenty-shared/application';
import { type FileFolder } from 'twenty-shared/types';
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
import { pascalCase } from 'twenty-shared/utils';
@@ -99,18 +102,69 @@ export class ApiService {
}
}
async syncApplication(manifest: Manifest): Promise<ApiResponse> {
async checkApplicationExist(
universalIdentifier: string,
): Promise<ApiResponse<boolean>> {
try {
const query = `
query CheckApplicationExist($universalIdentifier: UUID!) {
checkApplicationExist(universalIdentifier: $universalIdentifier)
}
`;
const response = await this.client.post(
'/metadata',
{
query,
variables: { universalIdentifier },
},
{
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
},
},
);
if (response.data.errors) {
return {
success: false,
error: response.data.errors[0],
};
}
return {
success: true,
data: response.data.data.checkApplicationExist,
message: `Successfully find application`,
};
} catch (error) {
return {
success: false,
error,
};
}
}
async createApplication(
manifest: Manifest,
): Promise<ApiResponse<ApplicationManifest>> {
try {
const mutation = `
mutation SyncApplication($manifest: JSON!, $packageJson: JSON!, $yarnLock: String!) {
syncApplication(manifest: $manifest, packageJson: $packageJson, yarnLock: $yarnLock)
mutation CreateOneApplication($input: CreateApplicationInput!) {
createOneApplication(input: $input) {
id
universalIdentifier
}
}
`;
const variables = {
manifest,
packageJson: manifest.packageJson,
yarnLock: manifest.yarnLock,
input: {
universalIdentifier: manifest.application.universalIdentifier,
name: manifest.application.displayName,
version: '0.0.1',
sourcePath: 'cli-sync',
},
};
const response: AxiosResponse = await this.client.post(
@@ -134,10 +188,54 @@ export class ApiService {
};
}
return {
success: true,
data: response.data.data.createOneApplication,
message: `Successfully create application: ${manifest.application.displayName}`,
};
} catch (error) {
return {
success: false,
error,
};
}
}
async syncApplication(manifest: Manifest): Promise<ApiResponse> {
try {
const mutation = `
mutation SyncApplication($manifest: JSON!) {
syncApplication(manifest: $manifest)
}
`;
const variables = { manifest };
const response: AxiosResponse = await this.client.post(
'/metadata',
{
query: mutation,
variables,
},
{
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
},
},
);
if (response.data.errors) {
return {
success: false,
error: response.data.errors[0],
};
}
return {
success: true,
data: response.data.data.syncApplication,
message: `Successfully synced application: ${manifest.packageJson.name}`,
message: `Successfully synced application: ${manifest.application.displayName}`,
};
} catch (error) {
return {
@@ -297,11 +395,9 @@ export class ApiService {
async executeLogicFunction({
functionId,
payload,
version = 'latest',
}: {
functionId: string;
payload: Record<string, unknown>;
version?: string;
}): Promise<
ApiResponse<{
data: unknown;
@@ -332,7 +428,6 @@ export class ApiService {
input: {
id: functionId,
payload,
version,
},
};
@@ -1,12 +1,14 @@
import chokidar, { type FSWatcher } from 'chokidar';
import crypto from 'crypto';
import * as fs from 'fs-extra';
import path from 'path';
import { FileFolder } from 'twenty-shared/types';
import { ASSETS_DIR, OUTPUT_DIR } from 'twenty-shared/application';
import { dirname, join, relative } from 'path';
import { type FileFolder } from 'twenty-shared/types';
import { OUTPUT_DIR } from 'twenty-shared/application';
export type AssetWatcherOptions = {
appPath: string;
fileFolder: FileFolder;
watchPaths: string[];
handleFileBuilt: (options: {
fileFolder: FileFolder;
builtPath: string;
@@ -15,26 +17,33 @@ export type AssetWatcherOptions = {
}) => void;
};
export class AssetWatcher {
export class FileUploadWatcher {
private appPath: string;
private watcher: FSWatcher | null = null;
private fileFolder: FileFolder;
private watchPaths: string[];
private handleFileBuilt: AssetWatcherOptions['handleFileBuilt'];
constructor(options: AssetWatcherOptions) {
this.appPath = options.appPath;
this.fileFolder = options.fileFolder;
this.watchPaths = options.watchPaths;
this.handleFileBuilt = options.handleFileBuilt;
}
async start(): Promise<void> {
const assetsPath = path.join(this.appPath, ASSETS_DIR);
const rootPaths = this.watchPaths.map((watchPath) =>
join(this.appPath, watchPath),
);
const exists = await fs.pathExists(assetsPath);
if (!exists) {
return;
for (const rootPath of rootPaths) {
const exists = await fs.pathExists(rootPath);
if (!exists) {
return;
}
}
this.watcher = chokidar.watch(assetsPath, {
this.watcher = chokidar.watch(rootPaths, {
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 50,
@@ -62,18 +71,18 @@ export class AssetWatcher {
}
private async copyAndNotify(absoluteFilePath: string): Promise<void> {
const sourcePath = path.relative(this.appPath, absoluteFilePath);
const outputPath = path.join(OUTPUT_DIR, sourcePath);
const absoluteOutputPath = path.join(this.appPath, outputPath);
const sourcePath = relative(this.appPath, absoluteFilePath);
const outputPath = join(OUTPUT_DIR, sourcePath);
const absoluteOutputPath = join(this.appPath, outputPath);
await fs.ensureDir(path.dirname(absoluteOutputPath));
await fs.ensureDir(dirname(absoluteOutputPath));
await fs.copy(absoluteFilePath, absoluteOutputPath);
const content = await fs.readFile(absoluteOutputPath);
const checksum = crypto.createHash('md5').update(content).digest('hex');
this.handleFileBuilt({
fileFolder: FileFolder.PublicAsset,
fileFolder: this.fileFolder,
builtPath: outputPath,
sourcePath,
checksum,
@@ -81,9 +90,9 @@ export class AssetWatcher {
}
private async handleUnlink(absoluteFilePath: string): Promise<void> {
const sourcePath = path.relative(this.appPath, absoluteFilePath);
const builtPath = path.join(OUTPUT_DIR, sourcePath);
const absoluteBuiltPath = path.join(this.appPath, builtPath);
const sourcePath = relative(this.appPath, absoluteFilePath);
const builtPath = join(OUTPUT_DIR, sourcePath);
const absoluteBuiltPath = join(this.appPath, builtPath);
await fs.remove(absoluteBuiltPath);
}
@@ -1,16 +1,17 @@
import {
type ApplicationManifest,
type Manifest,
type PackageJson,
type FieldManifest,
} from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { validateManifest } from '@/cli/utilities/build/manifest/validate-manifest';
import { manifestValidate } from '@/cli/utilities/build/manifest/manifest-validate';
const validApplication: ApplicationManifest = {
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'Test App',
defaultRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2',
packageJsonChecksum: '98592af7-4be9-4655-b5c4-9bef307a996c',
yarnLockChecksum: '580ee05f-15fe-4146-bac2-6c382483c94e',
};
const validField: FieldManifest = {
@@ -31,14 +32,12 @@ const validManifest: Manifest = {
roles: [],
publicAssets: [],
sources: {},
packageJson: {} as PackageJson,
yarnLock: '',
};
describe('validateManifest - objectExtensions', () => {
describe('manifestValidate', () => {
describe('valid object extensions', () => {
it('should pass validation with valid object extension by nameSingular', () => {
const result = validateManifest({
const result = manifestValidate({
...validManifest,
fields: [validField],
});
@@ -56,7 +55,7 @@ describe('validateManifest - objectExtensions', () => {
label: 'Custom Note',
};
const result = validateManifest({
const result = manifestValidate({
...validManifest,
fields: [extensionByUuid],
});
@@ -74,7 +73,7 @@ describe('validateManifest - objectExtensions', () => {
label: 'Nickname',
};
const result = validateManifest({
const result = manifestValidate({
...validManifest,
fields: [validField, anotherExtension],
});
@@ -101,7 +100,7 @@ describe('validateManifest - objectExtensions', () => {
},
],
};
const result = validateManifest({
const result = manifestValidate({
...validManifest,
fields: [extensionWithSelect],
});
@@ -132,7 +131,7 @@ describe('validateManifest - objectExtensions', () => {
},
];
const result = validateManifest({
const result = manifestValidate({
...validManifest,
fields: fieldsWithDuplicates,
});
@@ -149,7 +148,7 @@ describe('validateManifest - objectExtensions', () => {
it('should fail when extension field ID conflicts with object field ID', () => {
const sharedId = '550e8400-e29b-41d4-a716-446655440001';
const result = validateManifest({
const result = manifestValidate({
...validManifest,
objects: [
{
@@ -5,11 +5,12 @@ import {
TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING,
} from '@/cli/utilities/build/manifest/manifest-extract-config';
import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file';
import { findPathFile } from '@/cli/utilities/file/file-find';
import { parseJsoncFile } from '@/cli/utilities/file/file-jsonc';
import { type FrontComponentConfig, type LogicFunctionConfig } from '@/sdk';
import {
type FrontComponentConfig,
type LogicFunctionConfig,
type ApplicationConfig,
} from '@/sdk';
import { glob } from 'fast-glob';
import * as fs from 'fs-extra';
import { readFile } from 'fs-extra';
import { basename, extname, relative, sep } from 'path';
import {
@@ -23,8 +24,9 @@ import {
type ObjectManifest,
type RoleManifest,
} from 'twenty-shared/application';
import { type Sources } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { type Sources } from 'twenty-shared/types';
import * as fs from 'fs-extra';
const loadSources = async (appPath: string): Promise<string[]> => {
return await glob(['**/*.ts', '**/*.tsx'], {
@@ -108,11 +110,16 @@ export const buildManifest = async (
switch (entity) {
case ManifestEntityKey.Application: {
const extract = await extractManifestFromFile<ApplicationManifest>({
const extract = await extractManifestFromFile<ApplicationConfig>({
appPath,
filePath,
});
application = extract.config;
application = {
...extract.config,
yarnLockChecksum: null,
packageJsonChecksum: null,
};
errors.push(...extract.errors);
applicationFilePaths.push(relativePath);
break;
@@ -157,11 +164,13 @@ export const buildManifest = async (
const { handler: _, ...rest } = extract.config;
const relativeFilePath = relative(appPath, filePath);
const config: LogicFunctionManifest = {
...rest,
handlerName: 'default.handler',
sourceHandlerPath: filePath,
builtHandlerPath: filePath.replace(/\.tsx?$/, '.mjs'),
sourceHandlerPath: relativeFilePath,
builtHandlerPath: relativeFilePath.replace(/\.tsx?$/, '.mjs'),
builtHandlerChecksum: null,
};
@@ -179,11 +188,13 @@ export const buildManifest = async (
const { component, ...rest } = extract.config;
const relativeFilePath = relative(appPath, filePath);
const config: FrontComponentManifest = {
...rest,
componentName: component.name,
sourceComponentPath: filePath,
builtComponentPath: filePath.replace(/\.tsx?$/, '.mjs'),
sourceComponentPath: relativeFilePath,
builtComponentPath: relativeFilePath.replace(/\.tsx?$/, '.mjs'),
builtComponentChecksum: null,
};
@@ -219,15 +230,6 @@ export const buildManifest = async (
);
}
const packageJson = await parseJsoncFile(
await findPathFile(appPath, 'package.json'),
);
const yarnLock = await readFile(
await findPathFile(appPath, 'yarn.lock'),
'utf8',
);
const manifest = !application
? null
: {
@@ -239,8 +241,6 @@ export const buildManifest = async (
frontComponents,
publicAssets,
sources: await computeSources(appPath, filePaths),
packageJson,
yarnLock,
};
const entityFilePaths: EntityFilePaths = {
@@ -18,7 +18,7 @@ export type UpdateManifestChecksumParams = {
>;
};
export const updateManifestChecksums = ({
export const manifestUpdateChecksums = ({
manifest,
builtFileInfos,
}: UpdateManifestChecksumParams): Manifest => {
@@ -77,6 +77,16 @@ export const updateManifestChecksums = ({
),
};
}
if (fileFolder === FileFolder.Source) {
if (rootBuiltPath === 'package.json') {
result.application.packageJsonChecksum = checksum;
}
if (rootBuiltPath === 'yarn.lock') {
result.application.yarnLockChecksum = checksum;
}
}
}
return result;
};
@@ -34,7 +34,7 @@ const findUniversalIdentifiers = (obj: object): string[] => {
return universalIdentifiers;
};
export const validateManifest = (manifest: Manifest) => {
export const manifestValidate = (manifest: Manifest) => {
const errors: string[] = [];
const warnings: string[] = [];
@@ -41,6 +41,10 @@ export class ManifestWatcher {
const isAssetFile = relativePath.startsWith(ASSETS_DIR);
const isDependencyFile = ['package.json', 'yarn.lock'].includes(
relativePath,
);
const isTypeScriptFile =
relativePath.endsWith('.ts') || relativePath.endsWith('.tsx');
@@ -48,7 +52,7 @@ export class ManifestWatcher {
const shouldIgnore = isInIgnoredDir || !isTypeScriptFile || isHiddenFile;
if (shouldIgnore && !isAssetFile) {
if (shouldIgnore && !isAssetFile && !isDependencyFile) {
return;
}
@@ -1,7 +1,7 @@
import {
type ManifestBuildResult,
updateManifestChecksums,
} from '@/cli/utilities/build/manifest/update-manifest-checksums';
manifestUpdateChecksums,
} from '@/cli/utilities/build/manifest/manifest-update-checksums';
import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer';
import { ApiService } from '@/cli/utilities/api/api-service';
import { FileUploader } from '@/cli/utilities/file/file-uploader';
@@ -10,7 +10,7 @@ import type { Location } from 'esbuild';
import { type DevUiStateManager } from '@/cli/utilities/dev/dev-ui-state-manager';
import { type EventName } from 'chokidar/handler.js';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
import { validateManifest } from '@/cli/utilities/build/manifest/validate-manifest';
import { manifestValidate } from '@/cli/utilities/build/manifest/manifest-validate';
export type DevModeOrchestratorOptions = {
appPath: string;
@@ -248,7 +248,7 @@ export class DevModeOrchestrator {
return;
}
const validation = validateManifest(result.manifest);
const validation = manifestValidate(result.manifest);
if (!validation.isValid) {
for (const e of validation.errors) {
@@ -289,11 +289,58 @@ export class DevModeOrchestrator {
await this.handleManifestBuilt(result);
if (!this.fileUploader) {
const checkApplicationExistResult =
await this.apiService.checkApplicationExist(
result.manifest.application.universalIdentifier,
);
if (!checkApplicationExistResult.success) {
this.uiStateManager.addEvent({
message: `Failed to check if application ${result.manifest.application.universalIdentifier} already exists`,
status: 'error',
});
this.uiStateManager.updateManifestState({
manifestStatus: 'error',
error: `Failed to check if application already exists`,
});
return;
}
const applicationExists = checkApplicationExistResult.data;
if (!applicationExists) {
this.uiStateManager.addEvent({
message: 'Creating application',
status: 'info',
});
const createApplicationResult =
await this.apiService.createApplication(result.manifest);
if (createApplicationResult.success) {
this.uiStateManager.addEvent({
message: 'Application created',
status: 'success',
});
} else {
this.uiStateManager.addEvent({
message: `Application creation failed with error ${JSON.stringify(createApplicationResult.error, null, 2)}`,
status: 'error',
});
this.uiStateManager.updateManifestState({
manifestStatus: 'error',
error: `Application creation failed with error ${JSON.stringify(createApplicationResult.error, null, 2)}`,
});
return;
}
}
this.fileUploader = new FileUploader({
appPath: this.appPath,
applicationUniversalIdentifier:
result.manifest.application.universalIdentifier,
});
for (const [
builtPath,
{ fileFolder, sourcePath },
@@ -306,7 +353,7 @@ export class DevModeOrchestrator {
await Promise.all(this.activeUploads);
}
const manifest = updateManifestChecksums({
const manifest = manifestUpdateChecksums({
manifest: result.manifest,
builtFileInfos: this.builtFileInfos,
});
@@ -67,7 +67,7 @@ export class DevUiStateManager {
const event: UiEvent = {
id: ++this.eventIdCounter,
timestamp: new Date(),
message,
message: message.slice(0, 5_000),
status,
};
@@ -92,7 +92,7 @@ export class DevUiStateManager {
...this.state,
...(manifestStatus ? { manifestStatus } : {}),
...(appName ? { appName } : {}),
...(error ? { error } : { error: undefined }),
...(error ? { error: error.slice(0, 5_000) } : { error: undefined }),
};
this.notify();
@@ -43,10 +43,10 @@ export const parseJsoncString = (
return result;
};
export const parseJsoncFile = async (
export const parseJsoncFile = async <T = object>(
filePath: string,
options: JsoncParseOptions = {},
): Promise<any> => {
): Promise<T> => {
try {
const content = await fs.readFile(filePath, 'utf8');
return parseJsoncString(content, options);
@@ -0,0 +1,6 @@
import { type ApplicationManifest } from 'twenty-shared/application';
export type ApplicationConfig = Omit<
ApplicationManifest,
'packageJsonChecksum' | 'yarnLockChecksum'
>;
@@ -1,10 +1,8 @@
import { type ApplicationManifest } from 'twenty-shared/application';
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
import { type DefineEntity } from '@/sdk/common/types/define-entity.type';
import { type ApplicationConfig } from '@/sdk/application/application-config';
export const defineApplication: DefineEntity<ApplicationManifest> = (
config,
) => {
export const defineApplication: DefineEntity<ApplicationConfig> = (config) => {
const errors = [];
if (!config.universalIdentifier) {
@@ -15,6 +13,10 @@ export const defineApplication: DefineEntity<ApplicationManifest> = (
errors.push('Application must have a defaultRoleUniversalIdentifier');
}
if (!config.displayName || config.displayName.length === 0) {
errors.push('Application must have a non empty display name');
}
return createValidationResult({
config,
errors,
@@ -1,11 +1,11 @@
import {
type ApplicationManifest,
type FieldManifest,
type ObjectManifest,
type RoleManifest,
} from 'twenty-shared/application';
import { type FrontComponentConfig } from '@/sdk/front-components/front-component-config';
import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config';
import { type ApplicationConfig } from '@/sdk/application/application-config';
export type ValidationResult<T> = {
success: boolean;
@@ -14,7 +14,7 @@ export type ValidationResult<T> = {
};
export type DefinableEntity =
| ApplicationManifest
| ApplicationConfig
| ObjectManifest
| FieldManifest
| FrontComponentConfig
+1
View File
@@ -7,6 +7,7 @@
* |___/
*/
export type { ApplicationConfig } from './application/application-config';
export { defineApplication } from './application/define-application';
export type {
ValidationResult,
@@ -416,15 +416,17 @@ export class SeedWorkflowV1_16Command extends ActiveOrSuspendedWorkspacesMigrati
workspaceId: string,
name: string = 'Seed code step (v1.16)',
): Promise<string> {
const { id: logicFunctionLayerId } =
await this.logicFunctionLayerService.createCommonLayerIfNotExist(
workspaceId,
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const applicationId = workspaceCustomFlatApplication.id;
const { id: logicFunctionLayerId } =
await this.logicFunctionLayerService.createCommonLayer({
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
});
const id = uuidv4();
const universalIdentifier = uuidv4();
const now = new Date();
@@ -0,0 +1,19 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddFileEntityUniqueConstraint1770032815802
implements MigrationInterface
{
name = 'AddFileEntityUniqueConstraint1770032815802';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."file" ADD CONSTRAINT "IDX_APPLICATION_PATH_WORKSPACE_ID_APPLICATION_ID_UNIQUE" UNIQUE ("workspaceId", "applicationId", "path")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."file" DROP CONSTRAINT "IDX_APPLICATION_PATH_WORKSPACE_ID_APPLICATION_ID_UNIQUE"`,
);
}
}
@@ -0,0 +1,31 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddDependencyChecksumsToLogicFunctionLayer1770038963629
implements MigrationInterface
{
name = 'AddDependencyChecksumsToLogicFunctionLayer1770038963629';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."logicFunctionLayer" ADD "packageJsonChecksum" text`,
);
await queryRunner.query(
`ALTER TABLE "core"."logicFunctionLayer" RENAME COLUMN "checksum" TO "yarnLockChecksum"`,
);
await queryRunner.query(
`ALTER TABLE "core"."logicFunctionLayer" ADD "availablePackages" jsonb NOT NULL DEFAULT '{}'`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."logicFunctionLayer" DROP COLUMN "availablePackages"`,
);
await queryRunner.query(
`ALTER TABLE "core"."logicFunctionLayer" RENAME COLUMN "yarnLockChecksum" TO "checksum"`,
);
await queryRunner.query(
`ALTER TABLE "core"."logicFunctionLayer" DROP COLUMN "packageJsonChecksum"`,
);
}
}
@@ -1,16 +1,10 @@
import { ArgsType, Field } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { Manifest, PackageJson } from 'twenty-shared/application';
import { Manifest } from 'twenty-shared/application';
@ArgsType()
export class ApplicationInput {
@Field(() => GraphQLJSON, { nullable: false })
manifest: Manifest;
@Field(() => GraphQLJSON, { nullable: false })
packageJson: PackageJson;
@Field(() => String, { nullable: false })
yarnLock: string;
}
@@ -0,0 +1,31 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
@InputType()
export class CreateApplicationInput {
@IsString()
@IsNotEmpty()
@Field()
universalIdentifier: string;
@IsString()
@IsNotEmpty()
@Field()
name: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
description?: string;
@IsString()
@IsNotEmpty()
@Field()
version: string;
@IsString()
@IsNotEmpty()
@Field()
sourcePath: string;
}
@@ -5,12 +5,10 @@ import {
UsePipes,
} from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { PermissionFlagType } from 'twenty-shared/constants';
import { FileFolder } from 'twenty-shared/types';
import { type Repository } from 'typeorm';
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
@@ -30,7 +28,6 @@ import { ApplicationService } from 'src/engine/core-modules/application/services
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
@@ -41,6 +38,7 @@ import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/works
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { CreateApplicationInput } from 'src/engine/core-modules/application/dtos/create-application.input';
@UseGuards(
WorkspaceAuthGuard,
@@ -57,8 +55,6 @@ export class ApplicationResolver {
private readonly applicationService: ApplicationService,
private readonly fileStorageService: FileStorageService,
private readonly workspaceCacheService: WorkspaceCacheService,
@InjectRepository(FileEntity)
private readonly fileRepository: Repository<FileEntity>,
) {}
@Query(() => [ApplicationDTO])
@@ -69,31 +65,65 @@ export class ApplicationResolver {
return this.applicationService.findManyApplications(workspaceId);
}
@Query(() => Boolean)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async checkApplicationExist(
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
@Args('id', { type: () => UUIDScalarType, nullable: true }) id?: string,
@Args('universalIdentifier', { type: () => UUIDScalarType, nullable: true })
universalIdentifier?: string,
) {
return await this.applicationService.checkApplicationExist({
id,
universalIdentifier,
workspaceId,
});
}
@Query(() => ApplicationDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async findOneApplication(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
@Args('id', { type: () => UUIDScalarType, nullable: true }) id?: string,
@Args('universalIdentifier', { type: () => UUIDScalarType, nullable: true })
universalIdentifier?: string,
) {
return await this.applicationService.findOneApplicationOrThrow({
id,
universalIdentifier,
workspaceId,
});
}
@Mutation(() => ApplicationDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async createOneApplication(
@Args('input') input: CreateApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
return await this.applicationService.findOneApplication(id, workspaceId);
return await this.applicationService.create({
...input,
sourceType: 'local',
workspaceId,
});
}
@Mutation(() => Boolean)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async syncApplication(
@Args() { manifest, packageJson, yarnLock }: ApplicationInput,
@Args() { manifest }: ApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
await this.applicationSyncService.synchronizeFromManifest({
workspaceId,
manifest,
yarnLock,
packageJson,
});
return true;
}
@Mutation(() => Boolean)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async installApplication(
@Args() { workspaceMigration: { actions } }: InstallApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
@@ -132,6 +162,7 @@ export class ApplicationResolver {
}
@Mutation(() => Boolean)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async uninstallApplication(
@Args() { universalIdentifier }: UninstallApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
@@ -146,6 +177,7 @@ export class ApplicationResolver {
@Mutation(() => FileDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE))
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async uploadApplicationFile(
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
@Args({ name: 'file', type: () => GraphQLUpload })
@@ -11,8 +11,9 @@ import {
RelationFieldManifest,
RoleManifest,
} from 'twenty-shared/application';
import { FieldMetadataType, Sources } from 'twenty-shared/types';
import { FieldMetadataType, FileFolder, Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { PackageJson } from 'type-fest';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import {
@@ -39,6 +40,8 @@ import { PermissionFlagService } from 'src/engine/metadata-modules/permission-fl
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { computeMetadataNameFromLabelOrThrow } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@Injectable()
export class ApplicationSyncService {
@@ -58,21 +61,18 @@ export class ApplicationSyncService {
private readonly objectPermissionService: ObjectPermissionService,
private readonly fieldPermissionService: FieldPermissionService,
private readonly permissionService: PermissionFlagService,
private readonly fileStorageService: FileStorageService,
) {}
public async synchronizeFromManifest({
workspaceId,
manifest,
packageJson,
yarnLock,
}: ApplicationInput & {
workspaceId: string;
}) {
const application = await this.syncApplication({
workspaceId,
manifest,
packageJson,
yarnLock,
});
const ownerFlatApplication: FlatApplication = application;
@@ -126,12 +126,24 @@ export class ApplicationSyncService {
private async syncApplication({
workspaceId,
manifest,
packageJson,
yarnLock,
}: ApplicationInput & {
workspaceId: string;
}): Promise<ApplicationEntity> {
const name = manifest.application.displayName ?? packageJson.name;
const name = manifest.application.displayName;
const packageJson = JSON.parse(
(
await streamToBuffer(
await this.fileStorageService.readFile_v2({
applicationUniversalIdentifier:
manifest.application.universalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: 'package.json',
workspaceId,
}),
)
).toString('utf-8'),
) as PackageJson;
const application =
(await this.applicationService.findByUniversalIdentifier({
universalIdentifier: manifest.application.universalIdentifier,
@@ -150,13 +162,19 @@ export class ApplicationSyncService {
let logicFunctionLayerId = application.logicFunctionLayerId;
if (manifest.logicFunctions.length > 0) {
if (
manifest.logicFunctions.length > 0 &&
isDefined(manifest.application.packageJsonChecksum) &&
isDefined(manifest.application.yarnLockChecksum)
) {
if (!isDefined(logicFunctionLayerId)) {
logicFunctionLayerId = (
await this.logicFunctionLayerService.create(
{
packageJson,
yarnLock,
packageJsonChecksum: manifest.application.packageJsonChecksum,
yarnLockChecksum: manifest.application.yarnLockChecksum,
applicationUniversalIdentifier:
manifest.application.universalIdentifier,
},
workspaceId,
)
@@ -166,9 +184,10 @@ export class ApplicationSyncService {
await this.logicFunctionLayerService.update(
logicFunctionLayerId,
{
packageJson,
yarnLock,
packageJsonChecksum: manifest.application.packageJsonChecksum,
yarnLockChecksum: manifest.application.yarnLockChecksum,
},
manifest.application.universalIdentifier,
workspaceId,
);
}
@@ -123,12 +123,43 @@ export class ApplicationService {
});
}
async findOneApplication(
applicationId: string,
workspaceId: string,
): Promise<ApplicationEntity> {
const application = await this.applicationRepository.findOne({
where: { workspaceId, id: applicationId },
async checkApplicationExist({
id,
universalIdentifier,
workspaceId,
}: {
id?: string;
universalIdentifier?: string;
workspaceId: string;
}) {
return isDefined(
await this.findOneApplication({ id, universalIdentifier, workspaceId }),
);
}
async findOneApplication({
id,
universalIdentifier,
workspaceId,
}: {
id?: string;
universalIdentifier?: string;
workspaceId: string;
}): Promise<ApplicationEntity | null> {
if (!isDefined(id) && !isDefined(universalIdentifier)) {
throw new ApplicationException(
`Either id or universalIdentifier must be provided to find application.`,
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
);
}
const where = {
workspaceId,
...(isDefined(id) ? { id } : { universalIdentifier }),
};
return await this.applicationRepository.findOne({
where,
relations: [
'logicFunctions',
'agents',
@@ -136,10 +167,26 @@ export class ApplicationService {
'applicationVariables',
],
});
}
async findOneApplicationOrThrow({
id,
universalIdentifier,
workspaceId,
}: {
id?: string;
universalIdentifier?: string;
workspaceId: string;
}): Promise<ApplicationEntity> {
const application = await this.findOneApplication({
id,
universalIdentifier,
workspaceId,
});
if (!isDefined(application)) {
throw new ApplicationException(
`Application with id ${applicationId} not found`,
`Application with id ${id} or universalIdentifier ${universalIdentifier} not found`,
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
);
}
@@ -168,6 +215,12 @@ export class ApplicationService {
});
}
async createOneApplication(
data: Partial<ApplicationEntity> & { workspaceId: string },
): Promise<ApplicationEntity> {
return this.create(data);
}
async findTwentyStandardApplicationOrThrow(workspaceId: string): Promise<{
application: ApplicationEntity;
workspace: WorkspaceEntity;
@@ -3,6 +3,7 @@ import { Injectable, Logger } from '@nestjs/common';
// eslint-disable-next-line no-restricted-imports
import { lowerCase, upperFirst } from 'lodash';
import { type Manifest } from 'twenty-shared/application';
import { PackageJson } from 'type-fest';
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/dtos/marketplace-app.dto';
@@ -198,9 +199,18 @@ export class MarketplaceService {
return null;
}
const manifest = JSON.parse(manifestContent) as Manifest;
const packageJsonContent = await this.fetchGitHubFile(
`${appPath}/.twenty/output/package.json`,
);
const { application, packageJson } = manifest;
if (!packageJsonContent) {
return null;
}
const manifest = JSON.parse(manifestContent) as Manifest;
const packageJson = JSON.parse(packageJsonContent) as PackageJson;
const { application } = manifest;
const marketplaceData = application.marketplaceData;
if (!marketplaceData?.author || !marketplaceData?.category) {
@@ -209,10 +219,10 @@ export class MarketplaceService {
return {
id: application.universalIdentifier,
name: application.displayName ?? packageJson.name,
name: application.displayName,
description: application.description ?? '',
icon: application.icon ?? 'IconApps',
version: packageJson.version,
version: packageJson.version ?? '0.1.0',
author: marketplaceData.author,
category: marketplaceData.category,
logo: this.resolveAssetUrl(appPath, marketplaceData.logo),
@@ -17,6 +17,11 @@ export interface StorageDriver {
onStoragePath: string;
}): Promise<void>;
downloadFile(params: {
onStoragePath: string;
localPath: string;
}): Promise<void>;
delete(params: { folderPath: string; filename?: string }): Promise<void>;
move(params: {
from: { folderPath: string; filename?: string };
@@ -72,6 +72,19 @@ export class LocalDriver implements StorageDriver {
await fs.writeFile(filePath, params.sourceFile);
}
async downloadFile(params: {
onStoragePath: string;
localPath: string;
}): Promise<void> {
await this.createFolder(dirname(params.localPath));
const filePath = join(`${this.options.storagePath}/`, params.onStoragePath);
const content = await fs.readFile(filePath);
await fs.writeFile(params.localPath, content);
}
async downloadFolder(params: {
onStoragePath: string;
localPath: string;
@@ -1,8 +1,8 @@
import { Logger } from '@nestjs/common';
import fs from 'fs';
import { mkdir, readdir, readFile } from 'fs/promises';
import { join } from 'path';
import { readdir, readFile } from 'fs/promises';
import { dirname, join } from 'path';
import { Readable } from 'stream';
import { pipeline } from 'stream/promises';
@@ -95,6 +95,23 @@ export class S3Driver implements StorageDriver {
await this.s3Client.send(command);
}
private async createFolder(path: string) {
return fs.mkdirSync(path, { recursive: true });
}
async downloadFile(params: {
onStoragePath: string;
localPath: string;
}): Promise<void> {
await this.createFolder(dirname(params.localPath));
const fileStream = await this.readFile({
filePath: params.onStoragePath,
});
await pipeline(fileStream, fs.createWriteStream(params.localPath));
}
async downloadFolder(params: {
onStoragePath: string;
localPath: string;
@@ -124,7 +141,7 @@ export class S3Driver implements StorageDriver {
? join(params.localPath, relativePath)
: params.localPath;
await mkdir(localFolderPath, { recursive: true });
await this.createFolder(localFolderPath);
const fileStream = await this.readFile({
filePath: `${fromFolderPath}/${filename}`,
@@ -97,19 +97,28 @@ export class FileStorageService {
sourceFile,
});
const fileEntity = await this.fileRepository.save({
path: `${fileFolder}/${resourcePath}`,
workspaceId,
applicationId: application.id,
id: fileId,
size:
typeof sourceFile === 'string'
? Buffer.byteLength(sourceFile)
: sourceFile.length,
settings,
});
await this.fileRepository.upsert(
{
path: `${fileFolder}/${resourcePath}`,
workspaceId,
applicationId: application.id,
id: fileId,
size:
typeof sourceFile === 'string'
? Buffer.byteLength(sourceFile)
: sourceFile.length,
settings,
},
['path', 'workspaceId', 'applicationId'],
);
return fileEntity;
return await this.fileRepository.findOneOrFail({
where: {
path: `${fileFolder}/${resourcePath}`,
applicationId: application.id,
workspaceId,
},
});
}
/**
@@ -224,6 +233,18 @@ export class FileStorageService {
});
}
downloadFile_v2(
params: ResourceIdentifier & { localPath: string },
): Promise<void> {
const driver = this.fileStorageDriverFactory.getCurrentDriver();
const onStoragePath = this.buildOnStoragePath(params);
return driver.downloadFile({
onStoragePath,
localPath: params.localPath,
});
}
/**
* @deprecated Use delete_v2 instead
*/
@@ -8,6 +8,7 @@ import {
ManyToOne,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
@@ -17,6 +18,11 @@ import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/works
@Entity('file')
@Index('IDX_FILE_WORKSPACE_ID', ['workspaceId'])
@Unique('IDX_APPLICATION_PATH_WORKSPACE_ID_APPLICATION_ID_UNIQUE', [
'workspaceId',
'applicationId',
'path',
])
export class FileEntity extends WorkspaceRelatedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -34,14 +34,12 @@ export class FilesFieldResolver {
const stream = createReadStream();
const buffer = await streamToBuffer(stream);
const fileEntity = await this.filesFieldService.uploadFile({
return await this.filesFieldService.uploadFile({
file: buffer,
filename,
declaredMimeType: mimetype,
workspaceId,
applicationId: workspaceCustomApplicationId,
});
return fileEntity;
}
}
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import fs from 'fs/promises';
import { dirname, join } from 'path';
import crypto from 'crypto';
import { build } from 'esbuild';
import { FileFolder } from 'twenty-shared/types';
@@ -13,6 +14,7 @@ import {
getLogicFunctionBaseFolderPath,
getRelativePathFromBase,
} from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { FlatLogicFunctionLayer } from 'src/engine/metadata-modules/logic-function-layer/types/flat-logic-function-layer.type';
export type FunctionBuildParams = {
flatLogicFunction: FlatLogicFunction;
@@ -23,6 +25,64 @@ export type FunctionBuildParams = {
export class LogicFunctionBuildService {
constructor(private readonly fileStorageService: FileStorageService) {}
async hasLayerDependencies({
flatLogicFunctionLayer,
applicationUniversalIdentifier,
}: {
flatLogicFunctionLayer: FlatLogicFunctionLayer;
applicationUniversalIdentifier: string;
}): Promise<boolean> {
const packageJsonExists = await this.fileStorageService.checkFileExists_v2({
workspaceId: flatLogicFunctionLayer.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: 'package.json',
});
const yarnLockExists = await this.fileStorageService.checkFileExists_v2({
workspaceId: flatLogicFunctionLayer.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: 'yarn.lock',
});
return packageJsonExists && yarnLockExists;
}
async uploadDependencies({
flatLogicFunctionLayer,
applicationUniversalIdentifier,
}: {
flatLogicFunctionLayer: FlatLogicFunctionLayer;
applicationUniversalIdentifier: string;
}) {
await this.fileStorageService.writeFile_v2({
workspaceId: flatLogicFunctionLayer.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: 'package.json',
sourceFile: JSON.stringify(flatLogicFunctionLayer.packageJson, null, 2),
mimeType: undefined,
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
await this.fileStorageService.writeFile_v2({
workspaceId: flatLogicFunctionLayer.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: 'yarn.lock',
sourceFile: flatLogicFunctionLayer.yarnLock,
mimeType: undefined,
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
}
async isBuilt({
flatLogicFunction,
applicationUniversalIdentifier,
@@ -38,7 +98,7 @@ export class LogicFunctionBuildService {
async buildAndUpload({
flatLogicFunction,
applicationUniversalIdentifier,
}: FunctionBuildParams): Promise<void> {
}: FunctionBuildParams): Promise<{ checksum: string }> {
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
@@ -85,6 +145,10 @@ export class LogicFunctionBuildService {
toDelete: false,
},
});
return {
checksum: crypto.createHash('md5').update(builtFile).digest('hex'),
};
} finally {
await lambdaBuildDirectoryManager.clean();
}
@@ -0,0 +1,8 @@
var main = async (params) => {
const { a, b } = params;
const message = `Hello, input: ${a} and ${b}`;
return { message };
};
export {
main
};
@@ -29,7 +29,7 @@ import {
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-executor-driver.interface';
import { type FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { copyAndBuildDependencies } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-and-build-dependencies';
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-yarn-engine-and-build-dependencies';
import { copyExecutor } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-executor';
import { createZipFile } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/create-zip-file';
import {
@@ -139,12 +139,43 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
}
private getLayerName(flatLogicFunctionLayer: FlatLogicFunctionLayer) {
return flatLogicFunctionLayer.checksum;
return flatLogicFunctionLayer.yarnLockChecksum;
}
private async createLayerIfNotExists(
flatLogicFunctionLayer: FlatLogicFunctionLayer,
): Promise<string> {
private async copyDependenciesInMemory({
applicationUniversalIdentifier,
workspaceId,
inMemoryLayerFolderPath,
}: {
applicationUniversalIdentifier: string;
workspaceId: string;
inMemoryLayerFolderPath: string;
}) {
await Promise.all([
this.fileStorageService.downloadFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: 'package.json',
localPath: join(inMemoryLayerFolderPath, 'package.json'),
}),
this.fileStorageService.downloadFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: 'yarn.lock',
localPath: join(inMemoryLayerFolderPath, 'yarn.lock'),
}),
]);
}
private async createLayerIfNotExists({
flatLogicFunctionLayer,
applicationUniversalIdentifier,
}: {
flatLogicFunctionLayer: FlatLogicFunctionLayer;
applicationUniversalIdentifier: string;
}): Promise<string> {
const layerName = this.getLayerName(flatLogicFunctionLayer);
const listLayerParams: ListLayerVersionsCommandInput = {
@@ -171,10 +202,12 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
NODE_LAYER_SUBFOLDER,
);
await copyAndBuildDependencies(
nodeDependenciesFolder,
flatLogicFunctionLayer,
);
await this.copyDependenciesInMemory({
applicationUniversalIdentifier,
workspaceId: flatLogicFunctionLayer.workspaceId,
inMemoryLayerFolderPath: nodeDependenciesFolder,
});
await copyYarnEngineAndBuildDependencies(nodeDependenciesFolder);
await createZipFile(sourceTemporaryDir, lambdaZipPath);
@@ -257,15 +290,23 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
return false;
}
private async build(
flatLogicFunction: FlatLogicFunction,
flatLogicFunctionLayer: FlatLogicFunctionLayer,
) {
private async build({
flatLogicFunction,
flatLogicFunctionLayer,
applicationUniversalIdentifier,
}: {
flatLogicFunction: FlatLogicFunction;
flatLogicFunctionLayer: FlatLogicFunctionLayer;
applicationUniversalIdentifier: string;
}) {
if (await this.isAlreadyBuilt(flatLogicFunction, flatLogicFunctionLayer)) {
return;
}
const layerArn = await this.createLayerIfNotExists(flatLogicFunctionLayer);
const layerArn = await this.createLayerIfNotExists({
flatLogicFunctionLayer,
applicationUniversalIdentifier,
});
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
@@ -317,7 +358,11 @@ export class LambdaDriver implements LogicFunctionExecutorDriver {
payload,
env,
}: LogicFunctionExecuteParams): Promise<LogicFunctionExecuteResult> {
await this.build(flatLogicFunction, flatLogicFunctionLayer);
await this.build({
flatLogicFunction,
flatLogicFunctionLayer,
applicationUniversalIdentifier,
});
await this.waitFunctionUpdates(flatLogicFunction);
@@ -5,14 +5,14 @@ import { join } from 'path';
import { FileFolder } from 'twenty-shared/types';
import {
type LogicFunctionExecutorDriver,
type LogicFunctionExecuteParams,
type LogicFunctionExecuteResult,
type LogicFunctionExecutorDriver,
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-executor-driver.interface';
import { type FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/logic-function-executor-tmpdir-folder';
import { copyAndBuildDependencies } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-and-build-dependencies';
import { copyYarnEngineAndBuildDependencies } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-yarn-engine-and-build-dependencies';
import { ConsoleListener } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/intercept-console';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { type FlatLogicFunctionLayer } from 'src/engine/metadata-modules/logic-function-layer/types/flat-logic-function-layer.type';
@@ -38,13 +38,44 @@ export class LocalDriver implements LogicFunctionExecutorDriver {
) => {
return join(
LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER,
flatLogicFunctionLayer.checksum,
flatLogicFunctionLayer.yarnLockChecksum,
);
};
private async createLayerIfNotExists(
flatLogicFunctionLayer: FlatLogicFunctionLayer,
) {
private async copyDependenciesInMemory({
applicationUniversalIdentifier,
workspaceId,
inMemoryLayerFolderPath,
}: {
applicationUniversalIdentifier: string;
workspaceId: string;
inMemoryLayerFolderPath: string;
}) {
await Promise.all([
this.fileStorageService.downloadFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: 'package.json',
localPath: join(inMemoryLayerFolderPath, 'package.json'),
}),
this.fileStorageService.downloadFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: 'yarn.lock',
localPath: join(inMemoryLayerFolderPath, 'yarn.lock'),
}),
]);
}
private async createLayerIfNotExists({
flatLogicFunctionLayer,
applicationUniversalIdentifier,
}: {
flatLogicFunctionLayer: FlatLogicFunctionLayer;
applicationUniversalIdentifier: string;
}) {
const inMemoryLayerFolderPath = this.getInMemoryLayerFolderPath(
flatLogicFunctionLayer,
);
@@ -52,17 +83,28 @@ export class LocalDriver implements LogicFunctionExecutorDriver {
try {
await fs.access(inMemoryLayerFolderPath);
} catch {
await copyAndBuildDependencies(
await this.copyDependenciesInMemory({
applicationUniversalIdentifier,
workspaceId: flatLogicFunctionLayer.workspaceId,
inMemoryLayerFolderPath,
flatLogicFunctionLayer,
);
});
await copyYarnEngineAndBuildDependencies(inMemoryLayerFolderPath);
}
}
async delete() {}
private async build(flatLogicFunctionLayer: FlatLogicFunctionLayer) {
await this.createLayerIfNotExists(flatLogicFunctionLayer);
private async build({
flatLogicFunctionLayer,
applicationUniversalIdentifier,
}: {
flatLogicFunctionLayer: FlatLogicFunctionLayer;
applicationUniversalIdentifier: string;
}) {
await this.createLayerIfNotExists({
flatLogicFunctionLayer,
applicationUniversalIdentifier,
});
}
async execute({
@@ -72,7 +114,10 @@ export class LocalDriver implements LogicFunctionExecutorDriver {
payload,
env,
}: LogicFunctionExecuteParams): Promise<LogicFunctionExecuteResult> {
await this.build(flatLogicFunctionLayer);
await this.build({
flatLogicFunctionLayer,
applicationUniversalIdentifier,
});
const startTime = Date.now();
@@ -4,30 +4,16 @@ import { join } from 'path';
import { promisify } from 'util';
import { getLayerDependenciesDirName } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/get-layer-dependencies-dir-name';
import { type FlatLogicFunctionLayer } from 'src/engine/metadata-modules/logic-function-layer/types/flat-logic-function-layer.type';
const execFilePromise = promisify(execFile);
export const copyAndBuildDependencies = async (
export const copyYarnEngineAndBuildDependencies = async (
buildDirectory: string,
flatLogicFunctionLayer: FlatLogicFunctionLayer,
) => {
await fs.mkdir(buildDirectory, {
recursive: true,
});
const packageJson = flatLogicFunctionLayer.packageJson;
const yarnLock = flatLogicFunctionLayer.yarnLock;
await fs.writeFile(
join(buildDirectory, 'package.json'),
JSON.stringify(packageJson, null, 2),
'utf8',
);
await fs.writeFile(join(buildDirectory, 'yarn.lock'), yarnLock, 'utf8');
await fs.cp(getLayerDependenciesDirName('engine'), buildDirectory, {
recursive: true,
});
@@ -1,13 +1,11 @@
import fs from 'fs/promises';
import { join } from 'path';
import { type PackageJson } from 'twenty-shared/application';
import { getLayerDependenciesDirName } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/get-layer-dependencies-dir-name';
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/logic-function/logic-function-drivers/layers/last-layer-version';
export type LayerDependencies = {
packageJson: PackageJson;
packageJson: string;
yarnLock: string;
};
@@ -20,5 +18,5 @@ export const getLastCommonLayerDependencies = async (
fs.readFile(join(lastVersionLayerDirName, 'yarn.lock'), 'utf8'),
]);
return { packageJson: JSON.parse(packageJson), yarnLock };
return { packageJson, yarnLock };
};
@@ -16,7 +16,7 @@ const getAllFiles = async (
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await getAllFiles(rootDir, fullPath, files)));
await getAllFiles(rootDir, fullPath, files);
} else {
files.push({
path: path.relative(rootDir, dir),
@@ -12,6 +12,7 @@ import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.mod
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module';
@Module({
imports: [
@@ -22,6 +23,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
SubscriptionsModule,
WorkspaceCacheModule,
LogicFunctionBuildModule,
LogicFunctionLayerModule,
FileModule,
TypeOrmModule.forFeature([LogicFunctionEntity]),
],
@@ -6,7 +6,7 @@ import {
DEFAULT_API_URL_NAME,
} from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isEmptyObject } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import {
@@ -35,6 +35,7 @@ import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { cleanServerUrl } from 'src/utils/clean-server-url';
import { LogicFunctionLayerService } from 'src/engine/core-modules/logic-function/logic-function-layer/services/logic-function-layer.service';
const MIN_TOKEN_EXPIRATION_IN_SECONDS = 5;
@@ -68,6 +69,7 @@ export class LogicFunctionExecutorService
private readonly functionBuildService: LogicFunctionBuildService,
private readonly subscriptionService: SubscriptionService,
private readonly auditService: AuditService,
private readonly logicFunctionLayerService: LogicFunctionLayerService,
private readonly fileStorageService: FileStorageService,
@InjectRepository(LogicFunctionEntity)
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
@@ -178,6 +180,19 @@ export class LogicFunctionExecutorService
);
}
// TODO: remove when all logic functions are migrated
if (
!(await this.functionBuildService.hasLayerDependencies({
flatLogicFunctionLayer,
applicationUniversalIdentifier,
}))
) {
await this.functionBuildService.uploadDependencies({
flatLogicFunctionLayer,
applicationUniversalIdentifier,
});
}
if (
!(await this.functionBuildService.isBuilt({
flatLogicFunction,
@@ -189,6 +204,7 @@ export class LogicFunctionExecutorService
applicationUniversalIdentifier,
});
}
// END TODO
const resultLogicFunction = await this.callWithTimeout({
callback: () =>
@@ -291,30 +307,26 @@ export class LogicFunctionExecutorService
async getAvailablePackages(logicFunctionId: string) {
const logicFunction = await this.logicFunctionRepository.findOneOrFail({
where: { id: logicFunctionId },
relations: ['logicFunctionLayer'],
relations: ['logicFunctionLayer', 'application'],
});
const packageJson = logicFunction.logicFunctionLayer.packageJson;
if (isEmptyObject(logicFunction.logicFunctionLayer.availablePackages)) {
await this.logicFunctionLayerService.update(
logicFunction.logicFunctionLayer.id,
{},
logicFunction.application.universalIdentifier,
logicFunction.workspaceId,
);
const yarnLock = logicFunction.logicFunctionLayer.yarnLock;
const packageVersionRegex = /^"([^@]+)@.*?":\n\s+version: (.+)$/gm;
const versions: Record<string, string> = {};
let match: RegExpExecArray | null;
while ((match = packageVersionRegex.exec(yarnLock)) !== null) {
const packageName = match[1].split('@', 1)[0];
const version = match[2];
// @ts-expect-error legacy noImplicitAny
if (packageJson.dependencies?.[packageName]) {
versions[packageName] = version;
}
return (
await this.logicFunctionRepository.findOneOrFail({
where: { id: logicFunctionId },
relations: ['logicFunctionLayer'],
})
).logicFunctionLayer.availablePackages;
}
return versions;
return logicFunction.logicFunctionLayer.availablePackages;
}
private async throttleExecution(workspaceId: string) {
@@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { FileFolder } from 'twenty-shared/types';
import { PackageJson } from 'type-fest';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
@@ -11,6 +13,8 @@ import { CreateLogicFunctionLayerInput } from 'src/engine/metadata-modules/logic
import { getLastCommonLayerDependencies } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/get-last-common-layer-dependencies';
import { logicFunctionCreateHash } from 'src/engine/metadata-modules/logic-function/utils/logic-function-create-hash.utils';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@Injectable()
export class LogicFunctionLayerService {
@@ -18,23 +22,34 @@ export class LogicFunctionLayerService {
@InjectRepository(LogicFunctionLayerEntity)
private readonly logicFunctionLayerRepository: Repository<LogicFunctionLayerEntity>,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly fileStorageService: FileStorageService,
) {}
async create(
{ packageJson, yarnLock }: CreateLogicFunctionLayerInput,
{
packageJsonChecksum,
yarnLockChecksum,
applicationUniversalIdentifier,
}: CreateLogicFunctionLayerInput,
workspaceId: string,
) {
const checksum = logicFunctionCreateHash(yarnLock);
const logicFunctionLayer = this.logicFunctionLayerRepository.create({
packageJson,
yarnLock,
checksum,
packageJson: {}, // TODO: Delete when migration to Source files storage is done
yarnLock: '', // TODO: Delete when migration to Source files storage is done
packageJsonChecksum,
yarnLockChecksum,
workspaceId,
} as Omit<LogicFunctionLayerEntity, 'workspace'>);
const availablePackages = await this.getAvailablePackages({
workspaceId,
applicationUniversalIdentifier,
});
const savedLayer =
await this.logicFunctionLayerRepository.save(logicFunctionLayer);
const savedLayer = await this.logicFunctionLayerRepository.save({
...logicFunctionLayer,
availablePackages,
});
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'logicFunctionLayerMaps',
@@ -45,19 +60,19 @@ export class LogicFunctionLayerService {
async update(
id: string,
data: QueryDeepPartialEntity<LogicFunctionLayerEntity>,
data: QueryDeepPartialEntity<Omit<LogicFunctionLayerEntity, 'workspace'>>,
applicationUniversalIdentifier: string,
workspaceId: string,
) {
const checksum = data.yarnLock
? logicFunctionCreateHash(data.yarnLock as string)
: undefined;
const availablePackages = await this.getAvailablePackages({
workspaceId,
applicationUniversalIdentifier,
});
const updateData = { ...data, ...(checksum && { checksum }) };
const result = await this.logicFunctionLayerRepository.update(
id,
updateData,
);
const result = await this.logicFunctionLayerRepository.update(id, {
...data,
availablePackages,
});
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'logicFunctionLayerMaps',
@@ -66,12 +81,45 @@ export class LogicFunctionLayerService {
return result;
}
async createCommonLayerIfNotExist(workspaceId: string) {
async createCommonLayer({
workspaceId,
applicationUniversalIdentifier,
}: {
workspaceId: string;
applicationUniversalIdentifier: string;
}) {
const { packageJson, yarnLock } = await getLastCommonLayerDependencies();
const checksum = logicFunctionCreateHash(yarnLock);
await this.fileStorageService.writeFile_v2({
sourceFile: packageJson,
mimeType: undefined,
fileFolder: FileFolder.Source,
applicationUniversalIdentifier,
workspaceId,
resourcePath: 'package.json',
settings: { isTemporaryFile: false, toDelete: false },
});
await this.fileStorageService.writeFile_v2({
sourceFile: yarnLock,
mimeType: undefined,
fileFolder: FileFolder.Source,
applicationUniversalIdentifier,
workspaceId,
resourcePath: 'yarn.lock',
settings: { isTemporaryFile: false, toDelete: false },
});
const packageJsonChecksum = logicFunctionCreateHash(
JSON.stringify(packageJson),
);
const yarnLockChecksum = logicFunctionCreateHash(yarnLock);
const commonLayer = await this.logicFunctionLayerRepository.findOne({
where: {
checksum,
yarnLockChecksum,
packageJsonChecksum,
workspaceId,
},
});
@@ -80,6 +128,59 @@ export class LogicFunctionLayerService {
return commonLayer;
}
return this.create({ packageJson, yarnLock }, workspaceId);
return this.create(
{ packageJsonChecksum, yarnLockChecksum, applicationUniversalIdentifier },
workspaceId,
);
}
private async getAvailablePackages({
workspaceId,
applicationUniversalIdentifier,
}: {
workspaceId: string;
applicationUniversalIdentifier: string;
}) {
const packageJson = JSON.parse(
(
await streamToBuffer(
await this.fileStorageService.readFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: 'package.json',
}),
)
).toString('utf-8'),
) as PackageJson;
const yarnLock = (
await streamToBuffer(
await this.fileStorageService.readFile_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: 'yarn.lock',
}),
)
).toString('utf-8');
const packageVersionRegex =
/^"(@?[^@]+(?:\/[^@]+)?)@.*?":\n\s+version:\s*(.+)$/gm;
const versions: Record<string, string> = {};
let match: RegExpExecArray | null;
while ((match = packageVersionRegex.exec(yarnLock)) !== null) {
const packageName = match[1];
const version = match[2];
if (packageJson.dependencies?.[packageName]) {
versions[packageName] = version;
}
}
return versions;
}
}
@@ -93,7 +93,9 @@ export class PublicDomainService {
try {
await this.publicDomainRepository.insert(
publicDomain as QueryDeepPartialEntity<PublicDomainEntity>,
publicDomain as QueryDeepPartialEntity<
Omit<PublicDomainEntity, 'workspace'>
>,
);
} catch (error) {
await this.dnsManagerService.deleteHostnameSilently(formattedDomain, {
@@ -2,14 +2,16 @@ import { ArgsType, Field } from '@nestjs/graphql';
import { IsString } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { PackageJson } from 'twenty-shared/application';
@ArgsType()
export class CreateLogicFunctionLayerInput {
@Field(() => GraphQLJSON, { nullable: false })
packageJson: PackageJson;
packageJsonChecksum: string;
@IsString()
@Field(() => String, { nullable: false })
yarnLock: string;
yarnLockChecksum: string;
@Field(() => String)
applicationUniversalIdentifier: string;
}
@@ -1,4 +1,4 @@
import { PackageJson } from 'twenty-shared/application';
import { type PackageJson } from 'type-fest';
import {
Column,
CreateDateColumn,
@@ -23,8 +23,14 @@ export class LogicFunctionLayerEntity extends WorkspaceRelatedEntity {
@Column({ type: 'text', nullable: false })
yarnLock: string;
@Column({ type: 'text', nullable: true })
packageJsonChecksum?: string;
@Column({ type: 'text', nullable: false })
checksum: string;
yarnLockChecksum: string;
@Column({ type: 'jsonb', nullable: false, default: {} })
availablePackages: Record<string, string>;
@OneToMany(
() => LogicFunctionEntity,
@@ -6,10 +6,12 @@ export const fromLogicFunctionLayerEntityToFlatLogicFunctionLayer = (
): FlatLogicFunctionLayer => ({
id: entity.id,
packageJson: entity.packageJson,
packageJsonChecksum: entity.packageJsonChecksum,
yarnLock: entity.yarnLock,
checksum: entity.checksum,
yarnLockChecksum: entity.yarnLockChecksum,
workspaceId: entity.workspaceId,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
logicFunctionIds: entity.logicFunctions?.map((lf) => lf.id) ?? [],
availablePackages: entity.availablePackages ?? {},
});
@@ -57,11 +57,20 @@ export class LogicFunctionService {
let logicFunctionToCreateLayerId = input.logicFunctionLayerId;
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
if (!isDefined(logicFunctionToCreateLayerId)) {
const { id: commonLogicFunctionLayerId } =
await this.logicFunctionLayerService.createCommonLayerIfNotExist(
await this.logicFunctionLayerService.createCommonLayer({
workspaceId,
);
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
});
logicFunctionToCreateLayerId = commonLogicFunctionLayerId;
}
@@ -8,6 +8,7 @@ export const fromFlatLogicFunctionToLogicFunctionDto = ({
}): LogicFunctionDTO => {
return {
id: flatLogicFunction.id,
universalIdentifier: flatLogicFunction.universalIdentifier,
name: flatLogicFunction.name,
description: flatLogicFunction.description ?? undefined,
runtime: flatLogicFunction.runtime,
@@ -1,12 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';
import crypto from 'crypto';
import { Repository } from 'typeorm';
import { isObject } from '@sniptt/guards';
import { FileFolder, type Sources } from 'twenty-shared/types';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
@@ -14,20 +12,18 @@ import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-mana
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { getSeedProjectFiles } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/get-seed-project-files';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { FlatCreateLogicFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { LogicFunctionBuildService } from 'src/engine/core-modules/logic-function/logic-function-build/services/logic-function-build.service';
import { FlatCreateLogicFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type';
@Injectable()
export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
@@ -36,7 +32,6 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
) {
constructor(
private readonly fileStorageService: FileStorageService,
private readonly functionBuildService: LogicFunctionBuildService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
@@ -58,10 +53,14 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
const applicationUniversalIdentifier =
await this.getApplicationUniversalIdentifier(logicFunction.applicationId);
await this.buildAndSaveLogicFunction(
logicFunction,
applicationUniversalIdentifier,
);
let seedChecksum: string | undefined;
if (!isDefined(logicFunction.checksum)) {
seedChecksum = await this.seedLogicFunctionFiles(
logicFunction,
applicationUniversalIdentifier,
);
}
const logicFunctionRepository =
queryRunner.manager.getRepository<LogicFunctionEntity>(
@@ -71,6 +70,7 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
await logicFunctionRepository.insert({
...logicFunction,
workspaceId,
checksum: seedChecksum ?? logicFunction.checksum,
});
}
@@ -92,67 +92,57 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
return application.universalIdentifier;
}
private async writeSourcesToLocalFolder(
sources: Sources,
localPath: string,
): Promise<void> {
for (const key of Object.keys(sources)) {
const filePath = join(localPath, key);
const value = sources[key];
if (isObject(value)) {
await this.writeSourcesToLocalFolder(value as Sources, filePath);
continue;
}
await fs.mkdir(dirname(filePath), { recursive: true });
await fs.writeFile(filePath, value);
}
}
private async buildAndSaveLogicFunction(
private async seedLogicFunctionFiles(
logicFunction: FlatLogicFunction,
applicationUniversalIdentifier: string,
) {
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
): Promise<string> {
const seedProjectFiles = await getSeedProjectFiles;
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
const sourceFiles = seedProjectFiles.filter((file) =>
file.name.endsWith('index.ts'),
);
if (isDefined(logicFunction?.code)) {
await this.writeSourcesToLocalFolder(
logicFunction.code,
sourceTemporaryDir,
);
} else {
for (const file of await getSeedProjectFiles) {
const filePath = join(sourceTemporaryDir, file.path, file.name);
const builtFiles = seedProjectFiles.filter((file) =>
file.name.endsWith('.mjs'),
);
await fs.mkdir(join(sourceTemporaryDir, file.path), {
recursive: true,
});
await fs.writeFile(filePath, file.content);
}
}
const baseFolderPath = getLogicFunctionBaseFolderPath(
logicFunction.sourceHandlerPath,
if (sourceFiles.length !== 1 || builtFiles.length !== 1) {
throw new LogicFunctionException(
'Seed project should have one index.ts file and one index.mjs file',
LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED,
);
await this.fileStorageService.uploadFolder_v2({
workspaceId: logicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
localPath: sourceTemporaryDir,
});
} finally {
await lambdaBuildDirectoryManager.clean();
}
await this.functionBuildService.buildAndUpload({
flatLogicFunction: logicFunction,
const sourceFile = sourceFiles[0];
const builtFile = builtFiles[0];
await this.fileStorageService.writeFile_v2({
workspaceId: logicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: logicFunction.sourceHandlerPath,
sourceFile: sourceFile.content,
mimeType: 'application/typescript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
await this.fileStorageService.writeFile_v2({
workspaceId: logicFunction.workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: logicFunction.builtHandlerPath,
sourceFile: builtFile.content,
mimeType: 'application/javascript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
return crypto.createHash('md5').update(builtFile.content).digest('hex');
}
async rollbackForMetadata(
@@ -4,32 +4,32 @@ import { InjectRepository } from '@nestjs/typeorm';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';
import { isObject } from '@sniptt/guards';
import { FileFolder, type Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { FileFolder, Sources } from 'twenty-shared/types';
import { isObject } from '@sniptt/guards';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { LogicFunctionBuildService } from 'src/engine/core-modules/logic-function/logic-function-build/services/logic-function-build.service';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/services/logic-function-executor.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { FlatUpdateLogicFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { FlatUpdateLogicFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { LogicFunctionBuildService } from 'src/engine/core-modules/logic-function/logic-function-build/services/logic-function-build.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/lambda-build-directory-manager';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
@Injectable()
export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
@@ -37,11 +37,11 @@ export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationR
'logicFunction',
) {
constructor(
private readonly fileStorageService: FileStorageService,
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
private readonly functionBuildService: LogicFunctionBuildService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly fileStorageService: FileStorageService,
) {
super();
}
@@ -93,7 +93,7 @@ export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationR
}: {
flatLogicFunction: FlatLogicFunction;
}) {
this.logicFunctionExecutorService.delete(flatLogicFunction);
await this.logicFunctionExecutorService.delete(flatLogicFunction);
}
private async getApplicationUniversalIdentifier(
@@ -14,9 +14,11 @@ export type ApplicationMarketplaceData = {
export type ApplicationManifest = SyncableEntityOptions & {
defaultRoleUniversalIdentifier: string;
displayName?: string;
displayName: string;
description?: string;
icon?: string;
applicationVariables?: ApplicationVariables;
marketplaceData?: ApplicationMarketplaceData;
packageJsonChecksum: string | null;
yarnLockChecksum: string | null;
};
@@ -36,6 +36,5 @@ export type {
export type { Manifest } from './manifestType';
export type { ObjectFieldManifest } from './objectFieldManifest.type';
export type { ObjectManifest } from './objectManifestType';
export type { PackageJson } from './packageJsonType';
export type { RoleManifest } from './roleManifestType';
export type { SyncableEntityOptions } from './syncableEntityOptionsType';
@@ -5,7 +5,6 @@ import { type FieldManifest } from './fieldManifestType';
import { type FrontComponentManifest } from './frontComponentManifestType';
import { type LogicFunctionManifest } from './logicFunctionManifestType';
import { type ObjectManifest } from './objectManifestType';
import { type PackageJson } from './packageJsonType';
import { type RoleManifest } from './roleManifestType';
export type Manifest = {
@@ -17,6 +16,4 @@ export type Manifest = {
roles: RoleManifest[];
publicAssets: AssetManifest[];
sources: Sources;
packageJson: PackageJson;
yarnLock: string;
};
@@ -1,14 +0,0 @@
export type PackageJson = {
name: string;
license: string;
engines: {
node: string;
npm: string;
yarn: string;
};
packageManager: string;
version: string;
dependencies?: object;
devDependencies?: object;
scripts?: Record<string, string>;
};