Compare commits

...

14 Commits

Author SHA1 Message Date
Félix Malfait aac099f2e8 Merge branch 'main' into feat/seed-apps-for-dev-workspaces 2026-03-12 08:55:50 +01:00
Félix Malfait a3c0c62506 Merge branch 'main' into feat/seed-apps-for-dev-workspaces 2026-03-11 15:52:06 +01:00
Félix Malfait 390ae34ed8 Merge branch 'main' into feat/seed-apps-for-dev-workspaces 2026-03-11 14:52:19 +01:00
Félix Malfait fe59c333ac Merge remote-tracking branch 'origin/main' into feat/seed-apps-for-dev-workspaces
Made-with: Cursor

# Conflicts:
#	packages/twenty-sdk/src/clients/generated/metadata/types.ts
2026-03-11 14:11:16 +01:00
Félix Malfait 6c0da189cb Fix prettier formatting and regenerate GraphQL/SDK metadata
Made-with: Cursor
2026-03-11 14:03:35 +01:00
Félix Malfait 5aea16f070 Setup 2026-03-11 13:26:41 +01:00
Félix Malfait 2f542d6763 Merge branch 'main' into feat/seed-apps-for-dev-workspaces 2026-03-11 12:44:43 +01:00
Félix Malfait 4ac38449cb Use require.resolve instead of findMonorepoRoot for fixture paths
Follow the existing pattern from standard-front-component-upload.service.ts:
resolve package location via Node module resolution rather than traversing
the filesystem looking for nx.json.

Made-with: Cursor
2026-03-11 12:40:46 +01:00
Félix Malfait dab4cbcb7d Extract shared install logic into installFromExtractedDir
Both installFromLocalDirectory and doInstallApplication now delegate to
a single private method, eliminating the duplicated ensureApplicationExists
+ writeFilesToStorage + synchronizeFromManifest sequence.

Made-with: Cursor
2026-03-11 12:38:42 +01:00
Félix Malfait a73f586619 Revert hello-world view to use ViewKey enum instead of string literal
Made-with: Cursor
2026-03-11 12:37:32 +01:00
Félix Malfait d1126f507d Refactor app seeding to use committed fixture manifests
Instead of inlining manifest JSON in app-seeds.constant.ts, the seeder
now reads manifest.json directly from fixture app .twenty/output/
directories. Adds findMonorepoRoot() for robust path resolution and
graceful error handling so one failing app doesn't block others.

Made-with: Cursor
2026-03-11 12:24:23 +01:00
Félix Malfait 3a1f10e572 Change strat 2026-03-10 20:46:54 +01:00
Félix Malfait d16299a624 Merge branch 'main' into feat/seed-apps-for-dev-workspaces 2026-03-10 20:07:17 +01:00
Félix Malfait 61d058bb1f Seed dev workspaces with three fixture apps
Add installFromLocalDirectory() to ApplicationInstallService so apps can
be installed from a local directory without network access.  The seeder
creates an ApplicationRegistration, writes the manifest and package.json
to a temp dir, then calls the standard install pipeline
(ensureApplicationExists → writeFilesToStorage → synchronizeFromManifest).

Three apps are seeded into both dev workspaces:
- Hello World (NPM source) — full manifest with objects, fields, roles,
  skills, logic functions, views, and navigation menu items
- Postcard App (tarball source) — minimal manifest
- Minimal App (local source) — minimal manifest

Also adds a hello-world integration test to the SDK that validates the
generated manifest against an expected snapshot, which keeps the seed
manifest in sync with the app source.

