Upload files instead ofsources (#17608)
This commit is contained in:
@@ -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']>;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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']>;
|
||||
};
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
|
||||
+20
-57
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+9
-7
@@ -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',
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
+7
-7
@@ -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
-1
@@ -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",
|
||||
|
||||
+7
-3
@@ -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,
|
||||
},
|
||||
|
||||
+2
-39
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+27
-18
@@ -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);
|
||||
}
|
||||
+10
-11
@@ -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 = {
|
||||
|
||||
+11
-1
@@ -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;
|
||||
};
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export type { ApplicationConfig } from './application/application-config';
|
||||
export { defineApplication } from './application/define-application';
|
||||
export type {
|
||||
ValidationResult,
|
||||
|
||||
+6
-4
@@ -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();
|
||||
|
||||
+19
@@ -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"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+31
@@ -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
-7
@@ -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;
|
||||
}
|
||||
|
||||
+31
@@ -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;
|
||||
}
|
||||
+42
-10
@@ -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 })
|
||||
|
||||
+32
-13
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
+60
-7
@@ -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;
|
||||
|
||||
+14
-4
@@ -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),
|
||||
|
||||
+5
@@ -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}`,
|
||||
|
||||
+33
-12
@@ -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;
|
||||
|
||||
+1
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+65
-1
@@ -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();
|
||||
}
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
var main = async (params) => {
|
||||
const { a, b } = params;
|
||||
const message = `Hello, input: ${a} and ${b}`;
|
||||
return { message };
|
||||
};
|
||||
export {
|
||||
main
|
||||
};
|
||||
+60
-15
@@ -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);
|
||||
|
||||
|
||||
+57
-12
@@ -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();
|
||||
|
||||
|
||||
+1
-15
@@ -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,
|
||||
});
|
||||
+2
-4
@@ -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 };
|
||||
};
|
||||
|
||||
+1
-1
@@ -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),
|
||||
|
||||
+2
@@ -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]),
|
||||
],
|
||||
|
||||
+32
-20
@@ -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) {
|
||||
|
||||
+123
-22
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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, {
|
||||
|
||||
+5
-3
@@ -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;
|
||||
}
|
||||
|
||||
+8
-2
@@ -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,
|
||||
|
||||
+3
-1
@@ -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 ?? {},
|
||||
});
|
||||
|
||||
+11
-2
@@ -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;
|
||||
}
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+54
-64
@@ -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(
|
||||
|
||||
+14
-14
@@ -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>;
|
||||
};
|
||||
Reference in New Issue
Block a user