Made-with: Cursor
2026-03-10 20:05:07 +01:00
36 changed files with 3501 additions and 93 deletions
+10
View File
@@ -1,3 +1,13 @@
generated
# Ignore .twenty build output for all apps
.twenty
# Allow committed fixture metadata (used by dev seeder)
!fixtures/**/.twenty
fixtures/**/.twenty/*
!fixtures/**/.twenty/output
fixtures/**/.twenty/output/**
!fixtures/**/.twenty/output/manifest.json
!fixtures/**/.twenty/output/package.json
!fixtures/**/.twenty/output/yarn.lock
@@ -0,0 +1,54 @@
import path from 'path';
import { buildAndValidateManifest } from '@/cli/utilities/build/manifest/build-and-validate-manifest';
import { buildApplication } from '@/cli/utilities/build/common/build-application';
import { manifestUpdateChecksums } from '@/cli/utilities/build/manifest/manifest-update-checksums';
import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer';
const FIXTURE_APPS = ['hello-world-app', 'postcard-app', 'minimal-app'];
const buildFixture = async (fixtureName: string): Promise<void> => {
const appPath = path.resolve(__dirname, fixtureName);
console.log(`\n--- Building fixture: ${fixtureName} ---`);
console.log(` App path: ${appPath}`);
const manifestResult = await buildAndValidateManifest(appPath);
if (!manifestResult.success) {
console.error(` FAILED to build manifest: ${manifestResult.errors.join(', ')}`);
return;
}
const { manifest, filePaths } = manifestResult;
for (const warning of manifestResult.warnings) {
console.warn(` Warning: ${warning}`);
}
const buildResult = await buildApplication({ appPath, manifest, filePaths });
const updatedManifest = manifestUpdateChecksums({
manifest,
builtFileInfos: buildResult.builtFileInfos,
});
await writeManifestToOutput(appPath, updatedManifest);
console.log(` Built ${buildResult.builtFileInfos.size} files`);
console.log(` Output: ${path.join(appPath, '.twenty', 'output')}`);
};
const main = async () => {
for (const fixture of FIXTURE_APPS) {
await buildFixture(fixture);
}
console.log('\nDone.');
};
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
@@ -0,0 +1,2 @@
generated
node_modules
@@ -0,0 +1,404 @@
{
"application": {
"universalIdentifier": "6563e091-9f5b-4026-a3ea-7e3b3d09e218",
"displayName": "Hello world",
"description": "",
"defaultRoleUniversalIdentifier": "9238bc7b-d38f-4a1c-9d19-31ab7bc67a2f",
"yarnLockChecksum": null,
"packageJsonChecksum": null,
"apiClientChecksum": "6db455b2541abbd2d4e9f85469bdc532",
"preInstallLogicFunctionUniversalIdentifier": "1272ffdb-8e2f-492c-ab37-66c2b97e9c23",
"postInstallLogicFunctionUniversalIdentifier": "7a3f4684-51db-494d-833b-a747a3b90507"
},
"objects": [
{
"universalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
"nameSingular": "exampleItem",
"namePlural": "exampleItems",
"labelSingular": "Example item",
"labelPlural": "Example items",
"description": "A sample custom object",
"icon": "IconBox",
"labelIdentifierFieldMetadataUniversalIdentifier": "d2d7f6cd-33f6-456f-bf00-17adeca926ba",
"fields": [
{
"universalIdentifier": "d2d7f6cd-33f6-456f-bf00-17adeca926ba",
"type": "TEXT",
"name": "name",
"label": "Name",
"description": "Name of the example item",
"icon": "IconAbc"
},
{
"name": "id",
"label": "Id",
"description": "Id",
"icon": "Icon123",
"isNullable": false,
"defaultValue": "uuid",
"type": "UUID",
"universalIdentifier": "5ba62991-6034-555c-b984-5e84897eaa9f"
},
{
"name": "createdAt",
"label": "Creation date",
"description": "Creation date",
"icon": "IconCalendar",
"isNullable": false,
"defaultValue": "now",
"type": "DATE_TIME",
"universalIdentifier": "8b616045-237c-574d-8689-73f06acff8d8"
},
{
"name": "updatedAt",
"label": "Last update",
"description": "Last time the record was changed",
"icon": "IconCalendarClock",
"isNullable": false,
"defaultValue": "now",
"type": "DATE_TIME",
"universalIdentifier": "ced5587d-d9de-51bf-9ad0-74af6198fca4"
},
{
"name": "deletedAt",
"label": "Deleted at",
"description": "Deletion date",
"icon": "IconCalendarClock",
"isNullable": true,
"defaultValue": null,
"type": "DATE_TIME",
"universalIdentifier": "674ee694-0b0f-557f-993c-2b1404db0811"
},
{
"name": "createdBy",
"label": "Created by",
"description": "The creator of the record",
"icon": "IconCreativeCommonsSa",
"isNullable": false,
"defaultValue": {
"name": "''",
"source": "'MANUAL'"
},
"type": "ACTOR",
"universalIdentifier": "5ed1503f-2803-59d1-9b27-e4f1701b8c2a"
},
{
"name": "updatedBy",
"label": "Updated by",
"description": "The workspace member who last updated the record",
"icon": "IconUserCircle",
"isNullable": false,
"defaultValue": {
"name": "''",
"source": "'MANUAL'"
},
"type": "ACTOR",
"universalIdentifier": "3d3a80b4-14a0-55a0-b534-1b23f043c1d5"
},
{
"name": "position",
"label": "Position",
"description": "Position",
"icon": "IconHierarchy2",
"isNullable": false,
"defaultValue": 0,
"type": "POSITION",
"universalIdentifier": "a714b336-d765-5dff-b34b-fcc6ab037e3b"
},
{
"name": "searchVector",
"label": "Search vector",
"icon": "IconSearch",
"description": "Search vector",
"isNullable": true,
"defaultValue": null,
"type": "TS_VECTOR",
"universalIdentifier": "068064e1-c9da-5772-9293-9e57183cee67"
},
{
"name": "timelineActivities",
"label": "Timeline Activities",
"description": "Example items tied to the ExampleItem",
"icon": "IconBuildingSkyscraper",
"isNullable": true,
"type": "RELATION",
"universalSettings": {
"relationType": "ONE_TO_MANY"
},
"universalIdentifier": "e5feca94-536c-5779-8c33-05e39705bcd7",
"relationTargetFieldMetadataUniversalIdentifier": "3d140ea1-fc57-5c8c-96fd-7d1525ed6a69",
"relationTargetObjectMetadataUniversalIdentifier": "20202020-6736-4337-b5c4-8b39fae325a5"
},
{
"name": "favorites",
"label": "Favorites",
"description": "Example items tied to the ExampleItem",
"icon": "IconBuildingSkyscraper",
"isNullable": true,
"type": "RELATION",
"universalSettings": {
"relationType": "ONE_TO_MANY"
},
"universalIdentifier": "3781cc69-9d3f-58d0-927c-c1c552f37a1d",
"relationTargetFieldMetadataUniversalIdentifier": "3043fec0-091c-59ff-95ef-b9d95fbfa3e6",
"relationTargetObjectMetadataUniversalIdentifier": "20202020-ab56-4e05-92a3-e2414a499860"
},
{
"name": "attachments",
"label": "Attachments",
"description": "Example items tied to the ExampleItem",
"icon": "IconBuildingSkyscraper",
"isNullable": true,
"type": "RELATION",
"universalSettings": {
"relationType": "ONE_TO_MANY"
},
"universalIdentifier": "3ad98da8-42e1-50e3-b73b-8c06fa720e2a",
"relationTargetFieldMetadataUniversalIdentifier": "e1232464-8f83-5459-aa94-a40cda8d949a",
"relationTargetObjectMetadataUniversalIdentifier": "20202020-bd3d-4c60-8dca-571c71d4447a"
},
{
"name": "noteTargets",
"label": "Note Targets",
"description": "Example items tied to the ExampleItem",
"icon": "IconBuildingSkyscraper",
"isNullable": true,
"type": "RELATION",
"universalSettings": {
"relationType": "ONE_TO_MANY"
},
"universalIdentifier": "67cae4ab-7d3d-5a66-8d06-ca24568ebe76",
"relationTargetFieldMetadataUniversalIdentifier": "9eeb7b0d-37e4-5a93-97c9-3dbb77fdb318",
"relationTargetObjectMetadataUniversalIdentifier": "20202020-fff0-4b44-be82-bda313884400"
},
{
"name": "taskTargets",
"label": "Task Targets",
"description": "Example items tied to the ExampleItem",
"icon": "IconBuildingSkyscraper",
"isNullable": true,
"type": "RELATION",
"universalSettings": {
"relationType": "ONE_TO_MANY"
},
"universalIdentifier": "a3b9f10c-f780-58cc-bbc9-402ed5f136f6",
"relationTargetFieldMetadataUniversalIdentifier": "5d46d303-452d-5c9c-ab64-0a9768b87956",
"relationTargetObjectMetadataUniversalIdentifier": "20202020-5a9a-44e8-95df-771cd06d0fb1"
}
]
}
],
"fields": [
{
"objectUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
"universalIdentifier": "770d32c2-cf12-4ab2-b66d-73f92dc239b5",
"type": "NUMBER",
"name": "priority",
"label": "Priority",
"description": "Priority level for the example item (1-10)"
},
{
"name": "targetExampleItem",
"label": "ExampleItem",
"description": "ExampleItem Example item",
"icon": "IconTimelineEvent",
"isNullable": true,
"universalSettings": {
"relationType": "MANY_TO_ONE",
"onDelete": "SET_NULL",
"joinColumnName": "targetExampleItemId"
},
"universalIdentifier": "3d140ea1-fc57-5c8c-96fd-7d1525ed6a69",
"objectUniversalIdentifier": "20202020-6736-4337-b5c4-8b39fae325a5",
"relationTargetFieldMetadataUniversalIdentifier": "e5feca94-536c-5779-8c33-05e39705bcd7",
"relationTargetObjectMetadataUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
"type": "MORPH_RELATION",
"morphId": "20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1"
},
{
"name": "targetExampleItem",
"label": "ExampleItem",
"description": "ExampleItem Example item",
"icon": "IconHeart",
"isNullable": true,
"universalSettings": {
"relationType": "MANY_TO_ONE",
"onDelete": "SET_NULL",
"joinColumnName": "targetExampleItemId"
},
"universalIdentifier": "3043fec0-091c-59ff-95ef-b9d95fbfa3e6",
"objectUniversalIdentifier": "20202020-ab56-4e05-92a3-e2414a499860",
"relationTargetFieldMetadataUniversalIdentifier": "3781cc69-9d3f-58d0-927c-c1c552f37a1d",
"relationTargetObjectMetadataUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
"type": "RELATION"
},
{
"name": "targetExampleItem",
"label": "ExampleItem",
"description": "ExampleItem Example item",
"icon": "IconFileImport",
"isNullable": true,
"universalSettings": {
"relationType": "MANY_TO_ONE",
"onDelete": "SET_NULL",
"joinColumnName": "targetExampleItemId"
},
"universalIdentifier": "e1232464-8f83-5459-aa94-a40cda8d949a",
"objectUniversalIdentifier": "20202020-bd3d-4c60-8dca-571c71d4447a",
"relationTargetFieldMetadataUniversalIdentifier": "3ad98da8-42e1-50e3-b73b-8c06fa720e2a",
"relationTargetObjectMetadataUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
"type": "MORPH_RELATION",
"morphId": "20202020-f634-435d-ab8d-e1168b375c69"
},
{
"name": "targetExampleItem",
"label": "ExampleItem",
"description": "ExampleItem Example item",
"icon": "IconCheckbox",
"isNullable": true,
"universalSettings": {
"relationType": "MANY_TO_ONE",
"onDelete": "SET_NULL",
"joinColumnName": "targetExampleItemId"
},
"universalIdentifier": "9eeb7b0d-37e4-5a93-97c9-3dbb77fdb318",
"objectUniversalIdentifier": "20202020-fff0-4b44-be82-bda313884400",
"relationTargetFieldMetadataUniversalIdentifier": "67cae4ab-7d3d-5a66-8d06-ca24568ebe76",
"relationTargetObjectMetadataUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
"type": "MORPH_RELATION",
"morphId": "20202020-f635-435d-ab8d-e1168b375c70"
},
{
"name": "targetExampleItem",
"label": "ExampleItem",
"description": "ExampleItem Example item",
"icon": "IconCheckbox",
"isNullable": true,
"universalSettings": {
"relationType": "MANY_TO_ONE",
"onDelete": "SET_NULL",
"joinColumnName": "targetExampleItemId"
},
"universalIdentifier": "5d46d303-452d-5c9c-ab64-0a9768b87956",
"objectUniversalIdentifier": "20202020-5a9a-44e8-95df-771cd06d0fb1",
"relationTargetFieldMetadataUniversalIdentifier": "a3b9f10c-f780-58cc-bbc9-402ed5f136f6",
"relationTargetObjectMetadataUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
"type": "MORPH_RELATION",
"morphId": "20202020-f636-435d-ab8d-e1168b375c71"
}
],
"roles": [
{
"universalIdentifier": "9238bc7b-d38f-4a1c-9d19-31ab7bc67a2f",
"label": "Hello world default function role",
"description": "Hello world default function role",
"canReadAllObjectRecords": true,
"canUpdateAllObjectRecords": true,
"canSoftDeleteAllObjectRecords": true,
"canDestroyAllObjectRecords": false
}
],
"skills": [
{
"universalIdentifier": "d0940029-9d3c-40be-903a-52d65393028f",
"name": "example-skill",
"label": "Example Skill",
"description": "A sample skill for your application",
"icon": "IconBrain",
"content": "Add your skill instructions here. Skills provide context and capabilities to AI agents."
}
],
"agents": [],
"logicFunctions": [
{
"universalIdentifier": "2baa26eb-9aaf-4856-a4f4-30d6fd6480ee",
"name": "hello-world-logic-function",
"description": "A simple logic function",
"timeoutSeconds": 5,
"httpRouteTriggerSettings": {
"path": "/hello-world-logic-function",
"httpMethod": "GET",
"isAuthRequired": false
},
"toolInputSchema": {
"type": "object",
"properties": {}
},
"handlerName": "default.config.handler",
"sourceHandlerPath": "src/logic-functions/hello-world.ts",
"builtHandlerPath": "src/logic-functions/hello-world.mjs",
"builtHandlerChecksum": "f1a0565e18366e6c531438631137d23b"
},
{
"universalIdentifier": "7a3f4684-51db-494d-833b-a747a3b90507",
"name": "post-install",
"description": "Runs after installation to set up the application.",
"timeoutSeconds": 300,
"toolInputSchema": {
"type": "object",
"properties": {}
},
"handlerName": "default.config.handler",
"sourceHandlerPath": "src/logic-functions/post-install.ts",
"builtHandlerPath": "src/logic-functions/post-install.mjs",
"builtHandlerChecksum": "7b016a1819c4e2355a80a895464032a5"
},
{
"universalIdentifier": "1272ffdb-8e2f-492c-ab37-66c2b97e9c23",
"name": "pre-install",
"description": "Runs before installation to prepare the application.",
"timeoutSeconds": 300,
"toolInputSchema": {
"type": "object",
"properties": {}
},
"handlerName": "default.config.handler",
"sourceHandlerPath": "src/logic-functions/pre-install.ts",
"builtHandlerPath": "src/logic-functions/pre-install.mjs",
"builtHandlerChecksum": "e2151e31924929390a5933ebf7419465"
}
],
"frontComponents": [
{
"universalIdentifier": "d371f098-5b2c-42f0-898d-94459f1ee337",
"name": "hello-world-front-component",
"description": "A sample front component",
"componentName": "HelloWorld",
"sourceComponentPath": "src/front-components/hello-world.tsx",
"builtComponentPath": "src/front-components/hello-world.mjs",
"builtComponentChecksum": "826d5d3f633496d7c2a4f902589b83ed",
"isHeadless": false
}
],
"publicAssets": [],
"views": [
{
"universalIdentifier": "e004df40-29f3-47ba-b39d-d3a5c444367a",
"name": "All example items",
"objectUniversalIdentifier": "dfd43356-39b3-4b55-b4a7-279bec689928",
"icon": "IconList",
"key": "INDEX",
"position": 0,
"fields": [
{
"universalIdentifier": "496c40c2-5766-419c-93bf-20fdad3f34bb",
"fieldMetadataUniversalIdentifier": "d2d7f6cd-33f6-456f-bf00-17adeca926ba",
"position": 0,
"isVisible": true,
"size": 200
}
]
}
],
"navigationMenuItems": [
{
"universalIdentifier": "10f90627-e9c2-44b7-9742-bed77e3d1b17",
"name": "example-navigation-menu-item",
"icon": "IconList",
"color": "blue",
"position": 0,
"viewUniversalIdentifier": "e004df40-29f3-47ba-b39d-d3a5c444367a"
}
],
"pageLayouts": []
}
@@ -0,0 +1,26 @@
{
"name": "hello-world-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": {
"app:dev": "twenty app:dev",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
"dependencies": {
"twenty-sdk": "latest"
},
"devDependencies": {
"typescript": "^5.9.3",
"@types/node": "^24.7.2",
"@types/react": "^19.0.2",
"react": "^19.0.2",
"oxlint": "^0.16.0"
}
}
@@ -0,0 +1,12 @@
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './src/roles/default-role';
export const APPLICATION_UNIVERSAL_IDENTIFIER =
'6563e091-9f5b-4026-a3ea-7e3b3d09e218';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Hello world',
description: '',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
@@ -0,0 +1,26 @@
{
"name": "hello-world-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": {
"app:dev": "twenty app:dev",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
"dependencies": {
"twenty-sdk": "latest"
},
"devDependencies": {
"typescript": "^5.9.3",
"@types/node": "^24.7.2",
"@types/react": "^19.0.2",
"react": "^19.0.2",
"oxlint": "^0.16.0"
}
}
@@ -0,0 +1,11 @@
import { defineField, FieldType } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
export default defineField({
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
universalIdentifier: '770d32c2-cf12-4ab2-b66d-73f92dc239b5',
type: FieldType.NUMBER,
name: 'priority',
label: 'Priority',
description: 'Priority level for the example item (1-10)',
});
@@ -0,0 +1,17 @@
import { defineFrontComponent } from 'twenty-sdk';
export const HelloWorld = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Hello, World!</h1>
<p>This is your first front component.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'd371f098-5b2c-42f0-898d-94459f1ee337',
name: 'hello-world-front-component',
description: 'A sample front component',
component: HelloWorld,
});
@@ -0,0 +1,18 @@
import { defineLogicFunction } from 'twenty-sdk';
const handler = async (): Promise<{ message: string }> => {
return { message: 'Hello, World!' };
};
export default defineLogicFunction({
universalIdentifier: '2baa26eb-9aaf-4856-a4f4-30d6fd6480ee',
name: 'hello-world-logic-function',
description: 'A simple logic function',
timeoutSeconds: 5,
handler,
httpRouteTriggerSettings: {
path: '/hello-world-logic-function',
httpMethod: 'GET',
isAuthRequired: false,
},
});
@@ -0,0 +1,13 @@
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
universalIdentifier: '7a3f4684-51db-494d-833b-a747a3b90507',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
@@ -0,0 +1,13 @@
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '1272ffdb-8e2f-492c-ab37-66c2b97e9c23',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
@@ -0,0 +1,11 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
export default defineNavigationMenuItem({
universalIdentifier: '10f90627-e9c2-44b7-9742-bed77e3d1b17',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
});
@@ -0,0 +1,28 @@
import { defineObject, FieldType } from 'twenty-sdk';
export const EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER =
'dfd43356-39b3-4b55-b4a7-279bec689928';
export const NAME_FIELD_UNIVERSAL_IDENTIFIER =
'd2d7f6cd-33f6-456f-bf00-17adeca926ba';
export default defineObject({
universalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
nameSingular: 'exampleItem',
namePlural: 'exampleItems',
labelSingular: 'Example item',
labelPlural: 'Example items',
description: 'A sample custom object',
icon: 'IconBox',
labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.TEXT,
name: 'name',
label: 'Name',
description: 'Name of the example item',
icon: 'IconAbc',
},
],
});
@@ -0,0 +1,14 @@
import { defineRole } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'9238bc7b-d38f-4a1c-9d19-31ab7bc67a2f';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Hello world default function role',
description: 'Hello world default function role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
});
@@ -0,0 +1,13 @@
import { defineSkill } from 'twenty-sdk';
export const EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER =
'd0940029-9d3c-40be-903a-52d65393028f';
export default defineSkill({
universalIdentifier: EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER,
name: 'example-skill',
label: 'Example Skill',
description: 'A sample skill for your application',
icon: 'IconBrain',
content: 'Add your skill instructions here. Skills provide context and capabilities to AI agents.',
});
@@ -0,0 +1,22 @@
import { defineView } from 'twenty-sdk';
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, NAME_FIELD_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
export const EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER = 'e004df40-29f3-47ba-b39d-d3a5c444367a';
export default defineView({
universalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
name: 'All example items',
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
key: 'INDEX',
position: 0,
fields: [
{
universalIdentifier: '496c40c2-5766-419c-93bf-20fdad3f34bb',
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
size: 200,
},
],
});
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"]
}
},
"include": ["**/*"],
"exclude": ["node_modules", "dist", ".twenty"]
}
@@ -0,0 +1,342 @@
{
"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",
"yarnLockChecksum": "d41d8cd98f00b204e9800998ecf8427e",
"packageJsonChecksum": "ac87abb5113d9d09be71481efc123800",
"apiClientChecksum": null
},
"objects": [
{
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
"nameSingular": "myNote",
"namePlural": "myNotes",
"labelSingular": "My note",
"labelPlural": "My notes",
"description": "A simple root-level object",
"icon": "IconNote",
"labelIdentifierFieldMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000031",
"fields": [
{
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000031",
"type": "TEXT",
"label": "Title",
"name": "title"
},
{
"name": "id",
"label": "Id",
"description": "Id",
"icon": "Icon123",
"isNullable": false,
"defaultValue": "uuid",
"type": "UUID",
"universalIdentifier": "5bdb4f57-a349-56d0-b421-845fd55b3026"
},
{
"name": "name",
"label": "Name",
"description": "Name",
"icon": "IconAbc",
"isNullable": true,
"defaultValue": null,
"type": "TEXT",
"universalIdentifier": "86cc7b06-26de-5b7b-bf4a-31fa246dfd90"
},
{
"name": "createdAt",
"label": "Creation date",
"description": "Creation date",
"icon": "IconCalendar",
"isNullable": false,
"defaultValue": "now",
"type": "DATE_TIME",
"universalIdentifier": "abe3bc69-1a51-5814-a722-74f5a025e92a"
},
{
"name": "updatedAt",
"label": "Last update",
"description": "Last time the record was changed",
"icon": "IconCalendarClock",
"isNullable": false,
"defaultValue": "now",
"type": "DATE_TIME",
"universalIdentifier": "c296d408-9720-5a86-8586-3490fbf04b86"
},
{
"name": "deletedAt",
"label": "Deleted at",
"description": "Deletion date",
"icon": "IconCalendarClock",
"isNullable": true,
"defaultValue": null,
"type": "DATE_TIME",
"universalIdentifier": "488333a2-f4ff-5790-88ef-21f07f8f1985"
},
{
"name": "createdBy",
"label": "Created by",
"description": "The creator of the record",
"icon": "IconCreativeCommonsSa",
"isNullable": false,
"defaultValue": {
"name": "''",
"source": "'MANUAL'"
},
"type": "ACTOR",
"universalIdentifier": "fba6e01b-6579-554e-ad64-a676003c8e68"
},
{
"name": "updatedBy",
"label": "Updated by",
"description": "The workspace member who last updated the record",
"icon": "IconUserCircle",
"isNullable": false,
"defaultValue": {
"name": "''",
"source": "'MANUAL'"
},
"type": "ACTOR",
"universalIdentifier": "fa60f87d-15c8-5ff7-9da0-191fa14a922f"
},
{
"name": "position",
"label": "Position",
"description": "Position",
"icon": "IconHierarchy2",
"isNullable": false,
"defaultValue": 0,
"type": "POSITION",
"universalIdentifier": "4a5d7791-ddc9-53ee-bc46-699da7373290"
},
{
"name": "searchVector",
"label": "Search vector",
"icon": "IconSearch",
"description": "Search vector",
"isNullable": true,
"defaultValue": null,
"type": "TS_VECTOR",
"universalIdentifier": "0f00bc3b-f313-5206-8496-842af20a538a"
},
{
"name": "timelineActivities",
"label": "Timeline Activities",
"description": "My notes tied to the MyNote",
"icon": "IconBuildingSkyscraper",
"isNullable": true,
"type": "RELATION",
"universalSettings": {
"relationType": "ONE_TO_MANY"
},
"universalIdentifier": "12c84c1c-6e3f-54c5-a9a8-e93c7b46ac35",
"relationTargetFieldMetadataUniversalIdentifier": "7e28b1ea-9a94-5d36-bc03-3f12f476867a",
"relationTargetObjectMetadataUniversalIdentifier": "20202020-6736-4337-b5c4-8b39fae325a5"
},
{
"name": "favorites",
"label": "Favorites",
"description": "My notes tied to the MyNote",
"icon": "IconBuildingSkyscraper",
"isNullable": true,
"type": "RELATION",
"universalSettings": {
"relationType": "ONE_TO_MANY"
},
"universalIdentifier": "ed80c73b-25ff-5a3c-99c7-e27fa2f2611e",
"relationTargetFieldMetadataUniversalIdentifier": "698f12fe-3feb-55e3-b6cc-73375f1a8ae6",
"relationTargetObjectMetadataUniversalIdentifier": "20202020-ab56-4e05-92a3-e2414a499860"
},
{
"name": "attachments",
"label": "Attachments",
"description": "My notes tied to the MyNote",
"icon": "IconBuildingSkyscraper",
"isNullable": true,
"type": "RELATION",
"universalSettings": {
"relationType": "ONE_TO_MANY"
},
"universalIdentifier": "56a872c8-7a1e-563d-8772-3978c2c5c220",
"relationTargetFieldMetadataUniversalIdentifier": "721b12d8-7a3d-5e13-b8d5-62b3ad8cb8b5",
"relationTargetObjectMetadataUniversalIdentifier": "20202020-bd3d-4c60-8dca-571c71d4447a"
},
{
"name": "noteTargets",
"label": "Note Targets",
"description": "My notes tied to the MyNote",
"icon": "IconBuildingSkyscraper",
"isNullable": true,
"type": "RELATION",
"universalSettings": {
"relationType": "ONE_TO_MANY"
},
"universalIdentifier": "011595e8-7524-5ec7-9fbc-4bd3c42d7d24",
"relationTargetFieldMetadataUniversalIdentifier": "94b6f269-fc9a-5bcd-af5b-2bdac69f276b",
"relationTargetObjectMetadataUniversalIdentifier": "20202020-fff0-4b44-be82-bda313884400"
},
{
"name": "taskTargets",
"label": "Task Targets",
"description": "My notes tied to the MyNote",
"icon": "IconBuildingSkyscraper",
"isNullable": true,
"type": "RELATION",
"universalSettings": {
"relationType": "ONE_TO_MANY"
},
"universalIdentifier": "dd734fa3-26c3-53dd-91aa-577c313e1fa4",
"relationTargetFieldMetadataUniversalIdentifier": "50da3cfa-dea5-5c9b-b1b7-cb615e1df4c3",
"relationTargetObjectMetadataUniversalIdentifier": "20202020-5a9a-44e8-95df-771cd06d0fb1"
}
]
}
],
"fields": [
{
"name": "targetMyNote",
"label": "MyNote",
"description": "MyNote My note",
"icon": "IconTimelineEvent",
"isNullable": true,
"universalSettings": {
"relationType": "MANY_TO_ONE",
"onDelete": "SET_NULL",
"joinColumnName": "targetMyNoteId"
},
"universalIdentifier": "7e28b1ea-9a94-5d36-bc03-3f12f476867a",
"objectUniversalIdentifier": "20202020-6736-4337-b5c4-8b39fae325a5",
"relationTargetFieldMetadataUniversalIdentifier": "12c84c1c-6e3f-54c5-a9a8-e93c7b46ac35",
"relationTargetObjectMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
"type": "MORPH_RELATION",
"morphId": "20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1"
},
{
"name": "targetMyNote",
"label": "MyNote",
"description": "MyNote My note",
"icon": "IconHeart",
"isNullable": true,
"universalSettings": {
"relationType": "MANY_TO_ONE",
"onDelete": "SET_NULL",
"joinColumnName": "targetMyNoteId"
},
"universalIdentifier": "698f12fe-3feb-55e3-b6cc-73375f1a8ae6",
"objectUniversalIdentifier": "20202020-ab56-4e05-92a3-e2414a499860",
"relationTargetFieldMetadataUniversalIdentifier": "ed80c73b-25ff-5a3c-99c7-e27fa2f2611e",
"relationTargetObjectMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
"type": "RELATION"
},
{
"name": "targetMyNote",
"label": "MyNote",
"description": "MyNote My note",
"icon": "IconFileImport",
"isNullable": true,
"universalSettings": {
"relationType": "MANY_TO_ONE",
"onDelete": "SET_NULL",
"joinColumnName": "targetMyNoteId"
},
"universalIdentifier": "721b12d8-7a3d-5e13-b8d5-62b3ad8cb8b5",
"objectUniversalIdentifier": "20202020-bd3d-4c60-8dca-571c71d4447a",
"relationTargetFieldMetadataUniversalIdentifier": "56a872c8-7a1e-563d-8772-3978c2c5c220",
"relationTargetObjectMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
"type": "MORPH_RELATION",
"morphId": "20202020-f634-435d-ab8d-e1168b375c69"
},
{
"name": "targetMyNote",
"label": "MyNote",
"description": "MyNote My note",
"icon": "IconCheckbox",
"isNullable": true,
"universalSettings": {
"relationType": "MANY_TO_ONE",
"onDelete": "SET_NULL",
"joinColumnName": "targetMyNoteId"
},
"universalIdentifier": "94b6f269-fc9a-5bcd-af5b-2bdac69f276b",
"objectUniversalIdentifier": "20202020-fff0-4b44-be82-bda313884400",
"relationTargetFieldMetadataUniversalIdentifier": "011595e8-7524-5ec7-9fbc-4bd3c42d7d24",
"relationTargetObjectMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
"type": "MORPH_RELATION",
"morphId": "20202020-f635-435d-ab8d-e1168b375c70"
},
{
"name": "targetMyNote",
"label": "MyNote",
"description": "MyNote My note",
"icon": "IconCheckbox",
"isNullable": true,
"universalSettings": {
"relationType": "MANY_TO_ONE",
"onDelete": "SET_NULL",
"joinColumnName": "targetMyNoteId"
},
"universalIdentifier": "50da3cfa-dea5-5c9b-b1b7-cb615e1df4c3",
"objectUniversalIdentifier": "20202020-5a9a-44e8-95df-771cd06d0fb1",
"relationTargetFieldMetadataUniversalIdentifier": "dd734fa3-26c3-53dd-91aa-577c313e1fa4",
"relationTargetObjectMetadataUniversalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000030",
"type": "MORPH_RELATION",
"morphId": "20202020-f636-435d-ab8d-e1168b375c71"
}
],
"roles": [
{
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000040",
"label": "My role",
"description": "A simple root-level role",
"canReadAllObjectRecords": true,
"canUpdateAllObjectRecords": false,
"canSoftDeleteAllObjectRecords": false,
"canDestroyAllObjectRecords": false,
"canUpdateAllSettings": false,
"canBeAssignedToAgents": false,
"canBeAssignedToUsers": true,
"canBeAssignedToApiKeys": false
}
],
"skills": [],
"agents": [],
"logicFunctions": [
{
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000010",
"name": "my-function",
"timeoutSeconds": 5,
"httpRouteTriggerSettings": {
"path": "/my-function",
"httpMethod": "GET",
"isAuthRequired": false
},
"toolInputSchema": {
"type": "object",
"properties": {}
},
"handlerName": "default.config.handler",
"sourceHandlerPath": "my.function.ts",
"builtHandlerPath": "my.function.mjs",
"builtHandlerChecksum": "71f0af6b9dfeef884cc81ae75ca6ecb0"
}
],
"frontComponents": [
{
"universalIdentifier": "e1e2e3e4-e5e6-4000-8000-000000000020",
"name": "my-component",
"description": "A root-level front component",
"componentName": "MyComponent",
"sourceComponentPath": "my.front-component.tsx",
"builtComponentPath": "my.front-component.mjs",
"builtComponentChecksum": "37b905c5467584f03fa5ab2c8a05bc88",
"isHeadless": false
}
],
"publicAssets": [],
"views": [],
"navigationMenuItems": [],
"pageLayouts": []
}
@@ -0,0 +1,36 @@
{
"name": "minimal-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",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"app:uninstall": "twenty app:uninstall",
"help": "twenty help",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
"dependencies": {
"twenty-sdk": "latest"
},
"devDependencies": {
"typescript": "^5.9.3",
"@types/node": "^24.7.2",
"@types/react": "^19.0.2",
"react": "^19.0.2",
"oxlint": "^0.16.0"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
{
"name": "postcard-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",
"function:logs": "twenty function:logs",
"function:execute": "twenty function:execute",
"app:uninstall": "twenty app:uninstall",
"help": "twenty help",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
"dependencies": {
"twenty-sdk": "latest"
},
"devDependencies": {
"typescript": "^5.9.3",
"@types/node": "^24.7.2",
"@types/react": "^19.0.2",
"react": "^19.0.2",
"oxlint": "^0.16.0"
}
}
@@ -1,6 +1,7 @@
import { resolve } from 'path';
const FIXTURES_ROOT = resolve(__dirname, '../../../../../twenty-apps/fixtures');
const TWENTY_APPS_ROOT = resolve(__dirname, '../../../../../twenty-apps');
export const POSTCARD_APP_PATH = resolve(FIXTURES_ROOT, 'postcard-app');
export const MINIMAL_APP_PATH = resolve(FIXTURES_ROOT, 'minimal-app');
@@ -9,3 +10,4 @@ export const FUNCTION_EXECUTE_APP_PATH = resolve(
FIXTURES_ROOT,
'function-execute-app',
);
export const HELLO_WORLD_APP_PATH = resolve(TWENTY_APPS_ROOT, 'hello-world');
@@ -0,0 +1,24 @@
import { runAppDevInProcess } from '@/cli/__tests__/integration/utils/run-app-dev-in-process.util';
import { HELLO_WORLD_APP_PATH } from '@/cli/__tests__/apps/fixture-paths';
import { defineManifestTests } from './tests/manifest.tests';
describe('hello-world app:dev', () => {
beforeAll(async () => {
const result = await runAppDevInProcess({ appPath: HELLO_WORLD_APP_PATH });
if (!result.success) {
const diagnostics = JSON.stringify(
{ events: result.events, stepStatuses: result.stepStatuses },
null,
2,
);
throw new Error(
`app:dev did not produce manifest.json within timeout.\n${diagnostics}`,
);
}
}, 60000);
defineManifestTests(HELLO_WORLD_APP_PATH);
});
@@ -0,0 +1,429 @@
import { FieldType } from '@/sdk';
import type { Manifest } from 'twenty-shared/application';
import {
FieldMetadataType,
RelationOnDeleteAction,
RelationType,
ViewKey,
} from 'twenty-shared/types';
export const EXPECTED_MANIFEST: Manifest = {
application: {
universalIdentifier: '6563e091-9f5b-4026-a3ea-7e3b3d09e218',
displayName: 'Hello world',
description: '',
defaultRoleUniversalIdentifier: '9238bc7b-d38f-4a1c-9d19-31ab7bc67a2f',
yarnLockChecksum: '[checksum]',
packageJsonChecksum: '[checksum]',
apiClientChecksum: null,
preInstallLogicFunctionUniversalIdentifier:
'1272ffdb-8e2f-492c-ab37-66c2b97e9c23',
postInstallLogicFunctionUniversalIdentifier:
'7a3f4684-51db-494d-833b-a747a3b90507',
},
objects: [
{
universalIdentifier: 'dfd43356-39b3-4b55-b4a7-279bec689928',
nameSingular: 'exampleItem',
namePlural: 'exampleItems',
labelSingular: 'Example item',
labelPlural: 'Example items',
description: 'A sample custom object',
icon: 'IconBox',
labelIdentifierFieldMetadataUniversalIdentifier:
'd2d7f6cd-33f6-456f-bf00-17adeca926ba',
fields: [
{
universalIdentifier: 'd2d7f6cd-33f6-456f-bf00-17adeca926ba',
type: FieldType.TEXT,
name: 'name',
label: 'Name',
description: 'Name of the example item',
icon: 'IconAbc',
},
{
name: 'id',
label: 'Id',
description: 'Id',
icon: 'Icon123',
isNullable: false,
defaultValue: 'uuid',
type: FieldMetadataType.UUID,
universalIdentifier: '5ba62991-6034-555c-b984-5e84897eaa9f',
},
{
name: 'createdAt',
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isNullable: false,
defaultValue: 'now',
type: FieldMetadataType.DATE_TIME,
universalIdentifier: '8b616045-237c-574d-8689-73f06acff8d8',
},
{
name: 'updatedAt',
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isNullable: false,
defaultValue: 'now',
type: FieldMetadataType.DATE_TIME,
universalIdentifier: 'ced5587d-d9de-51bf-9ad0-74af6198fca4',
},
{
name: 'deletedAt',
label: 'Deleted at',
description: 'Deletion date',
icon: 'IconCalendarClock',
isNullable: true,
defaultValue: null,
type: FieldMetadataType.DATE_TIME,
universalIdentifier: '674ee694-0b0f-557f-993c-2b1404db0811',
},
{
name: 'createdBy',
label: 'Created by',
description: 'The creator of the record',
icon: 'IconCreativeCommonsSa',
isNullable: false,
defaultValue: {
name: "''",
source: "'MANUAL'",
},
type: FieldMetadataType.ACTOR,
universalIdentifier: '5ed1503f-2803-59d1-9b27-e4f1701b8c2a',
},
{
name: 'updatedBy',
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isNullable: false,
defaultValue: {
name: "''",
source: "'MANUAL'",
},
type: FieldMetadataType.ACTOR,
universalIdentifier: '3d3a80b4-14a0-55a0-b534-1b23f043c1d5',
},
{
name: 'position',
label: 'Position',
description: 'Position',
icon: 'IconHierarchy2',
isNullable: false,
defaultValue: 0,
type: FieldMetadataType.POSITION,
universalIdentifier: 'a714b336-d765-5dff-b34b-fcc6ab037e3b',
},
{
name: 'searchVector',
label: 'Search vector',
icon: 'IconSearch',
description: 'Search vector',
isNullable: true,
defaultValue: null,
type: FieldMetadataType.TS_VECTOR,
universalIdentifier: '068064e1-c9da-5772-9293-9e57183cee67',
},
{
name: 'timelineActivities',
label: 'Timeline Activities',
description: 'Example items tied to the ExampleItem',
icon: 'IconBuildingSkyscraper',
isNullable: true,
type: FieldType.RELATION,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
universalIdentifier: 'e5feca94-536c-5779-8c33-05e39705bcd7',
relationTargetFieldMetadataUniversalIdentifier:
'3d140ea1-fc57-5c8c-96fd-7d1525ed6a69',
relationTargetObjectMetadataUniversalIdentifier:
'20202020-6736-4337-b5c4-8b39fae325a5',
},
{
name: 'favorites',
label: 'Favorites',
description: 'Example items tied to the ExampleItem',
icon: 'IconBuildingSkyscraper',
isNullable: true,
type: FieldType.RELATION,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
universalIdentifier: '3781cc69-9d3f-58d0-927c-c1c552f37a1d',
relationTargetFieldMetadataUniversalIdentifier:
'3043fec0-091c-59ff-95ef-b9d95fbfa3e6',
relationTargetObjectMetadataUniversalIdentifier:
'20202020-ab56-4e05-92a3-e2414a499860',
},
{
name: 'attachments',
label: 'Attachments',
description: 'Example items tied to the ExampleItem',
icon: 'IconBuildingSkyscraper',
isNullable: true,
type: FieldType.RELATION,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
universalIdentifier: '3ad98da8-42e1-50e3-b73b-8c06fa720e2a',
relationTargetFieldMetadataUniversalIdentifier:
'e1232464-8f83-5459-aa94-a40cda8d949a',
relationTargetObjectMetadataUniversalIdentifier:
'20202020-bd3d-4c60-8dca-571c71d4447a',
},
{
name: 'noteTargets',
label: 'Note Targets',
description: 'Example items tied to the ExampleItem',
icon: 'IconBuildingSkyscraper',
isNullable: true,
type: FieldType.RELATION,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
universalIdentifier: '67cae4ab-7d3d-5a66-8d06-ca24568ebe76',
relationTargetFieldMetadataUniversalIdentifier:
'9eeb7b0d-37e4-5a93-97c9-3dbb77fdb318',
relationTargetObjectMetadataUniversalIdentifier:
'20202020-fff0-4b44-be82-bda313884400',
},
{
name: 'taskTargets',
label: 'Task Targets',
description: 'Example items tied to the ExampleItem',
icon: 'IconBuildingSkyscraper',
isNullable: true,
type: FieldType.RELATION,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
universalIdentifier: 'a3b9f10c-f780-58cc-bbc9-402ed5f136f6',
relationTargetFieldMetadataUniversalIdentifier:
'5d46d303-452d-5c9c-ab64-0a9768b87956',
relationTargetObjectMetadataUniversalIdentifier:
'20202020-5a9a-44e8-95df-771cd06d0fb1',
},
],
},
],
fields: [
{
objectUniversalIdentifier: 'dfd43356-39b3-4b55-b4a7-279bec689928',
universalIdentifier: '770d32c2-cf12-4ab2-b66d-73f92dc239b5',
type: FieldType.NUMBER,
name: 'priority',
label: 'Priority',
description: 'Priority level for the example item (1-10)',
},
{
name: 'targetExampleItem',
label: 'ExampleItem',
description: 'ExampleItem Example item',
icon: 'IconTimelineEvent',
isNullable: true,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'targetExampleItemId',
},
universalIdentifier: '3d140ea1-fc57-5c8c-96fd-7d1525ed6a69',
objectUniversalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5',
relationTargetFieldMetadataUniversalIdentifier:
'e5feca94-536c-5779-8c33-05e39705bcd7',
relationTargetObjectMetadataUniversalIdentifier:
'dfd43356-39b3-4b55-b4a7-279bec689928',
type: FieldType.MORPH_RELATION,
morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1',
},
{
name: 'targetExampleItem',
label: 'ExampleItem',
description: 'ExampleItem Example item',
icon: 'IconHeart',
isNullable: true,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'targetExampleItemId',
},
universalIdentifier: '3043fec0-091c-59ff-95ef-b9d95fbfa3e6',
objectUniversalIdentifier: '20202020-ab56-4e05-92a3-e2414a499860',
relationTargetFieldMetadataUniversalIdentifier:
'3781cc69-9d3f-58d0-927c-c1c552f37a1d',
relationTargetObjectMetadataUniversalIdentifier:
'dfd43356-39b3-4b55-b4a7-279bec689928',
type: FieldType.RELATION,
},
{
name: 'targetExampleItem',
label: 'ExampleItem',
description: 'ExampleItem Example item',
icon: 'IconFileImport',
isNullable: true,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'targetExampleItemId',
},
universalIdentifier: 'e1232464-8f83-5459-aa94-a40cda8d949a',
objectUniversalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a',
relationTargetFieldMetadataUniversalIdentifier:
'3ad98da8-42e1-50e3-b73b-8c06fa720e2a',
relationTargetObjectMetadataUniversalIdentifier:
'dfd43356-39b3-4b55-b4a7-279bec689928',
type: FieldType.MORPH_RELATION,
morphId: '20202020-f634-435d-ab8d-e1168b375c69',
},
{
name: 'targetExampleItem',
label: 'ExampleItem',
description: 'ExampleItem Example item',
icon: 'IconCheckbox',
isNullable: true,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'targetExampleItemId',
},
universalIdentifier: '9eeb7b0d-37e4-5a93-97c9-3dbb77fdb318',
objectUniversalIdentifier: '20202020-fff0-4b44-be82-bda313884400',
relationTargetFieldMetadataUniversalIdentifier:
'67cae4ab-7d3d-5a66-8d06-ca24568ebe76',
relationTargetObjectMetadataUniversalIdentifier:
'dfd43356-39b3-4b55-b4a7-279bec689928',
type: FieldType.MORPH_RELATION,
morphId: '20202020-f635-435d-ab8d-e1168b375c70',
},
{
name: 'targetExampleItem',
label: 'ExampleItem',
description: 'ExampleItem Example item',
icon: 'IconCheckbox',
isNullable: true,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'targetExampleItemId',
},
universalIdentifier: '5d46d303-452d-5c9c-ab64-0a9768b87956',
objectUniversalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1',
relationTargetFieldMetadataUniversalIdentifier:
'a3b9f10c-f780-58cc-bbc9-402ed5f136f6',
relationTargetObjectMetadataUniversalIdentifier:
'dfd43356-39b3-4b55-b4a7-279bec689928',
type: FieldType.MORPH_RELATION,
morphId: '20202020-f636-435d-ab8d-e1168b375c71',
},
],
roles: [
{
universalIdentifier: '9238bc7b-d38f-4a1c-9d19-31ab7bc67a2f',
label: 'Hello world default function role',
description: 'Hello world default function role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
},
],
skills: [
{
universalIdentifier: 'd0940029-9d3c-40be-903a-52d65393028f',
name: 'example-skill',
label: 'Example Skill',
description: 'A sample skill for your application',
icon: 'IconBrain',
content:
'Add your skill instructions here. Skills provide context and capabilities to AI agents.',
},
],
agents: [],
logicFunctions: [
{
universalIdentifier: '2baa26eb-9aaf-4856-a4f4-30d6fd6480ee',
name: 'hello-world-logic-function',
description: 'A simple logic function',
timeoutSeconds: 5,
httpRouteTriggerSettings: {
path: '/hello-world-logic-function',
httpMethod: 'GET',
isAuthRequired: false,
},
toolInputSchema: { type: 'object', properties: {} },
handlerName: 'default.config.handler',
sourceHandlerPath: 'src/logic-functions/hello-world.ts',
builtHandlerPath: 'src/logic-functions/hello-world.mjs',
builtHandlerChecksum: '[checksum]',
},
{
universalIdentifier: '7a3f4684-51db-494d-833b-a747a3b90507',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
toolInputSchema: { type: 'object', properties: {} },
handlerName: 'default.config.handler',
sourceHandlerPath: 'src/logic-functions/post-install.ts',
builtHandlerPath: 'src/logic-functions/post-install.mjs',
builtHandlerChecksum: '[checksum]',
},
{
universalIdentifier: '1272ffdb-8e2f-492c-ab37-66c2b97e9c23',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
toolInputSchema: { type: 'object', properties: {} },
handlerName: 'default.config.handler',
sourceHandlerPath: 'src/logic-functions/pre-install.ts',
builtHandlerPath: 'src/logic-functions/pre-install.mjs',
builtHandlerChecksum: '[checksum]',
},
],
frontComponents: [
{
universalIdentifier: 'd371f098-5b2c-42f0-898d-94459f1ee337',
name: 'hello-world-front-component',
description: 'A sample front component',
componentName: 'HelloWorld',
sourceComponentPath: 'src/front-components/hello-world.tsx',
builtComponentPath: 'src/front-components/hello-world.mjs',
builtComponentChecksum: '[checksum]',
isHeadless: false,
},
],
publicAssets: [],
views: [
{
universalIdentifier: 'e004df40-29f3-47ba-b39d-d3a5c444367a',
name: 'All example items',
objectUniversalIdentifier: 'dfd43356-39b3-4b55-b4a7-279bec689928',
icon: 'IconList',
key: ViewKey.INDEX,
position: 0,
fields: [
{
universalIdentifier: '496c40c2-5766-419c-93bf-20fdad3f34bb',
fieldMetadataUniversalIdentifier:
'd2d7f6cd-33f6-456f-bf00-17adeca926ba',
position: 0,
isVisible: true,
size: 200,
},
],
},
],
navigationMenuItems: [
{
universalIdentifier: '10f90627-e9c2-44b7-9742-bed77e3d1b17',
name: 'example-navigation-menu-item',
icon: 'IconList',
color: 'blue',
position: 0,
viewUniversalIdentifier: 'e004df40-29f3-47ba-b39d-d3a5c444367a',
},
],
pageLayouts: [],
};
@@ -0,0 +1,31 @@
import { join } from 'path';
import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util';
import { readJson } from '@/cli/utilities/file/fs-utils';
import { type Manifest } from 'twenty-shared/application';
import { EXPECTED_MANIFEST } from '../expected-manifest';
export const defineManifestTests = (appPath: string): void => {
const manifestOutputPath = join(appPath, '.twenty/output/manifest.json');
describe('manifest', () => {
it('should build manifest matching expected JSON', async () => {
const manifest = await readJson<Manifest>(manifestOutputPath);
expect(manifest).not.toBeNull();
expect(manifest.objects).toHaveLength(1);
expect(manifest.logicFunctions).toHaveLength(3);
expect(manifest.frontComponents).toHaveLength(1);
expect(manifest.roles).toHaveLength(1);
expect(manifest.skills).toHaveLength(1);
expect(manifest.fields).toHaveLength(6);
expect(manifest.views).toHaveLength(1);
expect(manifest.navigationMenuItems).toHaveLength(1);
expect(normalizeManifestForComparison(manifest)).toEqual(
normalizeManifestForComparison(EXPECTED_MANIFEST),
);
});
});
};
@@ -2857,6 +2857,8 @@ type Query {
currentUser: User!
currentWorkspace: Workspace!
getPublicWorkspaceDataByDomain(origin: String): PublicWorkspaceData!
findManyApplications: [Application!]!
findOneApplication(id: UUID, universalIdentifier: UUID): Application!
checkUserExists(email: String!, captchaToken: String): CheckUserExist!
checkWorkspaceInviteHashIsValid(inviteHash: String!): WorkspaceInviteHashValid!
findWorkspaceFromInviteHash(inviteHash: String!): Workspace!
@@ -2908,8 +2910,6 @@ type Query {
getEmailingDomains: [EmailingDomain!]!
findManyMarketplaceApps: [MarketplaceApp!]!
findOneMarketplaceApp(universalIdentifier: String!): MarketplaceApp!
findManyApplications: [Application!]!
findOneApplication(id: UUID, universalIdentifier: UUID): Application!
}
input LogicFunctionIdInput {
@@ -3130,6 +3130,10 @@ type Mutation {
updateWorkspace(data: UpdateWorkspaceInput!): Workspace!
deleteCurrentWorkspace: Workspace!
checkCustomDomainValidRecords: DomainValidRecords
installApplication(appRegistrationId: String!, version: String): Boolean!
runWorkspaceMigration(workspaceMigration: WorkspaceMigrationInput!): Boolean!
uninstallApplication(universalIdentifier: String!): Boolean!
updateOneApplicationVariable(key: String!, value: String!, applicationId: UUID!): Boolean!
getAuthorizationUrlForSSO(input: GetAuthorizationUrlForSSOInput!): GetAuthorizationUrlForSSO!
getLoginTokenFromCredentials(email: String!, password: String!, captchaToken: String, locale: String, verifyEmailRedirectPath: String, origin: String!): LoginToken!
signIn(email: String!, password: String!, captchaToken: String, locale: String, verifyEmailRedirectPath: String): AvailableWorkspacesAndAccessTokens!
@@ -3197,10 +3201,6 @@ type Mutation {
verifyEmailingDomain(id: String!): EmailingDomain!
createOneAppToken(input: CreateOneAppTokenInput!): AppToken!
installMarketplaceApp(universalIdentifier: String!, version: String): Boolean!
installApplication(appRegistrationId: String!, version: String): Boolean!
runWorkspaceMigration(workspaceMigration: WorkspaceMigrationInput!): Boolean!
uninstallApplication(universalIdentifier: String!): Boolean!
updateOneApplicationVariable(key: String!, value: String!, applicationId: UUID!): Boolean!
createDevelopmentApplication(universalIdentifier: String!, name: String!): DevelopmentApplication!
generateApplicationToken(applicationId: UUID!): ApplicationTokenPair!
syncApplication(manifest: JSON!): WorkspaceMigration!
@@ -3971,6 +3971,49 @@ input UpdateWorkspaceInput {
useRecommendedModels: Boolean
}
input WorkspaceMigrationInput {
actions: [WorkspaceMigrationDeleteActionInput!]!
}
input WorkspaceMigrationDeleteActionInput {
type: WorkspaceMigrationActionType!
metadataName: AllMetadataName!
universalIdentifier: String!
}
enum WorkspaceMigrationActionType {
delete
create
update
}
enum AllMetadataName {
fieldMetadata
objectMetadata
view
viewField
viewFieldGroup
viewGroup
viewSort
rowLevelPermissionPredicate
rowLevelPermissionPredicateGroup
viewFilterGroup
index
logicFunction
viewFilter
role
roleTarget
agent
skill
pageLayout
pageLayoutWidget
pageLayoutTab
commandMenuItem
navigationMenuItem
frontComponent
webhook
}
input GetAuthorizationUrlForSSOInput {
identityProviderId: UUID!
workspaceInviteHash: String
@@ -4118,49 +4161,6 @@ input CreateAppTokenInput {
expiresAt: DateTime!
}
input WorkspaceMigrationInput {
actions: [WorkspaceMigrationDeleteActionInput!]!
}
input WorkspaceMigrationDeleteActionInput {
type: WorkspaceMigrationActionType!
metadataName: AllMetadataName!
universalIdentifier: String!
}
enum WorkspaceMigrationActionType {
delete
create
update
}
enum AllMetadataName {
fieldMetadata
objectMetadata
view
viewField
viewFieldGroup
viewGroup
viewSort
rowLevelPermissionPredicate
rowLevelPermissionPredicateGroup
viewFilterGroup
index
logicFunction
viewFilter
role
roleTarget
agent
skill
pageLayout
pageLayoutWidget
pageLayoutTab
commandMenuItem
navigationMenuItem
frontComponent
webhook
}
enum FileFolder {
ProfilePicture
WorkspaceLogo
@@ -2553,6 +2553,8 @@ export interface Query {
currentUser: User
currentWorkspace: Workspace
getPublicWorkspaceDataByDomain: PublicWorkspaceData
findManyApplications: Application[]
findOneApplication: Application
checkUserExists: CheckUserExist
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid
findWorkspaceFromInviteHash: Workspace
@@ -2595,8 +2597,6 @@ export interface Query {
getEmailingDomains: EmailingDomain[]
findManyMarketplaceApps: MarketplaceApp[]
findOneMarketplaceApp: MarketplaceApp
findManyApplications: Application[]
findOneApplication: Application
__typename: 'Query'
}
@@ -2725,6 +2725,10 @@ export interface Mutation {
updateWorkspace: Workspace
deleteCurrentWorkspace: Workspace
checkCustomDomainValidRecords?: DomainValidRecords
installApplication: Scalars['Boolean']
runWorkspaceMigration: Scalars['Boolean']
uninstallApplication: Scalars['Boolean']
updateOneApplicationVariable: Scalars['Boolean']
getAuthorizationUrlForSSO: GetAuthorizationUrlForSSO
getLoginTokenFromCredentials: LoginToken
signIn: AvailableWorkspacesAndAccessTokens
@@ -2792,10 +2796,6 @@ export interface Mutation {
verifyEmailingDomain: EmailingDomain
createOneAppToken: AppToken
installMarketplaceApp: Scalars['Boolean']
installApplication: Scalars['Boolean']
runWorkspaceMigration: Scalars['Boolean']
uninstallApplication: Scalars['Boolean']
updateOneApplicationVariable: Scalars['Boolean']
createDevelopmentApplication: DevelopmentApplication
generateApplicationToken: ApplicationTokenPair
syncApplication: WorkspaceMigration
@@ -5546,6 +5546,8 @@ export interface QueryGenqlSelection{
currentUser?: UserGenqlSelection
currentWorkspace?: WorkspaceGenqlSelection
getPublicWorkspaceDataByDomain?: (PublicWorkspaceDataGenqlSelection & { __args?: {origin?: (Scalars['String'] | null)} })
findManyApplications?: ApplicationGenqlSelection
findOneApplication?: (ApplicationGenqlSelection & { __args?: {id?: (Scalars['UUID'] | null), universalIdentifier?: (Scalars['UUID'] | null)} })
checkUserExists?: (CheckUserExistGenqlSelection & { __args: {email: Scalars['String'], captchaToken?: (Scalars['String'] | null)} })
checkWorkspaceInviteHashIsValid?: (WorkspaceInviteHashValidGenqlSelection & { __args: {inviteHash: Scalars['String']} })
findWorkspaceFromInviteHash?: (WorkspaceGenqlSelection & { __args: {inviteHash: Scalars['String']} })
@@ -5594,8 +5596,6 @@ export interface QueryGenqlSelection{
getEmailingDomains?: EmailingDomainGenqlSelection
findManyMarketplaceApps?: MarketplaceAppGenqlSelection
findOneMarketplaceApp?: (MarketplaceAppGenqlSelection & { __args: {universalIdentifier: Scalars['String']} })
findManyApplications?: ApplicationGenqlSelection
findOneApplication?: (ApplicationGenqlSelection & { __args?: {id?: (Scalars['UUID'] | null), universalIdentifier?: (Scalars['UUID'] | null)} })
__typename?: boolean | number
__scalar?: boolean | number
}
@@ -5743,6 +5743,10 @@ export interface MutationGenqlSelection{
updateWorkspace?: (WorkspaceGenqlSelection & { __args: {data: UpdateWorkspaceInput} })
deleteCurrentWorkspace?: WorkspaceGenqlSelection
checkCustomDomainValidRecords?: DomainValidRecordsGenqlSelection
installApplication?: { __args: {appRegistrationId: Scalars['String'], version?: (Scalars['String'] | null)} }
runWorkspaceMigration?: { __args: {workspaceMigration: WorkspaceMigrationInput} }
uninstallApplication?: { __args: {universalIdentifier: Scalars['String']} }
updateOneApplicationVariable?: { __args: {key: Scalars['String'], value: Scalars['String'], applicationId: Scalars['UUID']} }
getAuthorizationUrlForSSO?: (GetAuthorizationUrlForSSOGenqlSelection & { __args: {input: GetAuthorizationUrlForSSOInput} })
getLoginTokenFromCredentials?: (LoginTokenGenqlSelection & { __args: {email: Scalars['String'], password: Scalars['String'], captchaToken?: (Scalars['String'] | null), locale?: (Scalars['String'] | null), verifyEmailRedirectPath?: (Scalars['String'] | null), origin: Scalars['String']} })
signIn?: (AvailableWorkspacesAndAccessTokensGenqlSelection & { __args: {email: Scalars['String'], password: Scalars['String'], captchaToken?: (Scalars['String'] | null), locale?: (Scalars['String'] | null), verifyEmailRedirectPath?: (Scalars['String'] | null)} })
@@ -5810,10 +5814,6 @@ export interface MutationGenqlSelection{
verifyEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {id: Scalars['String']} })
createOneAppToken?: (AppTokenGenqlSelection & { __args: {input: CreateOneAppTokenInput} })
installMarketplaceApp?: { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} }
installApplication?: { __args: {appRegistrationId: Scalars['String'], version?: (Scalars['String'] | null)} }
runWorkspaceMigration?: { __args: {workspaceMigration: WorkspaceMigrationInput} }
uninstallApplication?: { __args: {universalIdentifier: Scalars['String']} }
updateOneApplicationVariable?: { __args: {key: Scalars['String'], value: Scalars['String'], applicationId: Scalars['UUID']} }
createDevelopmentApplication?: (DevelopmentApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], name: Scalars['String']} })
generateApplicationToken?: (ApplicationTokenPairGenqlSelection & { __args: {applicationId: Scalars['UUID']} })
syncApplication?: (WorkspaceMigrationGenqlSelection & { __args: {manifest: Scalars['JSON']} })
@@ -6078,6 +6078,10 @@ export interface ActivateWorkspaceInput {displayName?: (Scalars['String'] | null
export interface UpdateWorkspaceInput {subdomain?: (Scalars['String'] | null),customDomain?: (Scalars['String'] | null),displayName?: (Scalars['String'] | null),logo?: (Scalars['String'] | null),inviteHash?: (Scalars['String'] | null),isPublicInviteLinkEnabled?: (Scalars['Boolean'] | null),allowImpersonation?: (Scalars['Boolean'] | null),isGoogleAuthEnabled?: (Scalars['Boolean'] | null),isMicrosoftAuthEnabled?: (Scalars['Boolean'] | null),isPasswordAuthEnabled?: (Scalars['Boolean'] | null),isGoogleAuthBypassEnabled?: (Scalars['Boolean'] | null),isMicrosoftAuthBypassEnabled?: (Scalars['Boolean'] | null),isPasswordAuthBypassEnabled?: (Scalars['Boolean'] | null),defaultRoleId?: (Scalars['UUID'] | null),isTwoFactorAuthenticationEnforced?: (Scalars['Boolean'] | null),trashRetentionDays?: (Scalars['Float'] | null),eventLogRetentionDays?: (Scalars['Float'] | null),fastModel?: (Scalars['String'] | null),smartModel?: (Scalars['String'] | null),aiAdditionalInstructions?: (Scalars['String'] | null),editableProfileFields?: (Scalars['String'][] | null),autoEnableNewAiModels?: (Scalars['Boolean'] | null),disabledAiModelIds?: (Scalars['String'][] | null),enabledAiModelIds?: (Scalars['String'][] | null),useRecommendedModels?: (Scalars['Boolean'] | null)}
export interface WorkspaceMigrationInput {actions: WorkspaceMigrationDeleteActionInput[]}
export interface WorkspaceMigrationDeleteActionInput {type: WorkspaceMigrationActionType,metadataName: AllMetadataName,universalIdentifier: Scalars['String']}
export interface GetAuthorizationUrlForSSOInput {identityProviderId: Scalars['UUID'],workspaceInviteHash?: (Scalars['String'] | null)}
export interface CreateApplicationRegistrationInput {name: Scalars['String'],description?: (Scalars['String'] | null),logoUrl?: (Scalars['String'] | null),author?: (Scalars['String'] | null),universalIdentifier?: (Scalars['String'] | null),oAuthRedirectUris?: (Scalars['String'][] | null),oAuthScopes?: (Scalars['String'][] | null),websiteUrl?: (Scalars['String'] | null),termsUrl?: (Scalars['String'] | null)}
@@ -6126,10 +6130,6 @@ appToken: CreateAppTokenInput}
export interface CreateAppTokenInput {expiresAt: Scalars['DateTime']}
export interface WorkspaceMigrationInput {actions: WorkspaceMigrationDeleteActionInput[]}
export interface WorkspaceMigrationDeleteActionInput {type: WorkspaceMigrationActionType,metadataName: AllMetadataName,universalIdentifier: Scalars['String']}
export interface SubscriptionGenqlSelection{
onDbEvent?: (OnDbEventGenqlSelection & { __args: {input: OnDbEventInput} })
onEventSubscription?: (EventSubscriptionGenqlSelection & { __args: {eventStreamId: Scalars['String']} })
+12
View File
@@ -247,6 +247,18 @@
"nx command-no-deps -- workspace:seed:dev"
],
"parallel": false
},
"with-apps": {
"cwd": "packages/twenty-server",
"commands": [
"nx ts-node-no-deps-transpile-only -- ./scripts/truncate-db.ts",
"nx ts-node-no-deps-transpile-only -- ./scripts/setup-db.ts",
"nx database:migrate",
"nx command-no-deps -- cache:flush",
"cd ../twenty-sdk && npx tsx ../twenty-apps/fixtures/build-fixture-outputs.ts",
"nx command-no-deps -- workspace:seed:dev --with-apps"
],
"parallel": false
}
},
"defaultConfiguration": "seed"
@@ -1,12 +1,17 @@
import { Logger } from '@nestjs/common';
import { Command, CommandRunner } from 'nest-commander';
import { Command, CommandRunner, Option } from 'nest-commander';
import {
SEED_APPLE_WORKSPACE_ID,
SEED_YCOMBINATOR_WORKSPACE_ID,
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
import { DevSeederService } from 'src/engine/workspace-manager/dev-seeder/services/dev-seeder.service';
type SeedDevOptions = {
withApps?: boolean;
};
@Command({
name: 'workspace:seed:dev',
description:
@@ -23,10 +28,21 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
super();
}
async run(): Promise<void> {
@Option({
flags: '--with-apps',
description: 'Include fixture app seeding (builds and installs apps)',
required: false,
})
parseWithApps(): boolean {
return true;
}
async run(_passedParams: string[], options: SeedDevOptions): Promise<void> {
try {
for (const workspaceId of this.workspaceIds) {
await this.devSeederService.seedDev(workspaceId);
await this.devSeederService.seedDev(workspaceId, {
includeApps: options.withApps,
});
}
} catch (error) {
this.logger.error(error);
@@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { promises as fs } from 'fs';
import { join, relative } from 'path';
import { type Manifest } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
@@ -94,6 +95,42 @@ export class ApplicationInstallService {
);
}
async installFromLocalDirectory(params: {
appRegistrationId: string;
extractedDir: string;
workspaceId: string;
}): Promise<boolean> {
const appRegistration = await this.appRegistrationRepository.findOne({
where: { id: params.appRegistrationId },
});
if (!appRegistration) {
throw new ApplicationException(
`Application registration with id ${params.appRegistrationId} not found`,
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
);
}
const manifestContent = await fs.readFile(
join(params.extractedDir, 'manifest.json'),
'utf-8',
);
const manifest: Manifest = JSON.parse(manifestContent);
await this.installFromExtractedDir({
appRegistration,
extractedDir: params.extractedDir,
manifest,
workspaceId: params.workspaceId,
});
this.logger.log(
`Installed app from local directory: ${appRegistration.universalIdentifier}`,
);
return true;
}
private async doInstallApplication(
appRegistration: ApplicationRegistrationEntity,
params: { version?: string; workspaceId: string },
@@ -111,30 +148,15 @@ export class ApplicationInstallService {
return true;
}
const universalIdentifier = appRegistration.universalIdentifier;
await this.ensureApplicationExists({
universalIdentifier,
name: resolvedPackage.manifest.application.displayName,
workspaceId: params.workspaceId,
applicationRegistrationId: appRegistration.id,
sourceType: appRegistration.sourceType,
});
await this.writeFilesToStorage(
resolvedPackage.extractedDir,
universalIdentifier,
params.workspaceId,
);
await this.applicationSyncService.synchronizeFromManifest({
workspaceId: params.workspaceId,
await this.installFromExtractedDir({
appRegistration,
extractedDir: resolvedPackage.extractedDir,
manifest: resolvedPackage.manifest,
applicationRegistrationId: appRegistration.id,
workspaceId: params.workspaceId,
});
this.logger.log(
`Successfully installed app ${universalIdentifier} v${resolvedPackage.packageJson.version ?? 'unknown'}`,
`Successfully installed app ${appRegistration.universalIdentifier} v${resolvedPackage.packageJson.version ?? 'unknown'}`,
);
return true;
@@ -153,6 +175,33 @@ export class ApplicationInstallService {
}
}
private async installFromExtractedDir(params: {
appRegistration: ApplicationRegistrationEntity;
extractedDir: string;
manifest: Manifest;
workspaceId: string;
}): Promise<void> {
await this.ensureApplicationExists({
universalIdentifier: params.appRegistration.universalIdentifier,
name: params.manifest.application.displayName,
workspaceId: params.workspaceId,
applicationRegistrationId: params.appRegistration.id,
sourceType: params.appRegistration.sourceType,
});
await this.writeFilesToStorage(
params.extractedDir,
params.appRegistration.universalIdentifier,
params.workspaceId,
);
await this.applicationSyncService.synchronizeFromManifest({
workspaceId: params.workspaceId,
manifest: params.manifest,
applicationRegistrationId: params.appRegistration.id,
});
}
private async writeFilesToStorage(
extractedDir: string,
applicationUniversalIdentifier: string,
@@ -0,0 +1,55 @@
import { dirname, join } from 'path';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
// Resolve via Node module resolution, same pattern as standard-front-component-upload.service.ts
const PACKAGES_ROOT = dirname(
dirname(require.resolve('twenty-standard-application/package.json')),
);
const FIXTURES_ROOT = join(PACKAGES_ROOT, 'twenty-apps', 'fixtures');
export type AppSeedDefinition = {
registration: {
universalIdentifier: string;
name: string;
description: string;
sourceType: ApplicationRegistrationSourceType;
sourcePackage?: string;
author?: string;
};
outputDir: string;
};
export const APP_SEEDS: AppSeedDefinition[] = [
{
registration: {
universalIdentifier: '6563e091-9f5b-4026-a3ea-7e3b3d09e218',
name: 'Hello World',
description: 'A sample marketplace app installed via NPM',
sourceType: ApplicationRegistrationSourceType.NPM,
sourcePackage: 'hello-world',
author: 'Twenty',
},
outputDir: join(FIXTURES_ROOT, 'hello-world-app', '.twenty', 'output'),
},
{
registration: {
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
name: 'Postcard App',
description: 'An internal app installed as tarball',
sourceType: ApplicationRegistrationSourceType.TARBALL,
author: 'Twenty',
},
outputDir: join(FIXTURES_ROOT, 'postcard-app', '.twenty', 'output'),
},
{
registration: {
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000001',
name: 'Minimal App',
description: 'A dev-mode app running locally',
sourceType: ApplicationRegistrationSourceType.LOCAL,
},
outputDir: join(FIXTURES_ROOT, 'minimal-app', '.twenty', 'output'),
},
];
@@ -3,6 +3,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { ApplicationInstallModule } from 'src/engine/core-modules/application/application-install/application-install.module';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
@@ -42,7 +44,12 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
ApplicationModule,
FeatureFlagModule,
FileStorageModule,
TypeOrmModule.forFeature([WorkspaceEntity, ObjectMetadataEntity]),
TypeOrmModule.forFeature([
WorkspaceEntity,
ObjectMetadataEntity,
ApplicationRegistrationEntity,
]),
ApplicationInstallModule,
ObjectPermissionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceCacheModule,
@@ -1,10 +1,12 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { Injectable, Logger } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { DataSource } from 'typeorm';
import { FeatureFlagKey } from 'twenty-shared/types';
import { DataSource, Repository } from 'typeorm';
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@@ -14,6 +16,10 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import {
APP_SEEDS,
type AppSeedDefinition,
} from 'src/engine/workspace-manager/dev-seeder/core/constants/app-seeds.constant';
import { SeededWorkspacesIds } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service';
import { seedCoreSchema } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util';
@@ -27,6 +33,8 @@ import { TwentyStandardApplicationService } from 'src/engine/workspace-manager/t
@Injectable()
export class DevSeederService {
private readonly logger = new Logger(DevSeederService.name);
constructor(
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly twentyConfigService: TwentyConfigService,
@@ -37,12 +45,18 @@ export class DevSeederService {
private readonly devSeederPermissionsService: DevSeederPermissionsService,
private readonly devSeederDataService: DevSeederDataService,
private readonly applicationService: ApplicationService,
private readonly applicationInstallService: ApplicationInstallService,
private readonly workspaceCacheService: WorkspaceCacheService,
@InjectRepository(ApplicationRegistrationEntity)
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {}
public async seedDev(workspaceId: SeededWorkspacesIds): Promise<void> {
public async seedDev(
workspaceId: SeededWorkspacesIds,
options: { includeApps?: boolean } = {},
): Promise<void> {
const isBillingEnabled = this.twentyConfigService.get('IS_BILLING_ENABLED');
const appVersion = this.twentyConfigService.get('APP_VERSION');
@@ -83,6 +97,10 @@ export class DevSeederService {
},
);
if (options.includeApps) {
await this.seedApps(workspaceId);
}
await this.devSeederMetadataService.seed({
dataSourceMetadata,
workspaceId,
@@ -166,4 +184,56 @@ export class DevSeederService {
await this.workspaceCacheStorageService.flush(workspaceId, undefined);
}
private async seedApps(workspaceId: string): Promise<void> {
for (const seed of APP_SEEDS) {
try {
await this.seedOneApp(seed, workspaceId);
} catch (error) {
this.logger.error(
`Failed to seed app "${seed.registration.name}": ${error}`,
);
}
}
}
private async seedOneApp(
seed: AppSeedDefinition,
workspaceId: string,
): Promise<void> {
let registration = await this.appRegistrationRepository.findOne({
where: {
universalIdentifier: seed.registration.universalIdentifier,
},
});
if (!registration) {
registration = await this.appRegistrationRepository.save(
this.appRegistrationRepository.create({
universalIdentifier: seed.registration.universalIdentifier,
name: seed.registration.name,
description: seed.registration.description,
sourceType: seed.registration.sourceType,
sourcePackage: seed.registration.sourcePackage,
author: seed.registration.author,
oAuthClientId: `20202020-seed-${seed.registration.universalIdentifier.slice(0, 8)}`,
oAuthClientSecretHash: 'seed',
oAuthRedirectUris: [],
oAuthScopes: [],
ownerWorkspaceId: workspaceId,
createdByUserId: null,
}),
);
}
await this.applicationInstallService.installFromLocalDirectory({
appRegistrationId: registration.id,
extractedDir: seed.outputDir,
workspaceId,
});
this.logger.log(
`Seeded app "${seed.registration.name}" (${seed.registration.sourceType})`,
);
}
}